diff --git a/.github/workflows/macosx-ci.yml b/.github/workflows/macosx-ci.yml index 6fab6b6c5d..774048cd66 100644 --- a/.github/workflows/macosx-ci.yml +++ b/.github/workflows/macosx-ci.yml @@ -55,7 +55,7 @@ jobs: - name: Brew install DeJaVu fonts run: brew tap homebrew/cask-fonts && brew install font-dejavu - name: Remove python's 2to3 link so that 'brew link' does not fail - run: rm '/usr/local/bin/2to3' + run: rm '/usr/local/bin/2to3' && rm '/usr/local/bin/2to3-3.11' - name: Install environment helpers with homebrew run: brew install ccache - name: Install dependencies with homebrew diff --git a/assets/test/nyan/pong.nyan b/assets/test/nyan/pong.nyan index 16bb6c957d..44510bf5a5 100644 --- a/assets/test/nyan/pong.nyan +++ b/assets/test/nyan/pong.nyan @@ -1,3 +1,5 @@ +!version 1 + PongGame(): ball : Ball player1 : Player diff --git a/cfg/converter/games/aoc_demo/version_hashes.toml b/cfg/converter/games/aoc_demo/version_hashes.toml new file mode 100644 index 0000000000..0b63af46f4 --- /dev/null +++ b/cfg/converter/games/aoc_demo/version_hashes.toml @@ -0,0 +1,17 @@ +# version hashes of Age of Empires 2: The Conqueror's Demo + +file_version = "2.0" +hash_algo = "SHA3-256" + +[age2_x1t] +paths = ["age2_x1t.exe"] + + [age2_x1t.map] + d02ef2f4935c5ef568eaa873442b4b2b547067ed8e3572758bbbbee8 = "1.0c" + 4568f37f309c0a89fa9e84a0c0f6c61b0f394c28adc82c0b3f2203b3 = "1.0c" + +[empires2_x1dat] +paths = ["Data/empires2_x1.dat"] + + [empires2_x1dat.map] + 6aab4c7468c3d319ec00f577835dc7799b70ffab1de6bfd25ac227b5 = "1.0c" diff --git a/cfg/converter/games/game_editions.toml b/cfg/converter/games/game_editions.toml index d28de9bd76..7b0331e81e 100644 --- a/cfg/converter/games/game_editions.toml +++ b/cfg/converter/games/game_editions.toml @@ -1,173 +1,287 @@ # game edition config file for openage -file_version = "1.0" +file_version = "1.1" [AOC] -name = "Age of Empires 2: The Conqueror's" +name = "Age of Empires 2: The Conqueror's" game_edition_id = "AOC" -subfolder = "aoc" -support = "yes" -targetmod = [ "aoe2-base", "aoe2-base-graphics" ] -expansions = [ ] +subfolder = "aoc" +support = "yes" +targetmod = ["aoe2_base", "aoe2_base_graphics"] +expansions = [] [AOC.mediapaths] - datfile = [ "data/empires2_x1_p1.dat" ] - gamedata = [ "data/gamedata_x1_p1.drs" ] - graphics = [ "data/graphics.drs" ] - language = [ "language.dll", "language_x1.dll", "language_x1_p1.dll" ] - palettes = [ "data/interfac.drs" ] - sounds = [ "data/sounds.drs", "data/sounds_x1.drs" ] - interface = [ "data/interfac.drs" ] - terrain = [ "data/terrain.drs" ] - blend = [ "data/blendomatic.dat" ] + datfile = ["data/empires2_x1_p1.dat"] + gamedata = ["data/gamedata_x1_p1.drs"] + graphics = ["data/graphics.drs"] + language = ["language.dll", "language_x1.dll", "language_x1_p1.dll"] + palettes = ["data/interfac.drs"] + sounds = ["data/sounds.drs", "data/sounds_x1.drs"] + interface = ["data/interfac.drs"] + terrain = ["data/terrain.drs"] + blend = ["data/blendomatic.dat"] + + [AOC.installpaths] + linux = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II", + # Lutris + "~/Games/age-of-empires-ii-the-conquerors", + ] + macos = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II", + ] + windows = [ + "C:/Program Files/Microsoft Games/Age of Empires II", + "C:/Program Files (x86)/Microsoft Games/Age of Empires II", + ] + + +[AOCDEMO] +name = "Age of Empires 2: The Conqueror's Trial Version" +game_edition_id = "AOCDEMO" +subfolder = "aoc_demo" +support = "yes" +targetmod = ["trial_base", "trial_graphics"] +expansions = [] + + [AOCDEMO.mediapaths] + datfile = ["data/empires2_x1.dat"] + gamedata = ["data/gamedata_x1.drs"] + graphics = ["data/graphics.drs"] + language = ["language.dll", "language_x1.dll"] + palettes = ["data/interfac.drs"] + sounds = ["data/sounds_x1.drs"] + interface = ["data/interfac.drs"] + terrain = ["data/Terrain.drs"] + blend = ["data/blendomatic.dat"] [AOK] -name = "Age of Empires 2: Age of Kings" +name = "Age of Empires 2: Age of Kings" game_edition_id = "AOK" -subfolder = "aok" -support = "nope" -targetmod = [ ] -expansions = [ ] +subfolder = "aok" +support = "nope" +targetmod = [] +expansions = [] [AOK.mediapaths] - datfile = [ "data/empires2.dat" ] - gamedata = [ "data/gamedata.drs" ] - graphics = [ "data/graphics.drs" ] - palettes = [ "data/interfac.drs" ] - sounds = [ "data/sounds.drs" ] - interface = [ "data/interfac.drs" ] - terrain = [ "data/terrain.drs" ] + datfile = ["data/empires2.dat"] + gamedata = ["data/gamedata.drs"] + graphics = ["data/graphics.drs"] + palettes = ["data/interfac.drs"] + sounds = ["data/sounds.drs"] + interface = ["data/interfac.drs"] + terrain = ["data/terrain.drs"] + + [AOK.installpaths] + linux = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II", + # Lutris + "~/Games/age-of-empires-ii-the-conquerors", + ] + macos = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II", + ] + windows = [ + "C:/Program Files/Microsoft Games/Age of Empires II", + "C:/Program Files (x86)/Microsoft Games/Age of Empires II", + ] + [AOE1DE] -name = "Age of Empires 1: Definitive Edition (Steam)" +name = "Age of Empires 1: Definitive Edition (Steam)" game_edition_id = "AOE1DE" -subfolder = "de1" -support = "yes" -targetmod = [ "aoe1-base", "aoe1-base-graphics" ] -expansions = [ ] +subfolder = "de1" +support = "yes" +targetmod = ["de1_base", "de1_base_graphics"] +expansions = [] [AOE1DE.mediapaths] - datfile = [ "Data/empires.dat" ] - graphics = [ "Assets/SLP/" ] - palettes = [ "Assets/Palettes/" ] - sounds = [ "Assets/Sounds/" ] - interface = [ "Data/DRS/interfac.drs", "Data/DRS/interfac_x1.drs" ] + datfile = ["Data/empires.dat"] + graphics = ["Assets/SLP/"] + palettes = ["Assets/Palettes/"] + sounds = ["Assets/Sounds/"] + interface = ["Data/DRS/interfac.drs", "Data/DRS/interfac_x1.drs"] language = [ - "Data/Localization/br/strings.txt", - "Data/Localization/de/strings.txt", - "Data/Localization/en/strings.txt", - "Data/Localization/es/strings.txt", - "Data/Localization/fr/strings.txt", - "Data/Localization/hi/strings.txt", - "Data/Localization/it/strings.txt", - "Data/Localization/jp/strings.txt", - "Data/Localization/ko/strings.txt", - "Data/Localization/mx/strings.txt", - "Data/Localization/ru/strings.txt", - "Data/Localization/vi/strings.txt", - "Data/Localization/zhs/strings.txt", - "Data/Localization/zht/strings.txt" - ] - terrain = [ "Assets/SLP/" ] + "Data/Localization/br/strings.txt", + "Data/Localization/de/strings.txt", + "Data/Localization/en/strings.txt", + "Data/Localization/es/strings.txt", + "Data/Localization/fr/strings.txt", + "Data/Localization/hi/strings.txt", + "Data/Localization/it/strings.txt", + "Data/Localization/jp/strings.txt", + "Data/Localization/ko/strings.txt", + "Data/Localization/mx/strings.txt", + "Data/Localization/ru/strings.txt", + "Data/Localization/vi/strings.txt", + "Data/Localization/zhs/strings.txt", + "Data/Localization/zht/strings.txt", + ] + terrain = ["Assets/SLP/"] + + [AOE1DE.installpaths] + linux = ["~/.steam/steam/steamapps/common/AoEDE"] + macos = ["~/.steam/steam/steamapps/common/AoEDE"] + windows = [ + "C:/Program Files/Steam/steamapps/common/AoEDE", + "C:/Program Files (x86)/Steam/steamapps/common/AoEDE", + ] + [ROR] -name = "Age of Empires 1: Rise of Rome" +name = "Age of Empires 1: Rise of Rome" game_edition_id = "ROR" -subfolder = "ror" -support = "yes" -targetmod = [ "aoe1-base", "aoe1-base-graphics" ] -expansions = [ ] +subfolder = "ror" +support = "yes" +targetmod = ["aoe1_base", "aoe1_base_graphics"] +expansions = [] [ROR.mediapaths] - datfile = [ "data2/empires.dat" ] - graphics = [ "data/graphics.drs", "data2/graphics.drs" ] - palettes = [ "data/Interfac.drs", "data2/Interfac.drs" ] - sounds = [ "data/sounds.drs", "data2/sounds.drs" ] - interface = [ "data/Interfac.drs", "data2/Interfac.drs" ] - language = [ "language.dll", "languagex.dll" ] - terrain = [ "data/Terrain.drs" ] - border = [ "data/Border.drs" ] + datfile = ["data2/empires.dat"] + graphics = ["data/graphics.drs", "data2/graphics.drs"] + palettes = ["data/Interfac.drs", "data2/Interfac.drs"] + sounds = ["data/sounds.drs", "data2/sounds.drs"] + interface = ["data/Interfac.drs", "data2/Interfac.drs"] + language = ["language.dll", "languagex.dll"] + terrain = ["data/Terrain.drs"] + border = ["data/Border.drs"] + + [ROR.installpaths] + linux = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires", + # Lutris + "~/Games/age-of-empires", + ] + macos = [ + "~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires", + "~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires", + ] + windows = [ + "C:/Program Files/Microsoft Games/Age of Empires", + "C:/Program Files (x86)/Microsoft Games/Age of Empires", + ] + [HDEDITION] -name = "Age of Empires 2: HD Edition" +name = "Age of Empires 2: HD Edition" game_edition_id = "HDEDITION" -subfolder = "hd" -support = "yes" -targetmod = [ "aoe2-base", "aoe2-base-graphics" ] -expansions = [ ] +subfolder = "hd" +support = "yes" +targetmod = ["hd_base", "hd_base_graphics"] +expansions = [] [HDEDITION.mediapaths] - datfile = [ "resources/_common/dat/empires2_x1_p1.dat" ] - gamedata = [ "resources/_common/drs/gamedata_x1/" ] - graphics = [ "resources/_common/drs/graphics/" ] - palettes = [ "resources/_common/drs/interface/" ] - sounds = [ "resources/_common/drs/sounds/" ] - interface = [ "resources/_common/drs/interface/" ] + datfile = ["resources/_common/dat/empires2_x1_p1.dat"] + gamedata = ["resources/_common/drs/gamedata_x1/"] + graphics = ["resources/_common/drs/graphics/"] + palettes = ["resources/_common/drs/interface/"] + sounds = ["resources/_common/drs/sounds/"] + interface = ["resources/_common/drs/interface/"] language = [ - "resources/br/strings/key-value/key-value-strings-utf8.txt", - "resources/de/strings/key-value/key-value-strings-utf8.txt", - "resources/en/strings/key-value/key-value-strings-utf8.txt", - "resources/es/strings/key-value/key-value-strings-utf8.txt", - "resources/fr/strings/key-value/key-value-strings-utf8.txt", - "resources/it/strings/key-value/key-value-strings-utf8.txt", - "resources/jp/strings/key-value/key-value-strings-utf8.txt", - "resources/ko/strings/key-value/key-value-strings-utf8.txt", - "resources/nl/strings/key-value/key-value-strings-utf8.txt", - "resources/ru/strings/key-value/key-value-strings-utf8.txt", - "resources/zh/strings/key-value/key-value-strings-utf8.txt" - ] - terrain = [ "resources/_common/terrain/textures/" ] + "resources/br/strings/key-value/key-value-strings-utf8.txt", + "resources/de/strings/key-value/key-value-strings-utf8.txt", + "resources/en/strings/key-value/key-value-strings-utf8.txt", + "resources/es/strings/key-value/key-value-strings-utf8.txt", + "resources/fr/strings/key-value/key-value-strings-utf8.txt", + "resources/it/strings/key-value/key-value-strings-utf8.txt", + "resources/jp/strings/key-value/key-value-strings-utf8.txt", + "resources/ko/strings/key-value/key-value-strings-utf8.txt", + "resources/nl/strings/key-value/key-value-strings-utf8.txt", + "resources/ru/strings/key-value/key-value-strings-utf8.txt", + "resources/zh/strings/key-value/key-value-strings-utf8.txt", + ] + terrain = ["resources/_common/terrain/textures/"] + + [HDEDITION.installpaths] + linux = ["~/.steam/steam/steamapps/common/Age2HD"] + macos = ["~/.steam/steam/steamapps/common/Age2HD"] + windows = [ + "C:/Program Files/Steam/steamapps/common/Age2HD", + "C:/Program Files (x86)/Steam/steamapps/common/Age2HD", + ] + [AOE2DE] -name = "Age of Empires 2: Definitive Edition" +name = "Age of Empires 2: Definitive Edition" game_edition_id = "AOE2DE" -subfolder = "de2" -support = "yes" -targetmod = [ "aoe2-base", "aoe2-base-graphics" ] -expansions = [ ] +subfolder = "de2" +support = "yes" +targetmod = ["de2_base", "de2_base_graphics"] +expansions = [] [AOE2DE.mediapaths] - datfile = [ "resources/_common/dat/empires2_x2_p1.dat" ] - gamedata = [ "resources/_common/drs/gamedata_x2/" ] - graphics = [ "resources/_common/drs/graphics/" ] + datfile = ["resources/_common/dat/empires2_x2_p1.dat"] + gamedata = ["resources/_common/drs/gamedata_x2/"] + graphics = ["resources/_common/drs/graphics/"] language = [ - "resources/br/strings/key-value/key-value-strings-utf8.txt", - "resources/de/strings/key-value/key-value-strings-utf8.txt", - "resources/en/strings/key-value/key-value-strings-utf8.txt", - "resources/es/strings/key-value/key-value-strings-utf8.txt", - "resources/fr/strings/key-value/key-value-strings-utf8.txt", - "resources/hi/strings/key-value/key-value-strings-utf8.txt", - "resources/it/strings/key-value/key-value-strings-utf8.txt", - "resources/jp/strings/key-value/key-value-strings-utf8.txt", - "resources/ko/strings/key-value/key-value-strings-utf8.txt", - "resources/ms/strings/key-value/key-value-strings-utf8.txt", - "resources/mx/strings/key-value/key-value-strings-utf8.txt", - "resources/ru/strings/key-value/key-value-strings-utf8.txt", - "resources/tr/strings/key-value/key-value-strings-utf8.txt", - "resources/tw/strings/key-value/key-value-strings-utf8.txt", - "resources/vi/strings/key-value/key-value-strings-utf8.txt", - "resources/zh/strings/key-value/key-value-strings-utf8.txt" - ] - palettes = [ "resources/_common/palettes/" ] - sounds = [ "wwise/" ] - interface = [ "resources/_common/drs/interface/" ] - terrain = [ "resources/_common/terrain/textures/" ] + "resources/br/strings/key-value/key-value-strings-utf8.txt", + "resources/de/strings/key-value/key-value-strings-utf8.txt", + "resources/en/strings/key-value/key-value-strings-utf8.txt", + "resources/es/strings/key-value/key-value-strings-utf8.txt", + "resources/fr/strings/key-value/key-value-strings-utf8.txt", + "resources/hi/strings/key-value/key-value-strings-utf8.txt", + "resources/it/strings/key-value/key-value-strings-utf8.txt", + "resources/jp/strings/key-value/key-value-strings-utf8.txt", + "resources/ko/strings/key-value/key-value-strings-utf8.txt", + "resources/ms/strings/key-value/key-value-strings-utf8.txt", + "resources/mx/strings/key-value/key-value-strings-utf8.txt", + "resources/ru/strings/key-value/key-value-strings-utf8.txt", + "resources/tr/strings/key-value/key-value-strings-utf8.txt", + "resources/tw/strings/key-value/key-value-strings-utf8.txt", + "resources/vi/strings/key-value/key-value-strings-utf8.txt", + "resources/zh/strings/key-value/key-value-strings-utf8.txt", + ] + palettes = ["resources/_common/palettes/"] + sounds = ["wwise/"] + interface = ["resources/_common/drs/interface/"] + terrain = ["resources/_common/terrain/textures/"] + + [AOE2DE.installpaths] + linux = ["~/.steam/steam/steamapps/common/AoE2DE"] + macos = ["~/.steam/steam/steamapps/common/AoE2DE"] + windows = [ + "C:/Program Files/Steam/steamapps/common/AoE2DE", + "C:/Program Files (x86)/Steam/steamapps/common/AoE2DE", + ] + [SWGB] -name = "Star Wars: Galactic Battlegrounds" +name = "Star Wars: Galactic Battlegrounds" game_edition_id = "SWGB" -subfolder = "swgb" -support = "yes" -targetmod = [ "swgb-base", "swgb-base-graphics" ] -expansions = [ "SWGB_CC" ] +subfolder = "swgb" +support = "yes" +targetmod = ["swgb_base", "swgb_base_graphics"] +expansions = ["SWGB_CC"] [SWGB.mediapaths] - datfile = [ "Game/Data/GENIE.DAT" ] - gamedata = [ "Game/Data/GAMEDATA.DRS" ] - graphics = [ "Game/Data/GRAPHICS.DRS" ] - language = [ "Game/language.dll" ] - palettes = [ "Game/Data/INTERFAC.DRS" ] - sounds = [ "Game/Data/SOUNDS.DRS" ] - interface = [ "Game/Data/INTERFAC.DRS" ] - terrain = [ "Game/Data/TERRAIN.DRS" ] - blend = [ "Game/Data/blendomatic.dat" ] + datfile = ["Game/Data/GENIE.DAT"] + gamedata = ["Game/Data/GAMEDATA.DRS"] + graphics = ["Game/Data/GRAPHICS.DRS"] + language = ["Game/language.dll"] + palettes = ["Game/Data/INTERFAC.DRS"] + sounds = ["Game/Data/SOUNDS.DRS"] + interface = ["Game/Data/INTERFAC.DRS"] + terrain = ["Game/Data/TERRAIN.DRS"] + blend = ["Game/Data/blendomatic.dat"] + + [SWGB.installpaths] + linux = [ + "~/.wine/drive_c/GOG Games/Star Wars - Galactic Battlegrounds", + "~/.steam/steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", + # Lutris + "~/Games/gog/star-wars-galactic-battlegrounds-saga", + ] + macos = [ + "~/.wine/drive_c/GOG Games/Star Wars - Galactic Battlegrounds", + "~/.steam/steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", + ] + windows = [ + "C:/GOG Games/Star Wars - Galactic Battlegrounds", + "C:/Program Files (x86)/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", + "C:/Program Files/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga", + ] diff --git a/doc/build_instructions/debian.md b/doc/build_instructions/debian.md index e2a7603956..5381ef165e 100644 --- a/doc/build_instructions/debian.md +++ b/doc/build_instructions/debian.md @@ -1,6 +1,6 @@ # Prerequisite steps for Debian users - `sudo apt-get update` - - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype6-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` + - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/opensuse.md b/doc/build_instructions/opensuse.md index 011844979b..e2b51c41ef 100644 --- a/doc/build_instructions/opensuse.md +++ b/doc/build_instructions/opensuse.md @@ -1,5 +1,5 @@ # Prerequisite steps for openSUSE users -- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libSDL2-devel libSDL2_image-devel libepoxy-devel libfreetype6 libogg-devel libopus-devel libpng-devel libtoml11-dev libqt6-qtdeclarative-devel libqt6-qtquickcontrols opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` +- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libSDL2-devel libSDL2_image-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev libqt6-qtdeclarative-devel libqt6-qtquickcontrols opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/build_instructions/ubuntu.md b/doc/build_instructions/ubuntu.md index d04c004960..bc151975e2 100644 --- a/doc/build_instructions/ubuntu.md +++ b/doc/build_instructions/ubuntu.md @@ -3,7 +3,7 @@ Run the following commands: - `sudo apt-get update` - - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype6-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` + - `sudo apt-get install g++ cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-dev libsdl2-dev libsdl2-image-dev libtoml11-dev python3-dev python3-mako python3-numpy python3-lz4 python3-pil python3-pip python3-pygments python3-toml qml6-module-qtquick-controls qt6-declarative-dev` You will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies. diff --git a/doc/media/patterns/sld.hexpat b/doc/media/patterns/sld.hexpat index f4451e942d..9d57cd5fb9 100644 --- a/doc/media/patterns/sld.hexpat +++ b/doc/media/patterns/sld.hexpat @@ -30,8 +30,8 @@ struct sld_header { struct sld_frame_header { u16 canvas_width; u16 canvas_height; - u16 canvas_hotspot_x; - u16 canvas_hotspot_y; + s16 canvas_hotspot_x; + s16 canvas_hotspot_y; u8 frame_type; u8 unknown5; u16 frame_index; diff --git a/doc/media/sld-files.md b/doc/media/sld-files.md index a024c72667..1f8f300619 100644 --- a/doc/media/sld-files.md +++ b/doc/media/sld-files.md @@ -6,17 +6,41 @@ Unlike the [SMX](smx-files.md) and [SLP](slp-files.md) formats, the SLD format d indexed palettes for pixel data. Instead, SLD image data utilizes lossy texture compression algorithms [DXT1](https://en.wikipedia.org/wiki/S3_Texture_Compression#DXT1) and [DXT4](https://en.wikipedia.org/wiki/S3_Texture_Compression#DXT1) (also known as BC1 and BC4, respectively). -1. [SLD File Format](#sld-file-format) - 1. [Header](#header) - 1. [Frame Header](#sld-frame-header) - 1. [Layers](#sld-layers) - 1. [Main Graphics](#sld-main-graphics-layer) - 1. [Shadow](#sld-shadow-graphics-layer) - 1. [Damage Mask](#sld-damage-mask-layer) - 1. [Playercolor Mask](#sld-playercolor-mask-layer) -1. [Excursion: DXT1/BC1 Block Compression](#excursion-dxt1bc1-block-compression) -1. [Excursion: DXT4/BC4 Block Compression](#excursion-dxt4bc4-block-compression) -1. [Processing the Command Array (Example)](#processing-the-command-array-example) +1. [SLD file format](#sld-file-format) + 1. [Header](#header) + 2. [SLD Frame](#sld-frame) + 1. [SLD Frame Header](#sld-frame-header) + 3. [SLD Layers](#sld-layers) + 1. [Layer Content Length](#layer-content-length) + 2. [SLD Main Graphics Layer](#sld-main-graphics-layer) + 1. [Header](#header-1) + 2. [Pixel Data](#pixel-data) + 1. [Command Array](#command-array) + 2. [Compressed Block Array](#compressed-block-array) + 3. [SLD Shadow Graphics Layer](#sld-shadow-graphics-layer) + 1. [Header](#header-2) + 2. [Pixel Data](#pixel-data-1) + 1. [Command Array](#command-array-1) + 2. [Compressed Block Array](#compressed-block-array-1) + 4. [SLD Damage Mask Layer](#sld-damage-mask-layer) + 1. [Header](#header-3) + 2. [Pixel Data](#pixel-data-2) + 1. [Command Array](#command-array-2) + 2. [Compressed Block Array](#compressed-block-array-2) + 5. [SLD Playercolor Mask Layer](#sld-playercolor-mask-layer) + 1. [Header](#header-4) + 2. [Pixel Data](#pixel-data-3) + 1. [Command Array](#command-array-3) + 2. [Compressed Block Array](#compressed-block-array-3) +2. [Excursion: DXT1/BC1 Block Compression](#excursion-dxt1bc1-block-compression) + 1. [Building the Lookup Table](#building-the-lookup-table) + 2. [Constructing the Pixel Block](#constructing-the-pixel-block) + 3. [Example](#example) +3. [Excursion: DXT4/BC4 Block Compression](#excursion-dxt4bc4-block-compression) + 1. [Building the Lookup Table](#building-the-lookup-table-1) + 2. [Constructing the Pixel Block](#constructing-the-pixel-block-1) + 3. [Example](#example-1) +4. [Processing the Command Array (Example)](#processing-the-command-array-example) In addition to the format description found here, we also provide a [pattern file](/doc/media/patterns/sld.hexpat) for the [imHex](https://imhex.werwolv.net/) @@ -74,8 +98,8 @@ The frame header contains 7 values: | ------- | ------ | --------------- | ------------------------- | | 2 bytes | uint16 | Canvas Width | 200, 0xC8 | | 2 bytes | uint16 | Canvas Height | 200, 0xC8 | -| 2 bytes | uint16 | Canvas Center X | 100, 0x64 | -| 2 bytes | uint16 | Canvas Center Y | 100, 0x64 | +| 2 bytes | int16 | Canvas Center X | 100, 0x64 | +| 2 bytes | int16 | Canvas Center Y | 100, 0x64 | | 1 bytes | uint8 | Frame Type | 7, 0b00000111 (bit field) | | 1 bytes | uint8 | Unkown | 1, 0x01 | | 2 bytes | uint16 | Frame index | 5, 0x0005 | @@ -84,8 +108,8 @@ The frame header contains 7 values: struct sld_frame_header { uint16 canvas_width; uint16 canvas_height; - uint16 canvas_hotspot_x; - uint16 canvas_hotspot_y; + int16 canvas_hotspot_x; + int16 canvas_hotspot_y; uint8 frame_type; uint8 unknown1; uint16 frame_index; diff --git a/etc/valgrind-python.supp b/etc/valgrind-python.supp index c723f91da4..e9afe93092 100644 --- a/etc/valgrind-python.supp +++ b/etc/valgrind-python.supp @@ -269,6 +269,14 @@ } +{ + Uninitialised byte(s) false alarm, see bpo-35561 + Memcheck:Param + epoll_ctl(event) + fun:epoll_ctl + fun:pyepoll_internal_ctl +} + { ZLIB problems, see test_gzip Memcheck:Cond @@ -289,6 +297,17 @@ fun:rl_initialize } +# Valgrind emits "Conditional jump or move depends on uninitialised value(s)" +# false alarms on GCC builtin strcmp() function. The GCC code is correct. +# +# Valgrind bug: https://bugs.kde.org/show_bug.cgi?id=264936 +{ + bpo-38118: Valgrind emits false alarm on GCC builtin strcmp() + Memcheck:Cond + fun:PyUnicode_Decode +} + + ### ### These occur from somewhere within the SSL, when running ### test_socket_sll. They are too general to leave on by default. @@ -483,4 +502,3 @@ Memcheck:Addr4 fun:PyUnicode_FSConverter } - diff --git a/libopenage/assets/mod_manager.cpp b/libopenage/assets/mod_manager.cpp index 974fa8a12a..65ac98498f 100644 --- a/libopenage/assets/mod_manager.cpp +++ b/libopenage/assets/mod_manager.cpp @@ -28,7 +28,7 @@ void ModManager::activate_modpacks(const std::vector &load_order) { for (const auto &modpack_id : load_order) { auto &modpack = this->available.at(modpack_id); this->active.emplace(modpack_id, std::make_shared(modpack)); - log::log(MSG(info) << "Loaded modpack: " << modpack_id); + log::log(MSG(info) << "Activated modpack: " << modpack_id); } } diff --git a/libopenage/engine/engine.cpp b/libopenage/engine/engine.cpp index 1278506629..c3a9985986 100644 --- a/libopenage/engine/engine.cpp +++ b/libopenage/engine/engine.cpp @@ -15,7 +15,8 @@ namespace openage::engine { Engine::Engine(mode mode, const util::Path &root_dir, - const std::vector &mods) : + const std::vector &mods, + bool debug_graphics) : running{true}, run_mode{mode}, root_dir{root_dir}, @@ -59,7 +60,7 @@ Engine::Engine(mode mode, if (this->run_mode == mode::FULL) { this->threads.emplace_back([&]() { - this->presenter->run(); + this->presenter->run(debug_graphics); // Make sure that the presenter gets destructed in the same thread // otherwise OpenGL complains about missing contexts diff --git a/libopenage/engine/engine.h b/libopenage/engine/engine.h index d01fe9c4c2..cd4d848c14 100644 --- a/libopenage/engine/engine.h +++ b/libopenage/engine/engine.h @@ -71,10 +71,12 @@ class Engine { * @param mode The run mode to use. * @param root_dir openage root directory. * @param mods The mods to load. + * @param debug_graphics If true, enable OpenGL debug logging. */ Engine(mode mode, const util::Path &root_dir, - const std::vector &mods); + const std::vector &mods, + bool debug_graphics = false); // engine should not be copied or moved Engine(const Engine &) = delete; diff --git a/libopenage/gamestate/CMakeLists.txt b/libopenage/gamestate/CMakeLists.txt index 52842b5397..c89d9cd52d 100644 --- a/libopenage/gamestate/CMakeLists.txt +++ b/libopenage/gamestate/CMakeLists.txt @@ -17,6 +17,7 @@ add_sources(libopenage add_subdirectory(activity/) add_subdirectory(api/) add_subdirectory(component/) +add_subdirectory(demo/) add_subdirectory(event/) add_subdirectory(system/) diff --git a/libopenage/gamestate/component/internal/ownership.cpp b/libopenage/gamestate/component/internal/ownership.cpp index 5fcfd3c007..d4d5151e5d 100644 --- a/libopenage/gamestate/component/internal/ownership.cpp +++ b/libopenage/gamestate/component/internal/ownership.cpp @@ -8,7 +8,7 @@ namespace openage::gamestate::component { Ownership::Ownership(const std::shared_ptr &loop, - const ownership_id_t owner_id, + const player_id_t owner_id, const time::time_t &creation_time) : owner(loop, 0) { this->owner.set_last(creation_time, owner_id); @@ -22,11 +22,11 @@ inline component_t Ownership::get_type() const { return component_t::OWNERSHIP; } -void Ownership::set_owner(const time::time_t &time, const ownership_id_t owner_id) { +void Ownership::set_owner(const time::time_t &time, const player_id_t owner_id) { this->owner.set_last(time, owner_id); } -const curve::Discrete &Ownership::get_owners() const { +const curve::Discrete &Ownership::get_owners() const { return this->owner; } diff --git a/libopenage/gamestate/component/internal/ownership.h b/libopenage/gamestate/component/internal/ownership.h index b28448a557..cc0f275249 100644 --- a/libopenage/gamestate/component/internal/ownership.h +++ b/libopenage/gamestate/component/internal/ownership.h @@ -8,6 +8,7 @@ #include "curve/discrete.h" #include "gamestate/component/internal_component.h" #include "gamestate/component/types.h" +#include "gamestate/types.h" #include "time/time.h" @@ -18,8 +19,6 @@ class EventLoop; namespace gamestate::component { -using ownership_id_t = uint64_t; - class Ownership : public InternalComponent { public: /** @@ -30,7 +29,7 @@ class Ownership : public InternalComponent { * @param creation_time Ingame creation time of the component. */ Ownership(const std::shared_ptr &loop, - const ownership_id_t owner_id, + const player_id_t owner_id, const time::time_t &creation_time); /** @@ -48,20 +47,20 @@ class Ownership : public InternalComponent { * @param time Time at which the owner ID is set. * @param owner_id New owner ID. */ - void set_owner(const time::time_t &time, const ownership_id_t owner_id); + void set_owner(const time::time_t &time, const player_id_t owner_id); /** * Get the owner IDs over time. * * @return Owner ID curve. */ - const curve::Discrete &get_owners() const; + const curve::Discrete &get_owners() const; private: /** * Owner ID storage over time. */ - curve::Discrete owner; + curve::Discrete owner; }; } // namespace gamestate::component diff --git a/libopenage/gamestate/demo/CMakeLists.txt b/libopenage/gamestate/demo/CMakeLists.txt new file mode 100644 index 0000000000..1ef4ca0b68 --- /dev/null +++ b/libopenage/gamestate/demo/CMakeLists.txt @@ -0,0 +1,8 @@ +add_sources(libopenage + demo_0.cpp + tests.cpp +) + +pxdgen( + tests.h +) diff --git a/libopenage/gamestate/demo/demo_0.cpp b/libopenage/gamestate/demo/demo_0.cpp new file mode 100644 index 0000000000..7989673d20 --- /dev/null +++ b/libopenage/gamestate/demo/demo_0.cpp @@ -0,0 +1,22 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "demo_0.h" + +#include "cvar/cvar.h" +#include "gamestate/simulation.h" +#include "time/time_loop.h" + + +namespace openage::gamestate::tests { + +void simulation_demo_0(const util::Path &path) { + auto cvar = std::make_shared(path); + auto time_loop = std::make_shared(); + auto simulation = std::make_shared(path, cvar, time_loop); + + simulation->set_modpacks({"engine"}); + + simulation->start(); +} + +} // namespace openage::gamestate::tests diff --git a/libopenage/gamestate/demo/demo_0.h b/libopenage/gamestate/demo/demo_0.h new file mode 100644 index 0000000000..006b0ec266 --- /dev/null +++ b/libopenage/gamestate/demo/demo_0.h @@ -0,0 +1,15 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + + +namespace openage::gamestate::tests { + +/** + * Show the initialization of a game instance. + */ +void simulation_demo_0(const util::Path &path); + +} // namespace openage::gamestate::tests diff --git a/libopenage/gamestate/demo/tests.cpp b/libopenage/gamestate/demo/tests.cpp new file mode 100644 index 0000000000..31b1ea5ed1 --- /dev/null +++ b/libopenage/gamestate/demo/tests.cpp @@ -0,0 +1,25 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "tests.h" + +#include "log/log.h" +#include "log/message.h" + +#include "gamestate/demo/demo_0.h" + + +namespace openage::gamestate::tests { + +void simulation_demo(int demo_id, const util::Path &path) { + switch (demo_id) { + case 0: + simulation_demo_0(path); + break; + + default: + log::log(MSG(err) << "Unknown renderer demo requested: " << demo_id << "."); + break; + } +} + +} // namespace openage::gamestate::tests diff --git a/libopenage/gamestate/demo/tests.h b/libopenage/gamestate/demo/tests.h new file mode 100644 index 0000000000..1722940708 --- /dev/null +++ b/libopenage/gamestate/demo/tests.h @@ -0,0 +1,20 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "../../util/compiler.h" +// pxd: from libopenage.util.path cimport Path + + +namespace openage { +namespace util { +class Path; +} // namespace util + +namespace gamestate::tests { + +// pxd: void simulation_demo(int demo_id, Path path) except + +OAAPI void simulation_demo(int demo_id, const util::Path &path); + +} // namespace gamestate::tests +} // namespace openage diff --git a/libopenage/gamestate/entity_factory.cpp b/libopenage/gamestate/entity_factory.cpp index f6aa7f4fc9..f6c3d6f660 100644 --- a/libopenage/gamestate/entity_factory.cpp +++ b/libopenage/gamestate/entity_factory.cpp @@ -31,6 +31,7 @@ #include "gamestate/game_entity.h" #include "gamestate/game_state.h" #include "gamestate/manager.h" +#include "gamestate/player.h" #include "gamestate/system/types.h" #include "log/message.h" #include "renderer/render_factory.h" @@ -152,17 +153,22 @@ std::shared_ptr create_test_activity() { } EntityFactory::EntityFactory() : - next_id{0}, + next_entity_id{0}, + next_player_id{0}, render_factory{nullptr} { } std::shared_ptr EntityFactory::add_game_entity(const std::shared_ptr &loop, const std::shared_ptr &state, + player_id_t owner_id, const nyan::fqon_t &nyan_entity) { - auto entity = std::make_shared(this->get_next_id()); + auto entity = std::make_shared(this->get_next_entity_id()); entity->set_manager(std::make_shared(loop, state, entity)); - init_components(loop, state, entity, nyan_entity); + // use the owner's data to initialize the entity + // this ensures that only the owner's tech upgrades apply + auto db_view = state->get_player(owner_id)->get_db_view(); + init_components(loop, db_view, entity, nyan_entity); if (this->render_factory) { entity->set_render_entity(this->render_factory->add_world_render_entity()); @@ -171,6 +177,17 @@ std::shared_ptr EntityFactory::add_game_entity(const std::shared_ptr return entity; } +std::shared_ptr EntityFactory::add_player(const std::shared_ptr & /* loop */, + const std::shared_ptr &state, + const nyan::fqon_t & /* player_setup */) { + auto player = std::make_shared(this->get_next_player_id(), + state->get_db_view()->new_child()); + + // TODO: Components (for resources, diplomacy, etc.) + + return player; +} + void EntityFactory::attach_renderer(const std::shared_ptr &render_factory) { std::unique_lock lock{this->mutex}; @@ -178,7 +195,7 @@ void EntityFactory::attach_renderer(const std::shared_ptr &loop, - const std::shared_ptr &state, + const std::shared_ptr &owner_db_view, const std::shared_ptr &entity, const nyan::fqon_t &nyan_entity) { auto position = std::make_shared(loop); @@ -190,13 +207,12 @@ void EntityFactory::init_components(const std::shared_ptr(loop); entity->add_component(command_queue); - auto db_view = state->get_nyan_db(); - auto nyan_obj = db_view->get_object(nyan_entity); + auto nyan_obj = owner_db_view->get_object(nyan_entity); nyan::set_t abilities = nyan_obj.get_set("GameEntity.abilities"); for (const auto &ability_val : abilities) { auto ability_fqon = std::dynamic_pointer_cast(ability_val.get_ptr())->get_name(); - auto ability_obj = db_view->get_object(ability_fqon); + auto ability_obj = owner_db_view->get_object(ability_fqon); auto ability_parent = ability_obj.get_parents()[0]; if (ability_parent == "engine.ability.type.Move") { @@ -218,7 +234,7 @@ void EntityFactory::init_components(const std::shared_ptr(setting.get_ptr()); - auto setting_obj = db_view->get_object(setting_obj_val->get_name()); + auto setting_obj = owner_db_view->get_object(setting_obj_val->get_name()); auto attribute = setting_obj.get_object("AttributeSetting.attribute"); auto start_value = setting_obj.get_int("AttributeSetting.starting_value"); @@ -238,9 +254,16 @@ void EntityFactory::init_components(const std::shared_ptradd_component(activity); } -entity_id_t EntityFactory::get_next_id() { - auto new_id = this->next_id; - this->next_id++; +entity_id_t EntityFactory::get_next_entity_id() { + auto new_id = this->next_entity_id; + this->next_entity_id++; + + return new_id; +} + +player_id_t EntityFactory::get_next_player_id() { + auto new_id = this->next_player_id; + this->next_player_id++; return new_id; } diff --git a/libopenage/gamestate/entity_factory.h b/libopenage/gamestate/entity_factory.h index 3960a08e53..5a369d995e 100644 --- a/libopenage/gamestate/entity_factory.h +++ b/libopenage/gamestate/entity_factory.h @@ -23,6 +23,7 @@ class RenderFactory; namespace gamestate { class GameEntity; class GameState; +class Player; /** * Creates game entities that contain data of objects inside the game world. @@ -43,14 +44,32 @@ class EntityFactory { * * @param loop Event loop for the gamestate. * @param state State of the game. + * @param owner_id ID of the player owning the entity. * @param nyan_entity fqon of the GameEntity data in the nyan database. * * @return New game entity. */ std::shared_ptr add_game_entity(const std::shared_ptr &loop, const std::shared_ptr &state, + player_id_t owner_id, const nyan::fqon_t &nyan_entity); + /** + * Create a new player. + * + * This just creates the player. The caller is responsible for initializing + * its components and placing it into the game. + * + * @param loop Event loop for the gamestate. + * @param state State of the game. + * @param player_setup fqon of the player setup in the nyan database. + * + * @return New player. + */ + std::shared_ptr add_player(const std::shared_ptr &loop, + const std::shared_ptr &state, + const nyan::fqon_t &player_setup); + /** * Attach a renderer which enables graphical display options for all ingame entities. * @@ -64,12 +83,12 @@ class EntityFactory { * Initialize components of a game entity. * * @param loop Event loop for the gamestate. - * @param state State of the game. + * @param owner_db_view View of the nyan database of the player owning the entity. * @param entity Game entity. * @param nyan_entity fqon of the GameEntity data in the nyan database. */ void init_components(const std::shared_ptr &loop, - const std::shared_ptr &state, + const std::shared_ptr &owner_db_view, const std::shared_ptr &entity, const nyan::fqon_t &nyan_entity); @@ -78,12 +97,24 @@ class EntityFactory { * * @return Unique ID for a game entity. */ - entity_id_t get_next_id(); + entity_id_t get_next_entity_id(); + + /** + * Get a unique ID for creating a player. + * + * @return Unique ID for a player. + */ + player_id_t get_next_player_id(); /** * ID of the next game entity to be created. */ - entity_id_t next_id; + entity_id_t next_entity_id; + + /** + * ID of the next player to be created. + */ + player_id_t next_player_id; /** * Factory for creating connector objects to the renderer which make game entities displayable. diff --git a/libopenage/gamestate/event/spawn_entity.cpp b/libopenage/gamestate/event/spawn_entity.cpp index 5389cf775c..5cc1cf27df 100644 --- a/libopenage/gamestate/event/spawn_entity.cpp +++ b/libopenage/gamestate/event/spawn_entity.cpp @@ -33,6 +33,13 @@ static const std::vector aoe1_test_entities = { "aoe1_base.data.game_entity.generic.temple.temple.Temple", "aoe1_base.data.game_entity.generic.academy.academy.Academy", }; +static const std::vector de1_test_entities = { + "de1_base.data.game_entity.generic.chariot_archer.chariot_archer.ChariotArcher", + "de1_base.data.game_entity.generic.bowman.bowman.Bowman", + "de1_base.data.game_entity.generic.hoplite.hoplite.Hoplite", + "de1_base.data.game_entity.generic.temple.temple.Temple", + "de1_base.data.game_entity.generic.academy.academy.Academy", +}; static const std::vector aoe2_test_entities = { "aoe2_base.data.game_entity.generic.knight.knight.Knight", "aoe2_base.data.game_entity.generic.monk.monk.Monk", @@ -40,6 +47,20 @@ static const std::vector aoe2_test_entities = { "aoe2_base.data.game_entity.generic.castle.castle.Castle", "aoe2_base.data.game_entity.generic.barracks.barracks.Barracks", }; +static const std::vector de2_test_entities = { + "de2_base.data.game_entity.generic.knight.knight.Knight", + "de2_base.data.game_entity.generic.monk.monk.Monk", + "de2_base.data.game_entity.generic.archer.archer.Archer", + "de2_base.data.game_entity.generic.castle.castle.Castle", + "de2_base.data.game_entity.generic.barracks.barracks.Barracks", +}; +static const std::vector hd_test_entities = { + "hd_base.data.game_entity.generic.knight.knight.Knight", + "hd_base.data.game_entity.generic.monk.monk.Monk", + "hd_base.data.game_entity.generic.archer.archer.Archer", + "hd_base.data.game_entity.generic.castle.castle.Castle", + "hd_base.data.game_entity.generic.barracks.barracks.Barracks", +}; static const std::vector swgb_test_entities = { "swgb_base.data.game_entity.generic.trooper.trooper.Trooper", "swgb_base.data.game_entity.generic.force_knight.force_knight.ForceKnight", @@ -47,6 +68,14 @@ static const std::vector swgb_test_entities = { "swgb_base.data.game_entity.generic.command_center.command_center.CommandCenter", "swgb_base.data.game_entity.generic.heavy_weapons_factory.heavy_weapons_factory.HeavyWeaponsFactory", }; +static const std::vector trial_test_entities = { + "trial_base.data.game_entity.generic.eagle_warrior.eagle_warrior.EagleWarrior", + "trial_base.data.game_entity.generic.jaguar_warrior.jaguar_warrior.JaguarWarrior", + "trial_base.data.game_entity.generic.plumed_archer.plumed_archer.PlumedArcher", + "trial_base.data.game_entity.generic.jungle_tree.jungle_tree.JungleTree", + "trial_base.data.game_entity.generic.barracks.barracks.Barracks", +}; + Spawner::Spawner(const std::shared_ptr &loop) : EventEntity(loop) { @@ -80,7 +109,7 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, const param_map ¶ms) { auto gstate = std::dynamic_pointer_cast(state); - auto nyan_db = gstate->get_nyan_db(); + auto nyan_db = gstate->get_db_view(); auto game_entities = nyan_db->get_obj_children_all("engine.util.game_entity.GameEntity"); @@ -94,18 +123,42 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, aoe1_test_entities.begin(), aoe1_test_entities.end()); } + else if (modpack_id == "de1_base") { + test_entities.insert(test_entities.end(), + de1_test_entities.begin(), + de1_test_entities.end()); + } else if (modpack_id == "aoe2_base") { test_entities.insert(test_entities.end(), aoe2_test_entities.begin(), aoe2_test_entities.end()); } + else if (modpack_id == "de2_base") { + test_entities.insert(test_entities.end(), + de2_test_entities.begin(), + de2_test_entities.end()); + } + else if (modpack_id == "hd_base") { + test_entities.insert(test_entities.end(), + hd_test_entities.begin(), + hd_test_entities.end()); + } else if (modpack_id == "swgb_base") { test_entities.insert(test_entities.end(), swgb_test_entities.begin(), swgb_test_entities.end()); } + else if (modpack_id == "trial_base") { + test_entities.insert(test_entities.end(), + trial_test_entities.begin(), + trial_test_entities.end()); + } } } + if (test_entities.empty()) { + // Do nothing because we don't have anything to spawn + return; + } static uint8_t index = 0; nyan::fqon_t nyan_entity = test_entities.at(index); @@ -115,7 +168,8 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, } // Create entity - auto entity = this->factory->add_game_entity(this->loop, gstate, nyan_entity); + player_id_t owner_id = params.get("owner", 0); + auto entity = this->factory->add_game_entity(this->loop, gstate, owner_id, nyan_entity); // Setup components auto entity_pos = std::dynamic_pointer_cast( @@ -127,7 +181,7 @@ void SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */, auto entity_owner = std::dynamic_pointer_cast( entity->get_component(component::component_t::OWNERSHIP)); - entity_owner->set_owner(time, params.get("owner", 0)); + entity_owner->set_owner(time, owner_id); auto activity = std::dynamic_pointer_cast( entity->get_component(component::component_t::ACTIVITY)); diff --git a/libopenage/gamestate/game.cpp b/libopenage/gamestate/game.cpp index b496ae7e1e..90dfe5ede3 100644 --- a/libopenage/gamestate/game.cpp +++ b/libopenage/gamestate/game.cpp @@ -11,6 +11,7 @@ #include "assets/mod_manager.h" #include "assets/modpack.h" +#include "gamestate/entity_factory.h" #include "gamestate/game_state.h" #include "gamestate/universe.h" #include "util/path.h" @@ -20,12 +21,19 @@ namespace openage::gamestate { Game::Game(const std::shared_ptr &event_loop, - const std::shared_ptr &mod_manager) : + const std::shared_ptr &mod_manager, + const std::shared_ptr &entity_factory) : db{nyan::Database::create()}, state{std::make_shared(this->db, event_loop)}, universe{std::make_shared(state)} { this->load_data(mod_manager); + // TODO: Testing player creation + auto player1 = entity_factory->add_player(event_loop, state, ""); + auto player2 = entity_factory->add_player(event_loop, state, ""); + state->add_player(player1); + state->add_player(player2); + // TODO: This lets the spawner event check which modpacks are loaded, // so that it can decide which entities it can spawn. // This can be removed when we spawn based on game logic rather than diff --git a/libopenage/gamestate/game.h b/libopenage/gamestate/game.h index 9c9eb1def4..aecfd3bd72 100644 --- a/libopenage/gamestate/game.h +++ b/libopenage/gamestate/game.h @@ -29,6 +29,7 @@ class Path; namespace gamestate { class GameState; +class EntityFactory; class Universe; /** @@ -46,12 +47,13 @@ class Game { /** * Create a new game. * - * @param root_dir openage root directory. * @param event_loop Event simulation loop for the gamestate. * @param mod_manager Mod manager. + * @param entity_factory Factory for creating entities. Used for creating the players. */ Game(const std::shared_ptr &event_loop, - const std::shared_ptr &mod_manager); + const std::shared_ptr &mod_manager, + const std::shared_ptr &entity_factory); ~Game() = default; /** diff --git a/libopenage/gamestate/game_state.cpp b/libopenage/gamestate/game_state.cpp index 759b638840..9cda363379 100644 --- a/libopenage/gamestate/game_state.cpp +++ b/libopenage/gamestate/game_state.cpp @@ -8,6 +8,7 @@ #include "log/log.h" #include "gamestate/game_entity.h" +#include "gamestate/player.h" namespace openage::gamestate { @@ -18,7 +19,7 @@ GameState::GameState(const std::shared_ptr &db, db_view{db->new_view()} { } -const std::shared_ptr &GameState::get_nyan_db() { +const std::shared_ptr &GameState::get_db_view() { return this->db_view; } @@ -29,6 +30,13 @@ void GameState::add_game_entity(const std::shared_ptr &entity) { this->game_entities[entity->get_id()] = entity; } +void GameState::add_player(const std::shared_ptr &player) { + if (this->players.contains(player->get_id())) [[unlikely]] { + throw Error(MSG(err) << "Player with ID " << player->get_id() << " already exists"); + } + this->players[player->get_id()] = player; +} + const std::shared_ptr &GameState::get_game_entity(entity_id_t id) const { if (!this->game_entities.contains(id)) [[unlikely]] { throw Error(MSG(err) << "Game entity with ID " << id << " does not exist"); @@ -40,6 +48,13 @@ const std::unordered_map> &GameState::g return this->game_entities; } +const std::shared_ptr &GameState::get_player(player_id_t id) const { + if (!this->players.contains(id)) [[unlikely]] { + throw Error(MSG(err) << "Player with ID " << id << " does not exist"); + } + return this->players.at(id); +} + const std::shared_ptr &GameState::get_mod_manager() const { return this->mod_manager; } diff --git a/libopenage/gamestate/game_state.h b/libopenage/gamestate/game_state.h index c091303548..d252acaadc 100644 --- a/libopenage/gamestate/game_state.h +++ b/libopenage/gamestate/game_state.h @@ -26,6 +26,7 @@ class EventLoop; namespace gamestate { class GameEntity; +class Player; /** * State of the game. @@ -45,11 +46,13 @@ class GameState : public openage::event::State { const std::shared_ptr &event_loop); /** - * Get the nyan database view of this state. - * - * @return nyan database view. - */ - const std::shared_ptr &get_nyan_db(); + * Get the nyan database view for the whole game. + * + * Players have individual views for their own data. + * + * @return nyan database view. + */ + const std::shared_ptr &get_db_view(); /** * Add a new game entity to the index. @@ -58,10 +61,18 @@ class GameState : public openage::event::State { */ void add_game_entity(const std::shared_ptr &entity); + /** + * Add a new player to the index. + * + * @param player New player. + */ + void add_player(const std::shared_ptr &player); + /** * Get a game entity by its ID. * * @param id ID of the game entity. + * * @return Game entity with the given ID. */ const std::shared_ptr &get_game_entity(entity_id_t id) const; @@ -74,8 +85,17 @@ class GameState : public openage::event::State { const std::unordered_map> &get_game_entities() const; /** - * TODO: Only for testing. - */ + * Get a player by its ID. + * + * @param id ID of the player. + * + * @return Player with the given ID. + */ + const std::shared_ptr &get_player(player_id_t id) const; + + /** + * TODO: Only for testing. + */ const std::shared_ptr &get_mod_manager() const; void set_mod_manager(const std::shared_ptr &mod_manager); @@ -90,6 +110,11 @@ class GameState : public openage::event::State { */ std::unordered_map> game_entities; + /** + * Map of all players in the current game by their ID. + */ + std::unordered_map> players; + /** * TODO: Only for testing */ diff --git a/libopenage/gamestate/player.cpp b/libopenage/gamestate/player.cpp index ab8d0d3d52..e57c4d8a35 100644 --- a/libopenage/gamestate/player.cpp +++ b/libopenage/gamestate/player.cpp @@ -1,8 +1,24 @@ -// Copyright 2018-2021 the openage authors. See copying.md for legal info. +// Copyright 2018-2023 the openage authors. See copying.md for legal info. #include "player.h" +#include "nyan/nyan.h" + + namespace openage::gamestate { +Player::Player(player_id_t id, + const std::shared_ptr &db_view) : + id{id}, + db_view{db_view} { +} + +player_id_t Player::get_id() const { + return this->id; +} + +const std::shared_ptr &Player::get_db_view() const { + return this->db_view; +} -} // openage::gamestate +} // namespace openage::gamestate diff --git a/libopenage/gamestate/player.h b/libopenage/gamestate/player.h index b5ad5d0bcd..db6c53b128 100644 --- a/libopenage/gamestate/player.h +++ b/libopenage/gamestate/player.h @@ -2,6 +2,14 @@ #pragma once +#include + +#include "gamestate/types.h" + + +namespace nyan { +class View; +} // namespace nyan namespace openage::gamestate { @@ -10,8 +18,48 @@ namespace openage::gamestate { */ class Player { public: - Player() = default; + /** + * Create a new player with the given id. + * + * @param id Unique player ID. + * @param db_view View of the nyan database used for the player. + */ + Player(player_id_t id, + const std::shared_ptr &db_view); + + // players can't be copied to prevent duplicate IDs + Player(const Player &) = delete; + Player(Player &&) = default; + + Player &operator=(const Player &) = delete; + Player &operator=(Player &&) = default; + ~Player() = default; + + /** + * Get the unique ID of the player. + * + * @return Unique player ID. + */ + player_id_t get_id() const; + + /** + * Get the nyan game database view for the player. + * + * @return nyan database view. + */ + const std::shared_ptr &get_db_view() const; + +private: + /** + * Player ID. Must be unique. + */ + const player_id_t id; + + /** + * Player view of the nyan game data database. + */ + std::shared_ptr db_view; }; } // namespace openage::gamestate diff --git a/libopenage/gamestate/simulation.cpp b/libopenage/gamestate/simulation.cpp index 1c045548bc..c925888815 100644 --- a/libopenage/gamestate/simulation.cpp +++ b/libopenage/gamestate/simulation.cpp @@ -54,7 +54,9 @@ void GameSimulation::start() { this->init_event_handlers(); - this->game = std::make_shared(event_loop, this->mod_manager); + this->game = std::make_shared(event_loop, + this->mod_manager, + this->entity_factory); this->running = true; diff --git a/libopenage/gamestate/types.h b/libopenage/gamestate/types.h index 7b90547de6..ad7423853b 100644 --- a/libopenage/gamestate/types.h +++ b/libopenage/gamestate/types.h @@ -11,4 +11,9 @@ namespace openage::gamestate { */ using entity_id_t = uint64_t; +/** + * Player IDs. + */ +using player_id_t = uint64_t; + } // namespace openage::gamestate diff --git a/libopenage/main.cpp b/libopenage/main.cpp index 4dcea511ad..e7794be40c 100644 --- a/libopenage/main.cpp +++ b/libopenage/main.cpp @@ -16,7 +16,7 @@ namespace openage { * This is the main entry point to the C++ part. */ int run_game(const main_arguments &args) { - // TODO: store args.fps_limit and args.gl_debug as default in the cvar system. + // TODO: store args.gl_debug as default in the cvar system. util::Timer timer; timer.start(); @@ -31,7 +31,7 @@ int run_game(const main_arguments &args) { run_mode = openage::engine::Engine::mode::HEADLESS; } - openage::engine::Engine engine{run_mode, args.root_path, args.mods}; + openage::engine::Engine engine{run_mode, args.root_path, args.mods, args.gl_debug}; engine.loop(); diff --git a/libopenage/main.h b/libopenage/main.h index 36d7cae692..0e60c386cc 100644 --- a/libopenage/main.h +++ b/libopenage/main.h @@ -6,8 +6,6 @@ // pxd: from libcpp.string cimport string // pxd: from libcpp.vector cimport vector #include -// pxd: from libc.stdint cimport int32_t -#include // pxd: from libopenage.util.path cimport Path #include "util/path.h" @@ -25,14 +23,12 @@ namespace openage { * * cppclass main_arguments: * Path root_path - * int32_t fps_limit * bool gl_debug * bool headless * vector[string] mods */ struct main_arguments { util::Path root_path; - int32_t fps_limit; bool gl_debug; bool headless; std::vector mods; diff --git a/libopenage/main/CMakeLists.txt b/libopenage/main/CMakeLists.txt index 0883a024ae..d470f86b45 100644 --- a/libopenage/main/CMakeLists.txt +++ b/libopenage/main/CMakeLists.txt @@ -1,5 +1,3 @@ -add_subdirectory("tests") - add_sources(libopenage tests.cpp ) @@ -7,3 +5,5 @@ add_sources(libopenage pxdgen( tests.h ) + +add_subdirectory("demo") diff --git a/libopenage/main/demo/CMakeLists.txt b/libopenage/main/demo/CMakeLists.txt new file mode 100644 index 0000000000..1a9db5d625 --- /dev/null +++ b/libopenage/main/demo/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory("pong") +add_subdirectory("presenter") +add_subdirectory("interactive") diff --git a/libopenage/main/demo/interactive/CMakeLists.txt b/libopenage/main/demo/interactive/CMakeLists.txt new file mode 100644 index 0000000000..7b279b45ee --- /dev/null +++ b/libopenage/main/demo/interactive/CMakeLists.txt @@ -0,0 +1,3 @@ +add_sources(libopenage + interactive.cpp +) diff --git a/libopenage/main/demo/interactive/interactive.cpp b/libopenage/main/demo/interactive/interactive.cpp new file mode 100644 index 0000000000..69a8d83948 --- /dev/null +++ b/libopenage/main/demo/interactive/interactive.cpp @@ -0,0 +1,63 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "interactive.h" + +#include "log/log.h" + +#include "assets/mod_manager.h" +#include "engine/engine.h" + + +namespace openage::main::tests::interactive { + +void demo(const util::Path &path) { + log::log(INFO << "Launching interactive demo..."); + + // check for available modpacks + auto modpack_folder = path / "assets" / "converted"; + auto mods = assets::ModManager::enumerate_modpacks(modpack_folder); + + // check if the engine modpack exists + auto mod_engine = std::find_if(mods.begin(), mods.end(), [](const assets::ModpackInfo &mod) { + return mod.id == "engine"; + }); + if (mod_engine == std::end(mods)) { + throw Error{MSG(err) << "Modpack 'engine' not found.\n" + << "You need to export it first by running " + << "'python -m openage convert-export-api'"}; + } + else { + // remove the engine modpack from the mod selection + // because it's not playable + mods.erase(mod_engine); + } + + // check if there are is at least one playable modpack + if (mods.empty()) { + throw Error{MSG(err) << "No playable modpacks found.\n" + << "Try exporting some with " + << "'python -m openage convert'"}; + } + + // select one of the modpacks + std::cout << "Choose modpack to use:" << std::endl; + for (size_t i = 0; i < mods.size(); ++i) { + std::cout << "(" << i << ")" + << " " << mods[i].id + << std::endl; + } + std::cout << "> "; + size_t mod_idx; + std::cin >> mod_idx; + + // start the engine with the selected modpack + auto engine = engine::Engine(engine::Engine::mode::FULL, + path, + {mods[mod_idx].id}); + + // engine main thread has to loop, otherwise RAII frees up + // the members of engine and we get allocation errors + engine.loop(); +} + +} // namespace openage::main::tests::interactive diff --git a/libopenage/main/demo/interactive/interactive.h b/libopenage/main/demo/interactive/interactive.h new file mode 100644 index 0000000000..6b51d591c2 --- /dev/null +++ b/libopenage/main/demo/interactive/interactive.h @@ -0,0 +1,12 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + + +namespace openage::main::tests::interactive { + +void demo(const util::Path &path); + +} // namespace openage::main::tests::interactive diff --git a/libopenage/main/tests/CMakeLists.txt b/libopenage/main/demo/pong/CMakeLists.txt similarity index 85% rename from libopenage/main/tests/CMakeLists.txt rename to libopenage/main/demo/pong/CMakeLists.txt index 9e79e90cc6..aeeaaf48d0 100644 --- a/libopenage/main/tests/CMakeLists.txt +++ b/libopenage/main/demo/pong/CMakeLists.txt @@ -3,6 +3,5 @@ add_sources(libopenage gamestate.cpp gui.cpp physics.cpp - presenter.cpp pong.cpp ) diff --git a/libopenage/main/tests/aicontroller.cpp b/libopenage/main/demo/pong/aicontroller.cpp similarity index 93% rename from libopenage/main/tests/aicontroller.cpp rename to libopenage/main/demo/pong/aicontroller.cpp index 63bd369267..bdd1b7df3a 100644 --- a/libopenage/main/tests/aicontroller.cpp +++ b/libopenage/main/demo/pong/aicontroller.cpp @@ -35,17 +35,21 @@ std::vector get_ai_inputs(const std::shared_ptr &player, if (speed[0] == 0) { tx_hit = std::numeric_limits::max(); - } else if (speed[0] > 0) { + } + else if (speed[0] > 0) { tx_hit = time::time_t::from_double((area_width - ball_pos[0]) / speed[0]); - } else if (speed[0] < 0) { + } + else if (speed[0] < 0) { tx_hit = time::time_t::from_double(ball_pos[0] / -speed[0]); } if (speed[1] == 0) { ty_hit = std::numeric_limits::max(); - } else if (speed[1] > 0) { + } + else if (speed[1] > 0) { ty_hit = time::time_t::from_double((area_height - ball_pos[1]) / speed[1]); - } else if (speed[1] < 0) { + } + else if (speed[1] < 0) { ty_hit = time::time_t::from_double(ball_pos[1] / -speed[1]); } @@ -90,4 +94,4 @@ std::vector get_ai_inputs(const std::shared_ptr &player, return ret; } -} // openage::main::tests::pong +} // namespace openage::main::tests::pong diff --git a/libopenage/main/tests/aicontroller.h b/libopenage/main/demo/pong/aicontroller.h similarity index 91% rename from libopenage/main/tests/aicontroller.h rename to libopenage/main/demo/pong/aicontroller.h index 5066a75f3f..9bdfd2502e 100644 --- a/libopenage/main/tests/aicontroller.h +++ b/libopenage/main/demo/pong/aicontroller.h @@ -2,7 +2,7 @@ #pragma once -#include "gamestate.h" +#include "main/demo/pong/gamestate.h" namespace openage::main::tests::pong { diff --git a/libopenage/main/tests/gamestate.cpp b/libopenage/main/demo/pong/gamestate.cpp similarity index 99% rename from libopenage/main/tests/gamestate.cpp rename to libopenage/main/demo/pong/gamestate.cpp index 2c2257677b..d789071ae6 100644 --- a/libopenage/main/tests/gamestate.cpp +++ b/libopenage/main/demo/pong/gamestate.cpp @@ -4,7 +4,7 @@ #include -#include "gui.h" +#include "main/demo/pong/gui.h" #include "log/log.h" #include "util/strings.h" diff --git a/libopenage/main/tests/gamestate.h b/libopenage/main/demo/pong/gamestate.h similarity index 100% rename from libopenage/main/tests/gamestate.h rename to libopenage/main/demo/pong/gamestate.h diff --git a/libopenage/main/tests/gui.cpp b/libopenage/main/demo/pong/gui.cpp similarity index 97% rename from libopenage/main/tests/gui.cpp rename to libopenage/main/demo/pong/gui.cpp index 2ef0897e2a..9800357ec0 100644 --- a/libopenage/main/tests/gui.cpp +++ b/libopenage/main/demo/pong/gui.cpp @@ -6,7 +6,7 @@ #include #include -#include "gamestate.h" +#include "main/demo/pong/gamestate.h" #include "log/log.h" #include "renderer/geometry.h" #include "renderer/opengl/context.h" @@ -33,7 +33,7 @@ const std::vector &Gui::get_inputs(const std::shared_ptr std::vector inputs; /* - for (all inputs from SDL) { + for (all inputs from window) { add key to inputs vector; } */ @@ -165,10 +165,6 @@ void main() { this->p2paddle}, this->renderer->get_display_target()); - glDepthFunc(GL_LEQUAL); - glDepthRange(0.0, 1.0); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - this->window.add_resize_callback( [=, this](size_t w, size_t h, double scale) { Eigen::Matrix4f proj_matrix = renderer::util::ortho_matrix_f( diff --git a/libopenage/main/tests/gui.h b/libopenage/main/demo/pong/gui.h similarity index 83% rename from libopenage/main/tests/gui.h rename to libopenage/main/demo/pong/gui.h index b817d55a27..b1b352fb21 100644 --- a/libopenage/main/tests/gui.h +++ b/libopenage/main/demo/pong/gui.h @@ -3,12 +3,12 @@ #pragma once -#include #include +#include -#include "../../time/time.h" -#include "../../renderer/renderer.h" -#include "../../renderer/opengl/window.h" +#include "renderer/opengl/window.h" +#include "renderer/renderer.h" +#include "time/time.h" namespace openage::main::tests::pong { @@ -30,7 +30,7 @@ class Gui { void log(const std::string &msg); - void add_resize_callback(const renderer::Window::resize_cb_t&); + void add_resize_callback(const renderer::Window::resize_cb_t &); void clear_resize_callbacks(); void update(); @@ -55,4 +55,4 @@ class Gui { std::vector resize_callbacks; }; -} // openage::main::tests::pong +} // namespace openage::main::tests::pong diff --git a/libopenage/main/tests/physics.cpp b/libopenage/main/demo/pong/physics.cpp similarity index 83% rename from libopenage/main/tests/physics.cpp rename to libopenage/main/demo/pong/physics.cpp index c37f9ae122..72a1f65c96 100644 --- a/libopenage/main/tests/physics.cpp +++ b/libopenage/main/demo/pong/physics.cpp @@ -4,10 +4,10 @@ #include -#include "gui.h" -#include "../../error/error.h" -#include "../../rng/global_rng.h" -#include "../../util/stringformatter.h" +#include "error/error.h" +#include "main/demo/pong/gui.h" +#include "rng/global_rng.h" +#include "util/stringformatter.h" namespace openage::main::tests::pong { @@ -15,11 +15,11 @@ namespace openage::main::tests::pong { class BallReflectWall : public event::DependencyEventHandler { public: - BallReflectWall() : event::DependencyEventHandler("demo.ball.reflect_wall") {} + BallReflectWall() : + event::DependencyEventHandler("demo.ball.reflect_wall") {} void setup_event(const std::shared_ptr &evnt, const std::shared_ptr &gstate) override { - auto state = std::dynamic_pointer_cast(gstate); // FIXME dependency to a full ball object so that any curve @@ -33,8 +33,7 @@ class BallReflectWall : public event::DependencyEventHandler { const std::shared_ptr &target, const std::shared_ptr &gstate, const time::time_t &now, - const event::EventHandler::param_map &/*param*/) override { - + const event::EventHandler::param_map & /*param*/) override { auto positioncurve = std::dynamic_pointer_cast>(target); auto state = std::dynamic_pointer_cast(gstate); auto speedcurve = state->ball->speed; @@ -55,22 +54,19 @@ class BallReflectWall : public event::DependencyEventHandler { time::time_t ty = 0; if (speed[1] > 0) { ty = time::time_t::from_double( - (screen_size[1] - pos[1]) / speed[1] - ); + (screen_size[1] - pos[1]) / speed[1]); } else if (speed[1] < 0) { ty = time::time_t::from_double( - pos[1] / -speed[1] - ); + pos[1] / -speed[1]); } state->ball->position->set_last(now + ty, pos + (speed * ty.to_double())); } time::time_t predict_invoke_time(const std::shared_ptr &target, - const std::shared_ptr &gstate, - const time::time_t &now) override { - + const std::shared_ptr &gstate, + const time::time_t &now) override { auto positioncurve = std::dynamic_pointer_cast>(target); auto state = std::dynamic_pointer_cast(gstate); @@ -92,7 +88,9 @@ class BallReflectWall : public event::DependencyEventHandler { util::FString str; str.fmt("WALL TY %f NOW %f, NOWTY %f ", - ty.to_double(), now.to_double(), (now + ty).to_double()); + ty.to_double(), + now.to_double(), + (now + ty).to_double()); state->gui->log(str); return now + ty; @@ -102,13 +100,11 @@ class BallReflectWall : public event::DependencyEventHandler { class BallReflectPanel : public event::DependencyEventHandler { public: - BallReflectPanel () - : + BallReflectPanel() : event::DependencyEventHandler("demo.ball.reflect_panel") {} void setup_event(const std::shared_ptr &evnt, const std::shared_ptr &gstate) override { - auto state = std::dynamic_pointer_cast(gstate); evnt->depend_on(state->ball); @@ -118,11 +114,10 @@ class BallReflectPanel : public event::DependencyEventHandler { } void invoke(event::EventLoop &mgr, - const std::shared_ptr &/*target*/, + const std::shared_ptr & /*target*/, const std::shared_ptr &gstate, const time::time_t &now, - const event::EventHandler::param_map &/*param*/) override { - + const event::EventHandler::param_map & /*param*/) override { auto state = std::dynamic_pointer_cast(gstate); auto pos = state->ball->position->get(now); @@ -134,11 +129,7 @@ class BallReflectPanel : public event::DependencyEventHandler { str.fmt("Panel hit [%i]", ++cnt); state->gui->log(str); - if (pos[0] <= 1 and - speed[0] < 0 and - (pos[1] < state->p1->position->get(now) - state->p1->size->get(now) / 2 or - pos[1] > state->p1->position->get(now) + state->p1->size->get(now) / 2)) { - + if (pos[0] <= 1 and speed[0] < 0 and (pos[1] < state->p1->position->get(now) - state->p1->size->get(now) / 2 or pos[1] > state->p1->position->get(now) + state->p1->size->get(now) / 2)) { // ball missed the paddle of player 1 auto l = state->p1->lives->get(now); l--; @@ -148,11 +139,7 @@ class BallReflectPanel : public event::DependencyEventHandler { Physics::reset(state, mgr, now); } - else if (pos[0] >= screen_size[0] - 1 and - speed[0] > 0 and - (pos[1] < state->p2->position->get(now) - state->p2->size->get(now) / 2 or - pos[1] > state->p2->position->get(now) + state->p2->size->get(now) / 2)) { - + else if (pos[0] >= screen_size[0] - 1 and speed[0] > 0 and (pos[1] < state->p2->position->get(now) - state->p2->size->get(now) / 2 or pos[1] > state->p2->position->get(now) + state->p2->size->get(now) / 2)) { // ball missed the paddel of player 2 auto l = state->p2->lives->get(now); l--; @@ -161,7 +148,7 @@ class BallReflectPanel : public event::DependencyEventHandler { Physics::reset(state, mgr, now); } - else if (pos[0] >= screen_size[0]- 1 || pos[0] <= 1) { + else if (pos[0] >= screen_size[0] - 1 || pos[0] <= 1) { speed[0] *= -1; state->ball->speed->set_last(now, speed); state->ball->position->set_last(now, pos); @@ -190,7 +177,8 @@ class BallReflectPanel : public event::DependencyEventHandler { if (speed[0] > 0) { tx.add(state->dbroot->get_object("pong.RightColor")); - } else { + } + else { tx.add(state->dbroot->get_object("pong.LeftColor")); } @@ -201,9 +189,8 @@ class BallReflectPanel : public event::DependencyEventHandler { } time::time_t predict_invoke_time(const std::shared_ptr &target, - const std::shared_ptr &gstate, - const time::time_t &now) override { - + const std::shared_ptr &gstate, + const time::time_t &now) override { auto positioncurve = std::dynamic_pointer_cast>(target); auto state = std::dynamic_pointer_cast(gstate); @@ -246,19 +233,17 @@ class BallReflectPanel : public event::DependencyEventHandler { class ResetGame : public event::OnceEventHandler { public: - ResetGame () - : + ResetGame() : event::OnceEventHandler("demo.reset") {} - void setup_event(const std::shared_ptr &/*target*/, - const std::shared_ptr &/*state*/) override {} + void setup_event(const std::shared_ptr & /*target*/, + const std::shared_ptr & /*state*/) override {} - void invoke(event::EventLoop &/*mgr*/, - const std::shared_ptr &/*target*/, + void invoke(event::EventLoop & /*mgr*/, + const std::shared_ptr & /*target*/, const std::shared_ptr &gstate, const time::time_t &now, - const event::EventHandler::param_map &/*param*/) override { - + const event::EventHandler::param_map & /*param*/) override { auto state = std::dynamic_pointer_cast(gstate); auto screen_size = state->area_size->get(now); @@ -285,9 +270,8 @@ class ResetGame : public event::OnceEventHandler { float dirx = (rng::random() % 2) ? 1 : -1; float diry = (rng::random() % 2) ? 1 : -1; auto init_speed = util::Vector2d( - dirx * (screen_size[0] / 8.0 + (rng::random() % (screen_size[0]/10))), - diry * (screen_size[1] / 10.0 + (rng::random() % (screen_size[1]/10))) - ); + dirx * (screen_size[0] / 8.0 + (rng::random() % (screen_size[0] / 10))), + diry * (screen_size[1] / 10.0 + (rng::random() % (screen_size[1] / 10)))); state->ball->speed->set_last(now, init_speed); auto pos = state->ball->position->get(now); @@ -328,9 +312,9 @@ class ResetGame : public event::OnceEventHandler { state->ball->position->set_last(now + ty, pos + init_speed * ty.to_double()); } - time::time_t predict_invoke_time(const std::shared_ptr &/*target*/, - const std::shared_ptr &/*state*/, - const time::time_t &old_time) override { + time::time_t predict_invoke_time(const std::shared_ptr & /*target*/, + const std::shared_ptr & /*state*/, + const time::time_t &old_time) override { return old_time; } }; @@ -339,7 +323,6 @@ class ResetGame : public event::OnceEventHandler { void Physics::init(const std::shared_ptr &state, const std::shared_ptr &loop, const time::time_t &now) { - loop->add_event_handler(std::make_shared()); loop->add_event_handler(std::make_shared()); loop->add_event_handler(std::make_shared()); @@ -355,7 +338,6 @@ void Physics::init(const std::shared_ptr &state, void Physics::reset(const std::shared_ptr &gstate, event::EventLoop &mgr, const time::time_t &now) { - auto state = std::dynamic_pointer_cast(gstate); mgr.create_event("demo.reset", state->ball->position, state, now); } @@ -366,18 +348,15 @@ void Physics::process_input(const std::shared_ptr &state, const std::vector &events, const std::shared_ptr &mgr, const time::time_t &now) { - // seconds into the future constexpr static auto predicted_movement_time = time::time_t::from_double(5.0); // pixels per second for paddle movement constexpr static double movement_speed = 350.0; - for (auto& evnt : events) { - + for (auto &evnt : events) { // Process only if the future has changed if (player->state->get(now).state != evnt.state) { - // log the new input in the state curve player->state->set_last(now, evnt); @@ -391,7 +370,7 @@ void Physics::process_input(const std::shared_ptr &state, extend_previous_prediction = true; } - switch(evnt.state) { + switch (evnt.state) { case PongEvent::UP: case PongEvent::DOWN: { float current_pos = player->position->get(now); @@ -419,10 +398,7 @@ void Physics::process_input(const std::shared_ptr &state, // change the position by integrating the speed curve. // TODO: add native integral to curves. - float new_pos = current_pos + - (((player->speed->get(move_stop_guess) + - player->speed->get(now)) / 2.0) * - predicted_movement_time.to_double()); + float new_pos = current_pos + (((player->speed->get(move_stop_guess) + player->speed->get(now)) / 2.0) * predicted_movement_time.to_double()); // if paddle will get out-of-bounds, calculate the bound hit time @@ -462,4 +438,4 @@ void Physics::process_input(const std::shared_ptr &state, } } -} // openage::main::tests::pong +} // namespace openage::main::tests::pong diff --git a/libopenage/main/tests/physics.h b/libopenage/main/demo/pong/physics.h similarity index 85% rename from libopenage/main/tests/physics.h rename to libopenage/main/demo/pong/physics.h index b29f643376..5db448b76f 100644 --- a/libopenage/main/tests/physics.h +++ b/libopenage/main/demo/pong/physics.h @@ -2,8 +2,8 @@ #pragma once -#include "gamestate.h" -#include "../../time/time.h" +#include "main/demo/pong/gamestate.h" +#include "time/time.h" #include @@ -12,7 +12,7 @@ namespace openage::event { class EventLoop; -} // openage::event +} // namespace openage::event namespace openage::main::tests::pong { @@ -33,4 +33,4 @@ class Physics { const time::time_t &); }; -} // openage::main::tests::pong +} // namespace openage::main::tests::pong diff --git a/libopenage/main/tests/pong.cpp b/libopenage/main/demo/pong/pong.cpp similarity index 92% rename from libopenage/main/tests/pong.cpp rename to libopenage/main/demo/pong/pong.cpp index 9cbc163fdb..724c7267a2 100644 --- a/libopenage/main/tests/pong.cpp +++ b/libopenage/main/demo/pong/pong.cpp @@ -1,21 +1,22 @@ // Copyright 2018-2023 the openage authors. See copying.md for legal info. +#include #include #include #include #include +#include #include -#include - #include -#include "aicontroller.h" #include "error/error.h" #include "event/event_loop.h" -#include "gui.h" #include "log/log.h" -#include "physics.h" +#include "main/demo/pong/aicontroller.h" +#include "main/demo/pong/gui.h" +#include "main/demo/pong/physics.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" #include "util/math_constants.h" #include "util/path.h" @@ -35,6 +36,7 @@ enum class timescale { }; void main(const util::Path &path) { + renderer::gui::GuiApplicationWithLogger gui_app{}; bool human_player = false; timescale speed = timescale::REALTIME; @@ -107,9 +109,10 @@ void main(const util::Path &path) { auto loop_start = Clock::now(); + gui_app.process_events(); + // process the input for both players // player 1 can be AI or human. - if (human_player) { phys.process_input(state, state->p1, inputs, loop, now); } @@ -150,7 +153,7 @@ void main(const util::Path &path) { if (speed == timescale::NOSLEEP) { // increase the simulation loop time a bit - SDL_Delay(5); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } dt_ms_t dt_us = Clock::now() - loop_start; @@ -159,7 +162,7 @@ void main(const util::Path &path) { dt_ms_t wait_time = per_frame - dt_us; if (wait_time > dt_ms_t::zero()) { - SDL_Delay(wait_time.count()); + std::this_thread::sleep_for(wait_time); } } diff --git a/libopenage/main/demo/pong/pong.h b/libopenage/main/demo/pong/pong.h new file mode 100644 index 0000000000..45bfbff095 --- /dev/null +++ b/libopenage/main/demo/pong/pong.h @@ -0,0 +1,16 @@ +// Copyright 2018-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + + +namespace openage::main::tests::pong { + +/** + * Run pong, the non-terminal variant. + */ +void main(const util::Path &path); + + +} // namespace openage::main::tests::pong diff --git a/libopenage/main/demo/presenter/CMakeLists.txt b/libopenage/main/demo/presenter/CMakeLists.txt new file mode 100644 index 0000000000..5a824fab5a --- /dev/null +++ b/libopenage/main/demo/presenter/CMakeLists.txt @@ -0,0 +1,3 @@ +add_sources(libopenage + presenter.cpp +) diff --git a/libopenage/main/tests/presenter.cpp b/libopenage/main/demo/presenter/presenter.cpp similarity index 75% rename from libopenage/main/tests/presenter.cpp rename to libopenage/main/demo/presenter/presenter.cpp index 364130a92d..c12cdb9ab9 100644 --- a/libopenage/main/tests/presenter.cpp +++ b/libopenage/main/demo/presenter/presenter.cpp @@ -4,6 +4,7 @@ #include "log/log.h" #include "presenter/presenter.h" +#include "time/time_loop.h" namespace openage::main::tests::presenter { @@ -11,7 +12,9 @@ namespace openage::main::tests::presenter { void demo(const util::Path &path) { log::log(INFO << "launching presenter test..."); + auto time_loop = std::make_shared(); openage::presenter::Presenter presenter{path}; + presenter.set_time_loop(time_loop); presenter.run(); } diff --git a/libopenage/main/tests/presenter.h b/libopenage/main/demo/presenter/presenter.h similarity index 88% rename from libopenage/main/tests/presenter.h rename to libopenage/main/demo/presenter/presenter.h index 8eded001db..6ff9c356d6 100644 --- a/libopenage/main/tests/presenter.h +++ b/libopenage/main/demo/presenter/presenter.h @@ -2,7 +2,7 @@ #pragma once -#include "../../util/path.h" +#include "util/path.h" namespace openage::main::tests::presenter { diff --git a/libopenage/main/tests.cpp b/libopenage/main/tests.cpp index 20b7369a52..aa0feb3c39 100644 --- a/libopenage/main/tests.cpp +++ b/libopenage/main/tests.cpp @@ -2,8 +2,9 @@ #include "tests.h" -#include "tests/pong.h" -#include "tests/presenter.h" +#include "demo/interactive/interactive.h" +#include "demo/pong/pong.h" +#include "demo/presenter/presenter.h" namespace openage::main::tests { @@ -11,14 +12,18 @@ namespace openage::main::tests { void engine_demo(int demo_id, const util::Path &path) { switch (demo_id) { - case 0: + case 0: { pong::main(path); - break; + } break; case 1: { presenter::demo(path); } break; + case 2: { + interactive::demo(path); + } break; + default: throw Error(ERR << "unknown engine demo " << demo_id << " requested."); } diff --git a/libopenage/main/tests/pong.h b/libopenage/main/tests/pong.h deleted file mode 100644 index 38d234ddee..0000000000 --- a/libopenage/main/tests/pong.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2018-2019 the openage authors. See copying.md for legal info. - -#pragma once - - -#include "../../util/path.h" - - -namespace openage::main::tests::pong { - - -/** - * Run pong, the non-terminal variant. - */ -void main(const util::Path& path); - - -} // openage::main::tests::pong diff --git a/libopenage/presenter/presenter.cpp b/libopenage/presenter/presenter.cpp index b1b08a2731..ab0f3b1f25 100644 --- a/libopenage/presenter/presenter.cpp +++ b/libopenage/presenter/presenter.cpp @@ -43,10 +43,10 @@ Presenter::Presenter(const util::Path &root_dir, time_loop{time_loop} {} -void Presenter::run() { - log::log(INFO << "presenter launching..."); +void Presenter::run(bool debug_graphics) { + log::log(INFO << "Presenter: Launching subsystems..."); - this->init_graphics(); + this->init_graphics(debug_graphics); this->init_input(); @@ -60,7 +60,8 @@ void Presenter::run() { this->window->update(); } - log::log(MSG(info) << "Draw loop exited"); + + log::log(MSG(info) << "Presenter: Draw loop exited"); if (this->simulation) { this->simulation->stop(); @@ -88,13 +89,20 @@ std::shared_ptr Presenter::init_window_system() { return std::make_shared(); } -void Presenter::init_graphics() { - log::log(INFO << "initializing graphics..."); +void Presenter::init_graphics(bool debug) { + log::log(INFO << "Presenter: Initializing graphics subsystems..."); this->gui_app = this->init_window_system(); - this->window = renderer::Window::create("openage presenter test", 1024, 768); + this->window = renderer::Window::create("openage presenter test", 1024, 768, debug); this->renderer = this->window->make_renderer(); + // Asset mangement + this->asset_manager = std::make_shared( + this->renderer, + this->root_dir / "assets" / "converted"); + auto missing_tex = this->root_dir / "assets" / "test" / "textures" / "test_missing.sprite"; + this->asset_manager->set_placeholder_animation(missing_tex); + // Camera this->camera = std::make_shared(this->renderer, this->window->get_size()); @@ -104,13 +112,6 @@ void Presenter::init_graphics() { this->camera_manager = std::make_shared(this->camera); - // Asset mangement - this->asset_manager = std::make_shared( - this->renderer, - this->root_dir / "assets" / "converted"); - auto missing_tex = this->root_dir / "assets" / "test" / "textures" / "test_missing.sprite"; - this->asset_manager->set_placeholder_animation(missing_tex); - // Skybox this->skybox_renderer = std::make_shared( this->window, @@ -147,9 +148,13 @@ void Presenter::init_graphics() { this->world_renderer); this->simulation->attach_renderer(render_factory); } + + log::log(INFO << "Presenter: Graphics subsystems initialized"); } void Presenter::init_gui() { + log::log(INFO << "Presenter: Initializing GUI with Qt backend"); + //// -- gui initialization // TODO: Do not use test GUI util::Path qml_root = this->root_dir / "assets" / "test" / "qml"; @@ -185,7 +190,7 @@ void Presenter::init_gui() { } void Presenter::init_input() { - log::log(INFO << "initializing inputs..."); + log::log(INFO << "Presenter: Initializing input subsystem..."); this->input_manager = std::make_shared(); @@ -205,6 +210,8 @@ void Presenter::init_input() { // setup simulation controls if (this->simulation) { + log::log(INFO << "Loading game simulation controls"); + // TODO: Remove hardcoding auto engine_controller = std::make_shared( std::unordered_set{0, 1, 2, 3}, 0); @@ -216,17 +223,21 @@ void Presenter::init_input() { // attach GUI if it's initialized if (this->gui) { + log::log(INFO << "Loading GUI controls"); this->input_manager->set_gui(this->gui->get_input_handler()); } // setup camera controls if (this->camera) { + log::log(INFO << "Loading camera controls"); auto camera_controller = std::make_shared(); auto camera_context = std::make_shared(); input::camera::setup_defaults(camera_context, this->camera, this->camera_manager); this->input_manager->set_camera_controller(camera_controller); input_ctx->set_camera_bindings(camera_context); } + + log::log(INFO << "Presenter: Input subsystem initialized"); } void Presenter::init_final_render_pass() { diff --git a/libopenage/presenter/presenter.h b/libopenage/presenter/presenter.h index 70ff733d43..6e7817ed2d 100644 --- a/libopenage/presenter/presenter.h +++ b/libopenage/presenter/presenter.h @@ -82,8 +82,10 @@ class Presenter { /** * Start the presenter and initialize subsystems. + * + * @param debug_graphics If true, enable OpenGL debug logging. */ - void run(); + void run(bool debug_graphics = false); /** * Set the game simulation controlled by this presenter. @@ -114,7 +116,7 @@ class Presenter { * - main renderer * - component renderers (Terrain, Game Entities, GUI) */ - void init_graphics(); + void init_graphics(bool debug = false); /** * Initialize the GUI. diff --git a/libopenage/renderer/CMakeLists.txt b/libopenage/renderer/CMakeLists.txt index a59fd57059..8a889de978 100644 --- a/libopenage/renderer/CMakeLists.txt +++ b/libopenage/renderer/CMakeLists.txt @@ -9,6 +9,7 @@ add_sources(libopenage text.cpp texture.cpp texture_array.cpp + types.cpp uniform_buffer.cpp uniform_input.cpp util.cpp diff --git a/libopenage/renderer/camera/camera.cpp b/libopenage/renderer/camera/camera.cpp index 7956e9fa70..604ddf440f 100644 --- a/libopenage/renderer/camera/camera.cpp +++ b/libopenage/renderer/camera/camera.cpp @@ -32,6 +32,11 @@ Camera::Camera(const std::shared_ptr &renderer, resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; auto ubo_info = resources::UniformBufferInfo{resources::ubo_layout_t::STD140, {view_input, proj_input}}; this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); + + log::log(INFO << "Created new camera at position " + << "(" << this->scene_pos[0] + << ", " << this->scene_pos[1] + << ", " << this->scene_pos[2] << ")"); } Camera::Camera(const std::shared_ptr &renderer, @@ -54,6 +59,11 @@ Camera::Camera(const std::shared_ptr &renderer, resources::UBOInput proj_input{"proj", resources::ubo_input_t::M4F32}; auto ubo_info = resources::UniformBufferInfo{resources::ubo_layout_t::STD140, {view_input, proj_input}}; this->uniform_buffer = renderer->add_uniform_buffer(ubo_info); + + log::log(INFO << "Created new camera at position " + << "(" << this->scene_pos[0] + << ", " << this->scene_pos[1] + << ", " << this->scene_pos[2] << ")"); } void Camera::look_at_scene(Eigen::Vector3f scene_pos) { diff --git a/libopenage/renderer/camera/camera.h b/libopenage/renderer/camera/camera.h index d82254cf5e..cb7b1868fb 100644 --- a/libopenage/renderer/camera/camera.h +++ b/libopenage/renderer/camera/camera.h @@ -55,14 +55,14 @@ class Camera { * @param scene_pos Position of the camera in the scene. * @param zoom Zoom level of the camera (defaults to 1.0f). * @param max_zoom_out Maximum zoom out level (defaults to 64.0f). - * @param default_zoom_ratio Default zoom level calibration (defaults to 1.0f). + * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)). */ Camera(const std::shared_ptr &renderer, util::Vector2s viewport_size, Eigen::Vector3f scene_pos, float zoom = 1.0f, float max_zoom_out = 64.0f, - float default_zoom_ratio = 1.0f); + float default_zoom_ratio = 1.0f / 49); ~Camera() = default; /** diff --git a/libopenage/renderer/demo/CMakeLists.txt b/libopenage/renderer/demo/CMakeLists.txt index 74e4ec228e..24a5c20d84 100644 --- a/libopenage/renderer/demo/CMakeLists.txt +++ b/libopenage/renderer/demo/CMakeLists.txt @@ -5,6 +5,7 @@ add_sources(libopenage demo_3.cpp demo_4.cpp demo_5.cpp + stresstest_0.cpp tests.cpp util.cpp ) diff --git a/libopenage/renderer/demo/demo_0.cpp b/libopenage/renderer/demo/demo_0.cpp index dbe78bcb6a..1db9061318 100644 --- a/libopenage/renderer/demo/demo_0.cpp +++ b/libopenage/renderer/demo/demo_0.cpp @@ -13,7 +13,7 @@ namespace openage::renderer::tests { void renderer_demo_0(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600); + opengl::GlWindow window("openage renderer test", 800, 600, true); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_1.cpp b/libopenage/renderer/demo/demo_1.cpp index a71bfce698..da6bad36d0 100644 --- a/libopenage/renderer/demo/demo_1.cpp +++ b/libopenage/renderer/demo/demo_1.cpp @@ -19,7 +19,7 @@ namespace openage::renderer::tests { void renderer_demo_1(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600); + opengl::GlWindow window("openage renderer test", 800, 600, true); auto renderer = window.make_renderer(); auto shaderdir = path / "assets" / "test" / "shaders"; diff --git a/libopenage/renderer/demo/demo_2.cpp b/libopenage/renderer/demo/demo_2.cpp index 66469555bf..931371c0c6 100644 --- a/libopenage/renderer/demo/demo_2.cpp +++ b/libopenage/renderer/demo/demo_2.cpp @@ -22,7 +22,7 @@ namespace openage::renderer::tests { void renderer_demo_2(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600); + opengl::GlWindow window("openage renderer test", 800, 600, true); auto renderer = window.make_renderer(); /* Load texture file standalone. */ diff --git a/libopenage/renderer/demo/demo_3.cpp b/libopenage/renderer/demo/demo_3.cpp index 449fc45769..6e0dc1f5ca 100644 --- a/libopenage/renderer/demo/demo_3.cpp +++ b/libopenage/renderer/demo/demo_3.cpp @@ -26,7 +26,7 @@ namespace openage::renderer::tests { void renderer_demo_3(const util::Path &path) { auto qtapp = std::make_shared(); - auto window = std::make_shared("openage renderer test", 800, 600); + auto window = std::make_shared("openage renderer test", 800, 600, true); auto renderer = window->make_renderer(); // Clock required by world renderer for timing animation frames diff --git a/libopenage/renderer/demo/demo_4.cpp b/libopenage/renderer/demo/demo_4.cpp index 95cc709ccb..82250cd1dc 100644 --- a/libopenage/renderer/demo/demo_4.cpp +++ b/libopenage/renderer/demo/demo_4.cpp @@ -20,7 +20,7 @@ namespace openage::renderer::tests { void renderer_demo_4(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600); + opengl::GlWindow window("openage renderer test", 800, 600, true); auto renderer = window.make_renderer(); /* Clock for timed display */ diff --git a/libopenage/renderer/demo/demo_5.cpp b/libopenage/renderer/demo/demo_5.cpp index d2ff2dc717..b8adc896e3 100644 --- a/libopenage/renderer/demo/demo_5.cpp +++ b/libopenage/renderer/demo/demo_5.cpp @@ -22,7 +22,7 @@ namespace openage::renderer::tests { void renderer_demo_5(const util::Path &path) { auto qtapp = std::make_shared(); - opengl::GlWindow window("openage renderer test", 800, 600); + opengl::GlWindow window("openage renderer test", 800, 600, true); auto renderer = window.make_renderer(); auto size = window.get_size(); diff --git a/libopenage/renderer/demo/stresstest_0.cpp b/libopenage/renderer/demo/stresstest_0.cpp new file mode 100644 index 0000000000..77b8f0f33b --- /dev/null +++ b/libopenage/renderer/demo/stresstest_0.cpp @@ -0,0 +1,220 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "stresstest_0.h" + +#include + +#include "renderer/camera/camera.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_factory.h" +#include "renderer/resources/assets/asset_manager.h" +#include "renderer/resources/shader_source.h" +#include "renderer/stages/camera/manager.h" +#include "renderer/stages/screen/screen_renderer.h" +#include "renderer/stages/skybox/skybox_renderer.h" +#include "renderer/stages/terrain/terrain_render_entity.h" +#include "renderer/stages/terrain/terrain_renderer.h" +#include "renderer/stages/world/world_render_entity.h" +#include "renderer/stages/world/world_renderer.h" +#include "renderer/uniform_buffer.h" +#include "time/clock.h" +#include "util/fps.h" + + +namespace openage::renderer::tests { + +void renderer_stresstest_0(const util::Path &path) { + auto qtapp = std::make_shared(); + + auto window = std::make_shared("openage renderer test", 1024, 768, true); + auto renderer = window->make_renderer(); + + // Clock required by world renderer for timing animation frames + auto clock = std::make_shared(); + + // Camera + // our viewport into the game world + auto camera = std::make_shared(renderer, + window->get_size(), + Eigen::Vector3f{17.0f, 10.0f, 7.0f}); + auto cam_unifs = camera->get_uniform_buffer()->create_empty_input(); + cam_unifs->update( + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + camera->get_uniform_buffer()->update_uniforms(cam_unifs); + + // Render stages + // every stage use a different subrenderer that manages renderables, + // shaders, textures & more. + std::vector> + render_passes{}; + + // TODO: Make this optional for subrenderers? + auto asset_manager = std::make_shared( + renderer, + path["assets"]["test"]); + + // Renders the background + auto skybox_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + skybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color + + // Renders the terrain in 3D + auto terrain_renderer = std::make_shared( + window, + renderer, + camera, + path["assets"]["shaders"], + asset_manager, + clock); + + // Renders units/buildings/other objects + auto world_renderer = std::make_shared( + window, + renderer, + camera, + path["assets"]["shaders"], + asset_manager, + clock); + + // Store the render passes of the renderers + // The order is important as its also the order in which they + // are rendered and drawn onto the screen. + render_passes.push_back(skybox_renderer->get_render_pass()); + render_passes.push_back(terrain_renderer->get_render_pass()); + render_passes.push_back(world_renderer->get_render_pass()); + + // Final output on screen has its own subrenderer + // It takes the outputs of all previous render passes + // and blends them together + auto screen_renderer = std::make_shared( + window, + renderer, + path["assets"]["shaders"]); + std::vector> targets{}; + for (auto &pass : render_passes) { + targets.push_back(pass->get_target()); + } + screen_renderer->set_render_targets(targets); + + window->add_resize_callback([&](size_t, size_t, double /*scale*/) { + // Acquire the render targets for all previous passes + std::vector> targets{}; + for (size_t i = 0; i < render_passes.size() - 1; ++i) { + targets.push_back(render_passes[i]->get_target()); + } + screen_renderer->set_render_targets(targets); + }); + + render_passes.push_back(screen_renderer->get_render_pass()); + + // Create some entities to populate the scene + auto render_factory = std::make_shared(terrain_renderer, world_renderer); + + // Terrain + auto terrain0 = render_factory->add_terrain_render_entity(); + + // Fill a 10x10 terrain grid with height values + auto terrain_size = util::Vector2s{10, 10}; + std::vector height_map{}; + height_map.reserve(terrain_size[0] * terrain_size[1]); + for (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) { + height_map.push_back(0.0f); + } + + // send the terrain data to the terrain renderer + terrain0->update(terrain_size, + height_map, + "./textures/test_terrain.terrain"); + + // World entities + std::vector> render_entities{}; + auto add_world_entity = [&](const coord::phys3 initial_pos, + const time::time_t time) { + const auto animation_path = "./textures/test_tank_mirrored.sprite"; + + auto position = curve::Continuous{nullptr, 0, "", nullptr, coord::phys3(0, 0, 0)}; + position.set_insert(time, initial_pos); + position.set_insert(time + 1, initial_pos + coord::phys3_delta{0, 3, 0}); + position.set_insert(time + 2, initial_pos + coord::phys3_delta{3, 6, 0}); + position.set_insert(time + 3, initial_pos + coord::phys3_delta{6, 6, 0}); + position.set_insert(time + 4, initial_pos + coord::phys3_delta{9, 3, 0}); + position.set_insert(time + 5, initial_pos + coord::phys3_delta{9, 0, 0}); + position.set_insert(time + 6, initial_pos + coord::phys3_delta{6, -3, 0}); + position.set_insert(time + 7, initial_pos + coord::phys3_delta{3, -3, 0}); + position.set_insert(time + 8, initial_pos); + + auto angle = curve::Segmented{nullptr, 0}; + angle.set_insert(time, coord::phys_angle_t::from_int(315)); + angle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(315), coord::phys_angle_t::from_int(270)); + angle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(225)); + angle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(180)); + angle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(180), coord::phys_angle_t::from_int(135)); + angle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(90)); + angle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(90), coord::phys_angle_t::from_int(45)); + angle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(0)); + angle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(315)); + + auto entity = render_factory->add_world_render_entity(); + entity->update(render_entities.size(), + position, + angle, + animation_path, + time); + render_entities.push_back(entity); + }; + + // Stop after 500 entities + size_t entity_limit = 500; + + clock->start(); + + util::FrameCounter timer; + + add_world_entity(coord::phys3(0.0f, 3.0f, 0.0f), clock->get_time()); + time::time_t next_entity = clock->get_real_time() + 0.1; + while (render_entities.size() <= entity_limit) { + // Print FPS + timer.frame(); + std::cout + << "Entities: " << render_entities.size() + << " -- " + << "FPS: " << timer.fps << "\r" << std::flush; + + qtapp->process_events(); + + // Advance time + clock->update_time(); + auto current_time = clock->get_real_time(); + if (current_time > next_entity) { + add_world_entity(coord::phys3(0.0f, 3.0f, 0.0f), clock->get_time()); + next_entity = current_time + 0.1; + } + + // Update the renderables of the subrenderers + terrain_renderer->update(); + world_renderer->update(); + + // Draw everything + for (auto &pass : render_passes) { + renderer->render(pass); + } + + renderer->check_error(); + + // Display final output on screen + window->update(); + } + + clock->stop(); + log::log(MSG(info) << "Stopped after rendering " << render_entities.size() << " entities"); + + window->close(); +} + +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/stresstest_0.h b/libopenage/renderer/demo/stresstest_0.h new file mode 100644 index 0000000000..c5beee5fdd --- /dev/null +++ b/libopenage/renderer/demo/stresstest_0.h @@ -0,0 +1,17 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include "util/path.h" + + +namespace openage::renderer::tests { + +/** + * Stresstest for the renderer. + * + * @param path Path to the openage asset directory. + */ +void renderer_stresstest_0(const util::Path &path); + +} // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/tests.cpp b/libopenage/renderer/demo/tests.cpp index 3de072267c..95f6bd40af 100644 --- a/libopenage/renderer/demo/tests.cpp +++ b/libopenage/renderer/demo/tests.cpp @@ -11,6 +11,7 @@ #include "renderer/demo/demo_3.h" #include "renderer/demo/demo_4.h" #include "renderer/demo/demo_5.h" +#include "renderer/demo/stresstest_0.h" namespace openage::renderer::tests { @@ -47,4 +48,16 @@ void renderer_demo(int demo_id, const util::Path &path) { } } +OAAPI void renderer_stresstest(int demo_id, const util::Path &path) { + switch (demo_id) { + case 0: + renderer_stresstest_0(path); + break; + + default: + log::log(MSG(err) << "Unknown renderer stresstest requested: " << demo_id << "."); + break; + } +} + } // namespace openage::renderer::tests diff --git a/libopenage/renderer/demo/tests.h b/libopenage/renderer/demo/tests.h index 05568ae8fe..dc9ccea8c2 100644 --- a/libopenage/renderer/demo/tests.h +++ b/libopenage/renderer/demo/tests.h @@ -16,5 +16,8 @@ namespace renderer::tests { // pxd: void renderer_demo(int demo_id, Path path) except + OAAPI void renderer_demo(int demo_id, const util::Path &path); +// pxd: void renderer_stresstest(int demo_id, Path path) except + +OAAPI void renderer_stresstest(int demo_id, const util::Path &path); + } // namespace renderer::tests } // namespace openage diff --git a/libopenage/renderer/opengl/CMakeLists.txt b/libopenage/renderer/opengl/CMakeLists.txt index 49f9487af1..eb93f1df56 100644 --- a/libopenage/renderer/opengl/CMakeLists.txt +++ b/libopenage/renderer/opengl/CMakeLists.txt @@ -15,6 +15,7 @@ add_sources(libopenage texture_array.cpp uniform_buffer.cpp uniform_input.cpp + util.cpp vertex_array.cpp window.cpp ) diff --git a/libopenage/renderer/opengl/context.cpp b/libopenage/renderer/opengl/context.cpp index 5424acad35..d65fe4aae1 100644 --- a/libopenage/renderer/opengl/context.cpp +++ b/libopenage/renderer/opengl/context.cpp @@ -21,7 +21,7 @@ namespace openage::renderer::opengl { static constexpr std::array, 1> gl_versions = {{{3, 3}}}; // for now we don't need any higher versions /// Finds out the supported graphics functions and OpenGL version of the device. -static gl_context_capabilities find_capabilities() { +gl_context_spec GlContext::find_spec() { QSurfaceFormat test_format{}; test_format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); test_format.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer); @@ -60,7 +60,7 @@ static gl_context_capabilities find_capabilities() { test_context.makeCurrent(&test_surface); - gl_context_capabilities caps{}; + gl_context_spec caps{}; // Texture parameters GLint temp; @@ -82,30 +82,18 @@ static gl_context_capabilities find_capabilities() { return caps; } -GlContext::GlContext(const std::shared_ptr &window) : +GlContext::GlContext(const std::shared_ptr &window, + bool debug) : window{window}, - log_handler{std::make_shared()} { - this->capabilities = find_capabilities(); - auto const &capabilities = this->capabilities; + log_handler{} { + this->specs = find_spec(); + auto const &specs = this->specs; - this->uniform_buffer_bindings = std::vector(capabilities.max_uniform_buffer_bindings); + this->uniform_buffer_bindings = std::vector(specs.max_uniform_buffer_bindings); - QSurfaceFormat format{}; - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - format.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer); - format.setMajorVersion(capabilities.major_version); - format.setMinorVersion(capabilities.minor_version); - - format.setAlphaBufferSize(8); - format.setDepthBufferSize(24); - format.setStencilBufferSize(8); - - // TODO: Make debug context optional - format.setOption(QSurfaceFormat::DebugContext); - - // TODO: Set format as default for all windows with QSurface::setDefaultFormat() - this->window->setFormat(format); - this->window->create(); + if (debug) { + this->log_handler = std::make_shared(); + } this->gl_context = std::make_shared(); this->gl_context->setFormat(this->window->requestedFormat()); @@ -116,45 +104,47 @@ GlContext::GlContext(const std::shared_ptr &window) : this->gl_context->makeCurrent(window.get()); - // Log handler requires a current context, so we start it after associating - // it with the window. - this->log_handler->start(); + if (debug) { + // Log handler requires a current context, so we start it after associating + // it with the window. + this->log_handler->start(); + } // We still have to verify that our version of libepoxy supports this version of OpenGL. - int epoxy_glv = capabilities.major_version * 10 + capabilities.minor_version; - if (!epoxy_is_desktop_gl() || epoxy_gl_version() < epoxy_glv) { + int epoxy_glv = specs.major_version * 10 + specs.minor_version; + if (not epoxy_is_desktop_gl() or epoxy_gl_version() < epoxy_glv) { throw Error(MSG(err) << "The used version of libepoxy does not support OpenGL version " - << capabilities.major_version << "." << capabilities.minor_version); + << specs.major_version << "." << specs.minor_version); } - log::log(MSG(info) << "Created OpenGL context version " << capabilities.major_version << "." << capabilities.minor_version); + log::log(MSG(info) << "Created OpenGL context version " << specs.major_version << "." << specs.minor_version); // To quote the standard doc: 'The value gives a rough estimate of the // largest texture that the GL can handle' // -> wat? anyways, we need at least 1024x1024. log::log(MSG(dbg) << "Maximum supported texture size: " - << capabilities.max_texture_size); - if (capabilities.max_texture_size < 1024) { + << specs.max_texture_size); + if (specs.max_texture_size < 1024) { throw Error(MSG(err) << "Maximum supported texture size is too small: " - << capabilities.max_texture_size); + << specs.max_texture_size); } log::log(MSG(dbg) << "Maximum supported texture units: " - << capabilities.max_texture_slots); - if (capabilities.max_texture_slots < 2) { + << specs.max_texture_slots); + if (specs.max_texture_slots < 2) { throw Error(MSG(err) << "Your GPU doesn't have enough texture units: " - << capabilities.max_texture_slots); + << specs.max_texture_slots); } } GlContext::GlContext(GlContext &&other) : - gl_context(other.gl_context), capabilities(other.capabilities) { + gl_context(other.gl_context), specs(other.specs) { other.gl_context = nullptr; } GlContext &GlContext::operator=(GlContext &&other) { this->gl_context = other.gl_context; - this->capabilities = other.capabilities; + this->specs = other.specs; other.gl_context = nullptr; return *this; @@ -164,8 +154,8 @@ std::shared_ptr GlContext::get_raw_context() const { return this->gl_context; } -gl_context_capabilities GlContext::get_capabilities() const { - return this->capabilities; +gl_context_spec GlContext::get_specs() const { + return this->specs; } void GlContext::check_error() { @@ -239,7 +229,7 @@ void GlContext::set_current_program(const std::shared_ptr &prog } size_t GlContext::get_uniform_buffer_binding() { - for (size_t i = 1; i < this->capabilities.max_uniform_buffer_bindings; ++i) { + for (size_t i = 1; i < this->specs.max_uniform_buffer_bindings; ++i) { if (not this->uniform_buffer_bindings[i]) { this->uniform_buffer_bindings[i] = true; return i; @@ -251,7 +241,7 @@ size_t GlContext::get_uniform_buffer_binding() { } void GlContext::free_uniform_buffer_binding(size_t binding_point) { - if (binding_point >= this->capabilities.max_uniform_buffer_bindings) [[unlikely]] { + if (binding_point >= this->specs.max_uniform_buffer_bindings) [[unlikely]] { throw Error(MSG(err) << "Cannot free invalid uniform buffer binding point: " << binding_point); } this->uniform_buffer_bindings[binding_point] = false; diff --git a/libopenage/renderer/opengl/context.h b/libopenage/renderer/opengl/context.h index a8d224921f..68e3e39c1f 100644 --- a/libopenage/renderer/opengl/context.h +++ b/libopenage/renderer/opengl/context.h @@ -17,7 +17,7 @@ class GlShaderProgram; /** * Stores information about context capabilities and limitations. */ -struct gl_context_capabilities { +struct gl_context_spec { /// The maximum number of vertex attributes in a shader. size_t max_vertex_attributes; /// The amount of texture units (GL_TEXTUREi) available. @@ -40,20 +40,14 @@ class GlContext { /** * Create a GL context for the given Qt window. * - * TODO: Currently, this also sets up the Qt window with QWindow::create() because - * the constructur also creates the context format. Ideally, QWindow creation should - * be handed to our own Window class. - * * @param window Window for the context. The context is made current to this window. - */ - explicit GlContext(const std::shared_ptr &window); + * @param debug If true, enable OpenGL debug logging. + */ + explicit GlContext(const std::shared_ptr &window, + bool debug = false); ~GlContext() = default; - /** - * Copy this context. - * - * It doesn't make sense to have more than one instance of the same context. - */ + // It doesn't make sense to have more than one instance of the same context. GlContext(const GlContext &) = delete; GlContext &operator=(const GlContext &) = delete; @@ -75,12 +69,15 @@ class GlContext { /** * Get the capabilities of this context. */ - gl_context_capabilities get_capabilities() const; + gl_context_spec get_specs() const; /** * Activate or deactivate VSync for this context. * - * @param on Pass \p true to activate VSync, \p false to deactivate. + * TODO: This currently does not work at runtime. vsync must be set before + * the QApplication is created. + * + * @param on \p true to activate VSync, \p false to deactivate. */ void set_vsync(bool on); @@ -139,6 +136,11 @@ class GlContext { */ void free_uniform_buffer_binding(size_t binding_point); + /** + * Find out the supported graphics functions and OpenGL version of the device. + */ + static gl_context_spec find_spec(); + private: /** * Associated Qt window. Held here so the context remains active. @@ -160,7 +162,7 @@ class GlContext { /** * Context capabilities, i.e. available OpenGL features and version. */ - gl_context_capabilities capabilities{}; + gl_context_spec specs{}; /** * The last-active shader program diff --git a/libopenage/renderer/opengl/lookup.h b/libopenage/renderer/opengl/lookup.h index be3987b262..e399e98399 100644 --- a/libopenage/renderer/opengl/lookup.h +++ b/libopenage/renderer/opengl/lookup.h @@ -28,7 +28,7 @@ static constexpr auto GL_PIXEL_FORMAT = datastructure::create_const_map( +static constexpr auto GL_UNIFORM_TYPE_SIZE = datastructure::create_const_map( std::pair(GL_FLOAT, 4), std::pair(GL_FLOAT_VEC2, 8), std::pair(GL_FLOAT_VEC3, 12), diff --git a/libopenage/renderer/opengl/shader_data.h b/libopenage/renderer/opengl/shader_data.h index ca83b2c96f..83d140e591 100644 --- a/libopenage/renderer/opengl/shader_data.h +++ b/libopenage/renderer/opengl/shader_data.h @@ -12,6 +12,9 @@ namespace openage::renderer::opengl { * Represents a uniform in the default (shader) block, i.e. not within a named block. */ struct GlUniform { + /** + * Data type of the uniform for setting with glUniform. + */ GLenum type; /** diff --git a/libopenage/renderer/opengl/shader_program.cpp b/libopenage/renderer/opengl/shader_program.cpp index 9ed6f771e5..f5a2315072 100644 --- a/libopenage/renderer/opengl/shader_program.cpp +++ b/libopenage/renderer/opengl/shader_program.cpp @@ -18,6 +18,7 @@ #include "renderer/opengl/texture.h" #include "renderer/opengl/uniform_buffer.h" #include "renderer/opengl/uniform_input.h" +#include "renderer/opengl/util.h" namespace openage::renderer::opengl { @@ -65,7 +66,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, GlSimpleObject(context, [](GLuint handle) { glDeleteProgram(handle); }), validated(false) { - const gl_context_capabilities &caps = context->get_capabilities(); + const gl_context_spec &caps = context->get_specs(); GLuint handle = glCreateProgram(); this->handle = handle; @@ -156,7 +157,7 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, GlInBlockUniform{ type, size_t(offset), - size_t(count) * GL_SHADER_TYPE_SIZE.get(type), + size_t(count) * GL_UNIFORM_TYPE_SIZE.get(type), size_t(stride), size_t(count)})); } @@ -181,6 +182,9 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, GLuint tex_unit = 0; // Extract information about uniforms in the default block. + + // the uniform ID is the index in the uniforms vector + uniform_id_t unif_id = 0; for (GLuint i_unif = 0; i_unif < unif_count; ++i_unif) { if (in_block_unifs.count(i_unif) == 1) { // Skip uniforms within named blocks. @@ -200,21 +204,24 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, GLuint loc = glGetUniformLocation(handle, name.data()); - this->uniforms.insert(std::make_pair( + this->uniforms.emplace_back(type, loc); + + this->uniforms_by_name.insert(std::make_pair( name.data(), - GlUniform{ - type, - loc})); + unif_id)); if (type == GL_SAMPLER_2D) { ENSURE(tex_unit < caps.max_texture_slots, "Tried to create an OpenGL shader that uses more texture sampler uniforms " << "than there are texture unit slots (" << caps.max_texture_slots << " available)."); - this->texunits_per_unifs.insert(std::make_pair(name.data(), tex_unit)); + this->texunits_per_unifs.insert(std::make_pair(unif_id, tex_unit)); tex_unit += 1; } + + // Increment uniform ID + unif_id += 1; } // Extract vertex attribute descriptions. @@ -257,9 +264,11 @@ GlShaderProgram::GlShaderProgram(const std::shared_ptr &context, if (!this->uniforms.empty()) { log::log(MSG(dbg) << "Uniforms: "); - for (auto const &pair : this->uniforms) { - log::log(MSG(dbg) << "(" << pair.second.location << ") " << pair.first << ": " - << GLSL_TYPE_NAME.get(pair.second.type)); + for (const auto &pair : this->uniforms_by_name) { + const auto &unif_info = this->uniforms[pair.second]; + log::log(MSG(dbg) << "(" << unif_info.location << ") " + << pair.first << ": " + << GLSL_TYPE_NAME.get(unif_info.type)); } } @@ -421,8 +430,12 @@ std::shared_ptr GlShaderProgram::new_unif_in() { return in; } +uniform_id_t GlShaderProgram::get_uniform_id(const char *name) { + return this->uniforms_by_name.at(name); +} + bool GlShaderProgram::has_uniform(const char *name) { - return this->uniforms.count(name) == 1; + return this->uniforms_by_name.contains(name); } void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_ptr const &buffer) { @@ -438,21 +451,24 @@ void GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_pt glUniformBlockBinding(*this->handle, block.index, block.binding_point); } -void GlShaderProgram::set_unif(std::shared_ptr const &in, const char *unif, void const *val, GLenum type) { +void GlShaderProgram::set_unif(std::shared_ptr const &in, + const char *unif, + void const *val, + GLenum type) { auto unif_in = std::dynamic_pointer_cast(in); - auto uniform = this->uniforms.find(unif); - ENSURE(uniform != std::end(this->uniforms), - "Tried to set uniform " << unif << " that does not exist in the shader program."); + auto uniform_id = this->uniforms_by_name.find(unif); + ENSURE(uniform_id != std::end(this->uniforms_by_name), + "Tried to set uniform '" << unif << "' that does not exist in the shader program."); - auto const &unif_data = uniform->second; + auto const &unif_info = this->uniforms.at(uniform_id->second); - ENSURE(type == unif_data.type, - "Tried to set uniform " << unif << " to a value of the wrong type."); + ENSURE(type == unif_info.type, + "Tried to set uniform '" << unif << "' to a value of the wrong type."); - size_t size = GL_SHADER_TYPE_SIZE.get(unif_data.type); + size_t size = get_uniform_type_size(type); - auto update_off = unif_in->update_offs.find(unif); + auto update_off = unif_in->update_offs.find(uniform_id->second); if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once // already wrote to this uniform since last upload size_t off = update_off->second; @@ -464,7 +480,38 @@ void GlShaderProgram::set_unif(std::shared_ptr const &in, const ch size_t prev_size = unif_in->update_data.size(); unif_in->update_data.resize(prev_size + size); memcpy(unif_in->update_data.data() + prev_size, val, size); - unif_in->update_offs.emplace(unif, prev_size); + unif_in->update_offs.emplace(uniform_id->second, prev_size); + } +} + +void GlShaderProgram::set_unif(std::shared_ptr const &in, + const uniform_id_t &unif_id, + void const *val, + GLenum type) { + auto unif_in = std::dynamic_pointer_cast(in); + + ENSURE(unif_id < this->uniforms.size(), + "Tried to set uniform '" << unif_id << "' that does not exist in the shader program."); + + auto const &unif_info = this->uniforms.at(unif_id); + ENSURE(type == unif_info.type, + "Tried to set uniform '" << unif_id << "' to a value of the wrong type."); + + size_t size = get_uniform_type_size(type); + + auto update_off = unif_in->update_offs.find(unif_id); + if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once + // already wrote to this uniform since last upload + size_t off = update_off->second; + memcpy(unif_in->update_data.data() + off, val, size); + } + else { + // first time writing to this uniform since last upload, so + // extend the buffer before storing the uniform value + size_t prev_size = unif_in->update_data.size(); + unif_in->update_data.resize(prev_size + size); + memcpy(unif_in->update_data.data() + prev_size, val, size); + unif_in->update_offs.emplace(unif_id, prev_size); } } @@ -535,4 +582,71 @@ void GlShaderProgram::set_tex(std::shared_ptr const &in, const cha this->set_unif(in, unif, &handle, GL_SAMPLER_2D); } +void GlShaderProgram::set_i32(std::shared_ptr const &in, const uniform_id_t &id, int32_t val) { + this->set_unif(in, id, &val, GL_INT); +} + +void GlShaderProgram::set_u32(std::shared_ptr const &in, const uniform_id_t &id, uint32_t val) { + this->set_unif(in, id, &val, GL_UNSIGNED_INT); +} + +void GlShaderProgram::set_f32(std::shared_ptr const &in, const uniform_id_t &id, float val) { + this->set_unif(in, id, &val, GL_FLOAT); +} + +void GlShaderProgram::set_f64(std::shared_ptr const &in, const uniform_id_t &id, double val) { + // TODO requires extension + this->set_unif(in, id, &val, GL_DOUBLE); +} + +void GlShaderProgram::set_bool(std::shared_ptr const &in, const uniform_id_t &id, bool val) { + this->set_unif(in, id, &val, GL_BOOL); +} + +void GlShaderProgram::set_v2f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2f const &val) { + this->set_unif(in, id, &val, GL_FLOAT_VEC2); +} + +void GlShaderProgram::set_v3f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3f const &val) { + this->set_unif(in, id, &val, GL_FLOAT_VEC3); +} + +void GlShaderProgram::set_v4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4f const &val) { + this->set_unif(in, id, &val, GL_FLOAT_VEC4); +} + +void GlShaderProgram::set_v2i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2i const &val) { + this->set_unif(in, id, &val, GL_INT_VEC2); +} + +void GlShaderProgram::set_v3i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3i const &val) { + this->set_unif(in, id, &val, GL_INT_VEC3); +} + +void GlShaderProgram::set_v4i32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4i const &val) { + this->set_unif(in, id, &val, GL_INT_VEC4); +} + +void GlShaderProgram::set_v2ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector2 const &val) { + this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC2); +} + +void GlShaderProgram::set_v3ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector3 const &val) { + this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC3); +} + +void GlShaderProgram::set_v4ui32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Vector4 const &val) { + this->set_unif(in, id, &val, GL_UNSIGNED_INT_VEC4); +} + +void GlShaderProgram::set_m4f32(std::shared_ptr const &in, const uniform_id_t &id, Eigen::Matrix4f const &val) { + this->set_unif(in, id, val.data(), GL_FLOAT_MAT4); +} + +void GlShaderProgram::set_tex(std::shared_ptr const &in, const uniform_id_t &id, std::shared_ptr const &val) { + auto tex = std::dynamic_pointer_cast(val); + GLuint handle = tex->get_handle(); + this->set_unif(in, id, &handle, GL_SAMPLER_2D); +} + } // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/shader_program.h b/libopenage/renderer/opengl/shader_program.h index 2ee4b16f1d..7fa2d96c7a 100644 --- a/libopenage/renderer/opengl/shader_program.h +++ b/libopenage/renderer/opengl/shader_program.h @@ -64,7 +64,9 @@ class GlShaderProgram final : public ShaderProgram */ const GlUniformBlock &get_uniform_block(const char *block_name) const; - bool has_uniform(const char *) override; + uniform_id_t get_uniform_id(const char *name) override; + + bool has_uniform(const char *name) override; /** * Binds a uniform block in the shader program to the same binding point as @@ -97,12 +99,55 @@ class GlShaderProgram final : public ShaderProgram void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) override; void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) override; + void set_i32(std::shared_ptr const &, const uniform_id_t &, int32_t) override; + void set_u32(std::shared_ptr const &, const uniform_id_t &, uint32_t) override; + void set_f32(std::shared_ptr const &, const uniform_id_t &, float) override; + void set_f64(std::shared_ptr const &, const uniform_id_t &, double) override; + void set_bool(std::shared_ptr const &, const uniform_id_t &, bool) override; + void set_v2f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2f const &) override; + void set_v3f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3f const &) override; + void set_v4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4f const &) override; + void set_v2i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2i const &) override; + void set_v3i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3i const &) override; + void set_v4i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4i const &) override; + void set_v2ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2 const &) override; + void set_v3ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3 const &) override; + void set_v4ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4 const &) override; + void set_m4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Matrix4f const &) override; + void set_tex(std::shared_ptr const &, const uniform_id_t &, std::shared_ptr const &) override; + private: + /** + * Set the uniform value via uniform name from a uniform input. + * + * This method should only be used for debugging and not for performance-critical code. + * String lookups are much slower than using the uniform ID. + * If performance is important, use the alternative \p set_unif(..) implementation + * that works on IDs instead. + * + * @param unif_in Uniform input. + * @param name Name of the uniform. + * @param value Value to set. + * @param type Type of the value. + */ void set_unif(std::shared_ptr const &, const char *, void const *, GLenum); - /// Maps uniform names to their descriptions. Contains only + /** + * Set the uniform value via uniform ID from a uniform input. + * + * @param unif_in Uniform input. + * @param id ID of the uniform. + * @param value Value to set. + * @param type Type of the value. + */ + void set_unif(std::shared_ptr const &, const uniform_id_t &, void const *, GLenum); + + /// Uniforms in the shader program. Contains only /// uniforms in the default block, i.e. not within named blocks. - std::unordered_map uniforms; + std::vector uniforms; + + /// Maps uniform names to their ID (the index in the uniform vector). + std::unordered_map uniforms_by_name; /// Maps uniform block names to their descriptions. std::unordered_map uniform_blocks; @@ -111,7 +156,8 @@ class GlShaderProgram final : public ShaderProgram std::unordered_map attribs; /// Maps sampler uniform names to their assigned texture units. - std::unordered_map texunits_per_unifs; + std::unordered_map texunits_per_unifs; + /// Maps texture units to the texture handles that are currently bound to them. std::unordered_map textures_per_texunits; diff --git a/libopenage/renderer/opengl/uniform_buffer.cpp b/libopenage/renderer/opengl/uniform_buffer.cpp index 08513c1506..28f78edb18 100644 --- a/libopenage/renderer/opengl/uniform_buffer.cpp +++ b/libopenage/renderer/opengl/uniform_buffer.cpp @@ -7,6 +7,8 @@ #include "renderer/opengl/lookup.h" #include "renderer/opengl/texture.h" #include "renderer/opengl/uniform_input.h" +#include "renderer/opengl/util.h" + namespace openage::renderer::opengl { @@ -84,7 +86,7 @@ void GlUniformBuffer::set_unif(std::shared_ptr const &in, co ENSURE(type == unif_data.type, "Tried to set uniform " << unif << " to a value of the wrong type."); - size_t size = GL_SHADER_TYPE_SIZE.get(unif_data.type); + size_t size = get_uniform_type_size(type); auto update_off = unif_in->update_offs.find(unif); if (update_off != std::end(unif_in->update_offs)) [[likely]] { // always used after the uniform value is written once diff --git a/libopenage/renderer/opengl/uniform_input.h b/libopenage/renderer/opengl/uniform_input.h index 17ff6d4df0..4fdb79e72e 100644 --- a/libopenage/renderer/opengl/uniform_input.h +++ b/libopenage/renderer/opengl/uniform_input.h @@ -5,13 +5,13 @@ #include #include -#include "../renderer.h" -#include "../uniform_input.h" +#include "renderer/renderer.h" +#include "renderer/types.h" +#include "renderer/uniform_input.h" namespace openage { namespace renderer { - class ShaderProgram; class UniformBuffer; @@ -30,11 +30,11 @@ class GlUniformInput final : public UniformInput { * We store uniform updates lazily. They are only actually uploaded to GPU * when a draw call is made. * - * \p update_offs maps the uniform names to where their + * \p update_offs maps the uniform IDs to where their * value is in \p update_data in terms of a byte-wise offset. This is only a partial * valuation, so not all uniforms have to be present here. */ - std::unordered_map update_offs; + std::unordered_map update_offs; /** * Buffer containing untyped uniform update data. diff --git a/libopenage/renderer/opengl/util.cpp b/libopenage/renderer/opengl/util.cpp new file mode 100644 index 0000000000..7a8a6e2f91 --- /dev/null +++ b/libopenage/renderer/opengl/util.cpp @@ -0,0 +1,68 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "util.h" + +#include "error/error.h" + + +namespace openage::renderer::opengl { + +size_t get_uniform_type_size(GLenum type) { + switch (type) { + case GL_BOOL: + return 1; + break; + case GL_BOOL_VEC2: + return 2; + break; + case GL_BOOL_VEC3: + return 3; + break; + case GL_BOOL_VEC4: + case GL_FLOAT: + case GL_INT: + case GL_UNSIGNED_INT: + case GL_SAMPLER_1D: + case GL_SAMPLER_2D: + case GL_SAMPLER_2D_ARRAY: + case GL_SAMPLER_3D: + case GL_SAMPLER_CUBE: + return 4; + break; + + case GL_FLOAT_VEC2: + case GL_INT_VEC2: + case GL_UNSIGNED_INT_VEC2: + return 8; + break; + + case GL_FLOAT_VEC3: + case GL_INT_VEC3: + case GL_UNSIGNED_INT_VEC3: + return 12; + break; + + case GL_FLOAT_VEC4: + case GL_INT_VEC4: + case GL_UNSIGNED_INT_VEC4: + case GL_FLOAT_MAT2: + return 16; + break; + + case GL_FLOAT_MAT3: + return 36; + break; + + case GL_FLOAT_MAT4: + return 64; + break; + + default: + throw Error(MSG(err) << "Unknown GL uniform type: " << type); + break; + } + + return 0; +} + +} // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/util.h b/libopenage/renderer/opengl/util.h new file mode 100644 index 0000000000..3ad5b4626e --- /dev/null +++ b/libopenage/renderer/opengl/util.h @@ -0,0 +1,21 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include + + +namespace openage::renderer::opengl { + +/** + * Get the sizes of a uniform value with a given uniform type. + * + * @param type Uniform type. + * + * @return Size of uniform value (in bytes). + */ +size_t get_uniform_type_size(GLenum type); + +} // namespace openage::renderer::opengl diff --git a/libopenage/renderer/opengl/window.cpp b/libopenage/renderer/opengl/window.cpp index ac218379c3..daedbf7537 100644 --- a/libopenage/renderer/opengl/window.cpp +++ b/libopenage/renderer/opengl/window.cpp @@ -17,7 +17,10 @@ namespace openage::renderer::opengl { -GlWindow::GlWindow(const std::string &title, size_t width, size_t height) : +GlWindow::GlWindow(const std::string &title, + size_t width, + size_t height, + bool debug) : Window{width, height} { if (QGuiApplication::instance() == nullptr) { // Qt windows need to attach to a QtGuiApplication @@ -32,7 +35,27 @@ GlWindow::GlWindow(const std::string &title, size_t width, size_t height) : this->window->setSurfaceType(QSurface::OpenGLSurface); - this->context = std::make_shared(this->window); + auto gl_specs = GlContext::find_spec(); + QSurfaceFormat format{}; + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + format.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer); + + format.setMajorVersion(gl_specs.major_version); + format.setMinorVersion(gl_specs.minor_version); + + format.setAlphaBufferSize(8); + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + + if (debug) { + format.setOption(QSurfaceFormat::DebugContext); + } + + // TODO: Set format as default for all windows with QSurface::setDefaultFormat() + this->window->setFormat(format); + this->window->create(); + + this->context = std::make_shared(this->window, debug); if (not this->context->get_raw_context()->isValid()) { throw Error{MSG(err) << "Failed to create Qt OpenGL context."}; } diff --git a/libopenage/renderer/opengl/window.h b/libopenage/renderer/opengl/window.h index 9d5741386b..9d1600a329 100644 --- a/libopenage/renderer/opengl/window.h +++ b/libopenage/renderer/opengl/window.h @@ -21,8 +21,18 @@ class GlContext; class GlWindow final : public Window { public: - /// Create a shiny window with the given title. - GlWindow(const std::string &title, size_t width, size_t height); + /** + * Create a shiny window with the given title. + * + * @param title The window title. + * @param width Width (in pixels). + * @param height Height (in pixels). + * @param debug If true, enable OpenGL debug logging. + */ + GlWindow(const std::string &title, + size_t width, + size_t height, + bool debug = false); ~GlWindow(); void set_size(size_t width, size_t height) override; diff --git a/libopenage/renderer/resources/assets/asset_manager.cpp b/libopenage/renderer/resources/assets/asset_manager.cpp index 284b2d27fb..6b5ee3a8d7 100644 --- a/libopenage/renderer/resources/assets/asset_manager.cpp +++ b/libopenage/renderer/resources/assets/asset_manager.cpp @@ -30,6 +30,7 @@ AssetManager::AssetManager(const std::shared_ptr &renderer, cache{std::make_shared()}, texture_manager{std::make_shared(renderer)}, asset_base_dir{asset_base_dir} { + log::log(INFO << "Created asset manager"); } const std::shared_ptr &AssetManager::request_animation(const util::Path &path) { diff --git a/libopenage/renderer/resources/assets/cache.cpp b/libopenage/renderer/resources/assets/cache.cpp index 5ab4db4229..bb3c96953e 100644 --- a/libopenage/renderer/resources/assets/cache.cpp +++ b/libopenage/renderer/resources/assets/cache.cpp @@ -8,133 +8,109 @@ namespace openage::renderer::resources { const std::shared_ptr &AssetCache::get_animation(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_animations.at(flat_path); + return this->loaded_animations.at(path); } const std::shared_ptr &AssetCache::get_blpattern(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_blpatterns.at(flat_path); + return this->loaded_blpatterns.at(path); } const std::shared_ptr &AssetCache::get_bltable(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_bltables.at(flat_path); + return this->loaded_bltables.at(path); } const std::shared_ptr &AssetCache::get_palette(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_palettes.at(flat_path); + return this->loaded_palettes.at(path); } const std::shared_ptr &AssetCache::get_terrain(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_terrains.at(flat_path); + return this->loaded_terrains.at(path); } const std::shared_ptr &AssetCache::get_texture(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_textures.at(flat_path); + return this->loaded_textures.at(path); } void AssetCache::add_animation(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_animations.insert({flat_path, info}); + this->loaded_animations.insert({path, info}); } void AssetCache::add_blpattern(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_blpatterns.insert({flat_path, info}); + this->loaded_blpatterns.insert({path, info}); } void AssetCache::add_bltable(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_bltables.insert({flat_path, info}); + this->loaded_bltables.insert({path, info}); } void AssetCache::add_palette(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_palettes.insert({flat_path, info}); + this->loaded_palettes.insert({path, info}); } void AssetCache::add_terrain(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_terrains.insert({flat_path, info}); + this->loaded_terrains.insert({path, info}); } void AssetCache::add_texture(const util::Path &path, const std::shared_ptr info) { - auto flat_path = path.resolve_native_path(); - this->loaded_textures.insert({flat_path, info}); + this->loaded_textures.insert({path, info}); } void AssetCache::remove_animation(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_animations.erase(flat_path); + this->loaded_animations.erase(path); } void AssetCache::remove_blpattern(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_blpatterns.erase(flat_path); + this->loaded_blpatterns.erase(path); } void AssetCache::remove_bltable(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_bltables.erase(flat_path); + this->loaded_bltables.erase(path); } void AssetCache::remove_palette(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_palettes.erase(flat_path); + this->loaded_palettes.erase(path); } void AssetCache::remove_terrain(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_terrains.erase(flat_path); + this->loaded_terrains.erase(path); } void AssetCache::remove_texture(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded_textures.erase(flat_path); + this->loaded_textures.erase(path); } bool AssetCache::check_animation_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_animations.contains(flat_path); + return this->loaded_animations.contains(path); } bool AssetCache::check_blpattern_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_blpatterns.contains(flat_path); + return this->loaded_blpatterns.contains(path); } bool AssetCache::check_bltable_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_bltables.contains(flat_path); + return this->loaded_bltables.contains(path); } bool AssetCache::check_palette_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_palettes.contains(flat_path); + return this->loaded_palettes.contains(path); } bool AssetCache::check_terrain_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_terrains.contains(flat_path); + return this->loaded_terrains.contains(path); } bool AssetCache::check_texture_cache(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - return this->loaded_textures.contains(flat_path); + return this->loaded_textures.contains(path); } } // namespace openage::renderer::resources diff --git a/libopenage/renderer/resources/assets/cache.h b/libopenage/renderer/resources/assets/cache.h index 504a02bfd2..34ec62567e 100644 --- a/libopenage/renderer/resources/assets/cache.h +++ b/libopenage/renderer/resources/assets/cache.h @@ -6,12 +6,10 @@ #include #include +#include "util/path.h" -namespace openage { -namespace util { -class Path; -} -namespace renderer::resources { + +namespace openage::renderer::resources { class Animation2dInfo; class BlendPatternInfo; class BlendTableInfo; @@ -91,12 +89,12 @@ class AssetCache { bool check_texture_cache(const util::Path &path); private: - using anim_cache_t = std::unordered_map>; - using blpattern_cache_t = std::unordered_map>; - using bltable_cache_t = std::unordered_map>; - using palette_cache_t = std::unordered_map>; - using terrain_cache_t = std::unordered_map>; - using texture_cache_t = std::unordered_map>; + using anim_cache_t = std::unordered_map>; + using blpattern_cache_t = std::unordered_map>; + using bltable_cache_t = std::unordered_map>; + using palette_cache_t = std::unordered_map>; + using terrain_cache_t = std::unordered_map>; + using texture_cache_t = std::unordered_map>; /** * Cache of already loaded animations. @@ -129,5 +127,4 @@ class AssetCache { texture_cache_t loaded_textures; }; -} // namespace renderer::resources -} // namespace openage +} // namespace openage::renderer::resources diff --git a/libopenage/renderer/resources/assets/texture_manager.cpp b/libopenage/renderer/resources/assets/texture_manager.cpp index 5cf3b2db0c..e7f9b9a4db 100644 --- a/libopenage/renderer/resources/assets/texture_manager.cpp +++ b/libopenage/renderer/resources/assets/texture_manager.cpp @@ -14,33 +14,30 @@ TextureManager::TextureManager(const std::shared_ptr &renderer) : } const std::shared_ptr &TextureManager::request(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - if (not this->loaded.contains(flat_path)) { + if (not this->loaded.contains(path)) { // create if not loaded auto tex_data = resources::Texture2dData(path); - this->loaded.insert({flat_path, this->renderer->add_texture(tex_data)}); + this->loaded.insert({path, this->renderer->add_texture(tex_data)}); } - return this->loaded.at(flat_path); + return this->loaded.at(path); } void TextureManager::add(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - if (not this->loaded.contains(flat_path)) { + if (not this->loaded.contains(path)) { // create if not loaded auto tex_data = resources::Texture2dData(path); - this->loaded.insert({flat_path, this->renderer->add_texture(tex_data)}); + this->loaded.insert({path, this->renderer->add_texture(tex_data)}); } } void TextureManager::add(const util::Path &path, const std::shared_ptr &texture) { auto flat_path = path.resolve_native_path(); - this->loaded.insert({flat_path, texture}); + this->loaded.insert({path, texture}); } void TextureManager::remove(const util::Path &path) { - auto flat_path = path.resolve_native_path(); - this->loaded.erase(flat_path); + this->loaded.erase(path); } void TextureManager::set_placeholder(const util::Path &path) { diff --git a/libopenage/renderer/resources/assets/texture_manager.h b/libopenage/renderer/resources/assets/texture_manager.h index 73c8318c6b..cf7dbdab60 100644 --- a/libopenage/renderer/resources/assets/texture_manager.h +++ b/libopenage/renderer/resources/assets/texture_manager.h @@ -99,7 +99,7 @@ class TextureManager { */ std::shared_ptr renderer; - using texture_cache_t = std::unordered_map>; + using texture_cache_t = std::unordered_map>; /** * Cache of already created textures. diff --git a/libopenage/renderer/shader_program.h b/libopenage/renderer/shader_program.h index 594f66dbc6..de515f213a 100644 --- a/libopenage/renderer/shader_program.h +++ b/libopenage/renderer/shader_program.h @@ -10,6 +10,7 @@ #include #include "renderer/resources/mesh_data.h" +#include "renderer/types.h" #include "renderer/uniform_input.h" @@ -18,20 +19,30 @@ namespace renderer { class Texture2d; class UniformBuffer; + class ShaderProgram : public std::enable_shared_from_this { friend UniformInput; public: virtual ~ShaderProgram() = default; + /** + * Get the ID of a uniform variable in the shader program. + * + * @param unif Name of the uniform. + * + * @return ID of the uniform in the shader. + */ + virtual uniform_id_t get_uniform_id(const char *name) = 0; + /** * Check whether the shader program contains a uniform variable with the given ID. * - * @param unif ID of the uniform. + * @param unif Name of the uniform. * * @return true if the shader program contains the uniform, false otherwise. */ - virtual bool has_uniform(const char *unif) = 0; + virtual bool has_uniform(const char *name) = 0; /** * Binds a uniform block in the shader program to the same binding point as @@ -99,6 +110,23 @@ class ShaderProgram : public std::enable_shared_from_this { virtual void set_v4ui32(std::shared_ptr const &, const char *, Eigen::Vector4 const &) = 0; virtual void set_m4f32(std::shared_ptr const &, const char *, Eigen::Matrix4f const &) = 0; virtual void set_tex(std::shared_ptr const &, const char *, std::shared_ptr const &) = 0; + + virtual void set_i32(std::shared_ptr const &, const uniform_id_t &, int32_t) = 0; + virtual void set_u32(std::shared_ptr const &, const uniform_id_t &, uint32_t) = 0; + virtual void set_f32(std::shared_ptr const &, const uniform_id_t &, float) = 0; + virtual void set_f64(std::shared_ptr const &, const uniform_id_t &, double) = 0; + virtual void set_bool(std::shared_ptr const &, const uniform_id_t &, bool) = 0; + virtual void set_v2f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2f const &) = 0; + virtual void set_v3f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3f const &) = 0; + virtual void set_v4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4f const &) = 0; + virtual void set_v2i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2i const &) = 0; + virtual void set_v3i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3i const &) = 0; + virtual void set_v4i32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4i const &) = 0; + virtual void set_v2ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector2 const &) = 0; + virtual void set_v3ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector3 const &) = 0; + virtual void set_v4ui32(std::shared_ptr const &, const uniform_id_t &, Eigen::Vector4 const &) = 0; + virtual void set_m4f32(std::shared_ptr const &, const uniform_id_t &, Eigen::Matrix4f const &) = 0; + virtual void set_tex(std::shared_ptr const &, const uniform_id_t &, std::shared_ptr const &) = 0; }; } // namespace renderer diff --git a/libopenage/renderer/stages/screen/screen_renderer.cpp b/libopenage/renderer/stages/screen/screen_renderer.cpp index a0dd110c8a..1fa045abcf 100644 --- a/libopenage/renderer/stages/screen/screen_renderer.cpp +++ b/libopenage/renderer/stages/screen/screen_renderer.cpp @@ -24,6 +24,8 @@ ScreenRenderer::ScreenRenderer(const std::shared_ptr & /* window */, renderer::opengl::GlContext::check_error(); this->initialize_render_pass(shaderdir); + + log::log(INFO << "Created render stage 'Screen'"); } std::shared_ptr ScreenRenderer::get_render_pass() { diff --git a/libopenage/renderer/stages/skybox/skybox_renderer.cpp b/libopenage/renderer/stages/skybox/skybox_renderer.cpp index b646bf7705..cc2777d324 100644 --- a/libopenage/renderer/stages/skybox/skybox_renderer.cpp +++ b/libopenage/renderer/stages/skybox/skybox_renderer.cpp @@ -28,6 +28,8 @@ SkyboxRenderer::SkyboxRenderer(const std::shared_ptr &window, window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { this->resize(width, height); }); + + log::log(INFO << "Created render stage 'Skybox'"); } std::shared_ptr SkyboxRenderer::get_render_pass() { diff --git a/libopenage/renderer/stages/terrain/terrain_model.cpp b/libopenage/renderer/stages/terrain/terrain_model.cpp index d663b19dae..4b73b556c9 100644 --- a/libopenage/renderer/stages/terrain/terrain_model.cpp +++ b/libopenage/renderer/stages/terrain/terrain_model.cpp @@ -37,6 +37,11 @@ void TerrainRenderModel::set_camera(const std::shared_ptrrender_entity) { + return; + } + // Check render entity for updates if (not this->render_entity->is_changed()) { return; diff --git a/libopenage/renderer/stages/terrain/terrain_renderer.cpp b/libopenage/renderer/stages/terrain/terrain_renderer.cpp index 1b8b2e7a7c..62b7de8207 100644 --- a/libopenage/renderer/stages/terrain/terrain_renderer.cpp +++ b/libopenage/renderer/stages/terrain/terrain_renderer.cpp @@ -37,6 +37,8 @@ TerrainRenderer::TerrainRenderer(const std::shared_ptr &window, }); this->model->set_camera(this->camera); + + log::log(INFO << "Created render stage 'Terrain'"); } std::shared_ptr TerrainRenderer::get_render_pass() { diff --git a/libopenage/renderer/stages/world/world_object.cpp b/libopenage/renderer/stages/world/world_object.cpp index 918578b4f0..79960d236c 100644 --- a/libopenage/renderer/stages/world/world_object.cpp +++ b/libopenage/renderer/stages/world/world_object.cpp @@ -90,7 +90,7 @@ void WorldObject::update_uniforms(const time::time_t &time) { // Object world position auto current_pos = this->position.get(time); - this->uniforms->update("obj_world_position", current_pos.to_world_space()); + this->uniforms->update(this->obj_world_position, current_pos.to_world_space()); // Direction angle the object is facing towards currently auto angle_degrees = this->angle.get(time).to_float(); @@ -102,10 +102,10 @@ void WorldObject::update_uniforms(const time::time_t &time) { // Flip subtexture horizontally if angle is mirrored if (angle->is_mirrored()) { - this->uniforms->update("flip_x", true); + this->uniforms->update(this->flip_x, true); } else { - this->uniforms->update("flip_x", false); + this->uniforms->update(this->flip_x, false); } // Current frame index considering current time @@ -132,11 +132,11 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto &tex_info = animation_info->get_texture(tex_idx); auto &tex_manager = this->asset_manager->get_texture_manager(); auto &texture = tex_manager->request(tex_info->get_image_path().value()); - this->uniforms->update("tex", texture); + this->uniforms->update(this->tex, texture); // Subtexture coordinates.inside texture auto coords = tex_info->get_subtex_info(subtex_idx).get_tile_params(); - this->uniforms->update("tile_params", coords); + this->uniforms->update(this->tile_params, coords); // scale and keep width x height ratio of texture // when the viewport size changes @@ -148,14 +148,14 @@ void WorldObject::update_uniforms(const time::time_t &time) { auto scale_vec = Eigen::Vector2f{ scale * (static_cast(subtex_size[0]) / screen_size[0]), scale * (static_cast(subtex_size[1]) / screen_size[1])}; - this->uniforms->update("scale", scale_vec); + this->uniforms->update(this->scale, scale_vec); // Move subtexture in scene so that its anchor point is at the object's position auto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params(); auto anchor_offset = Eigen::Vector2f{ scale * (static_cast(anchor[0]) / screen_size[0]), scale * (static_cast(anchor[1]) / screen_size[1])}; - this->uniforms->update("anchor_offset", anchor_offset); + this->uniforms->update(this->anchor_offset, anchor_offset); } uint32_t WorldObject::get_id() { diff --git a/libopenage/renderer/stages/world/world_object.h b/libopenage/renderer/stages/world/world_object.h index 9c08cddad5..2893084ed3 100644 --- a/libopenage/renderer/stages/world/world_object.h +++ b/libopenage/renderer/stages/world/world_object.h @@ -12,6 +12,7 @@ #include "curve/discrete.h" #include "curve/segmented.h" #include "renderer/resources/mesh_data.h" +#include "renderer/types.h" #include "time/time.h" @@ -114,6 +115,17 @@ class WorldObject { */ void set_uniforms(const std::shared_ptr &uniforms); + /** + * Shader uniform IDs for setting uniform values. + */ + inline static uniform_id_t obj_world_position; + inline static uniform_id_t flip_x; + inline static uniform_id_t flip_y; + inline static uniform_id_t tex; + inline static uniform_id_t tile_params; + inline static uniform_id_t scale; + inline static uniform_id_t anchor_offset; + private: /** * Stores whether a new renderable for this object needs to be created diff --git a/libopenage/renderer/stages/world/world_renderer.cpp b/libopenage/renderer/stages/world/world_renderer.cpp index 5882ef83d8..69bc67b54c 100644 --- a/libopenage/renderer/stages/world/world_renderer.cpp +++ b/libopenage/renderer/stages/world/world_renderer.cpp @@ -32,10 +32,13 @@ WorldRenderer::WorldRenderer(const std::shared_ptr &window, auto size = window->get_size(); this->initialize_render_pass(size[0], size[1], shaderdir); + this->init_uniform_ids(); window->add_resize_callback([this](size_t width, size_t height, double /*scale*/) { this->resize(width, height); }); + + log::log(INFO << "Created render stage 'World'"); } std::shared_ptr WorldRenderer::get_render_pass() { @@ -126,4 +129,14 @@ void WorldRenderer::initialize_render_pass(size_t width, this->render_pass = this->renderer->add_render_pass({}, fbo); } +void WorldRenderer::init_uniform_ids() { + WorldObject::obj_world_position = this->display_shader->get_uniform_id("obj_world_position"); + WorldObject::flip_x = this->display_shader->get_uniform_id("flip_x"); + WorldObject::flip_y = this->display_shader->get_uniform_id("flip_y"); + WorldObject::tex = this->display_shader->get_uniform_id("tex"); + WorldObject::tile_params = this->display_shader->get_uniform_id("tile_params"); + WorldObject::scale = this->display_shader->get_uniform_id("scale"); + WorldObject::anchor_offset = this->display_shader->get_uniform_id("anchor_offset"); +} + } // namespace openage::renderer::world diff --git a/libopenage/renderer/stages/world/world_renderer.h b/libopenage/renderer/stages/world/world_renderer.h index 0dc7195ebe..e7e9e0c596 100644 --- a/libopenage/renderer/stages/world/world_renderer.h +++ b/libopenage/renderer/stages/world/world_renderer.h @@ -89,6 +89,8 @@ class WorldRenderer { size_t height, const util::Path &shaderdir); + void init_uniform_ids(); + /** * Reference to the openage renderer. */ diff --git a/libopenage/renderer/types.cpp b/libopenage/renderer/types.cpp new file mode 100644 index 0000000000..8a10a58282 --- /dev/null +++ b/libopenage/renderer/types.cpp @@ -0,0 +1,10 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#include "types.h" + + +namespace openage::renderer { + +// this file is intentionally empty + +} // namespace openage::renderer diff --git a/libopenage/renderer/types.h b/libopenage/renderer/types.h new file mode 100644 index 0000000000..56fdee7b49 --- /dev/null +++ b/libopenage/renderer/types.h @@ -0,0 +1,15 @@ +// Copyright 2023-2023 the openage authors. See copying.md for legal info. + +#pragma once + +#include + + +namespace openage::renderer { + +/** + * IDs for shader uniforms. + */ +using uniform_id_t = uint32_t; + +} diff --git a/libopenage/renderer/uniform_input.cpp b/libopenage/renderer/uniform_input.cpp index 4385461ff0..8ad865ca04 100644 --- a/libopenage/renderer/uniform_input.cpp +++ b/libopenage/renderer/uniform_input.cpp @@ -81,6 +81,76 @@ void UniformInput::update(const char *unif, Eigen::Matrix4f const &val) { this->program->set_m4f32(this->shared_from_this(), unif, val); } + +void UniformInput::update(const uniform_id_t &id, int32_t val) { + this->program->set_i32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, uint32_t val) { + this->program->set_u32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, float val) { + this->program->set_f32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, double val) { + this->program->set_f64(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, bool val) { + this->program->set_bool(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector2f const &val) { + this->program->set_v2f32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector3f const &val) { + this->program->set_v3f32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector4f const &val) { + this->program->set_v4f32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector2i const &val) { + this->program->set_v2i32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector3i const &val) { + this->program->set_v3i32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector4i const &val) { + this->program->set_v4i32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector2 const &val) { + this->program->set_v2ui32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector3 const &val) { + this->program->set_v3ui32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Vector4 const &val) { + this->program->set_v4ui32(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, std::shared_ptr const &val) { + this->program->set_tex(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, std::shared_ptr &val) { + this->program->set_tex(this->shared_from_this(), id, val); +} + +void UniformInput::update(const uniform_id_t &id, Eigen::Matrix4f const &val) { + this->program->set_m4f32(this->shared_from_this(), id, val); +} + + UniformBufferInput::UniformBufferInput(std::shared_ptr const &buffer) : buffer{buffer} {} diff --git a/libopenage/renderer/uniform_input.h b/libopenage/renderer/uniform_input.h index 85b197e4fa..515a0a0e4e 100644 --- a/libopenage/renderer/uniform_input.h +++ b/libopenage/renderer/uniform_input.h @@ -9,6 +9,7 @@ #include "error/error.h" #include "log/message.h" +#include "renderer/types.h" #include "util/compiler.h" @@ -85,6 +86,24 @@ class UniformInput : public DataInput void update(const char *unif, std::shared_ptr &val) override; void update(const char *unif, Eigen::Matrix4f const &val) override; + void update(const uniform_id_t &id, int32_t val); + void update(const uniform_id_t &id, uint32_t val); + void update(const uniform_id_t &id, float val); + void update(const uniform_id_t &id, double val); + void update(const uniform_id_t &id, bool val); + void update(const uniform_id_t &id, Eigen::Vector2f const &val); + void update(const uniform_id_t &id, Eigen::Vector3f const &val); + void update(const uniform_id_t &id, Eigen::Vector4f const &val); + void update(const uniform_id_t &id, Eigen::Vector2i const &val); + void update(const uniform_id_t &id, Eigen::Vector3i const &val); + void update(const uniform_id_t &id, Eigen::Vector4i const &val); + void update(const uniform_id_t &id, Eigen::Vector2 const &val); + void update(const uniform_id_t &id, Eigen::Vector3 const &val); + void update(const uniform_id_t &id, Eigen::Vector4 const &val); + void update(const uniform_id_t &id, std::shared_ptr const &val); + void update(const uniform_id_t &id, std::shared_ptr &val); + void update(const uniform_id_t &id, Eigen::Matrix4f const &val); + /** * Catch-all template in order to handle unsupported types and avoid infinite recursion. * @@ -97,6 +116,18 @@ class UniformInput : public DataInput << "' using unsupported type '" << util::typestring() << "'"); } + /** + * Catch-all template in order to handle unsupported types and avoid infinite recursion. + * + * @param unif ID of the uniform. + */ + template + void update(const uniform_id_t &id, T) { + // TODO: maybe craft an static_assert that contains the `unif` content + throw Error(MSG(err) << "Tried to set uniform with ID " << id + << " using unsupported type '" << util::typestring() << "'"); + } + /** * Updates the given uniform input with new uniform values similarly to new_uniform_input. * For example, update_uniform_input(in, "awesome", true) will set the "awesome" uniform @@ -110,6 +141,19 @@ class UniformInput : public DataInput this->update(vals...); } + /** + * Updates the given uniform input with new uniform values similarly to new_uniform_input. + * For example, update_uniform_input(in, "awesome", true) will set the "awesome" uniform + * in addition to whatever values were in the uniform input before. + * + * @param unif ID of the uniform. + */ + template + void update(const uniform_id_t &id, T val, Ts... vals) { + this->update(id, val); + this->update(vals...); + } + /** * Get the shader program the uniform input is used for. * diff --git a/libopenage/renderer/vulkan/windowvk.cpp b/libopenage/renderer/vulkan/windowvk.cpp index 99593f905d..fe1ef44296 100644 --- a/libopenage/renderer/vulkan/windowvk.cpp +++ b/libopenage/renderer/vulkan/windowvk.cpp @@ -1,4 +1,4 @@ -// Copyright 2017-2022 the openage authors. See copying.md for legal info. +// Copyright 2017-2023 the openage authors. See copying.md for legal info. #include "windowvk.h" @@ -35,15 +35,15 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL vlk_debug_cb( #endif /// Queries the Vulkan implementation for available extensions and layers. -static vlk_capabilities find_capabilities() { - vlk_capabilities caps; +static vlk_spec find_spec() { + vlk_spec specs; // Find which layers are available. auto layers = vk_do_ritual(vkEnumerateInstanceLayerProperties); log::log(MSG(dbg) << "Available Vulkan layers:"); for (auto const &lr : layers) { - caps.layers.insert(lr.layerName); + specs.layers.insert(lr.layerName); log::log(MSG(dbg) << "\t" << lr.layerName); } @@ -56,7 +56,7 @@ static vlk_capabilities find_capabilities() { auto props = vk_do_ritual(vkEnumerateInstanceExtensionProperties, nullptr); for (auto const &p : props) { - caps.extensions.emplace(p.extensionName); + specs.extensions.emplace(p.extensionName); } // Then retrieve extensions from layers. @@ -64,20 +64,20 @@ static vlk_capabilities find_capabilities() { auto lr_props = vk_do_ritual(vkEnumerateInstanceExtensionProperties, lr.layerName); for (auto const &p : lr_props) { - caps.extensions.emplace(p.extensionName); + specs.extensions.emplace(p.extensionName); } } log::log(MSG(dbg) << "Available Vulkan extensions:"); - for (const auto &ext : caps.extensions) { + for (const auto &ext : specs.extensions) { log::log(MSG(dbg) << "\t" << ext); } - return caps; + return specs; } VlkWindow::VlkWindow(const char *title, size_t width, size_t height) : - Window(width, height), capabilities(find_capabilities()) { + Window(width, height), capabilities(find_spec()) { std::vector extension_names; #ifndef NDEBUG diff --git a/libopenage/renderer/vulkan/windowvk.h b/libopenage/renderer/vulkan/windowvk.h index 0978c1302c..e4788f6e54 100644 --- a/libopenage/renderer/vulkan/windowvk.h +++ b/libopenage/renderer/vulkan/windowvk.h @@ -18,7 +18,7 @@ namespace openage { namespace renderer { namespace vulkan { -struct vlk_capabilities { +struct vlk_spec { /// Names of available layers. std::set layers; /// Names of available extensions. @@ -37,7 +37,7 @@ class VlkWindow : public openage::renderer::Window { VkSurfaceKHR get_surface() const; private: - vlk_capabilities capabilities; + vlk_spec capabilities; VkInstance instance; VkSurfaceKHR surface; diff --git a/libopenage/renderer/window.cpp b/libopenage/renderer/window.cpp index f3bcdc8ac8..5cdd3b2052 100644 --- a/libopenage/renderer/window.cpp +++ b/libopenage/renderer/window.cpp @@ -12,11 +12,12 @@ namespace openage::renderer { std::shared_ptr Window::create(const std::string &title, size_t width, - size_t height) { + size_t height, + bool debug) { // currently we only have a functional GL window // TODO: support other renderer windows // and add some selection mechanism. - return std::make_shared(title, width, height); + return std::make_shared(title, width, height, debug); } diff --git a/libopenage/renderer/window.h b/libopenage/renderer/window.h index 3ef4c3686b..18c7ce04d8 100644 --- a/libopenage/renderer/window.h +++ b/libopenage/renderer/window.h @@ -28,11 +28,14 @@ class Window { * @param title Window title shown in the Desktop Environment. * @param width Width in pixels. * @param height Height in pixels. + * @param debug If true, enable OpenGL debug logging. + * * @return The created Window instance. */ static std::shared_ptr create(const std::string &title, size_t width, - size_t height); + size_t height, + bool debug = false); virtual ~Window() = default; diff --git a/libopenage/time/time_loop.h b/libopenage/time/time_loop.h index 0611b5b30e..b48916d4e4 100644 --- a/libopenage/time/time_loop.h +++ b/libopenage/time/time_loop.h @@ -10,7 +10,7 @@ namespace openage::time { class Clock; /** - * Manage and update internal time via a clock. + * Manages the passage of simulation time and real time. */ class TimeLoop { public: diff --git a/libopenage/util/compress/lzxd.cpp b/libopenage/util/compress/lzxd.cpp index 6d3230a997..49100f2bf4 100644 --- a/libopenage/util/compress/lzxd.cpp +++ b/libopenage/util/compress/lzxd.cpp @@ -35,14 +35,14 @@ namespace openage::util::compress { constexpr unsigned LZX_MIN_MATCH = 2; //constexpr unsigned LZX_MAX_MATCH = 257; // seems to be unused. I'm scared. constexpr unsigned LZX_NUM_CHARS = 256; -constexpr unsigned LZX_BLOCKTYPE_INVALID = 0; // also blocktypes 4-7 invalid +constexpr unsigned LZX_BLOCKTYPE_INVALID = 0; // also blocktypes 4-7 invalid constexpr unsigned LZX_BLOCKTYPE_VERBATIM = 1; constexpr unsigned LZX_BLOCKTYPE_ALIGNED = 2; constexpr unsigned LZX_BLOCKTYPE_UNCOMPRESSED = 3; constexpr unsigned LZX_PRETREE_NUM_ELEMENTS = 20; constexpr unsigned LZX_ALIGNED_NUM_ELEMENTS = 8; -constexpr unsigned LZX_NUM_PRIMARY_LENGTHS = 7; // missing from spec! -constexpr unsigned LZX_NUM_SECONDARY_LENGTHS = 249; // length tree #elements +constexpr unsigned LZX_NUM_PRIMARY_LENGTHS = 7; // missing from spec! +constexpr unsigned LZX_NUM_SECONDARY_LENGTHS = 249; // length tree #elements // LZX huffman constants: tweak tablebits as desired constexpr unsigned LZX_PRETREE_MAXSYMBOLS = LZX_PRETREE_NUM_ELEMENTS; @@ -53,7 +53,7 @@ constexpr unsigned LZX_LENGTH_MAXSYMBOLS = LZX_NUM_SECONDARY_LENGTHS + 1; constexpr unsigned LZX_LENGTH_TABLEBITS = 12; constexpr unsigned LZX_ALIGNED_MAXSYMBOLS = LZX_ALIGNED_NUM_ELEMENTS; constexpr unsigned LZX_ALIGNED_TABLEBITS = 7; -constexpr unsigned LZX_LENTABLE_SAFETY = 64; // table decoding overruns are allowed +constexpr unsigned LZX_LENTABLE_SAFETY = 64; // table decoding overruns are allowed /* @@ -75,21 +75,13 @@ constexpr unsigned LZX_LENTABLE_SAFETY = 64; // table decoding overruns */ static const unsigned position_base[] = { - 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, - 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, - 16384, 24576, 32768, 49152, 65536, 98304, 131072, 196608, 262144, - 393216, 524288, 655360, 786432, 917504, 1048576, 1179648, 1310720, - 1441792, 1572864, 1703936, 1835008, 1966080, 2097152 -}; + 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360, 786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936, 1835008, 1966080, 2097152}; static const unsigned char extra_bits[] = { - 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, - 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, - 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17 -}; + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}; -template +template class HuffmanTable { private: class LZXDStream *lzx; @@ -119,7 +111,8 @@ class HuffmanTable { */ bool is_empty; - explicit HuffmanTable(class LZXDStream *lzx) : lzx{lzx}, is_empty{true} {} + explicit HuffmanTable(class LZXDStream *lzx) : + lzx{lzx}, is_empty{true} {} /** * Decodes the next huffman symbol from the input bitstream, @@ -153,26 +146,26 @@ class HuffmanTable { class LZXDStream { public: - ssize_t output_pos; // number of output bytes + ssize_t output_pos; // number of output bytes - unsigned char *window; // decoding window - unsigned int window_size; // window size - unsigned int window_posn; // decompression offset within window - unsigned int frame_posn; // current frame offset within in window - unsigned int frame; // the number of 32kb frames processed - unsigned int reset_interval; // which frame do we reset the compressor? + unsigned char *window; // decoding window + unsigned int window_size; // window size + unsigned int window_posn; // decompression offset within window + unsigned int frame_posn; // current frame offset within in window + unsigned int frame; // the number of 32kb frames processed + unsigned int reset_interval; // which frame do we reset the compressor? - unsigned int R0, R1, R2; // for the LRU offset system - unsigned int block_length; // uncompressed length of this LZX block - unsigned int block_remaining; // uncompressed bytes still left to decode + unsigned int R0, R1, R2; // for the LRU offset system + unsigned int block_length; // uncompressed length of this LZX block + unsigned int block_remaining; // uncompressed bytes still left to decode - signed int e8_magic; // magic value that's used by the intel E8 transform. - // this field is read from the first 33 bits of the stream - // after reset, and called intel_filesize in libmspack. - bool header_read; // indicates whether the e8_magic header has been read. + signed int e8_magic; // magic value that's used by the intel E8 transform. + // this field is read from the first 33 bits of the stream + // after reset, and called intel_filesize in libmspack. + bool header_read; // indicates whether the e8_magic header has been read. - unsigned char block_type; // type of the current block - unsigned char posn_slots; // how many posn slots in stream? + unsigned char block_type; // type of the current block + unsigned char posn_slots; // how many posn slots in stream? // IO buffering BitStream<4096> bits; @@ -189,11 +182,12 @@ class LZXDStream { LZXDStream(const LZXDStream &other) = delete; LZXDStream(LZXDStream &&other) = delete; - LZXDStream &operator =(const LZXDStream &other) = delete; - LZXDStream &operator =(LZXDStream &&other) = delete; + LZXDStream &operator=(const LZXDStream &other) = delete; + LZXDStream &operator=(LZXDStream &&other) = delete; /** See the doc for LZXDecompressor::decompress_next_frame in lzxd.h */ unsigned decompress_next_frame(unsigned char *output_buf); + private: /** * Initializes the next block. @@ -230,12 +224,13 @@ class LZXDStream { * What the hell, Microsoft? */ void postprocess_intel_e8(unsigned char *output_buf, int frame_size); + public: void reset_state(); }; -template +template int HuffmanTable::read_sym() { lzx->bits.ensure_bits(HUFF_MAXBITS); uint16_t sym = table[lzx->bits.peek_bits(tablebits)]; @@ -254,11 +249,12 @@ int HuffmanTable::read_sym() { } -template +template void HuffmanTable::make_decode_table() { if (this->try_make_decode_table()) { this->is_empty = false; - } else { + } + else { if (allow_empty) { // allow an empty tree, but don't decode symbols with it @@ -269,14 +265,15 @@ void HuffmanTable::make_decode_table() { } } this->is_empty = true; - } else { + } + else { throw Error(MSG(err) << "failed to build HuffmanTable"); } } } -template +template bool HuffmanTable::try_make_decode_table() { uint16_t sym, next_symbol; unsigned int leaf, fill; @@ -333,14 +330,14 @@ bool HuffmanTable::try_make_decode_table for (fill = 0; fill < (bit_num - tablebits); fill++) { // if this path hasn't been taken yet, 'allocate' two entries if (table[leaf] == 0xFFFF) { - table[(next_symbol << 1) + 0] = 0xFFFF; - table[(next_symbol << 1) + 1] = 0xFFFF; - table[leaf] = next_symbol++; + table[(next_symbol << 1) + 0] = 0xFFFF; + table[(next_symbol << 1) + 1] = 0xFFFF; + table[leaf] = next_symbol++; } // follow the path and select either left or right for next bit leaf = table[leaf] << 1; - if ((pos >> (15-fill)) & 1) + if ((pos >> (15 - fill)) & 1) leaf++; } table[leaf] = sym; @@ -359,7 +356,7 @@ bool HuffmanTable::try_make_decode_table } -template +template void HuffmanTable::read_lengths(unsigned int first, unsigned int last) { // bit buffer and huffman symbol decode variables unsigned int x, y; @@ -372,7 +369,7 @@ void HuffmanTable::read_lengths(unsigned lzx->htpre.make_decode_table(); - for (x = first; x < last; ) { + for (x = first; x < last;) { z = lzx->htpre.read_sym(); if (z == 17) { // code = 17, run of ([read 4 bits]+4) zeros @@ -417,20 +414,18 @@ void HuffmanTable::read_lengths(unsigned LZXDStream::LZXDStream(read_callback_t read_callback, unsigned int window_bits, - unsigned int reset_interval) - : - output_pos{0}, - window_size{static_cast(1) << window_bits}, - window_posn{0}, - frame_posn{0}, - frame{0}, - reset_interval{reset_interval}, - bits{std::move(read_callback)}, - htpre{this}, - htmain{this}, - htlength{this}, - htaligned{this} { - + unsigned int reset_interval) : + output_pos{0}, + window_size{static_cast(1) << window_bits}, + window_posn{0}, + frame_posn{0}, + frame{0}, + reset_interval{reset_interval}, + bits{std::move(read_callback)}, + htpre{this}, + htmain{this}, + htlength{this}, + htaligned{this} { // LZX supports window sizes of 2^15 (32 KiB) through 2^21 (2 MiB) if (window_bits < 15 || window_bits > 21) { throw Error(MSG(err) << "Bad requested window size: 2^" << window_bits << " bytes"); @@ -590,7 +585,7 @@ int LZXDStream::decode_symbol_from_verbatim_block() { if (match_offset > this->window_posn) { // j = length from match offset to end of window int j = match_offset - this->window_posn; - if (j > (int) this->window_size) [[unlikely]] { + if (j > (int)this->window_size) [[unlikely]] { throw Error(MSG(err) << "decrunch: match offset beyond window boundaries"); } unsigned char *runsrc = &window[this->window_size - j]; @@ -605,7 +600,8 @@ int LZXDStream::decode_symbol_from_verbatim_block() { while (i-- > 0) { *rundest++ = *runsrc++; } - } else { + } + else { unsigned char *runsrc = rundest - match_offset; while (i-- > 0) { *rundest++ = *runsrc++; @@ -613,7 +609,7 @@ int LZXDStream::decode_symbol_from_verbatim_block() { } this->window_posn += match_length; - return match_length; + return match_length; } @@ -700,7 +696,7 @@ int LZXDStream::decode_symbol_from_aligned_block() { if (match_offset > this->window_posn) { // j = length from match offset to end of window int j = match_offset - this->window_posn; - if (j > (int) this->window_size) [[unlikely]] { + if (j > (int)this->window_size) [[unlikely]] { throw Error(MSG(err) << "decrunch: match offset beyond window boundaries"); } unsigned char *runsrc = &window[this->window_size - j]; @@ -715,7 +711,8 @@ int LZXDStream::decode_symbol_from_aligned_block() { while (i-- > 0) { *rundest++ = *runsrc++; } - } else { + } + else { unsigned char *runsrc = rundest - match_offset; while (i-- > 0) { *rundest++ = *runsrc++; @@ -723,7 +720,7 @@ int LZXDStream::decode_symbol_from_aligned_block() { } this->window_posn += match_length; - return match_length; + return match_length; } @@ -762,7 +759,8 @@ unsigned LZXDStream::decompress_next_frame(unsigned char *output_buf) { // read e8_magic (32 bits). this->e8_magic = this->bits.read_bits(16) << 16; this->e8_magic |= this->bits.read_bits(16); - } else { + } + else { // e8_magic is zero. this->e8_magic = 0; } @@ -778,9 +776,8 @@ unsigned LZXDStream::decompress_next_frame(unsigned char *output_buf) { // In theory, a symbol may have overshot the last frame's boundary. // If it did, the amount of data would be available in frame_size. - throw Error(MSG(err) << - "untested code path: extra frame data available from last frame. " << - "frame size = " << this->frame_posn - this->window_posn); + throw Error(MSG(err) << "untested code path: extra frame data available from last frame. " + << "frame size = " << this->frame_posn - this->window_posn); } // decode symbols until we have enough data for the frame. @@ -793,7 +790,14 @@ unsigned LZXDStream::decompress_next_frame(unsigned char *output_buf) { break; } - this->init_next_block(); + try { + this->init_next_block(); + } + catch (Error &e) { + // next block is beyond end of input stream + // TODO: Figure out why this error happens + break; + } } unsigned int symbol_size; @@ -891,22 +895,20 @@ void LZXDStream::postprocess_intel_e8(unsigned char *buf, int frame_size) { // read the next 4 bytes as little endian int32_t abs_offset = - (buf[pos + 1] << 0) | - (buf[pos + 2] << 8) | - (buf[pos + 3] << 16) | - (buf[pos + 4] << 24); + (buf[pos + 1] << 0) | (buf[pos + 2] << 8) | (buf[pos + 3] << 16) | (buf[pos + 4] << 24); if (abs_offset >= -file_pos && abs_offset < this->e8_magic) { int32_t rel_offset; if (abs_offset >= 0) { rel_offset = abs_offset - file_pos; - } else { + } + else { rel_offset = abs_offset + this->e8_magic; } // write the next 4 bytes as little endian - buf[pos + 1] = static_cast(rel_offset >> 0); - buf[pos + 2] = static_cast(rel_offset >> 8); + buf[pos + 1] = static_cast(rel_offset >> 0); + buf[pos + 2] = static_cast(rel_offset >> 8); buf[pos + 3] = static_cast(rel_offset >> 16); buf[pos + 4] = static_cast(rel_offset >> 24); } @@ -919,8 +921,7 @@ void LZXDStream::postprocess_intel_e8(unsigned char *buf, int frame_size) { LZXDecompressor::LZXDecompressor(read_callback_t read_callback, unsigned int window_bits, - unsigned int reset_interval) - : + unsigned int reset_interval) : stream{new LZXDStream{std::move(read_callback), window_bits, reset_interval}} {} @@ -934,4 +935,4 @@ unsigned LZXDecompressor::decompress_next_frame(unsigned char *output_buf) { } -} // openage::util::compress +} // namespace openage::util::compress diff --git a/libopenage/util/path.cpp b/libopenage/util/path.cpp index c0c57894b9..ff23253533 100644 --- a/libopenage/util/path.cpp +++ b/libopenage/util/path.cpp @@ -343,6 +343,13 @@ const Path::parts_t &Path::get_parts() const { return this->parts; } +size_t Path::get_hash() const { + if (not this->hash.has_value()) { + this->hash = std::hash{}(this->get_native_path()); + } + return this->hash.value(); +} + std::ostream &operator<<(std::ostream &stream, const Path &path) { if (path.fsobj == nullptr) { diff --git a/libopenage/util/path.h b/libopenage/util/path.h index c67c079c89..d16e1734c6 100644 --- a/libopenage/util/path.h +++ b/libopenage/util/path.h @@ -1,4 +1,4 @@ -// Copyright 2015-2019 the openage authors. See copying.md for legal info. +// Copyright 2015-2023 the openage authors. See copying.md for legal info. #pragma once @@ -12,6 +12,8 @@ // pxd: from libcpp.vector cimport vector #include +#include + // pxd: from libopenage.pyinterface.pyobject cimport PyObj #include "../pyinterface/pyobject.h" #include "file.h" @@ -19,8 +21,7 @@ // pxd: from libopenage.util.fslike.fslike cimport FSLike -namespace openage { -namespace util { +namespace openage::util { namespace fslike { class FSLike; @@ -45,7 +46,6 @@ class FSLike; */ class OAAPI Path { public: - /** * Storage type for a part of a path access. * Basically this is the name of a file or directory. @@ -72,18 +72,17 @@ class OAAPI Path { * You will probably never call that manually. */ Path(const py::Obj &fslike, - const parts_t &parts={}); + const parts_t &parts = {}); /** * Construct a path from a fslike pointer. */ Path(std::shared_ptr fslike, - const parts_t &parts={}); + const parts_t &parts = {}); virtual ~Path() = default; public: - bool exists() const; bool is_file() const; bool is_dir() const; @@ -91,7 +90,7 @@ class OAAPI Path { std::vector list(); std::vector iterdir(); bool mkdirs(); - File open(const std::string &mode="r") const; + File open(const std::string &mode = "r") const; File open_r() const; File open_w() const; File open_rw() const; @@ -106,7 +105,7 @@ class OAAPI Path { * * This basically is the same as resolve_*().get_native_path(). */ - std::string resolve_native_path(const std::string &mode="r") const; + std::string resolve_native_path(const std::string &mode = "r") const; std::string resolve_native_path_r() const; std::string resolve_native_path_w() const; @@ -135,19 +134,24 @@ class OAAPI Path { Path joinpath(const parts_t &subpaths) const; Path joinpath(const part_t &subpath) const; - Path operator [](const parts_t &subpaths) const; - Path operator [](const part_t &subpath) const; - Path operator /(const part_t &subpath) const; + Path operator[](const parts_t &subpaths) const; + Path operator[](const part_t &subpath) const; + Path operator/(const part_t &subpath) const; Path with_name(const part_t &name) const; Path with_suffix(const part_t &suffix) const; - bool operator ==(const Path &other) const; - bool operator !=(const Path &other) const; + bool operator==(const Path &other) const; + bool operator!=(const Path &other) const; fslike::FSLike *get_fsobj() const; const parts_t &get_parts() const; + // compute hash from the resolved native path + // the hash value is cashed after first access to avoid + // further Python calls + size_t get_hash() const; + private: /** * Return the native path @@ -161,13 +165,18 @@ class OAAPI Path { */ std::string get_native_path() const; + // TODO: Find a better method that works without Python access + // Cached hash value based on resolved native path + // computed lazily on first access to avoid expensive + // Python calls + mutable std::optional hash; protected: std::shared_ptr fsobj; parts_t parts; - friend std::ostream &operator <<(std::ostream &stream, const Path &path); + friend std::ostream &operator<<(std::ostream &stream, const Path &path); }; @@ -184,4 +193,15 @@ std::string filename(const std::string &fullpath); std::string dirname(const std::string &fullpath); -}} // openage::util +} // namespace openage::util + +namespace std { + +template <> +struct hash { + size_t operator()(const openage::util::Path &path) const { + return path.get_hash(); + } +}; + +} // namespace std diff --git a/openage/CMakeLists.txt b/openage/CMakeLists.txt index fb3937b34c..a9233357dd 100644 --- a/openage/CMakeLists.txt +++ b/openage/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(cppinterface) add_subdirectory(cvar) add_subdirectory(event) add_subdirectory(game) +add_subdirectory(gamestate) add_subdirectory(log) add_subdirectory(main) add_subdirectory(nyan) diff --git a/openage/cabextract/cab.py b/openage/cabextract/cab.py index f8b55ce53c..3f04d45947 100644 --- a/openage/cabextract/cab.py +++ b/openage/cabextract/cab.py @@ -230,11 +230,11 @@ class CABFile(FileCollection): descriptions. Most CAB file issues should cause the constructor to fail. """ - def __init__(self, cab: FileLikeObject): + def __init__(self, cab: FileLikeObject, offset: int = 0): super().__init__() # read header - cab.seek(0) + cab.seek(offset) header = CFHeader.read(cab) # verify magic number @@ -264,12 +264,12 @@ def __init__(self, cab: FileLikeObject): dbg(header) self.header = header - self.folders = tuple(self.read_folder_headers(cab)) + self.folders = tuple(self.read_folder_headers(cab, offset)) # {filename: fileobj}, {subdirname: subdir} self.rootdir = OrderedDict(), OrderedDict() - for fileobj in self.read_file_headers(cab): + for fileobj in self.read_file_headers(cab, offset): if self.is_file(fileobj.path) or self.is_dir(fileobj.path): raise ValueError( "CABFile has multiple entries with the same path: " + @@ -293,7 +293,11 @@ def open_r(fileobj=fileobj): def __repr__(self): return "CABFile" - def read_folder_headers(self, cab: FileLikeObject) -> Generator[CFFolder, None, None]: + def read_folder_headers( + self, + cab: FileLikeObject, + offset: int + ) -> Generator[CFFolder, None, None]: """ Called during the constructor run. @@ -314,7 +318,7 @@ def read_folder_headers(self, cab: FileLikeObject) -> Generator[CFFolder, None, # create compressed data stream compressed_data_stream = CABFolderStream( cab, - folder.coffCabStart, + folder.coffCabStart + offset, folder.cCFData, self.header.reserved_data.cbCFData) @@ -351,15 +355,15 @@ def read_folder_headers(self, cab: FileLikeObject) -> Generator[CFFolder, None, dbg(folder) yield folder - def read_file_headers(self, cab: FileLikeObject) -> Generator[CFFile, None, None]: + def read_file_headers(self, cab: FileLikeObject, offset: int) -> Generator[CFFile, None, None]: """ Called during the constructor run. Reads the headers for all files and yields CFFile objects. """ # seek to the correct position - if cab.tell() != self.header.coffFiles: - cab.seek(self.header.coffFiles) + if cab.tell() != self.header.coffFiles + offset: + cab.seek(self.header.coffFiles + offset) dbg("cabfile has nonstandard format: seek to header.coffFiles was required") for _ in range(self.header.cFiles): diff --git a/openage/convert/entity_object/conversion/aoc/genie_object_container.py b/openage/convert/entity_object/conversion/aoc/genie_object_container.py index 510cb5e8cb..c4a04376b9 100644 --- a/openage/convert/entity_object/conversion/aoc/genie_object_container.py +++ b/openage/convert/entity_object/conversion/aoc/genie_object_container.py @@ -59,7 +59,7 @@ def __init__(self): # Auxiliary self.strings: StringResource = None - self.existing_graphics: list[str] = None + self.existing_graphics: set[str] = None # Phase 1: Genie-like objects # ConverterObject types (the data from the game) diff --git a/openage/convert/entity_object/export/metadata_export.py b/openage/convert/entity_object/export/metadata_export.py index 2230395dee..a4d2fd35ed 100644 --- a/openage/convert/entity_object/export/metadata_export.py +++ b/openage/convert/entity_object/export/metadata_export.py @@ -56,15 +56,18 @@ def add_graphics_metadata( replay_delay: float, frame_count: int, angle_count: int, - mirror_mode: int + mirror_mode: int, + start_angle: int = 0, ): """ Add metadata from the GenieGraphic object. :param tex_filename: Filename of the .texture file. + :param start_angle: Angle used for the first frame in the .texture file. """ self.graphics_metadata[img_filename] = (tex_filename, layer_mode, layer_pos, frame_rate, - replay_delay, frame_count, angle_count, mirror_mode) + replay_delay, frame_count, angle_count, mirror_mode, + start_angle) def dump(self) -> str: """ @@ -73,26 +76,37 @@ def dump(self) -> str: sprite_file = SpriteMetadata(self.targetdir, self.filename) tex_index = 0 + + # if len(self.graphics_metadata) == 0: + # raise ValueError("No graphics metadata in sprite file.") + for img_filename, metadata in self.graphics_metadata.items(): tex_filename = metadata[0] sprite_file.add_texture(tex_index, tex_filename) sprite_file.add_layer(tex_index, *metadata[1:5]) - degree = 0 frame_count = metadata[5] angle_count = metadata[6] mirror_mode = metadata[7] + start_angle = metadata[8] if angle_count == 0: angle_count = 1 + degree = 0 + if start_angle and angle_count > 1: + # set the angle of the first frame in the sprite list + # in openage, angles are defined clockwise, with 0 being the + # front-facing angle (i.e. the unit sprite faces the camera) + degree = start_angle % 360 + degree_step = 360 / angle_count for angle_index in range(angle_count): mirror_from = None if mirror_mode: if degree > 180: mirrored_angle = (angle_index - angle_count) * (-1) - mirror_from = int(mirrored_angle * degree_step) + mirror_from = (start_angle + int(mirrored_angle * degree_step)) % 360 sprite_file.add_angle(int(degree), mirror_from) @@ -111,7 +125,7 @@ def dump(self) -> str: subtex_index ) - degree += degree_step + degree = (degree + degree_step) % 360 tex_index += 1 diff --git a/openage/convert/main.py b/openage/convert/main.py index e56fad58d9..d9461eaeec 100644 --- a/openage/convert/main.py +++ b/openage/convert/main.py @@ -9,11 +9,9 @@ from datetime import datetime import typing -from ..log import info, err from ..util.fslike.directory import CaseIgnoringDirectory from ..util.fslike.wrapper import (DirectoryCreator, Synchronizer as AccessSynchronizer) -from ..util.strings import format_progress from .service.debug_info import debug_cli_args, debug_game_version, debug_mounts from .service.init.conversion_required import conversion_required from .service.init.mount_asset_dirs import mount_asset_dirs @@ -25,14 +23,14 @@ if typing.TYPE_CHECKING: from argparse import ArgumentParser, Namespace from openage.util.fslike.directory import Directory + from openage.util.fslike.path import Path def convert_assets( assets: Directory, args: Namespace, - srcdir: Directory = None, - prev_source_dir_path: str = None -) -> str: + srcdir: Directory = None +) -> None: """ Perform asset conversion. @@ -47,10 +45,6 @@ def convert_assets( This method prepares srcdir and targetdir to allow a pleasant, unified conversion experience, then passes them to .driver.convert(). """ - # acquire conversion source directory - if srcdir is None: - srcdir = acquire_conversion_source_dir(prev_source_dir_path) - converted_path = assets / "converted" converted_path.mkdirs() targetdir = DirectoryCreator(converted_path).root @@ -79,6 +73,14 @@ def convert_assets( auxiliary_files_dir = args.cfg_dir / "converter" / "games" args.avail_game_eds, args.avail_game_exps = create_version_objects(auxiliary_files_dir) + # try to get previously used source dir + asset_locations_path = assets / "converted" / "asset_locations.cache" + prev_srcdirs = get_prev_srcdir_paths(asset_locations_path) + + # acquire conversion source directory + if srcdir is None: + srcdir = acquire_conversion_source_dir(args.avail_game_eds, prev_srcdirs) + # Acquire game version info args.game_version = get_game_version(srcdir, args.avail_game_eds, args.avail_game_exps) debug_game_version(args.debugdir, args.debug_info, args) @@ -110,29 +112,45 @@ def flag(name): # import here so codegen.py doesn't depend on it. from .tool.driver import convert - converted_count = 0 - total_count = None - for current_item in convert(args): - if isinstance(current_item, int): - # convert is informing us about the estimated number of remaining - # items. - total_count = current_item + converted_count - continue - - # TODO a GUI would be nice here. - - if total_count is None: - info("[%s] %s", converted_count, current_item) - else: - info("[%s] %s", format_progress(converted_count, total_count), current_item) - - converted_count += 1 + # Run the conversion process + convert(args) # clean args del args.srcdir del args.targetdir - return data_dir.resolve_native_path() + # Remember the asset location if it is not already in the cache + if prev_srcdirs is None: + asset_locations_path.touch() + prev_srcdirs = set() + + used_asset_path = data_dir.resolve_native_path().decode('utf-8') + if used_asset_path not in prev_srcdirs: + with asset_locations_path.open("a") as file_obj: + if len(prev_srcdirs) > 0: + file_obj.write("\n") + + file_obj.write(used_asset_path) + + +def get_prev_srcdir_paths(asset_location_path: Path) -> set[str] | None: + """ + Get previously used source directories from a cache file. + + :param asset_location_path: Path to the cache file. + :type asset_location_path: Path + :return: Previously used source directories. + :rtype: set[str] | None + """ + prev_source_dirs: set[str] = set() + try: + with asset_location_path.open("r") as file_obj: + prev_source_dirs.update(file_obj.read().split("\n")) + + except FileNotFoundError: + prev_source_dirs = None + + return prev_source_dirs def init_subparser(cli: ArgumentParser): @@ -241,10 +259,8 @@ def main(args, error): from ..assets import get_asset_path outdir = get_asset_path(args.output_dir) - if args.force or wanna_convert() or conversion_required(outdir, args): - if not convert_assets(outdir, args, srcdir): - err("game asset conversion failed") - return 1 + if args.force or wanna_convert() or conversion_required(outdir): + convert_assets(outdir, args, srcdir) else: print("assets are up to date; no conversion is required.") diff --git a/openage/convert/processor/conversion/CMakeLists.txt b/openage/convert/processor/conversion/CMakeLists.txt index 36c71a759e..3fbf0528f4 100644 --- a/openage/convert/processor/conversion/CMakeLists.txt +++ b/openage/convert/processor/conversion/CMakeLists.txt @@ -3,6 +3,7 @@ add_py_modules( ) add_subdirectory(aoc) +add_subdirectory(aoc_demo) add_subdirectory(de1) add_subdirectory(de2) add_subdirectory(hd) diff --git a/openage/convert/processor/conversion/aoc/ability_subprocessor.py b/openage/convert/processor/conversion/aoc/ability_subprocessor.py index 93620a740f..8e53371b2f 100644 --- a/openage/convert/processor/conversion/aoc/ability_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/ability_subprocessor.py @@ -35,6 +35,9 @@ from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup +FLOAT32_MAX = 3.4028234663852886e+38 + + class AoCAbilitySubprocessor: """ Creates raw API objects for abilities in AoC. @@ -6968,6 +6971,12 @@ def transfer_storage_ability(line: GenieGameEntityGroup) -> ForwardRef: break + else: + garrisoned = line.garrison_entities[0] + storage_name = name_lookup_dict[garrisoned.get_id()][0] + storage_entity = garrisoned + garrisoned_forward_ref = ForwardRef(storage_entity, storage_name) + ability_raw_api_object.add_raw_member("storage_element", garrisoned_forward_ref, "engine.ability.type.TransferStorage") @@ -7040,7 +7049,9 @@ def turn_ability(line: GenieGameEntityGroup) -> ForwardRef: # Ships/Trebuchets turn slower if turn_speed_unmodified > 0: turn_yaw = current_unit["max_yaw_per_sec_moving"].value - turn_speed = degrees(turn_yaw) + + if not turn_yaw == FLOAT32_MAX: + turn_speed = degrees(turn_yaw) ability_raw_api_object.add_raw_member("turn_speed", turn_speed, diff --git a/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py b/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py index f413f1fc15..f8c185d5a8 100644 --- a/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods # @@ -1914,6 +1914,9 @@ def min_range_upgrade( patch_target_forward_ref = ForwardRef(line, patch_target_ref) patch_target_parent = "engine.ability.type.RangedDiscreteEffect" + else: + return [] + # Wrapper wrapper_name = f"Change{game_entity_name}MinRangeWrapper" wrapper_ref = f"{obj_name}.{wrapper_name}" diff --git a/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py index e470c21cb6..477c52ad1e 100644 --- a/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods,invalid-name # @@ -1155,6 +1155,31 @@ def reveal_enemy_upgrade( return patches + @staticmethod + def siege_conversion_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Any, + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the siege conversion effect (ID: 29). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: Any + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO + + return patches + @staticmethod def ship_conversion_upgrade( converter_group: ConverterObjectGroup, diff --git a/openage/convert/processor/conversion/aoc_demo/CMakeLists.txt b/openage/convert/processor/conversion/aoc_demo/CMakeLists.txt new file mode 100644 index 0000000000..ad5d23b8b9 --- /dev/null +++ b/openage/convert/processor/conversion/aoc_demo/CMakeLists.txt @@ -0,0 +1,5 @@ +add_py_modules( + __init__.py + modpack_subprocessor.py + processor.py +) diff --git a/openage/convert/processor/conversion/aoc_demo/__init__.py b/openage/convert/processor/conversion/aoc_demo/__init__.py new file mode 100644 index 0000000000..74bc2ab9d0 --- /dev/null +++ b/openage/convert/processor/conversion/aoc_demo/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. + +""" +Drives the conversion process for AoE2: The Conquerors Demo. +""" diff --git a/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py new file mode 100644 index 0000000000..ffd171214e --- /dev/null +++ b/openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py @@ -0,0 +1,50 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-few-public-methods + +""" +Organize export data (nyan objects, media, scripts, etc.) +into modpacks. +""" +from __future__ import annotations +import typing + +from ....entity_object.conversion.modpack import Modpack +from ..aoc.modpack_subprocessor import AoCModpackSubprocessor + +if typing.TYPE_CHECKING: + from openage.convert.entity_object.conversion.aoc.genie_object_container\ + import GenieObjectContainer + + +class DemoModpackSubprocessor: + """ + Creates the modpacks containing the nyan files and media from the AoC Demo conversion. + """ + + @classmethod + def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]: + """ + Return all modpacks that can be created from the gamedata. + """ + demo_base = cls._get_demo_base(full_data_set) + + return [demo_base] + + @classmethod + def _get_demo_base(cls, full_data_set: GenieObjectContainer) -> Modpack: + """ + Create the aoe2-base modpack. + """ + modpack = Modpack("trial_base") + + mod_def = modpack.get_info() + + mod_def.set_info("trial_base", "0.5", versionstr="Trial", repo="openage") + + mod_def.add_include("data/**") + + AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set) + AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set) + + return modpack diff --git a/openage/convert/processor/conversion/aoc_demo/processor.py b/openage/convert/processor/conversion/aoc_demo/processor.py new file mode 100644 index 0000000000..a147ffe9e6 --- /dev/null +++ b/openage/convert/processor/conversion/aoc_demo/processor.py @@ -0,0 +1,92 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-few-public-methods + +""" +Convert data from the AoC Demo to openage formats. +""" +from __future__ import annotations +import typing + +from .....log import info +from ....service.debug_info import debug_converter_objects, \ + debug_converter_object_groups +from ..aoc.processor import AoCProcessor +from ..aoc.nyan_subprocessor import AoCNyanSubprocessor +from ..aoc.media_subprocessor import AoCMediaSubprocessor +from .modpack_subprocessor import DemoModpackSubprocessor + + +if typing.TYPE_CHECKING: + from argparse import Namespace + from openage.convert.entity_object.conversion.stringresource import StringResource + from openage.convert.entity_object.conversion.modpack import Modpack + from openage.convert.value_object.read.value_members import ArrayMember + from openage.convert.entity_object.conversion.aoc.genie_object_container \ + import GenieObjectContainer + + +class DemoProcessor: + """ + Main processor for converting data from the AoC Demo. + """ + + @classmethod + def convert( + cls, + gamespec: ArrayMember, + args: Namespace, + string_resources: StringResource, + existing_graphics: list[str] + ) -> list[Modpack]: + """ + Input game speification and media here and get a set of + modpacks back. + + :param gamespec: Gamedata from empires.dat read in by the + reader functions. + :type gamespec: ...dataformat.value_members.ArrayMember + :returns: A list of modpacks. + :rtype: list + """ + # pylint: disable=protected-access + + info("Starting conversion...") + + # Create a new container for the conversion process + dataset = AoCProcessor._pre_processor( + gamespec, + args.game_version, + string_resources, + existing_graphics + ) + debug_converter_objects(args.debugdir, args.debug_info, dataset) + + # Create the custom openae formats (nyan, sprite, terrain) + dataset = AoCProcessor._processor(dataset) + debug_converter_object_groups(args.debugdir, args.debug_info, dataset) + + # Create modpack definitions + modpacks = cls._post_processor(dataset) + + return modpacks + + @classmethod + def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]: + """ + Convert API-like Python objects to nyan. + + :param full_data_set: GenieObjectContainer instance that + contains all relevant data for the conversion + process. + :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer + """ + info("Creating nyan objects...") + + AoCNyanSubprocessor.convert(full_data_set) + + info("Creating requests for media export...") + + AoCMediaSubprocessor.convert(full_data_set) + + return DemoModpackSubprocessor.get_modpacks(full_data_set) diff --git a/openage/convert/processor/conversion/de1/CMakeLists.txt b/openage/convert/processor/conversion/de1/CMakeLists.txt index 6d28c7f735..c2fb63366c 100644 --- a/openage/convert/processor/conversion/de1/CMakeLists.txt +++ b/openage/convert/processor/conversion/de1/CMakeLists.txt @@ -1,5 +1,6 @@ add_py_modules( __init__.py media_subprocessor.py + modpack_subprocessor.py processor.py ) diff --git a/openage/convert/processor/conversion/de1/media_subprocessor.py b/openage/convert/processor/conversion/de1/media_subprocessor.py index de20eeb211..b1a4f8ece1 100644 --- a/openage/convert/processor/conversion/de1/media_subprocessor.py +++ b/openage/convert/processor/conversion/de1/media_subprocessor.py @@ -105,7 +105,7 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: frame_count = graphic["frame_count"].value angle_count = graphic["angle_count"].value - mirror_mode = graphic["mirroring_mode"].value + # mirror_mode = graphic["mirroring_mode"].value sprite_meta_export.add_graphics_metadata(target_filename, texture_meta_filename, layer_mode, @@ -114,9 +114,10 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: replay_delay, frame_count, angle_count, - mirror_mode) + mirror_mode=0, + start_angle=270) - # Notify metadata export about SMX metadata when the file is exported + # Notify metadata export about SLP metadata when the file is exported export_request.add_observer(texture_meta_export) export_request.add_observer(sprite_meta_export) diff --git a/openage/convert/processor/conversion/de1/modpack_subprocessor.py b/openage/convert/processor/conversion/de1/modpack_subprocessor.py new file mode 100644 index 0000000000..9cc3f5a95d --- /dev/null +++ b/openage/convert/processor/conversion/de1/modpack_subprocessor.py @@ -0,0 +1,50 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-few-public-methods + +""" +Organize export data (nyan objects, media, scripts, etc.) +into modpacks. +""" +from __future__ import annotations +import typing + +from ....entity_object.conversion.modpack import Modpack +from ..aoc.modpack_subprocessor import AoCModpackSubprocessor + +if typing.TYPE_CHECKING: + from openage.convert.entity_object.conversion.aoc.genie_object_container\ + import GenieObjectContainer + + +class DE1ModpackSubprocessor: + """ + Creates the modpacks containing the nyan files and media from the DE2 conversion. + """ + + @classmethod + def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]: + """ + Return all modpacks that can be created from the gamedata. + """ + de1_base = cls._get_aoe1_base(full_data_set) + + return [de1_base] + + @classmethod + def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack: + """ + Create the aoe2-base modpack. + """ + modpack = Modpack("de1_base") + + mod_def = modpack.get_info() + + mod_def.set_info("de1_base", "0.5", versionstr="1.0a", repo="openage") + + mod_def.add_include("data/**") + + AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set) + AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set) + + return modpack diff --git a/openage/convert/processor/conversion/de1/processor.py b/openage/convert/processor/conversion/de1/processor.py index d6726903e2..c505c68467 100644 --- a/openage/convert/processor/conversion/de1/processor.py +++ b/openage/convert/processor/conversion/de1/processor.py @@ -14,11 +14,11 @@ debug_converter_object_groups from ....service.read.nyan_api_loader import load_api from ..aoc.processor import AoCProcessor -from ..ror.modpack_subprocessor import RoRModpackSubprocessor from ..ror.nyan_subprocessor import RoRNyanSubprocessor from ..ror.pregen_subprocessor import RoRPregenSubprocessor from ..ror.processor import RoRProcessor from .media_subprocessor import DE1MediaSubprocessor +from .modpack_subprocessor import DE1ModpackSubprocessor if typing.TYPE_CHECKING: from argparse import Namespace @@ -170,7 +170,7 @@ def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]: DE1MediaSubprocessor.convert(full_data_set) - return RoRModpackSubprocessor.get_modpacks(full_data_set) + return DE1ModpackSubprocessor.get_modpacks(full_data_set) @staticmethod def extract_genie_graphics(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None: @@ -201,7 +201,7 @@ def extract_genie_graphics(gamespec: ArrayMember, full_data_set: GenieObjectCont graphic_members = raw_graphic.value graphic = GenieGraphic(graphic_id, full_data_set, members=graphic_members) - if filename not in full_data_set.existing_graphics: + if filename.lower() not in full_data_set.existing_graphics: graphic.exists = False full_data_set.genie_graphics.update({graphic.get_id(): graphic}) diff --git a/openage/convert/processor/conversion/de2/media_subprocessor.py b/openage/convert/processor/conversion/de2/media_subprocessor.py index 797590f19d..df16babc35 100644 --- a/openage/convert/processor/conversion/de2/media_subprocessor.py +++ b/openage/convert/processor/conversion/de2/media_subprocessor.py @@ -97,7 +97,7 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: frame_count = graphic["frame_count"].value angle_count = graphic["angle_count"].value - mirror_mode = graphic["mirroring_mode"].value + # mirror_mode = graphic["mirroring_mode"].value sprite_meta_export.add_graphics_metadata(target_filename, texture_meta_filename, layer_mode, @@ -106,7 +106,8 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: replay_delay, frame_count, angle_count, - mirror_mode) + mirror_mode=0, + start_angle=270) # Notify metadata export about SMX metadata when the file is exported export_request.add_observer(texture_meta_export) diff --git a/openage/convert/processor/conversion/de2/tech_subprocessor.py b/openage/convert/processor/conversion/de2/tech_subprocessor.py index 025d67b090..2f2aaf9e76 100644 --- a/openage/convert/processor/conversion/de2/tech_subprocessor.py +++ b/openage/convert/processor/conversion/de2/tech_subprocessor.py @@ -63,6 +63,10 @@ class DE2TechSubprocessor: 57: AoCUpgradeAttributeSubprocessor.kidnap_storage_upgrade, 30: DE2UpgradeAttributeSubprocessor.herdable_capacity_upgrade, + 59: DE2UpgradeAttributeSubprocessor.charge_attack_upgrade, + 60: DE2UpgradeAttributeSubprocessor.charge_regen_upgrade, + 61: DE2UpgradeAttributeSubprocessor.charge_event_upgrade, + 62: DE2UpgradeAttributeSubprocessor.charge_type_upgrade, 63: AoCUpgradeAttributeSubprocessor.ignore_armor_upgrade, 100: AoCUpgradeAttributeSubprocessor.resource_cost_upgrade, 101: AoCUpgradeAttributeSubprocessor.creation_time_upgrade, @@ -75,6 +79,9 @@ class DE2TechSubprocessor: 108: AoCUpgradeAttributeSubprocessor.garrison_heal_upgrade, 109: DE2UpgradeAttributeSubprocessor.regeneration_rate_upgrade, 110: DE2UpgradeAttributeSubprocessor.villager_pop_space_upgrade, + 111: DE2UpgradeAttributeSubprocessor.min_convert_upgrade, + 112: DE2UpgradeAttributeSubprocessor.max_convert_upgrade, + 113: DE2UpgradeAttributeSubprocessor.convert_chance_upgrade, } upgrade_resource_funcs = { @@ -85,6 +92,7 @@ class DE2TechSubprocessor: 4: AoCUpgradeResourceSubprocessor.starting_population_space_upgrade, 27: AoCUpgradeResourceSubprocessor.monk_conversion_upgrade, 28: AoCUpgradeResourceSubprocessor.building_conversion_upgrade, + 29: AoCUpgradeResourceSubprocessor.siege_conversion_upgrade, 32: AoCUpgradeResourceSubprocessor.bonus_population_upgrade, 35: AoCUpgradeResourceSubprocessor.faith_recharge_rate_upgrade, 36: AoCUpgradeResourceSubprocessor.farm_food_upgrade, @@ -144,6 +152,10 @@ class DE2TechSubprocessor: 254: DE2UpgradeResourceSubprocessor.herdable_garrison_upgrade, 262: DE2UpgradeResourceSubprocessor.bengali_conversion_resistance_upgrade, 266: DE2UpgradeResourceSubprocessor.doi_paper_money_upgrade, + 267: DE2UpgradeResourceSubprocessor.forager_wood_gather_upgrade, + 268: DE2UpgradeResourceSubprocessor.resource_decay_upgrade, + 269: DE2UpgradeResourceSubprocessor.tech_reward_upgrade, + 274: DE2UpgradeResourceSubprocessor.chieftains_upgrade, } @classmethod diff --git a/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py b/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py index c5d7a66ecf..f60ee8eb64 100644 --- a/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py +++ b/openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=too-few-public-methods # @@ -24,6 +24,146 @@ class DE2UpgradeAttributeSubprocessor: Creates raw API objects for attribute upgrade effects in DE2. """ + @staticmethod + def charge_attack_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the charge attack modify effect (ID: 59). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def charge_event_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the charge event modify effect (ID: 61). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def charge_regen_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the charge regen modify effect (ID: 60). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def charge_type_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the charge type modify effect (ID: 62). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def convert_chance_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the convert chance modify effect (ID: 113). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def herdable_capacity_upgrade( converter_group: ConverterObjectGroup, @@ -54,6 +194,62 @@ def herdable_capacity_upgrade( return patches + @staticmethod + def min_convert_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the min convert interval modify effect (ID: 111). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + + @staticmethod + def max_convert_upgrade( + converter_group: ConverterObjectGroup, + line: GenieGameEntityGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the max convert interval modify effect (ID: 112). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param line: Unit/Building line that has the ability. + :type line: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: int, float + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def regeneration_rate_upgrade( converter_group: ConverterObjectGroup, diff --git a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py index a5b6a0b4fb..a394061fa1 100644 --- a/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py +++ b/openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods,invalid-name # @@ -73,6 +73,31 @@ def burgundian_vineyards_upgrade( return patches + @staticmethod + def chieftains_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for activating looting (ID: 274). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def cliff_attack_upgrade( converter_group: ConverterObjectGroup, @@ -574,6 +599,31 @@ def folwark_mill_id_upgrade( return patches + @staticmethod + def forager_wood_gather_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the portugese forage wood gather effect (ID: 267). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def free_kipchaks_upgrade( converter_group: ConverterObjectGroup, @@ -649,6 +699,31 @@ def relic_food_production_upgrade( return patches + @staticmethod + def resource_decay_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the resource decay modifier effect (ID: 268). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def reveal_enemy_tcs_upgrade( converter_group: ConverterObjectGroup, @@ -726,6 +801,31 @@ def stone_gold_gen_upgrade( return patches + @staticmethod + def tech_reward_upgrade( + converter_group: ConverterObjectGroup, + value: typing.Union[int, float], + operator: MemberOperator, + team: bool = False + ) -> list[ForwardRef]: + """ + Creates a patch for the spanish tech reward effect (ID: 269). + + :param converter_group: Tech/Civ that gets the patch. + :type converter_group: ...dataformat.converter_object.ConverterObjectGroup + :param value: Value used for patching the member. + :type value: MemberOperator + :param operator: Operator used for patching the member. + :type operator: MemberOperator + :returns: The forward references for the generated patches. + :rtype: list + """ + patches = [] + + # TODO: Implement + + return patches + @staticmethod def trade_food_bonus_upgrade( converter_group: ConverterObjectGroup, diff --git a/openage/convert/processor/conversion/hd/media_subprocessor.py b/openage/convert/processor/conversion/hd/media_subprocessor.py index 737f698c79..935643efa0 100644 --- a/openage/convert/processor/conversion/hd/media_subprocessor.py +++ b/openage/convert/processor/conversion/hd/media_subprocessor.py @@ -45,9 +45,9 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: graphic_targetdirs = sprite.resolve_graphics_location() metadata_filename = f"{sprite.get_filename()}.{'sprite'}" - metadata_export = SpriteMetadataExport(sprite.resolve_sprite_location(), - metadata_filename) - full_data_set.metadata_exports.append(metadata_export) + sprite_meta_export = SpriteMetadataExport(sprite.resolve_sprite_location(), + metadata_filename) + full_data_set.metadata_exports.append(sprite_meta_export) for graphic in ref_graphics: graphic_id = graphic.get_id() @@ -97,18 +97,19 @@ def create_graphics_requests(full_data_set: GenieObjectContainer) -> None: frame_count = graphic["frame_count"].value angle_count = graphic["angle_count"].value mirror_mode = graphic["mirroring_mode"].value - metadata_export.add_graphics_metadata(target_filename, - texture_meta_filename, - layer_mode, - layer_pos, - frame_rate, - replay_delay, - frame_count, - angle_count, - mirror_mode) + sprite_meta_export.add_graphics_metadata(target_filename, + texture_meta_filename, + layer_mode, + layer_pos, + frame_rate, + replay_delay, + frame_count, + angle_count, + mirror_mode) # Notify metadata export about SLP metadata when the file is exported - export_request.add_observer(metadata_export) + export_request.add_observer(texture_meta_export) + export_request.add_observer(sprite_meta_export) handled_graphic_ids.add(graphic_id) diff --git a/openage/convert/processor/export/texture_merge.pyx b/openage/convert/processor/export/texture_merge.pyx index 7aa01e34ee..f726d0af02 100644 --- a/openage/convert/processor/export/texture_merge.pyx +++ b/openage/convert/processor/export/texture_merge.pyx @@ -68,10 +68,10 @@ cdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) exc else: if packer_type == PackerType.ROW: - packer = BestPacker([RowPacker(margin=MARGIN, aspect_ratio=1)]) + packer = BestPacker([RowPacker(margin=MARGIN)]) elif packer_type == PackerType.COLUMN: - packer = BestPacker([ColumnPacker(margin=MARGIN, aspect_ratio=1)]) + packer = BestPacker([ColumnPacker(margin=MARGIN)]) elif packer_type == PackerType.BINPACK: packer = BestPacker([BinaryTreePacker(margin=MARGIN, aspect_ratio=1)]) diff --git a/openage/convert/service/export/png/binpack.pyx b/openage/convert/service/export/png/binpack.pyx index 6be570b72e..30e39cd95f 100644 --- a/openage/convert/service/export/png/binpack.pyx +++ b/openage/convert/service/export/png/binpack.pyx @@ -1,4 +1,4 @@ -# Copyright 2016-2022 the openage authors. See copying.md for legal info. +# Copyright 2016-2023 the openage authors. See copying.md for legal info. # # cython: infer_types=True,profile=False # TODO pylint: disable=C,R @@ -239,7 +239,7 @@ cdef class BinaryTreePacker(Packer): block.height + self.margin) if node != NULL: - res = self.split_node(node, + node = self.split_node(node, block.width + self.margin, block.height + self.margin) else: diff --git a/openage/convert/service/init/CMakeLists.txt b/openage/convert/service/init/CMakeLists.txt index 5600eac8b2..3c7e404c69 100644 --- a/openage/convert/service/init/CMakeLists.txt +++ b/openage/convert/service/init/CMakeLists.txt @@ -1,7 +1,9 @@ add_py_modules( __init__.py + api_export_required.py changelog.py conversion_required.py + modpack_search.py mount_asset_dirs.py version_detect.py ) diff --git a/openage/convert/service/init/api_export_required.py b/openage/convert/service/init/api_export_required.py new file mode 100644 index 0000000000..fe677ec246 --- /dev/null +++ b/openage/convert/service/init/api_export_required.py @@ -0,0 +1,45 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. + +""" +Test whether the openage nyan API modpack is present. +""" +from __future__ import annotations +import typing + +import toml + +from ....log import info, dbg + +from .modpack_search import get_modpack_info + +if typing.TYPE_CHECKING: + from openage.util.fslike.directory import Directory + + +CURRENT_API_VERSION = "0.4.0" + + +def api_export_required(asset_dir: Directory) -> bool: + """ + Returns true if the openage nyan API modpack cannot be found. + + TODO: Remove once the API modpack is generated by default. + """ + modpack_dir = asset_dir / "converted" / "engine" + + try: + modpack_info = get_modpack_info(modpack_dir) + version = modpack_info["info"]["version"] + + if version != CURRENT_API_VERSION: + info("openage nyan API modpack is outdated") + dbg("version is %s, expected %s", version, CURRENT_API_VERSION) + return True + + info("openage nyan API modpack is up to date") + return False + + except (FileNotFoundError, TypeError, toml.TomlDecodeError): + info("openage nyan API modpack not found") + + return True diff --git a/openage/convert/service/init/conversion_required.py b/openage/convert/service/init/conversion_required.py index 7f144bd834..0c09b4ea67 100644 --- a/openage/convert/service/init/conversion_required.py +++ b/openage/convert/service/init/conversion_required.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. """ Test whether there already are converted modpacks present. @@ -6,66 +6,48 @@ from __future__ import annotations import typing -from . import changelog -from ....log import info, dbg +from ....log import info -if typing.TYPE_CHECKING: - from argparse import Namespace +from .modpack_search import enumerate_modpacks +if typing.TYPE_CHECKING: from openage.util.fslike.directory import Directory -def conversion_required(asset_dir: Directory, args: Namespace) -> bool: +def conversion_required(asset_dir: Directory) -> bool: """ - Returns true if an asset conversion is required to run the game. + Check if an asset conversion is required to run the game. - Sets options in args according to what sorts of conversion are required. + Asset conversions are required if: + - the modpack folder does not exist + - no modpacks inside the modpack folder exist + - the converted assets are outdated - TODO: Reimplement change detection for new converter. + :param asset_dir: The asset directory to check. + :type asset_dir: Directory + :return: True if an asset conversion is required, else False. """ - version_path = asset_dir / 'converted' / changelog.ASSET_VERSION_FILENAME - # determine the version of assets try: - with version_path.open() as fileobj: - asset_version = fileobj.read().strip() - - try: - asset_version = int(asset_version) - except ValueError: - info("Converted asset version has improper format; " - "expected integer number") - asset_version = -1 + # TODO: Do not use hardcoded path to "converted" directory. + # The mod directory should be its own configurable path + modpacks = enumerate_modpacks(asset_dir / "converted") except FileNotFoundError: - # assets have not been converted yet - info("No converted assets have been found") - asset_version = -1 - - changes = changelog.changes(asset_version,) + # modpack folder not created yet + modpacks = set() - if not changes: - dbg("Converted assets are up to date") - return False - - if asset_version >= 0 and asset_version != changelog.ASSET_VERSION: - info("Found converted assets with version %d, " - "but need version %d", asset_version, changelog.ASSET_VERSION) - - info("Converting %s", ", ".join(sorted(changes))) - - # try to resolve resolve the output path - target_path = asset_dir.resolve_native_path_w() - if not target_path: - raise OSError(f"could not resolve a writable asset path in {asset_dir}") - - info("Will save to '%s'", target_path.decode(errors="replace")) + if not modpacks or \ + len(modpacks) == 1 and "engine" in modpacks: # engine is not usable on its own + info("No converted assets have been found") + return True - for component in changelog.COMPONENTS: - if component not in changes: - # don't reconvert this component: - setattr(args, f"no_{component}", True) + # TODO: Check if modpack version of converted assets are up to date + # if not changes: + # dbg("Converted assets are up to date") + # return False - if "metadata" in changes: - args.no_pickle_cache = True + # if asset_version >= 0 and asset_version != changelog.ASSET_VERSION: + # info("Found converted assets with version %d, " + # "but need version %d", asset_version, changelog.ASSET_VERSION) - return True + return False diff --git a/openage/convert/service/init/modpack_search.py b/openage/convert/service/init/modpack_search.py new file mode 100644 index 0000000000..986d25be2b --- /dev/null +++ b/openage/convert/service/init/modpack_search.py @@ -0,0 +1,102 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. + +""" +Search for and enumerate openage modpacks. +""" +from __future__ import annotations +import typing + +import toml + +from ....log import info, dbg + +if typing.TYPE_CHECKING: + from openage.util.fslike.directory import Directory + + +def enumerate_modpacks(modpacks_dir: Directory) -> set[str]: + """ + Enumerate openage modpacks in a directory. + + :param asset_dir: The asset directory to search in. + :type asset_dir: Directory + :returns: A list of modpack names that were found. + :rtype: set[str] + """ + if not modpacks_dir.exists(): + info("openage modpack directory has not been created yet") + raise FileNotFoundError("openage modpack directory not found") + + modpacks: set[str] = set() + for check_dir in modpacks_dir.iterdir(): + if check_dir.is_dir(): + try: + modpack_info = get_modpack_info(check_dir) + modpack_name = modpack_info["info"]["name"] + info("Found modpack %s", modpack_name) + modpacks.add(modpack_name) + + except (FileNotFoundError, TypeError, toml.TomlDecodeError): + dbg("No modpack found in directory: %s", check_dir) + + return modpacks + + +def get_modpack_info(modpack_dir: Directory) -> dict[str, typing.Any]: + """ + Get information about an openage modpack from its definition file. + + :param modpack_dir: Modpack root directory. + :type modpack_dir: Directory + :returns: Modpack information. + :rtype: dict[str, typing.Any] + + :raises FileNotFoundError: If the modpack definition file could not be found. + :raises TypeError: If the modpack definition file could not be parsed. + :raises toml.TomlDecodeError: If the modpack definition file is malformed. + """ + if not modpack_dir.exists(): + info("Modpack directory %s not found", modpack_dir.root.name) + raise FileNotFoundError("Modpack directory not found") + + modpack_def = modpack_dir / "modpack.toml" + dbg("Checking modpack definition file %s", modpack_def) + try: + with modpack_def.open() as fileobj: + content = toml.loads(fileobj.read()) + + return content + + except FileNotFoundError as err: + dbg("Modpack definition file not found; ncould not find %s", modpack_def) + raise err + + except TypeError as err: + dbg("Cannot parse modpack definition file %s; content is not a string", modpack_def) + raise err + + except toml.TomlDecodeError as err: + dbg("Cannot parse modpack definition file %s; content is not TOML or malformed", + modpack_def) + raise err + + +def query_modpack(proposals: set[str]) -> str: + """ + Query interactively for a modpack from a selection of proposals. + """ + print("\nPlease select a modpack before starting.") + print("Insert the index of one of the proposals (Default = 0):") + + proposals = sorted(proposals) + for index, proposal in enumerate(proposals): + print(f"({index}) {proposal}") + + user_selection = input("> ") + if user_selection.isdecimal() and int(user_selection) < len(proposals): + selection = proposals[int(user_selection)] + + else: + selection = proposals[0] + + return selection diff --git a/openage/convert/service/init/version_detect.py b/openage/convert/service/init/version_detect.py index 70d157d979..792ce9708b 100644 --- a/openage/convert/service/init/version_detect.py +++ b/openage/convert/service/init/version_detect.py @@ -192,6 +192,11 @@ def create_game_obj( game_mediapaths.append( (media_type, game_info['mediapaths'][media_type])) + # add install dirs for the game + game_installpaths = {} + if 'installpaths' in game_info: + game_installpaths = game_info['installpaths'] + # add version hashes from the auxiliary file specific for the game game_hash_path = aux_path["version_hashes.toml"] with game_hash_path.open() as game_hash_toml: @@ -229,4 +234,5 @@ def create_game_obj( game_mediapaths, modpacks, **flags) return GameEdition(game_name, game_id, support, game_version_info, - game_mediapaths, modpacks, expansions, **flags) + game_mediapaths, game_installpaths, modpacks, + expansions, **flags) diff --git a/openage/convert/service/read/palette.py b/openage/convert/service/read/palette.py index 303e63e8f0..315d1ca38f 100644 --- a/openage/convert/service/read/palette.py +++ b/openage/convert/service/read/palette.py @@ -1,4 +1,6 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-many-branches """ Module for reading palette files. @@ -80,4 +82,8 @@ def get_palettes( palettes[palette_id] = palette + else: + raise RuntimeError("no valid palette converter found for game edition" + f"{game_edition.edition_name}") + return palettes diff --git a/openage/convert/service/read/register_media.py b/openage/convert/service/read/register_media.py index dae9a4d2c9..761a5cf9e2 100644 --- a/openage/convert/service/read/register_media.py +++ b/openage/convert/service/read/register_media.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. """ Module for registering media files. @@ -13,12 +13,12 @@ from argparse import Namespace -def get_existing_graphics(args: Namespace) -> list[str]: +def get_existing_graphics(args: Namespace) -> set[str]: """ List the graphics files that exist in the graphics file paths. """ - filenames = [] + filenames = set() for filepath in args.srcdir[MediaType.GRAPHICS.value].iterdir(): - filenames.append(filepath.stem) + filenames.add(filepath.stem) return filenames diff --git a/openage/convert/tool/api_export.py b/openage/convert/tool/api_export.py index 3972195153..36f85abafd 100644 --- a/openage/convert/tool/api_export.py +++ b/openage/convert/tool/api_export.py @@ -11,7 +11,7 @@ from openage.convert.service.read.nyan_api_loader import load_api from openage.nyan.import_tree import ImportTree from openage.util.fslike.directory import Directory -from openage.util.fslike.union import Union +from openage.util.fslike.union import Union, UnionPath from openage.util.fslike.wrapper import (DirectoryCreator, Synchronizer as AccessSynchronizer) @@ -32,13 +32,24 @@ def main(args, error): """ del error # unused + path = Union().root + path.mount(Directory(args.dir)) + + export_api(path) + + +def export_api(exportdir: UnionPath) -> None: + """ + Export the nyan API of the engine to the target directory. + + :param exportdir: The target directory for the modpack folder. + :type exportdir: Directory + """ modpack = create_modpack() info("Dumping info file...") - path = Union().root - path.mount(Directory(args.dir).root) - targetdir = DirectoryCreator(path).root + targetdir = DirectoryCreator(exportdir).root outdir = AccessSynchronizer(targetdir).root / "engine" # Modpack info file @@ -57,6 +68,9 @@ def main(args, error): def create_modpack() -> Modpack: """ Create the nyan API as a modpack. + + :return: The modpack containing the nyan API. + :rtype: Modpack """ modpack = Modpack("engine") @@ -74,6 +88,9 @@ def create_modpack() -> Modpack: def create_nyan_files(modpack: Modpack) -> None: """ Create the nyan files from the API objects. + + :param modpack: The modpack to add the nyan files to. + :type modpack: Modpack """ api_objects = load_api() created_nyan_files: dict[str, NyanFile] = {} @@ -108,5 +125,8 @@ def create_nyan_files(modpack: Modpack) -> None: def set_static_aliases(import_tree: ImportTree) -> None: """ Create the import tree for the nyan files. + + :param import_tree: The import tree to add the aliases to. + :type import_tree: ImportTree """ import_tree.add_alias(("engine", "root"), "root") diff --git a/openage/convert/tool/driver.py b/openage/convert/tool/driver.py index c056f8c3b1..a1b8bbfddf 100644 --- a/openage/convert/tool/driver.py +++ b/openage/convert/tool/driver.py @@ -1,4 +1,6 @@ # Copyright 2015-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-many-return-statements """ Receives cleaned-up srcdir and targetdir objects from .main, and drives the @@ -25,17 +27,12 @@ from openage.convert.value_object.init.game_version import GameVersion -def convert(args: Namespace) -> typing.Generator[str, None, None]: +def convert(args: Namespace) -> None: """ args must hold srcdir and targetdir (FS-like objects), plus any additional configuration options. - - Yields progress information in the form of: - strings (filenames) that indicate the currently-converted object - ints that predict the amount of objects remaining """ - # data conversion - yield from convert_metadata(args) + convert_metadata(args) # with args.targetdir[GAMESPEC_VERSION_FILENAME].open('w') as fil: # fil.write(EmpiresDat.get_hash(args.game_version)) @@ -45,7 +42,7 @@ def convert(args: Namespace) -> typing.Generator[str, None, None]: info(f"asset conversion complete; asset version: {ASSET_VERSION}", ) -def convert_metadata(args: Namespace) -> typing.Generator[str, None, None]: +def convert_metadata(args: Namespace) -> None: """ Converts the metadata part. """ @@ -53,8 +50,9 @@ def convert_metadata(args: Namespace) -> typing.Generator[str, None, None]: info("converting metadata") # data_formatter = DataFormatter() + args.converter = get_converter(args.game_version) + # required for player palette and color lookup during SLP conversion. - yield "palette" palettes = get_palettes(args.srcdir, args.game_version) # store for use by convert_media @@ -67,10 +65,7 @@ def convert_metadata(args: Namespace) -> typing.Generator[str, None, None]: if gamedata_path.exists(): gamedata_path.removerecursive() - args.converter = get_converter(args.game_version) - # Read .dat - yield "empires.dat" debug_gamedata_format(args.debugdir, args.debug_info, args.game_version) gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag("no_pickle_cache")) @@ -99,15 +94,15 @@ def convert_metadata(args: Namespace) -> typing.Generator[str, None, None]: ModpackExporter.export(modpack, args) debug_modpack(args.debugdir, args.debug_info, modpack) - yield "player color palette" + # TODO: player palettes # player_palette = PlayerColorTable(palette) # data_formatter.add_data(player_palette.dump("player_palette")) - yield "terminal color palette" + # TODO: terminal color files # termcolortable = ColorTable(URXVTCOLS) # data_formatter.add_data(termcolortable.dump("termcolors")) - yield "game specification files" + # TODO: gamespec files # data_formatter.export(args.targetdir, ("csv",)) if args.flag('gen_extra_files'): @@ -139,6 +134,13 @@ def get_converter(game_version: GameVersion): from ..processor.conversion.aoc.processor import AoCProcessor return AoCProcessor + if game_edition.game_id == "AOCDEMO": + # treat the demo as AoC during conversion + # TODO: maybe introduce a config parameter for this purpose? + game_edition.game_id = "AOC" + from ..processor.conversion.aoc_demo.processor import DemoProcessor + return DemoProcessor + if game_edition.game_id == "HDEDITION": from ..processor.conversion.hd.processor import HDProcessor return HDProcessor diff --git a/openage/convert/tool/singlefile.py b/openage/convert/tool/singlefile.py index 57935ff6ff..78b67380b0 100644 --- a/openage/convert/tool/singlefile.py +++ b/openage/convert/tool/singlefile.py @@ -18,10 +18,10 @@ from ..value_object.read.media.drs import DRS AOC_GAME_VERSION = GameVersion( - edition=GameEdition("Dummy AOC", "AOC", "YES", [], [], [], []) + edition=GameEdition("Dummy AOC", "AOC", "YES", [], [], {}, [], []) ) SWGB_GAME_VERSION = GameVersion( - edition=GameEdition("Dummy SWGB", "SWGB", "YES", [], [], [], []) + edition=GameEdition("Dummy SWGB", "SWGB", "YES", [], [], {}, [], []) ) diff --git a/openage/convert/tool/subtool/acquire_sourcedir.py b/openage/convert/tool/subtool/acquire_sourcedir.py index 0cb6cafbbe..5d9368e526 100644 --- a/openage/convert/tool/subtool/acquire_sourcedir.py +++ b/openage/convert/tool/subtool/acquire_sourcedir.py @@ -1,19 +1,32 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. +# +# pylint: disable=too-many-branches """ Acquire the sourcedir for the game that is supposed to be converted. """ +from __future__ import annotations +import platform +import typing + from configparser import ConfigParser import os from pathlib import Path import subprocess import sys -from tempfile import NamedTemporaryFile from typing import AnyStr, Generator +import shutil +import tempfile +from urllib.request import urlopen + from ....log import warn, info, dbg from ....util.files import which -from ....util.fslike.directory import CaseIgnoringDirectory +from ....util.fslike.directory import CaseIgnoringDirectory, Directory + +if typing.TYPE_CHECKING: + from openage.convert.value_object.init.game_version import GameEdition + STANDARD_PATH_IN_32BIT_WINEPREFIX =\ "drive_c/Program Files/Microsoft Games/Age of Empires II/" @@ -26,6 +39,8 @@ REGISTRY_SUFFIX_AOK = "Age of Empires\\2.0" REGISTRY_SUFFIX_TC = "Age of Empires II: The Conquerors Expansion\\1.0" +TRIAL_URL = 'https://archive.org/download/AgeOfEmpiresIiTheConquerorsDemo/Age2XTrial.exe' + def expand_relative_path(path: str) -> AnyStr: """Expand relative path to an absolute one, including abbreviations like @@ -51,6 +66,24 @@ def wanna_convert() -> bool: return answer +def wanna_download_trial() -> bool: + """ + Ask the user if the AoC trial should be downloaded. + """ + answer = None + while answer is None: + print(" Do you want to download the AoC trial version? [Y/n]") + + user_selection = input("> ") + if user_selection.lower() in {"yes", "y", ""}: + answer = True + + elif user_selection.lower() in {"no", "n"}: + answer = False + + return answer + + def wanna_use_wine() -> bool: """ Ask the user if wine should be used. @@ -150,54 +183,118 @@ def query_source_dir(proposals: set[str]) -> AnyStr: return sourcedir -def acquire_conversion_source_dir(prev_source_dir_path: str = None) -> Path: +def acquire_conversion_source_dir( + avail_game_eds: list[GameEdition], + prev_srcdir_paths: set[str] = None +) -> Path: """ Acquires source dir for the asset conversion. Returns a file system-like object that holds all the required files. """ + try: + # TODO: use some sort of GUI for this (GTK, QtQuick, zenity?) + # probably best if directly integrated into the main GUI. + proposals = set() - if 'AGE2DIR' in os.environ: - sourcedir = os.environ['AGE2DIR'] - print("found environment variable 'AGE2DIR'") + # previously used source dirs + if prev_srcdir_paths: + for prev_srcdir_path in prev_srcdir_paths: + if Path(prev_srcdir_path).is_dir(): + proposals.add(prev_srcdir_path) - else: - try: - # TODO: use some sort of GUI for this (GTK, QtQuick, zenity?) - # probably best if directly integrated into the main GUI. + # commonly used install dirs + current_platform = platform.system() + for game_edition in avail_game_eds: + install_paths = game_edition.install_paths + candidates = [] + if current_platform == 'Linux' and 'linux' in install_paths: + candidates = install_paths["linux"] + + elif current_platform == 'Darwin' and 'macos' in install_paths: + candidates = install_paths["macos"] - proposals = set() + elif current_platform == 'Windows' and 'windows' in install_paths: + candidates = install_paths["windows"] - if prev_source_dir_path and Path(prev_source_dir_path).is_dir(): - prev_source_dir = CaseIgnoringDirectory( - prev_source_dir_path).root - proposals.add( - prev_source_dir.resolve_native_path().decode('utf-8', errors='replace') - ) + else: + continue - call_wine = wanna_use_wine() + for candidate in candidates: + if Path(expand_relative_path(candidate)).is_dir(): + proposals.add(candidate) - if call_wine: - set_custom_wineprefix() + # TODO: Reimplement wine support - for proposal in source_dir_proposals(call_wine): - if Path(expand_relative_path(proposal)).is_dir(): - proposals.add(proposal) + use_trial = False + if len(proposals) == 0: + print("\nopenage requires a local game installation for conversion") + print("but no local installation could be found automatically.") + use_trial = wanna_download_trial() + if use_trial: + sourcedir = download_trial() + + else: sourcedir = query_source_dir(proposals) - except KeyboardInterrupt: - print("\nInterrupted, aborting") - sys.exit(0) - except EOFError: - print("\nEOF, aborting") - sys.exit(0) + except KeyboardInterrupt: + print("\nInterrupted, aborting") + sys.exit(0) + except EOFError: + print("\nEOF, aborting") + sys.exit(0) print(f"converting from '{sourcedir}'") return CaseIgnoringDirectory(sourcedir).root +def download_trial() -> AnyStr: + """ + Download and extract the AoC trial version. + + Does not work yet. TODO: Find an exe unpack solution that works on all platforms + """ + print(f"Downloading AoC trial version from {TRIAL_URL}") + # pylint: disable=consider-using-with + tempdir = tempfile.mkdtemp() + with urlopen(TRIAL_URL) as response: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + shutil.copyfileobj(response, tmp_file) + + from ....cabextract.cab import CABFile + + cab = CABFile(tmp_file, 0x65678) + + sourcedir = Directory(tempdir).root + print(f"Extracting game files to {sourcedir}...") + dirs = [cab.root] + + # Loop over all files in the CAB archive and extract them + # to the tempdir + while len(dirs) > 0: + cur_src_dir = dirs[0] + cur_tgt_dir = sourcedir + + for part in cur_src_dir.parts: + cur_tgt_dir = cur_tgt_dir[part] + cur_tgt_dir.mkdirs() + + dirs.remove(cur_src_dir) + + for path in cur_src_dir.iterdir(): + if path.is_dir(): + dirs.append(path) + + if path.is_file(): + with cur_tgt_dir[path.name].open("wb") as target_file: + with path.open("rb") as source_file: + target_file.write(source_file.read()) + + return tempdir + + def wine_to_real_path(path: str) -> str: """ Turn a Wine file path (C:\\xyz) into a local filesystem path (~/.wine/xyz) @@ -210,7 +307,7 @@ def unescape_winereg(value: str): return value.strip('"').replace(r'\\\\', '\\') -def source_dir_proposals(call_wine: bool) -> Generator[str, None, None]: +def wine_srcdir_proposals() -> Generator[str, None, None]: """Yield a list of directory names where an installation might be found""" if "WINEPREFIX" in os.environ: yield "$WINEPREFIX/" + STANDARD_PATH_IN_32BIT_WINEPREFIX @@ -219,16 +316,11 @@ def source_dir_proposals(call_wine: bool) -> Generator[str, None, None]: yield "~/.wine/" + STANDARD_PATH_IN_32BIT_WINEPREFIX yield "~/.wine/" + STANDARD_PATH_IN_64BIT_WINEPREFIX yield "~/.wine/" + STANDARD_PATH_IN_WINEPREFIX_STEAM - yield "~/.steam/steam/steamapps/common/Age2HD" - - if not call_wine: - # user wants wine not to be called - return try: info("using the wine registry to query an installation location...") # get wine registry key of the age installation - with NamedTemporaryFile(mode='rb') as reg_file: + with tempfile.NamedTemporaryFile(mode='rb') as reg_file: if not subprocess.call(('wine', 'regedit', '/E', reg_file.name, REGISTRY_KEY)): diff --git a/openage/convert/tool/subtool/version_select.py b/openage/convert/tool/subtool/version_select.py index 37ad65a685..a585ae2f1b 100644 --- a/openage/convert/tool/subtool/version_select.py +++ b/openage/convert/tool/subtool/version_select.py @@ -11,6 +11,7 @@ from ....log import warn, info from ...service.init.version_detect import iterate_game_versions from ...value_object.init.game_version import Support +from ...value_object.init.game_version import GameVersion if typing.TYPE_CHECKING: from openage.convert.value_object.init.game_version import GameEdition, \ diff --git a/openage/convert/value_object/conversion/de2/internal_nyan_names.py b/openage/convert/value_object/conversion/de2/internal_nyan_names.py index dd561c1887..6d1fe7f380 100644 --- a/openage/convert/value_object/conversion/de2/internal_nyan_names.py +++ b/openage/convert/value_object/conversion/de2/internal_nyan_names.py @@ -1,4 +1,4 @@ -# Copyright 2020-2022 the openage authors. See copying.md for legal info. +# Copyright 2020-2023 the openage authors. See copying.md for legal info. # # pylint: disable=line-too-long @@ -12,14 +12,14 @@ # contains only new units of DE2 UNIT_LINE_LOOKUPS = { 705: ("Cow", "cow"), - 1225: ("Konnik", "konnik"), # Castle unit + 1225: ("KonnikCastle", "konnik"), # Castle unit 1228: ("Keshik", "keshik"), 1231: ("Kipchak", "kipchak"), 1234: ("Leitis", "leitis"), 1239: ("Ibex", "ibex"), 1243: ("Goose", "goose"), 1245: ("Pig", "pig"), - 1254: ("Konnik", "konnik"), # Krepost unit + 1254: ("KonnikKrepost", "konnik"), # Krepost unit 1258: ("Ram", "ram"), # replacement for ID 35? 1263: ("FlamingCamel", "flaming_camel"), @@ -46,6 +46,20 @@ 1751: ("ShrivamshaRider", "shrivamsha_rider"), 1755: ("CamelScout", "camel_scout"), # technically in line with Camel rider 1759: ("RRatha", "rratha"), # Ranged Rhata + + # ROR + 1790: ("Centurion", "centurion"), + 1795: ("Dromon", "dromon"), + + # TODO: These are upgrades + 1737: ("EliteUrumiSwordsman", "elite_urumi_swordsman"), + 1743: ("EliteChakramThrower", "elite_chakram_thrower"), + 1746: ("SiegeElephant", "siege_elephant"), + 1749: ("EliteGhumlam", "elite_ghumlam"), + 1753: ("EliteShrivamshaRider", "elite_shrivamsha_rider"), + 1761: ("EliteRRatha", "elite_rratha"), + 1792: ("EliteCenturion", "elite_centurion"), + 1793: ("Legionary", "legionary"), } # key: head unit id; value: (nyan object name, filename prefix) @@ -124,6 +138,12 @@ 838: ("SiegeElephant", "siege_elephant"), 840: ("EliteGhulam", "elite_ghulam"), 843: ("EliteSHrivamshaRider", "elite_shrivamsha_rider"), + + # ROR + 882: ("EliteCenturion", "elite_centurion"), + 883: ("Ballistas", "ballistas"), + 884: ("Comitatensis", "comitatensis"), + 885: ("Legionary", "legionary"), } # key: civ index; value: (nyan object name, filename prefix) @@ -147,6 +167,9 @@ 40: ("Dravidians", "dravidians"), 41: ("Bengalis", "bengalis"), 42: ("Gurjaras", "gurjaras"), + + # ROR + 43: ("Romans", "romans"), } # key: civ index; value: (civ ids, nyan object name, filename prefix) @@ -154,6 +177,7 @@ GRAPHICS_SET_LOOKUPS = { 0: ((0, 1, 2, 13, 14, 36), "WesternEuropean", "western_european"), 4: ((7, 37), "Byzantine", "byzantine"), + 6: ((19, 24, 43), "Mediterranean", "mediterranean"), 7: ((20, 40, 41, 42), "Indian", "indian"), 8: ((22, 23, 32, 35, 38, 39), "EasternEuropean", "eastern_european"), 11: ((33, 34), "CentralAsian", "central_asian"), @@ -176,6 +200,11 @@ ARMOR_CLASS_LOOKUPS = { 0: "Wonder", 31: "AntiLetis", + 32: "DE2Condottiero", + 34: "DE2FishingShip", + 35: "DE2Mameluke", 36: "DE2Hero", - 37: "HussiteWagon", + 37: "SiegeBallista", + 38: "DE2Skirmisher", + 39: "DE2CamelRider", } diff --git a/openage/convert/value_object/init/game_version.py b/openage/convert/value_object/init/game_version.py index b3e0b8364c..e4364c3dde 100644 --- a/openage/convert/value_object/init/game_version.py +++ b/openage/convert/value_object/init/game_version.py @@ -32,7 +32,7 @@ class GameBase: def __init__( self, game_id: str, - support: bool, + support: Support, game_version_info: list[tuple[list[str], dict[str, str]]], media_paths: list[tuple[str, list[str]]], modpacks: list[str], @@ -119,7 +119,7 @@ def __init__( self, name: str, game_id: str, - support: bool, + support: Support, game_version_info: list[tuple[list[str], dict[str, str]]], media_paths: list[tuple[str, list[str]]], modpacks: list[str], @@ -160,9 +160,10 @@ def __init__( self, name: str, game_id: str, - support: bool, + support: Support, game_version_info: list[tuple[list[str], dict[str, str]]], media_paths: list[tuple[str, list[str]]], + install_paths: dict[str, list[str]], modpacks: list[str], expansions: list[str], **flags @@ -184,6 +185,7 @@ def __init__( **flags ) + self.install_paths = install_paths self.edition_name = name self.expansions = tuple(expansions) diff --git a/openage/convert/value_object/read/media/datfile/lookup_dicts.py b/openage/convert/value_object/read/media/datfile/lookup_dicts.py index 8a909855e3..df0ba23d05 100644 --- a/openage/convert/value_object/read/media/datfile/lookup_dicts.py +++ b/openage/convert/value_object/read/media/datfile/lookup_dicts.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 the openage authors. See copying.md for legal info. +# Copyright 2021-2023 the openage authors. See copying.md for legal info. """ Lookup dicts for the EnumLookupMember instances. @@ -318,6 +318,8 @@ 14: "TEAM_ATTRIBUTE_RELSET", 15: "TEAM_ATTRIBUTE_MUL", 16: "TEAM_RESOURCE_MUL", + 17: "TEAM_SPAWN_UNIT", + 18: "TEAM_RESEARCH_TIME_MODIFY", # same as 0-6 but applied to enemies 20: "ENEMY_ATTRIBUTE_ABSSET", @@ -429,6 +431,8 @@ 149: "SHEAR", 150: "REGENERATION", 151: "FEITORIA", + 154: "LOOT", # Chieftains tech; looting on killing villagers, monks, trade carts + 155: "BOOST_MOVE_AND_ATTACK", 768: "UNKNOWN_768", 1024: "UNKNOWN_1024", } @@ -493,12 +497,14 @@ 29: "EAGLE_WARRIOR", 30: "HD_CAMEL", 31: "DE2_UNKOWN_31", - 32: "DE2_UNKOWN_32", + 32: "DE2_CONDOTTIERO", 33: "DE2_UNKOWN_33", - 34: "DE2_UNKOWN_34", - 35: "DE2_UNKOWN_35", - 36: "DE2_UNKOWN_36", - 37: "DE2_UNKOWN_36", + 34: "DE2_FISHING_SHIP", + 35: "DE2_MAMELUKE", + 36: "DE2_HERO", + 37: "DE2_SIEGE_BALLISTA", + 38: "DE2_SKIRMISHER", + 39: "DE2_CAMEL_RIDER", } UNIT_CLASSES = { @@ -723,11 +729,12 @@ 18: "UNKNOWN_18", 34: "UNKNOWN_34", 66: "UNKNOWN_66", - 138: "PIERCE", # attack units behind target (Ghulam) + 130: "DE2_PIERCE", # attack units behind target (Ghulam); used since Return of Rome + 138: "DE2_PIERCE", # attack units behind target (Ghulam); unused since Return of Rome } CREATABLE_TYPES = { - 0: "NONHUMAN", # building, animal, ship + 0: "NONHUMAN", # animal, ship 1: "VILLAGER", # villager, king 2: "MELEE", # soldier, siege, predator, trader 3: "MOUNTED", # camel rider @@ -735,6 +742,7 @@ 5: "RANGED_PROJECTILE", # archer 6: "RANGED_MAGIC", # monk 21: "TRANSPORT_SHIP", + 255: "OTHER", # building, relic } GARRISON_TYPES = { diff --git a/openage/convert/value_object/read/media/datfile/unit.py b/openage/convert/value_object/read/media/datfile/unit.py index eda53f9cf5..c0cdc29172 100644 --- a/openage/convert/value_object/read/media/datfile/unit.py +++ b/openage/convert/value_object/read/media/datfile/unit.py @@ -823,6 +823,9 @@ def get_data_format_members( (SKIP, "reload_time_displayed", StorageType.FLOAT_MEMBER, "float"), ]) + if game_version.edition.game_id == "AOE2DE": + data_format.append((READ_GEN, "blast_damage", StorageType.FLOAT_MEMBER, "float")) + return data_format @@ -917,7 +920,7 @@ def get_data_format_members( data_format.extend([ (SKIP, "flank_attack_modifier", StorageType.FLOAT_MEMBER, "float"), (READ_GEN, "creatable_type", StorageType.ID_MEMBER, EnumLookupMember( - raw_type="int8_t", + raw_type="uint8_t", type_name="creatable_types", lookup_dict=CREATABLE_TYPES )), @@ -939,6 +942,9 @@ def get_data_format_members( (READ_GEN, "charge_regen_rate", StorageType.FLOAT_MEMBER, "float"), (READ_GEN, "charge_cost", StorageType.ID_MEMBER, "int16_t"), (READ_GEN, "charge_type", StorageType.ID_MEMBER, "int16_t"), + (READ_GEN, "min_convert_mod", StorageType.FLOAT_MEMBER, "float"), + (READ_GEN, "max_convert_mod", StorageType.FLOAT_MEMBER, "float"), + (READ_GEN, "convert_chance_mod", StorageType.FLOAT_MEMBER, "float"), ]) data_format.extend([ diff --git a/openage/convert/value_object/read/media/sld.pyx b/openage/convert/value_object/read/media/sld.pyx index 7e3fe0450e..50440bc356 100644 --- a/openage/convert/value_object/read/media/sld.pyx +++ b/openage/convert/value_object/read/media/sld.pyx @@ -1,4 +1,4 @@ -# Copyright 2022-2022 the openage authors. See copying.md for legal info. +# Copyright 2022-2023 the openage authors. See copying.md for legal info. # # cython: infer_types=True @@ -146,8 +146,8 @@ cdef class SLD: # Dimensions of main layer (reused for dmg mask and playercolor) cdef unsigned short main_width cdef unsigned short main_height - cdef unsigned short main_hotspot_x - cdef unsigned short main_hotspot_y + cdef short main_hotspot_x + cdef short main_hotspot_y # Our position in the file bytes cdef unsigned int current_offset diff --git a/openage/convert/value_object/read/media/slp.pyx b/openage/convert/value_object/read/media/slp.pyx index b8fe501993..2a40ac691f 100644 --- a/openage/convert/value_object/read/media/slp.pyx +++ b/openage/convert/value_object/read/media/slp.pyx @@ -185,9 +185,12 @@ class SLP: if frame_info.properties & 0x07 == 0x07: self.main_frames.append(SLPFrame32(frame_info, data)) - elif self.version in (b'3.0\x00', b'4.0X', b'4.1X'): + elif self.version in (b'3.0\x00', b'4.0X'): self.main_frames.append(SLPMainFrameDE(frame_info, data)) + elif self.version in (b'4.1X',): + self.main_frames.append(SLPMainFrameDE41(frame_info, data)) + else: self.main_frames.append(SLPMainFrameAoC(frame_info, data)) @@ -272,13 +275,14 @@ class FrameInfo: " version") def __repr__(self): - ret = ( - " % 9d|" % self.qdl_table_offset, - "% 13d|" % self.outline_table_offset, - "% 7d) | " % self.palette_offset, + ret = ( "%s\n" % self.repr_header(), + " " + "(% #9x|" % self.qdl_table_offset, + "% #13x|" % self.outline_table_offset, + "% #7x) | " % self.palette_offset, "% 10d | " % self.properties, "% 5d x% 7d | " % self.size, - "% 4d /% 5d" % self.hotspot, + "% 4d /% 5d | " % self.hotspot, "% 4s" % self.version.decode('ascii'), ) return "".join(ret) @@ -914,6 +918,239 @@ cdef class SLPMainFrameDE(SLPFrame): # end of row reached, return the created pixel array. return + +cdef class SLPMainFrameDE41(SLPFrame): + """ + SLPFrame for the main graphics sprite since SLP version 4.1. + These have additional decay data inside the color pixels. + """ + + def __init__(self, frame_info, data): + super().__init__(frame_info, data) + + @cython.boundscheck(False) + cdef void process_drawing_cmds(self, + const uint8_t[::1] &data_raw, + vector[pixel] &row_data, + Py_ssize_t rowid, + Py_ssize_t first_cmd_offset, + size_t expected_size): + """ + create palette indices (colors) for the drawing commands + found for this row in the SLP frame. + """ + # position in the data blob, we start at the first command of this row + cdef Py_ssize_t dpos = first_cmd_offset + + # is the end of the current row reached? + cdef bool eor = False + + cdef uint8_t cmd + cdef uint8_t color + cdef uint8_t nextbyte + cdef uint8_t lower_nibble + cdef uint8_t higher_nibble + cdef uint8_t lowest_crumb + cdef cmd_pack cpack + cdef int pixel_count + + # work through commands till end of row. + while not eor: + if row_data.size() > expected_size: + raise Exception( + f"Only {expected_size:d} pixels should be drawn in row {rowid:d}, " + f"but we have {row_data.size():d} already!" + ) + + # fetch drawing instruction + cmd = data_raw[dpos] + + lower_nibble = 0x0f & cmd + higher_nibble = 0xf0 & cmd + lowest_crumb = 0b00000011 & cmd + + # opcode: cmd, rowid: rowid + + if lower_nibble == 0x0F: + # eol (end of line) command, this row is finished now. + eor = True + continue + + elif lowest_crumb == 0b00000000: + # color_list command + # draw the following bytes as palette colors + + pixel_count = cmd >> 2 + for _ in range(pixel_count): + dpos += 2 + color = data_raw[dpos] + + row_data.push_back(pixel(color_standard, color)) + + elif lowest_crumb == 0b00000001: + # skip command + # draw 'count' transparent pixels + # count = cmd >> 2; if count == 0: count = nextbyte + + cpack = cmd_or_next(data_raw, cmd, 2, dpos) + dpos = cpack.dpos + for _ in range(cpack.count): + row_data.push_back(pixel(color_transparent, 0)) + + elif lower_nibble == 0x02: + # big_color_list command + # draw (higher_nibble << 4 + nextbyte) following palette colors + + dpos += 1 + nextbyte = data_raw[dpos] + pixel_count = (higher_nibble << 4) + nextbyte + + for _ in range(pixel_count): + dpos += 2 + color = data_raw[dpos] + + row_data.push_back(pixel(color_standard, color)) + + elif lower_nibble == 0x03: + # big_skip command + # draw (higher_nibble << 4 + nextbyte) + # transparent pixels + + dpos += 1 + nextbyte = data_raw[dpos] + pixel_count = (higher_nibble << 4) + nextbyte + + for _ in range(pixel_count): + row_data.push_back(pixel(color_transparent, 0)) + + elif lower_nibble == 0x06: + # player_color_list command + # we have to draw the player color for cmd>>4 times, + # or if that is 0, as often as the next byte says. + cpack = cmd_or_next(data_raw, cmd, 4, dpos) + dpos = cpack.dpos + for _ in range(cpack.count): + dpos += 2 + color = data_raw[dpos] + + # version 3.0 uses extra palettes for player colors + row_data.push_back(pixel(color_player_v4, color)) + + elif lower_nibble == 0x07: + # fill command + # draw 'count' pixels with color of next byte + + cpack = cmd_or_next(data_raw, cmd, 4, dpos) + dpos = cpack.dpos + + dpos += 1 + color = data_raw[dpos] + + for _ in range(cpack.count): + row_data.push_back(pixel(color_standard, color)) + + elif lower_nibble == 0x0A: + # fill player color command + # draw the player color for 'count' times + + cpack = cmd_or_next(data_raw, cmd, 4, dpos) + dpos = cpack.dpos + + dpos += 1 + color = data_raw[dpos] + + for _ in range(cpack.count): + # since version 3.0 uses extra palettes for player colors + row_data.push_back(pixel(color_player_v4, color)) + + elif lower_nibble == 0x0B: + # shadow command + # draw a transparent shadow pixel for 'count' times + + cpack = cmd_or_next(data_raw, cmd, 4, dpos) + dpos = cpack.dpos + + for _ in range(cpack.count): + row_data.push_back(pixel(color_shadow, 0)) + + elif lower_nibble == 0x0E: + # "extended" commands. higher nibble specifies the instruction. + + if higher_nibble == 0x00: + # render hint xflip command + # render hint: only draw the following command, + # if this sprite is not flipped left to right + spam("render hint: xfliptest") + + elif higher_nibble == 0x10: + # render h notxflip command + # render hint: only draw the following command, + # if this sprite IS flipped left to right. + spam("render hint: !xfliptest") + + elif higher_nibble == 0x20: + # table use normal command + # set the transform color table to normal, + # for the standard drawing commands + spam("image wants normal color table now") + + elif higher_nibble == 0x30: + # table use alternat command + # set the transform color table to alternate, + # this affects all following standard commands + spam("image wants alternate color table now") + + elif higher_nibble == 0x40: + # outline_1 command + # the next pixel shall be drawn as special color 1, + # if it is obstructed later in rendering + row_data.push_back(pixel(color_special_1, 0)) + + elif higher_nibble == 0x60: + # outline_2 command + # same as above, but special color 2 + row_data.push_back(pixel(color_special_2, 0)) + + elif higher_nibble == 0x50: + # outline_span_1 command + # same as above, but span special color 1 nextbyte times. + + dpos += 1 + pixel_count = data_raw[dpos] + + for _ in range(pixel_count): + row_data.push_back(pixel(color_special_1, 0)) + + elif higher_nibble == 0x70: + # outline_span_2 command + # same as above, using special color 2 + + dpos += 1 + pixel_count = data_raw[dpos] + + for _ in range(pixel_count): + row_data.push_back(pixel(color_special_2, 0)) + + elif higher_nibble == 0x80: + # dither command + raise NotImplementedError("dither not implemented") + + elif higher_nibble in (0x90, 0xA0): + # 0x90: premultiplied alpha + # 0xA0: original alpha + raise NotImplementedError("extended alpha not implemented") + + else: + raise Exception( + f"unknown slp drawing command: " + + f"{cmd:#x} in row {rowid:d}") + + dpos += 1 + + # end of row reached, return the created pixel array. + return + + cdef class SLPShadowFrame(SLPFrame): """ SLPFrame for the shadow graphics in SLP version 4.0 and 4.1. diff --git a/openage/game/main.py b/openage/game/main.py index be658d50ab..15fb0a3962 100644 --- a/openage/game/main.py +++ b/openage/game/main.py @@ -8,9 +8,7 @@ from __future__ import annotations import typing - -from ..convert.tool.subtool.acquire_sourcedir import wanna_convert -from ..log import err, info +from ..log import info if typing.TYPE_CHECKING: from argparse import ArgumentParser @@ -20,10 +18,6 @@ def init_subparser(cli: ArgumentParser) -> None: """ Initializes the parser for game-specific args. """ cli.set_defaults(entrypoint=main) - cli.add_argument( - "--fps", type=int, - help="upper limit for fps. this limit is imposed on top of vsync") - cli.add_argument( "--gl-debug", action='store_true', help="throw exceptions directly from the OpenGL calls") @@ -33,7 +27,7 @@ def init_subparser(cli: ArgumentParser) -> None: help="run without displaying graphics") cli.add_argument( - "--modpacks", nargs="+", type=bytes, + "--modpacks", nargs="+", required=True, help="list of modpacks to load") @@ -50,6 +44,8 @@ def main(args, error): from .. import config from ..assets import get_asset_path from ..convert.main import conversion_required, convert_assets + from ..convert.tool.api_export import export_api + from ..convert.service.init.api_export_required import api_export_required from ..cppinterface.setup import setup as cpp_interface_setup from ..cvar.location import get_config_path from ..util.fslike.union import Union @@ -67,42 +63,30 @@ def main(args, error): root = Union().root # mount the assets folder union at "assets/" - root["assets"].mount(get_asset_path(args.asset_dir)) + asset_path = get_asset_path(args.asset_dir) + root["assets"].mount(asset_path) # mount the config folder at "cfg/" root["cfg"].mount(get_config_path(args.cfg_dir)) args.cfg_dir = root["cfg"] + # ensure that the openage API is present + if api_export_required(asset_path): + # export to assets folder + converted_path = asset_path / "converted" + converted_path.mkdirs() + export_api(converted_path) + # ensure that the assets have been converted - if wanna_convert() or conversion_required(root["assets"], args): - # try to get previously used source dir - asset_location_path = root["cfg"] / "asset_location" - try: - with asset_location_path.open("r") as file_obj: - prev_source_dir_path = file_obj.read().strip() - except FileNotFoundError: - prev_source_dir_path = None - used_asset_path = convert_assets( - root["assets"], - args, - prev_source_dir_path=prev_source_dir_path - ) - - if used_asset_path: - # Remember the asset location - with asset_location_path.open("wb") as file_obj: - file_obj.write(used_asset_path) - else: - err("game asset conversion failed") - return 1 + if conversion_required(asset_path): + convert_assets(asset_path, args) # modpacks - if args.modpacks: - mods = [] - for modpack in args.modpacks: - mods.append(modpack.encode("utf-8")) + mods = [] + for modpack in args.modpacks: + mods.append(modpack.encode("utf-8")) - args.modpacks = mods + args.modpacks = mods # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/game/main_cpp.pyx b/openage/game/main_cpp.pyx index d34b22bf5c..e7697b0ddb 100644 --- a/openage/game/main_cpp.pyx +++ b/openage/game/main_cpp.pyx @@ -1,6 +1,8 @@ # Copyright 2015-2023 the openage authors. See copying.md for legal info. from cpython.ref cimport PyObject +from libcpp.string cimport string +from libcpp.vector cimport vector from libopenage.main cimport main_arguments, run_game as run_game_cpp from libopenage.util.path cimport Path as Path_cpp @@ -23,18 +25,18 @@ def run_game(args, root_path): args_cpp.root_path = Path_cpp(PyObj(root_path.fsobj), root_path.parts) - # frame limiting - if args.fps is not None: - args_cpp.fps_limit = args.fps - else: - args_cpp.fps_limit = 0 - # opengl debugging args_cpp.gl_debug = args.gl_debug # headless mode args_cpp.headless = args.headless + # mods + if args.modpacks is not None: + args_cpp.mods = args.modpacks + else: + args_cpp.mods = vector[string]() + # run the game! with nogil: result = run_game_cpp(args_cpp) diff --git a/openage/gamestate/CMakeLists.txt b/openage/gamestate/CMakeLists.txt new file mode 100644 index 0000000000..03ab1f8e49 --- /dev/null +++ b/openage/gamestate/CMakeLists.txt @@ -0,0 +1,7 @@ +add_cython_modules( + tests.pyx +) + +add_py_modules( + __init__.py +) diff --git a/openage/gamestate/__init__.py b/openage/gamestate/__init__.py new file mode 100644 index 0000000000..548a8308bd --- /dev/null +++ b/openage/gamestate/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. + +""" +openage game simulation +""" diff --git a/openage/gamestate/tests.pyx b/openage/gamestate/tests.pyx new file mode 100644 index 0000000000..6b614b8aa6 --- /dev/null +++ b/openage/gamestate/tests.pyx @@ -0,0 +1,50 @@ +# Copyright 2023-2023 the openage authors. See copying.md for legal info. + +""" +tests for the games simulation. +""" + +import argparse + +from libopenage.util.path cimport Path as Path_cpp +from libopenage.pyinterface.pyobject cimport PyObj +from cpython.ref cimport PyObject +from libopenage.gamestate.demo.tests cimport simulation_demo as simulation_demo_c + + +def simulation_demo(list argv): + """ + invokes the available simulation demos. + """ + + cmd = argparse.ArgumentParser( + prog='... simulation_demo', + description='Demo of the game simulation') + cmd.add_argument("test_id", type=int, help="id of the demo to run.") + cmd.add_argument("--asset-dir", + help="Use this as an additional asset directory.") + cmd.add_argument("--cfg-dir", + help="Use this as an additional config directory.") + + args = cmd.parse_args(argv) + + from ..cvar.location import get_config_path + from ..assets import get_asset_path + from ..util.fslike.union import Union + + # create virtual file system for data paths + root = Union().root + + # mount the assets folder union at "assets/" + root["assets"].mount(get_asset_path(args.asset_dir)) + + # mount the config folder at "cfg/" + root["cfg"].mount(get_config_path(args.cfg_dir)) + + cdef int simulation_test_id = args.test_id + + cdef Path_cpp root_cpp = Path_cpp(PyObj(root.fsobj), + root.parts) + + with nogil: + simulation_demo_c(simulation_test_id, root_cpp) diff --git a/openage/main/main.py b/openage/main/main.py index e503635795..685e5b6bc9 100644 --- a/openage/main/main.py +++ b/openage/main/main.py @@ -6,7 +6,7 @@ from __future__ import annotations import typing -from ..log import err, info +from ..log import info if typing.TYPE_CHECKING: from argparse import ArgumentParser @@ -16,10 +16,6 @@ def init_subparser(cli: ArgumentParser): """ Initializes the parser for game-specific args. """ cli.set_defaults(entrypoint=main) - cli.add_argument( - "--fps", type=int, - help="upper limit for fps. this limit is imposed on top of vsync") - cli.add_argument( "--gl-debug", action='store_true', help="throw exceptions directly from the OpenGL calls") @@ -47,6 +43,9 @@ def main(args, error): from .. import config from ..assets import get_asset_path from ..convert.main import conversion_required, convert_assets + from ..convert.service.init.api_export_required import api_export_required + from ..convert.tool.api_export import export_api + from ..convert.tool.subtool.acquire_sourcedir import wanna_convert from ..cppinterface.setup import setup as cpp_interface_setup from ..cvar.location import get_config_path from ..util.fslike.union import Union @@ -64,29 +63,23 @@ def main(args, error): root = Union().root # mount the assets folder union at "assets/" - root["assets"].mount(get_asset_path(args.asset_dir)) + asset_path = get_asset_path(args.asset_dir) + root["assets"].mount(asset_path) # mount the config folder at "cfg/" root["cfg"].mount(get_config_path(args.cfg_dir)) + args.cfg_dir = root["cfg"] + + # ensure that the openage API is present + if api_export_required(asset_path): + # export to assets folder + converted_path = asset_path / "converted" + converted_path.mkdirs() + export_api(converted_path) # ensure that the assets have been converted - if conversion_required(root["assets"], args): - # try to get previously used source dir - asset_location_path = root["cfg"] / "asset_location" - try: - with asset_location_path.open("rb") as file_obj: - prev_source_dir_path = file_obj.read().strip() - except FileNotFoundError: - prev_source_dir_path = None - used_asset_path = convert_assets(root["assets"], args, - prev_source_dir_path=prev_source_dir_path) - if used_asset_path: - # Remember the asset location - with asset_location_path.open("wb") as file_obj: - file_obj.write(used_asset_path) - else: - err("game asset conversion failed") - return 1 + if wanna_convert() or conversion_required(asset_path): + convert_assets(asset_path, args) # pass modpacks to engine if args.modpacks: @@ -96,5 +89,10 @@ def main(args, error): args.modpacks = mods + else: + from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack + avail_modpacks = enumerate_modpacks(asset_path / "converted") + args.modpacks = [query_modpack(avail_modpacks).encode("utf-8")] + # start the game, continue in main_cpp.pyx! return run_game(args, root) diff --git a/openage/main/main_cpp.pyx b/openage/main/main_cpp.pyx index dd64aa5754..e7697b0ddb 100644 --- a/openage/main/main_cpp.pyx +++ b/openage/main/main_cpp.pyx @@ -1,7 +1,6 @@ # Copyright 2015-2023 the openage authors. See copying.md for legal info. from cpython.ref cimport PyObject -from libcpp.memory cimport make_unique from libcpp.string cimport string from libcpp.vector cimport vector @@ -26,12 +25,6 @@ def run_game(args, root_path): args_cpp.root_path = Path_cpp(PyObj(root_path.fsobj), root_path.parts) - # frame limiting - if args.fps is not None: - args_cpp.fps_limit = args.fps - else: - args_cpp.fps_limit = 0 - # opengl debugging args_cpp.gl_debug = args.gl_debug diff --git a/openage/renderer/tests.pyx b/openage/renderer/tests.pyx index 5c8ae5dab6..1234cfaa38 100644 --- a/openage/renderer/tests.pyx +++ b/openage/renderer/tests.pyx @@ -10,6 +10,7 @@ from libopenage.util.path cimport Path as Path_cpp from libopenage.pyinterface.pyobject cimport PyObj from cpython.ref cimport PyObject from libopenage.renderer.demo.tests cimport renderer_demo as renderer_demo_c +from libopenage.renderer.demo.tests cimport renderer_stresstest as renderer_stresstest_c def renderer_demo(list argv): """ @@ -18,7 +19,7 @@ def renderer_demo(list argv): cmd = argparse.ArgumentParser( prog='... renderer_demo', - description='Demo of the new renderer') + description='Demo of the renderer') cmd.add_argument("test_id", type=int, help="id of the demo to run.") cmd.add_argument("--asset-dir", help="Use this as an additional asset directory.") @@ -27,7 +28,6 @@ def renderer_demo(list argv): args = cmd.parse_args(argv) - from ..cvar.location import get_config_path from ..assets import get_asset_path from ..util.fslike.union import Union @@ -48,3 +48,41 @@ def renderer_demo(list argv): with nogil: renderer_demo_c(renderer_test_id, root_cpp) + + +def renderer_stresstest(list argv): + """ + invokes the available render demos. + """ + + cmd = argparse.ArgumentParser( + prog='... renderer_stresstest', + description='Stresstest for the renderer') + cmd.add_argument("test_id", type=int, help="id of the demo to run.") + cmd.add_argument("--asset-dir", + help="Use this as an additional asset directory.") + cmd.add_argument("--cfg-dir", + help="Use this as an additional config directory.") + + args = cmd.parse_args(argv) + + from ..cvar.location import get_config_path + from ..assets import get_asset_path + from ..util.fslike.union import Union + + # create virtual file system for data paths + root = Union().root + + # mount the assets folder union at "assets/" + root["assets"].mount(get_asset_path(args.asset_dir)) + + # mount the config folder at "cfg/" + root["cfg"].mount(get_config_path(args.cfg_dir)) + + cdef int renderer_test_id = args.test_id + + cdef Path_cpp root_cpp = Path_cpp(PyObj(root.fsobj), + root.parts) + + with nogil: + renderer_stresstest_c(renderer_test_id, root_cpp) diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 5ce6f11dbd..939732bd0d 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -50,8 +50,12 @@ def demos_py(): "encodes an opus file from a wave file") yield ("openage.event.demo.curvepong", "play pong on steroids through future prediction") + yield ("openage.gamestate.tests.simulation_demo", + "showcases the game simulation") yield ("openage.renderer.tests.renderer_demo", - "showcases the new renderer") + "showcases the renderer") + yield ("openage.renderer.tests.renderer_stresstest", + "stresstests for the renderer") yield ("openage.main.tests.engine_demo", "showcases the engine features") diff --git a/packaging/docker/devenv/Dockerfile.ubuntu.2204 b/packaging/docker/devenv/Dockerfile.ubuntu.2204 index 5eef76af16..40b1dd3abc 100644 --- a/packaging/docker/devenv/Dockerfile.ubuntu.2204 +++ b/packaging/docker/devenv/Dockerfile.ubuntu.2204 @@ -14,7 +14,7 @@ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y sudo \ libeigen3-dev \ libepoxy-dev \ libfontconfig1-dev \ - libfreetype6-dev \ + libfreetype-dev \ libharfbuzz-dev \ libogg-dev \ libopus-dev \