[
  {
    "path": ".clang-format",
    "content": "﻿---\n# SFT codestyle\n# Tab indent + space alignment\n# see documentation in doc/code_style/ for details and explainations.\nLanguage:        Cpp\nAccessModifierOffset: -4\nAlignAfterOpenBracket: Align\nAlignArrayOfStructures: None\nAlignConsecutiveAssignments: false\nAlignConsecutiveBitFields: false\nAlignConsecutiveDeclarations: false\nAlignConsecutiveMacros: false\nAlignEscapedNewlines: DontAlign\nAlignOperands:   Align\nAlignTrailingComments: true\nAllowAllArgumentsOnNextLine: true\nAllowAllParametersOfDeclarationOnNextLine: true\nAllowShortBlocksOnASingleLine: Never\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortEnumsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: Empty\nAllowShortIfStatementsOnASingleLine: Never\nAllowShortLambdasOnASingleLine: All\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterDefinitionReturnType: None\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: Yes\nBinPackArguments: false\nBinPackParameters: false\nBraceWrapping:\n  AfterCaseLabel:  false\n  AfterClass:      false\n  AfterControlStatement: Never\n  AfterEnum:       false\n  AfterExternBlock: false\n  AfterFunction:   false\n  AfterNamespace:  false\n  AfterObjCDeclaration: false\n  AfterStruct:     false\n  AfterUnion:      false\n  BeforeCatch:     true\n  BeforeElse:      true\n  BeforeLambdaBody: false\n  BeforeWhile:     true\n  IndentBraces:    false\n  SplitEmptyFunction: false\n  SplitEmptyNamespace: false\n  SplitEmptyRecord: false\nBreakAfterJavaFieldAnnotations: true\nBreakBeforeBinaryOperators: NonAssignment\nBreakBeforeBraces: Custom\nBreakBeforeInheritanceComma: false\nBreakBeforeTernaryOperators: false\nBreakConstructorInitializers: AfterColon\nBreakInheritanceList: BeforeComma\nBreakStringLiterals: false\nColumnLimit:     0\nCompactNamespaces: false\nConstructorInitializerAllOnOneLineOrOnePerLine: false\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nCpp11BracedListStyle: true\nDeriveLineEnding: true\nDerivePointerAlignment: false\nDisableFormat:   false\nExperimentalAutoDetectBinPacking: false\nFixNamespaceComments: true\nForEachMacros:\n  - foreach\n  - Q_FOREACH\n  - BOOST_FOREACH\nIncludeBlocks:   Preserve\nIncludeCategories:\n  - Regex:           '.*'\n    Priority:        3\n    SortPriority:    0\nIncludeIsMainRegex: ''\nIncludeIsMainSourceRegex: ''\nIndentCaseBlocks: false\nIndentCaseLabels: false\nIndentExternBlock: NoIndent\nIndentGotoLabels: false\nIndentPPDirectives: BeforeHash\nIndentWidth:     4\nIndentWrappedFunctionNames: false\n# clang-format-16 InsertNewlineAtEOF: true\nInsertTrailingCommas: Wrapped\nKeepEmptyLinesAtTheStartOfBlocks: false\nMacroBlockBegin: ''\nMacroBlockEnd:   ''\nMaxEmptyLinesToKeep: 2\nNamespaceIndentation: None\nPenaltyBreakAssignment: 2\nPenaltyBreakBeforeFirstCallParameter: 1\nPenaltyBreakComment: 300\nPenaltyBreakFirstLessLess: 120\nPenaltyBreakString: 1000\nPenaltyBreakTemplateDeclaration: 10\nPenaltyExcessCharacter: 1000000\nPenaltyReturnTypeOnItsOwnLine: 200\nPointerAlignment: Right\nReflowComments: true\nSortIncludes:    CaseInsensitive\nSortUsingDeclarations: true\nSpaceAfterCStyleCast: false\nSpaceAfterLogicalNot: false\nSpaceAfterTemplateKeyword: true\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCaseColon: false\nSpaceBeforeCpp11BracedList: false\nSpaceBeforeCtorInitializerColon: true\nSpaceBeforeInheritanceColon: true\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceBeforeSquareBrackets: false\nSpaceInEmptyBlock: false\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 1\nSpacesInAngles:  Never\nSpacesInCStyleCastParentheses: false\nSpacesInConditionalStatement: false\nSpacesInContainerLiterals: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStandard:        Latest\nTabWidth:        4\nUseCRLF:         false\nUseTab:          AlignWithSpaces\n...\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATES/pull_request_template.md",
    "content": "### Merge Checklist\n\n<!-- Complete all tasks below to make sure that we can merge your pull request. -->\n<!-- If you are an experienced contributor, you may delete this section ;-) -->\n\n- [ ] I have read the [contribution guide](doc/contributing.md)\n- [ ] I have added my info to [copying.md](copying.md) (only first time contributors)\n- [ ] I have run `make checkmerge` and fixed all mentioned problems\n\n\n### Description\n\n<!-- Describe the changes this pull request makes to the code base. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATES/release_template.md",
    "content": "### Checklist\n\n- [ ] Changelog\n    - [ ] Release date\n    - [ ] Version number in title\n    - [ ] Full commit log\n- [ ] Bump version in `openage_version`\n"
  },
  {
    "path": ".github/workflows/macosx-ci.yml",
    "content": "name: macOS-CI\non: [push, pull_request]\n\njobs:\n  # https://docs.github.com/en/actions/reference/software-installed-on-github-hosted-runners\n  # we install stuff not already there\n  macos-build:\n    runs-on: macos-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v4\n\n      # caching dependencies and ccache\n      # https://docs.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows\n      - name: Get timestamp\n        id: timestamp\n        shell: cmake -P {0}\n        run: |\n          string(TIMESTAMP current_date \"%Y-%m-%d-%H:%M:%S\" UTC)\n          message(\"timestamp=${current_date}\" >> $GITHUB_OUTPUT)\n      - name: Cache dependencies\n        uses: actions/cache@v4\n        id: cache-deps\n        with:\n          path: |\n            ~/Library/Caches/pip\n            ~/Library/Caches/Homebrew\n          key: '${{ runner.os }}-deps-${{ steps.timestamp.outputs.timestamp }}'\n          restore-keys: |\n            ${{ runner.os }}-deps-\n      - name: Cache ccache dir\n        uses: actions/cache@v4\n        id: cache-ccache\n        with:\n          path: ~/Library/Caches/ccache\n          key: '${{ runner.os }}-ccache-${{ steps.timestamp.outputs.timestamp }}'\n          restore-keys: |\n            ${{ runner.os }}-ccache-\n      - name: Brew update-reset\n        run: brew update-reset\n      - name: Brew update\n        run: brew update\n      - name: Brew install DeJaVu fonts\n        run: brew install --cask font-dejavu\n      - name: Install environment helpers with homebrew\n        run: brew install --force ccache\n      - name: Install LLVM with homebrew\n        run: brew install --force llvm\n      - name: Install openage dependencies with homebrew\n        run: brew install --force cmake python3 libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen\n      - name: Install nyan dependencies with homebrew\n        run: brew install --force flex make\n      - name: Install python3 packages\n        # cython, numpy and pygments are in homebrew,\n        # but \"cython is keg-only, which means it was not symlinked into /usr/local\"\n        # numpy pulls gcc as dep? and pygments doesn't work.\n        run: pip3 install --upgrade --break-system-packages cython numpy mako lz4 pillow pygments setuptools toml\n      - name: Configure\n        run: ./configure --compiler=\"$(brew --prefix llvm)/bin/clang++\" --mode=release --ccache --download-nyan\n      - name: Build\n        run: make -j$(sysctl -n hw.logicalcpu) VERBOSE=1\n      - name: Test\n        run: make test\n"
  },
  {
    "path": ".github/workflows/ubuntu-24.04.yml",
    "content": "name: Ubuntu 24.04 CI\n\non: [push, workflow_dispatch]\n\njobs:\n  build-devenv:\n    runs-on: ubuntu-24.04\n    steps:\n    - uses: actions/checkout@v4\n    - name: Build the Docker image\n      run: sudo DOCKER_BUILDKIT=1 docker build ./packaging/docker/devenv --file ./packaging/docker/devenv/Dockerfile.ubuntu.2404 --tag openage-devenv:latest\n      shell: bash\n    - name: Save the Docker image\n      run: |\n        mkdir -p /tmp/staging\n        sudo docker save openage-devenv:latest | gzip > /tmp/staging/devenv.tar.gz\n      shell: bash\n    - name: Publish the Docker image\n      uses: actions/upload-artifact@v4\n      with:\n        name: devenv-image-compressed.tar.gz\n        path: '/tmp/staging/devenv.tar.gz'\n        if-no-files-found: error\n        retention-days: 30\n\n  build:\n    runs-on: ubuntu-24.04\n    needs: build-devenv\n    steps:\n    - uses: actions/checkout@v4\n    - name: Create tmp path\n      run: mkdir -p /tmp/image\n      shell: bash\n    - name: Download devenv image\n      uses: actions/download-artifact@v4\n      with:\n        name: devenv-image-compressed.tar.gz\n        path: '/tmp/image'\n    - name: Load Docker image\n      run: sudo docker load --input /tmp/image/devenv.tar.gz\n    - name: Build openage\n      run: |\n        sudo docker run --rm -v \"$(pwd)\":/mnt/openage -w /mnt/openage openage-devenv:latest \\\n          bash -c 'mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$(which gcc) -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_CXX_FLAGS='' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -G Ninja .. && cmake --build . --parallel $(nproc) -- -k1'\n    - name: Compress build artifacts\n      run: |\n        mkdir -p /tmp/openage\n        tar -czvf /tmp/openage/openage-build.tar.gz ./build\n    - name: Publish build artifacts\n      uses: actions/upload-artifact@v4\n      with:\n        name: openage-build.tar.gz\n        path: '/tmp/openage/openage-build.tar.gz'\n        if-no-files-found: error\n        retention-days: 30\n"
  },
  {
    "path": ".github/workflows/windows-server-2019.yml",
    "content": "name: Windows Server 2019 CI\n\non: [push, workflow_dispatch]\n\njobs:\n  build-x64:\n    runs-on: windows-2019\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        submodules: recursive\n        path: source\n    - name: Inspect environment\n      run: |\n        vswhere -latest\n      shell: pwsh\n    - name: Setup Python\n      uses: actions/setup-python@v5\n      with:\n        python-version: '3.9'\n        architecture: 'x64'\n    - name: Install dependencies [vcpkg]\n      run: |\n        mkdir download\n        cd download\n        $zipfile = \"openage-dep-x64-windows.zip\"\n        Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.1/openage-dep-x64-windows.zip -OutFile $zipfile\n        Expand-Archive -Path $zipfile -DestinationPath . -Force\n        Remove-Item $zipfile\n        (Get-ChildItem . -Recurse -File).FullName\n      shell: pwsh\n    - name: Install dependencies [winflexbison]\n      run: |\n        cd download\n        $zipfile = \"winflexbison-2.5.24.zip\"\n        Invoke-WebRequest https://github.com/lexxmark/winflexbison/releases/download/v2.5.24/win_flex_bison-2.5.24.zip -OutFile $zipfile\n        mkdir winflexbison\n        Expand-Archive -Path $zipfile -DestinationPath ./winflexbison -Force\n        Remove-Item $zipfile\n        (Get-ChildItem ./winflexbison -Recurse -File).FullName\n      shell: pwsh\n    - name: Install dependencies [Python]\n      run: |\n        python -m pip install --upgrade pip\n        python -m pip install --upgrade Cython wheel numpy lz4 toml pillow pygments pyreadline3 mako\n      shell: pwsh\n    - name: Build\n      run: |\n        $TOOLCHAIN_FILE = Join-Path download openage-dep-x64-windows scripts buildsystems vcpkg.cmake | Resolve-Path\n        $FLEX_PATH = (Get-ChildItem ./download -Recurse -Force -Filter 'win_flex.exe')[0].FullName\n        mkdir build\n        cd build\n        cmake -DCMAKE_TOOLCHAIN_FILE=\"$TOOLCHAIN_FILE\" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TRY_COMPILE_CONFIGURATION=Debug -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE=\"$FLEX_PATH\" -G \"Visual Studio 16 2019\" -A x64 ../source\n        cmake --build . --config Debug -- -nologo -maxCpuCount\n      shell: pwsh\n    - name: Package\n      run: |\n        mkdir package\n        cd package\n        mkdir dll\n        cd ..\n        $STAGING_PATH = Resolve-Path package\n        $DLL_PATH = Join-Path package dll | Resolve-Path\n\n        cd build\n        $NYAN_DLL = (Get-ChildItem . -Recurse -Force -Filter 'nyan.dll')[0].FullName\n        $OPENAGE_DLL = (Get-ChildItem . -Recurse -Force -Filter 'openage.dll')[0].FullName\n        $NATIVE_OUTPUT = Split-Path -Path $OPENAGE_DLL -Parent\n        Copy-Item -Path ./openage -Destination $STAGING_PATH -Recurse\n        Copy-Item -Path $NYAN_DLL -Destination $DLL_PATH\n        Copy-Item -Path (Join-Path $NATIVE_OUTPUT *.dll) -Destination $DLL_PATH\n        Copy-Item -Path run.* -Destination $STAGING_PATH\n      shell: pwsh\n    - name: Test\n      run: |\n        $DLL_PATH = Join-Path package dll | Resolve-Path\n        cd package\n        python -m openage --add-dll-search-path $DLL_PATH --version\n        python -m openage --add-dll-search-path $DLL_PATH test -a\n      shell: pwsh\n    - name: Publish build artifacts\n      uses: actions/upload-artifact@v4\n      if: ${{ always() }}\n      with:\n        name: build-files\n        path: './build'\n        if-no-files-found: error\n        retention-days: 30\n    - name: Publish packaged artifacts\n      uses: actions/upload-artifact@v4\n      if: ${{ always() }}\n      with:\n        name: package-files\n        path: './package'\n        if-no-files-found: error\n        retention-days: 30\n"
  },
  {
    "path": ".github/workflows/windows-server-2022.yml",
    "content": "name: Windows Server 2022 CI\n\non: [push, workflow_dispatch]\n\njobs:\n  build-x64:\n    runs-on: windows-2022\n    steps:\n    - uses: actions/checkout@v4\n      with:\n        submodules: recursive\n        path: source\n    - name: Inspect environment\n      run: |\n        vswhere -latest\n      shell: pwsh\n    - name: Setup Python\n      uses: actions/setup-python@v5\n      with:\n        python-version: '3.9'\n        architecture: 'x64'\n    - name: Install dependencies [vcpkg]\n      run: |\n        mkdir download\n        cd download\n        $zipfile = \"openage-dep-x64-windows.zip\"\n        Invoke-WebRequest https://github.com/SFTtech/openage-dependencies/releases/download/v0.5.1/openage-dep-x64-windows.zip -OutFile $zipfile\n        Expand-Archive -Path $zipfile -DestinationPath . -Force\n        Remove-Item $zipfile\n        (Get-ChildItem . -Recurse -File).FullName\n      shell: pwsh\n    - name: Install dependencies [winflexbison]\n      run: |\n        cd download\n        $zipfile = \"winflexbison-2.5.24.zip\"\n        Invoke-WebRequest https://github.com/lexxmark/winflexbison/releases/download/v2.5.24/win_flex_bison-2.5.24.zip -OutFile $zipfile\n        mkdir winflexbison\n        Expand-Archive -Path $zipfile -DestinationPath ./winflexbison -Force\n        Remove-Item $zipfile\n        (Get-ChildItem ./winflexbison -Recurse -File).FullName\n      shell: pwsh\n    - name: Install dependencies [Python]\n      run: |\n        python -m pip install --upgrade pip\n        python -m pip install --upgrade Cython wheel numpy lz4 toml pillow pygments pyreadline3 mako\n      shell: pwsh\n    - name: Build\n      run: |\n        $TOOLCHAIN_FILE = Join-Path download openage-dep-x64-windows scripts buildsystems vcpkg.cmake | Resolve-Path\n        $FLEX_PATH = (Get-ChildItem ./download -Recurse -Force -Filter 'win_flex.exe')[0].FullName\n        mkdir build\n        cd build\n        cmake -DCMAKE_TOOLCHAIN_FILE=\"$TOOLCHAIN_FILE\" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TRY_COMPILE_CONFIGURATION=Debug -DCMAKE_CXX_FLAGS='/Zc:__cplusplus /permissive- /EHsc' -DCMAKE_EXE_LINKER_FLAGS='' -DCMAKE_MODULE_LINKER_FLAGS='' -DCMAKE_SHARED_LINKER_FLAGS='' -DDOWNLOAD_NYAN=YES -DCXX_OPTIMIZATION_LEVEL=auto -DCXX_SANITIZE_FATAL=False -DCXX_SANITIZE_MODE=none -DWANT_BACKTRACE=if_available -DWANT_GPERFTOOLS_PROFILER=if_available -DWANT_GPERFTOOLS_TCMALLOC=False -DWANT_INOTIFY=if_available -DWANT_NCURSES=if_available -DWANT_OPENGL=if_available -DWANT_VULKAN=if_available -DFLEX_EXECUTABLE=\"$FLEX_PATH\" -G \"Visual Studio 17 2022\" -A x64 ../source\n        cmake --build . --config Debug -- -nologo -maxCpuCount\n      shell: pwsh\n    - name: Package\n      run: |\n        mkdir package\n        cd package\n        mkdir dll\n        cd ..\n        $STAGING_PATH = Resolve-Path package\n        $DLL_PATH = Join-Path package dll | Resolve-Path\n\n        cd build\n        $NYAN_DLL = (Get-ChildItem . -Recurse -Force -Filter 'nyan.dll')[0].FullName\n        $OPENAGE_DLL = (Get-ChildItem . -Recurse -Force -Filter 'openage.dll')[0].FullName\n        $NATIVE_OUTPUT = Split-Path -Path $OPENAGE_DLL -Parent\n        Copy-Item -Path ./openage -Destination $STAGING_PATH -Recurse\n        Copy-Item -Path $NYAN_DLL -Destination $DLL_PATH\n        Copy-Item -Path (Join-Path $NATIVE_OUTPUT *.dll) -Destination $DLL_PATH\n        Copy-Item -Path run.* -Destination $STAGING_PATH\n      shell: pwsh\n    - name: Test\n      run: |\n        $DLL_PATH = Join-Path package dll | Resolve-Path\n        cd package\n        python -m openage --add-dll-search-path $DLL_PATH --version\n        python -m openage --add-dll-search-path $DLL_PATH test -a\n      shell: pwsh\n    - name: Publish build artifacts\n      uses: actions/upload-artifact@v4\n      if: ${{ always() }}\n      with:\n        name: build-files\n        path: './build'\n        if-no-files-found: error\n        retention-days: 30\n    - name: Publish packaged artifacts\n      uses: actions/upload-artifact@v4\n      if: ${{ always() }}\n      with:\n        name: package-files\n        path: './package'\n        if-no-files-found: error\n        retention-days: 30\n"
  },
  {
    "path": ".gitignore",
    "content": "# editor-specific files\n\\#*\n.#*\n*~\n.*.swp\n/CMakeLists.txt.user\n*.temp\n\n# ELF files\n*.o\n*.a\n*.so\n\n# python bytecode\n*.pyc\n*.pyo\n__pycache__\n*.pyd\n\n# MSVC files\n*.pdb\n*.dll\n*.exe\n\n# workflow\n*.orig\n/TODO.org\n\n# build system\n/bin\n/.bin\n/build\n/deps\n/.ccls-cache\n/.cache\n\n# Nix build output\n/result\n\n# root dir run script\n/run\n/run.cpp\n/run.html\n\n# CMake in-source builds\n/DartConfiguration.tcl\n/codegen_depend_cache\n/codegen_target_cache\n/Doxyfile\n/Testing\n/py/setup.py\nCTestTestfile.cmake\nCMakeFiles\ncmake_install.cmake\nCMakeCache.txt\nMakefile\n!/Makefile\n\n# debugging\ncallgrind.out.*\nperf.data*\n.gdb_history\n\n# code search\n/.ignore\n/.globalrc\n/GPATH\n/GRTAGS\n/GTAGS\n\n# IDEs\n.vscode\n.idea\n\n# CMake + ccls\n/compile_commands.json\n\n# Virtual Environments\n/.venv/\n\n# macOS\n.DS_Store\n\n# copyrighted assets\n/assets/converted/\n/assets/terrain/\n"
  },
  {
    "path": ".mailmap",
    "content": "Michael Enßlin <michael@ensslin.cc>\nJonas Jelten <jj@sft.lol> <jonas.jelten@gmail.com>\nJonas Jelten <jj@sft.lol> <jelten@in.tum.de>\nJonas Jelten <jj@sft.lol> <jj@sft.mx>\nRenée Kooi <renee@kooi.me> <renekooi@outlook.com>\nRenée Kooi <renee@kooi.me> <rene@kooi.me>\nIngo Saftbaumer <?> <matthias@bogad.at>\nIngo Saftbaumer <?> <matthias.bogad@tum.de>\nJames Mintram <jamesmintram@gmail.com> <james@lemonmoosegames.com>\nSam Schetterer <samschet@gmail.com> <schets@users.noreply.github.com>\nJimmy Berry <jimmy@boombatower.com>\nJonathan Remnant <jono4728@gmail.com>\nHenry Snoek <?> <snoek09@users.noreply.github.com>\ncoop shell (Michael Enßlin, Jonas Jelten, Andre Kupka) <coop@sft.mx>\nFranz-Niclas Muschter <fm@stusta.net> <franz-niclas.muschter@stusta.net>\nNiklas Fiekas <niklas.fiekas@backscattering.de> <niklas.fiekas@tu-clausthal.de>\nWojciech Nawrocki <wjnawrocki@protonmail.com> <wjnawrocki+gh@protonmail.com>\nSimon San <?> <14062932+simonsan@users.noreply.github.com>\nTobias Feldballe <tobias@osandweb.dk> <tobi.fp@gmail.com>\nTobias Feldballe <tobias@osandweb.dk> <tf@jumpstory.com>\nJonas Borchelt <jonas.borchelt@connected.link>\nDerek Frogget <fro22003@byui.edu> <114030121+derekfrogget@users.noreply.github.com>\nNikhil Ghosh <nghosh606@gmail.com>\nDavid Wever <dmwever@crimson.ua.edu> <56411717+dmwever@users.noreply.github.com>\nNgô Xuân Minh <xminh.ngo.00@gmail.com>\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# >=3.16 finding numpy with the findpython3 module\ncmake_minimum_required(VERSION 3.16)\n\n\n# main build configuration file\n\n# text art: figlet -f rounded \"[SFT] openage\" | sed -e 's/\\\\/\\\\\\\\/g'\nmessage(\"\n\n ___  ______ _______ _______ ___\n|  _)/ _____|_______|_______|_  |\n| | ( (____  _____      _     | |    ___  ____  _____ ____  _____  ____ _____\n| |  \\\\____ \\\\|  ___)    | |    | |   / _ \\\\|  _ \\\\| ___ |  _ \\\\(____ |/ _  | ___ |\n| |_ _____) ) |        | |   _| |  | |_| | |_| | ____| | | / ___ ( (_| | ____|\n|___|______/|_|        |_|  (___|   \\\\___/|  __/|_____)_| |_\\\\_____|\\\\___ |_____)\n                                         |_|                     (_____|\n\nWelcome to the SFT technologies computer-aided openage build system!\n\nYou have chosen, or been chosen, to attempt the daring task of building openage.\nIf you have installed all the dependencies that are conveniently listed in\n[doc/building.md], this _might_ just work!\n\nIf it doesn't, consider reporting the issue, or ask for help:\n  * GitHub: https://github.com/SFTtech/openage\n  * Matrix: #sfttech:matrix.org\n\")\n\n\n##################################################\n# main buildsystem setup entry point\nproject(openage CXX)\n\n# C++ standard requirement\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\n# Python and Cython requirements\nset(PYTHON_MIN_VERSION 3.9)\nset(CYTHON_MIN_VERSION 3.0.10)\nset(CYTHON_MIN_VERSION_FALLBACK 0.29.31)\nset(CYTHON_MAX_VERSION_FALLBACK 3.0.7)\n\n# CMake policies\nforeach(pol\n        CMP0074  # use <pkg>_ROOT vars in find_package()\n        CMP0067  # honor language standard in try_compile()\n        CMP0071  # enable automoc for generated files\n        CMP0072  # prefers GLVND by default FindOpenGL\n        CMP0048  # project() command manages VERSION variables\n        CMP0094  # take the first satisfying Python version\n        CMP0082  # run add_subdirectory() in the declaration order\n        CMP0102  # Don't create empty cache entries\n       )\n\tif (POLICY ${pol})\n\t\tcmake_policy(SET ${pol} NEW)\n\tendif()\nendforeach()\n\n# don't print 'Built target ...' messages\n# upstream since cmake v3.4.0-rc1 (by commit 1d3984780df8)\nset_property(GLOBAL PROPERTY TARGET_MESSAGES OFF)\n\n# Ensure CMAKE_BUILD_TYPE is set correctly.\nif(NOT CMAKE_BUILD_TYPE)\n\tset(CMAKE_BUILD_TYPE \"Debug\")\nendif()\nstring(TOUPPER \"CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}\" BUILD_TYPE_CXX_FLAGS)\n\n\n##################################################\n# options: keep up to date with those in ./configure!\nif(NOT DEFINED WANT_BACKTRACE)\n\tset(WANT_BACKTRACE if_available)\nendif()\n\nif(NOT DEFINED WANT_INOTIFY)\n\tset(WANT_INOTIFY if_available)\nendif()\n\nif(NOT DEFINED WANT_OPENGL)\n\tset(WANT_OPENGL if_available)\nendif()\n\nif(NOT DEFINED WANT_VULKAN)\n\tset(WANT_VULKAN if_available)\nendif()\n\nif(NOT DEFINED WANT_GPERFTOOLS_PROFILER)\n\tset(WANT_GPERFTOOLS_PROFILER if_available)\nendif()\n\nif(NOT DEFINED WANT_GPERFTOOLS_TCMALLOC)\n\tset(WANT_GPERFTOOLS_TCMALLOC false)\nendif()\n\nif(NOT DEFINED WANT_NCURSES)\n\tset(WANT_NCURSES if_available)\nendif()\n\nif(NOT DEFINED WANT_IWYU)\n\tset(WANT_IWYU false)\nendif()\n\n##################################################\n# static content filesystem locations\nif(NOT DEFINED GLOBAL_ASSET_DIR)\n\tset(ASSET_DIR \"share/openage\")\n\tif(MSVC)\n\t\tset(GLOBAL_ASSET_DIR \"${ASSET_DIR}\")\n\telse()\n\t\tset(GLOBAL_ASSET_DIR \"${CMAKE_INSTALL_PREFIX}/${ASSET_DIR}\")\n\tendif()\nendif()\n\nif(NOT DEFINED GLOBAL_CONFIG_DIR)\n\tset(CONFIG_DIR \"etc/openage\")\n\tif(MSVC)\n\t\tset(GLOBAL_CONFIG_DIR \"${CONFIG_DIR}\")\n\telse()\n\t\tset(GLOBAL_CONFIG_DIR \"${CMAKE_INSTALL_PREFIX}/${CONFIG_DIR}\")\n\tendif()\nendif()\n\n\n##################################################\n# ccache setup\n\n# distros can also do this but they don't use this mechanism\noption(ENABLE_CCACHE \"prefix each compile command with ccache\")\n\nif(ENABLE_CCACHE)\n\tfind_program(CCACHE_FOUND \"ccache\")\n\n\tif(CCACHE_FOUND)\n\t\tset_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)\n\t\tset_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)\n\telse()\n\t\tmessage(FATAL_ERROR \"ccache not found, but you requested it\")\n\tendif(CCACHE_FOUND)\nendif()\n\n\n##################################################\n# clang tidy static analysis\noption(\n\tENABLE_CLANG_TIDY\n\t\"activate clang tidy messages\"\n\tOFF\n)\nif(ENABLE_CLANG_TIDY)\n\tset(CMAKE_CXX_CLANG_TIDY \"clang-tidy;-checks=-*,readability-*\")\nendif()\n\n\n# option processing is now done.\n\n##################################################\n# include buildsystem features\n\n# add search paths to helper modules\nset(BUILDSYSTEM_DIR \"${CMAKE_SOURCE_DIR}/buildsystem\")\nset(CMAKE_MODULE_PATH \"${BUILDSYSTEM_DIR}\" \"${BUILDSYSTEM_DIR}/modules/\")\n\n# prioritize macOS frameworks since they're probably newer\n# than the system libraries\nset(CMAKE_FIND_FRAMEWORK LAST)\nset(CMAKE_FIND_APPBUNDLE LAST)\n\n# load helper modules\ninclude(GNUInstallDirs)\ninclude(CheckInSourceBuild)\ninclude(HandleCXXOptions)\ninclude(CheckCompilerFeatures)\ninclude(CMakeParseArguments)\ninclude(HandlePythonOptions)\ninclude(CheckRuntimeDependencies)\ninclude(DetectProjectVersion)\ninclude(DependencyFetch)\ninclude(FindPackageHandleStandardArgs)\n\n# include build configuration modules\ninclude(CTest)\n\n# initialize language support\ninclude(codegen)\ninclude(cpp)\ninclude(doxygen)\ninclude(options)\ninclude(python)\ninclude(util)\n\n\n# now that all modules and settings are loaded,\n# apply those to the project.\n\n\n##################################################\n# set project version\nif(USED_GIT_VERSION)\n\t# VERSION_FULL_STRING is the full git describe\n\tset(VERSION_FULL_STRING \"${PROJECT_VERSION}\")\n\t# PROJECT_VERSION is MAJOR.MINOR.PATCH.TWEAK with Commit-Count as Tweak\n\tSTRING(REGEX REPLACE \"v(([0-9]+.|[0-9]+)+)-([0-9]+)-g([a-f0-9]+)\"\n\t\t\t\"\\\\1.\\\\3\" PROJECT_VERSION \"${VERSION_FULL_STRING}\")\nendif()\nproject(openage VERSION \"${PROJECT_VERSION}\")\n\n# set CI version\nif(DEFINED ENV{CI_CFG_VERSION})\n    set(CI_CFG_VERSION \"$ENV{CI_CFG_VERSION}\")\nelse()\n    set(CI_CFG_VERSION \"NOT SET\")\nendif()\n\n\n##################################################\n# documentation generation\n\n# create documentation\ndoxygen_configure(libopenage/ openage/ doc/ README.md)\n\n\n##################################################\n# static content\nadd_subdirectory(assets/)\nadd_subdirectory(dist/)\nadd_subdirectory(cfg/)\n\n\n##################################################\n# C++ content\nadd_subdirectory(libopenage/)\n\n\n##################################################\n# Python content (uses the C++ library)\n\n# create a virtual library that, when linked to,\n# injects a header inclusion, and links to libopenage.\n# -> all cython modules get our hacks included and link to libopenage.\nadd_library(pyext_libopenage INTERFACE)\nif(MSVC)\n\tset(FORCE_INCLUDE_CXXFLAG \"/FI\")\nelse()\n\tset(FORCE_INCLUDE_CXXFLAG \"-include\")\nendif()\ntarget_compile_options(pyext_libopenage INTERFACE\n\t${FORCE_INCLUDE_CXXFLAG} \"${CMAKE_SOURCE_DIR}/libopenage/pyinterface/hacks.h\"\n)\ntarget_link_libraries(pyext_libopenage INTERFACE libopenage)\nset(PYEXT_LINK_LIBRARY pyext_libopenage)\n\nconfigure_file(run.py.in run.py)\nadd_cython_modules(EMBED NOINSTALL ${CMAKE_CURRENT_BINARY_DIR}/run.py)\nadd_py_modules(BININSTALL ${CMAKE_CURRENT_BINARY_DIR}/run.py AS openage)\nadd_subdirectory(openage/)\n\npython_finalize()\n\n\n##################################################\n# packaging.\n\n# Ensure that packaging is always the last step.\nadd_subdirectory(packaging)\n\n\n##################################################\n# show build configuration overview\n\nmessage(\"\")\nprint_config_options()\n\nmessage(\"${PROJECT_NAME} ${PROJECT_VERSION}\n\n   version string | ${VERSION_FULL_STRING}\n         compiler | ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\n           python | ${PYTHON_VERSION_STRING}\n       build type | ${CMAKE_BUILD_TYPE}\n         cxxflags | ${CMAKE_CXX_FLAGS} ${EXTRA_FLAGS}\n build type flags | ${${BUILD_TYPE_CXX_FLAGS}}\n        build dir | ${CMAKE_BINARY_DIR}\n   install prefix | ${CMAKE_INSTALL_PREFIX}\npy install prefix | ${CMAKE_PY_INSTALL_PREFIX}\n\")\n\n##################################################\n# done! that was easy, right?\n"
  },
  {
    "path": "Makefile",
    "content": "# type 'make help' for a list/explaination of recipes.\n\nBUILDDIR = bin\n\nMAKEARGS += $(if $(VERBOSE),,--no-print-directory)\n\n.PHONY: default\ndefault: build\n\n.PHONY: all\nall: default\n\n$(BUILDDIR):\n\t@echo \"call ./configure to initialize the build directory.\"\n\t@echo \"also see ./configure --help, and doc/building.md\"\n\t@echo \"\"\n\t@false\n\n.PHONY: install\ninstall: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) install\n\n.PHONY: run\nrun: build\n\tcd $(BUILDDIR) && ./run main\n\n.PHONY: test\ntest: tests checkfast\n\n.PHONY: tests\ntests: build\n\tcd $(BUILDDIR) && ./run test -a\n\n.PHONY: build\nbuild: $(BUILDDIR)\n\t@$(MAKE) $(MAKEARGS) -C $(BUILDDIR)\n\n.PHONY: ninja\nninja: $(BUILDDIR)\n\t@ninja -C $(BUILDDIR)\n\n.PHONY: libopenage\nlibopenage: $(BUILDDIR)\n\t@$(MAKE) $(MAKEARGS) -C $(BUILDDIR) libopenage\n\n.PHONY: codegen\ncodegen: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cppgen\n\n.PHONY: cppgen\ncppgen: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cppgen\n\n.PHONY: pxdgen\npxdgen: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) pxdgen\n\n.PHONY: compilepy\ncompilepy: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) compilepy\n\n.PHONY: inplacemodules\ninplacemodules:\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) inplacemodules\n\n.PHONY: cythonize\ncythonize: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cythonize\n\n.PHONY: doc\ndoc: $(BUILDDIR)\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) doc\n\n.PHONY: cleanelf\ncleanelf: $(BUILDDIR)\n\t@# removes all object files and binaries\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) clean\n\n.PHONY: cleancodegen\ncleancodegen: $(BUILDDIR)\n\t@# removes all sourcefiles created by codegen\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cleancodegen\n\n.PHONY: cleanpxdgen\ncleanpxdgen: $(BUILDDIR)\n\t@# removes all generated .pxd files\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cleanpxdgen\n\n.PHONY: cleancython\ncleancython: $(BUILDDIR)\n\t@# removes all .cpp files created by Cython\n\t$(MAKE) $(MAKEARGS) -C $(BUILDDIR) cleancython\n\n.PHONY: clean\nclean: $(BUILDDIR) cleancodegen cleanpxdgen cleancython cleanelf\n\t@# removes object files, binaries, py modules, generated code\n\n.PHONY: cleaninsourcebuild\ncleaninsourcebuild:\n\t@echo \"cleaning remains of in-source builds\"\n\trm -rf DartConfiguration.tcl codegen_depend_cache codegen_target_cache Doxyfile Testing\n\t@find . -not -path \"./.bin/*\" -type f -name CTestTestfile.cmake              -print -delete\n\t@find . -not -path \"./.bin/*\" -type f -name cmake_install.cmake              -print -delete\n\t@find . -not -path \"./.bin/*\" -type f -name CMakeCache.txt                   -print -delete\n\t@find . -not -path \"./.bin/*\" -type f -name Makefile -not -path \"./Makefile\" -print -delete\n\t@find . -not -path \"./.bin/*\" -type d -name CMakeFiles                       -print -exec rm -r {} +\n\n.PHONY: cleanbuilddirs\ncleanbuilddirs: cleaninsourcebuild\n\t@if test -d bin; then $(MAKE) $(MAKEARGS) -C bin clean cleancython cleanpxdgen cleancodegen || true; fi\n\t@echo cleaning symlinks to build directories\n\trm -f bin\n\t@echo cleaning build directories\n\trm -rf .bin\n\t@echo cleaning cmake-time generated code\n\trm -f Doxyfile py/openage/config.py libopenage/config.h libopenage/config.cpp\n\n.PHONY: mrproper\nmrproper: cleanbuilddirs\n\t@echo cleaning converted assets\n\trm -rf userassets\n\n.PHONY: mrproperer\nmrproperer: mrproper\n\t@if ! test -d .git; then echo \"mrproperer is only available for gitrepos.\"; false; fi\n\t@echo removing ANYTHING that is not checked into the git repo\n\t@echo ENTER to confirm\n\t@read val\n\tgit clean -x -d -f\n\n.PHONY: checkfast\ncheckfast:\n\tpython3 -m buildsystem.codecompliance --fast\n\n.PHONY: checkmerge\ncheckmerge:\n\tpython3 -m buildsystem.codecompliance --merge\n\n.PHONY: checkchanged\ncheckchanged:\n\tpython3 -m buildsystem.codecompliance --merge --only-changed-files=origin/master\n\n.PHONY: checkuncommited\ncheckuncommited:\n\tpython3 -m buildsystem.codecompliance --merge --only-changed-files=HEAD\n\n.PHONY: checkpy\ncheckpy:\n\tpython3 -m buildsystem.codecompliance --pystyle --pylint\n\n.PHONY: checkall\ncheckall:\n\tpython3 -m buildsystem.codecompliance --all\n\n.PHONY: help\nhelp: $(BUILDDIR)/Makefile\n\t@echo \"openage Makefile\"\n\t@echo \"\"\n\t@echo \"wrapper that mostly forwards recipes to the cmake-generated Makefile in bin/\"\n\t@echo \"\"\n\t@echo \"targets:\"\n\t@echo \"\"\n\t@echo \"build              -> build entire project\"\n\t@echo \"libopenage         -> build libopenage\"\n\t@echo \"pxdgen             -> generate .pxd files\"\n\t@echo \"cythonize          -> compile .pyx files to .cpp\"\n\t@echo \"compilepy          -> compile .py files to .pyc\"\n\t@echo \"inplacemodules     -> create in-place modules\"\n\t@echo \"codegen            -> generate cpp sources\"\n\t@echo \"doc                -> create documentation files\"\n\t@echo \"\"\n\t@echo \"cleanelf           -> remove C++ ELF files\"\n\t@echo \"cleancodegen       -> undo 'make codegen'\"\n\t@echo \"cleancython        -> undo 'make cythonize inplacemodules'\"\n\t@echo \"cleanpxdgen        -> undo 'make pxdgen'\"\n\t@echo \"clean              -> undo 'make' (all of the above)\"\n\t@echo \"cleanbuilddirs     -> undo 'make' and './configure'\"\n\t@echo \"cleaninsourcebuild -> undo in-source build accidents\"\n\t@echo \"mrproper           -> as above, but additionally delete user assets\"\n\t@echo \"mrproperer         -> leaves nothing but ashes\"\n\t@echo \"\"\n\t@echo \"run                -> run openage\"\n\t@echo \"tests              -> run the tests (py + cpp)\"\n\t@echo \"\"\n\t@echo \"checkall           -> full code compliance check\"\n\t@echo \"checkmerge         -> code compliance check for merging to master\"\n\t@echo \"checkfast          -> fast checks only\"\n\t@echo \"checkchanged       -> full check for all files changed since origin/master\"\n\t@echo \"checkuncommited    -> full check for all currently uncommited files\"\n\t@echo \"checkpy            -> check python compliance\"\n\t@echo \"\"\n\t@echo \"test               -> tests + checkfast. this is what you should use for regular devbuilds\"\n\t@echo \"\"\n\t@echo \"CMake help:\"\n\t@test -d $(BUILDDIR) && $(MAKE) -C $(BUILDDIR) help || echo \"no builddir is configured\"\n"
  },
  {
    "path": "README.md",
    "content": "[![openage](/assets/logo/banner.svg)](http://openage.dev)\n=========================================================\n\n**openage**: a volunteer project to create a free engine clone of the *Genie Engine* used by *Age of Empires*, *Age of Empires II (HD)* and *Star Wars: Galactic Battlegrounds*, comparable to projects like [OpenMW](https://openmw.org/), [OpenRA](http://openra.net/),  [OpenSAGE](https://github.com/OpenSAGE/OpenSAGE/), [OpenTTD](https://openttd.org/) and [OpenRCT2](https://openrct2.org/).\n\nopenage uses the original game assets (such as sounds and graphics), but (for obvious reasons) doesn't ship them.\nTo play, you require *[any of the original games (AoE1, AoE2)](/doc/media_convert.md)* or their *Definitive Edition* releases.\n\n[![github stars](https://img.shields.io/github/stars/SFTtech/openage.svg)](https://github.com/SFTtech/openage/stargazers)\n[![#sfttech on matrix.org](/assets/doc/matrixroom.svg)](https://matrix.to/#/#sfttech:matrix.org)\n[![GPL licensed](/assets/doc/license.svg)](/legal/GPLv3)\n\n\nContact\n-------\n| Contact          | Where?                                                                                             |\n| ---------------- | -------------------------------------------------------------------------------------------------- |\n| Issue Tracker    | [GitHub SFTtech/openage]                                                                           |\n| Development Blog | [blog.openage.dev]                                                                                 |\n| Subreddit        | [![reddit](/assets/doc/reddit.svg) /r/openage](https://www.reddit.com/r/openage/)                  |\n| Discussions      | [GitHub Discussions]                                                                               |\n| Matrix Chat      | [![matrix](/assets/doc/matrix.svg) `#sfttech:matrix.org`](https://matrix.to/#/#sfttech:matrix.org) |\n| Money Sink       | [![money sink](/assets/doc/liberapay.svg)](https://liberapay.com/SFTtech)                          |\n\n[GitHub SFTtech/openage]: https://github.com/SFTtech/openage/issues\n[blog.openage.dev]: https://blog.openage.dev\n[GitHub Discussions]: https://github.com/SFTtech/openage/discussions\n\nTechnical foundation\n--------------------\n\n| Technology   | Component                                                     |\n| ------------ | ------------------------------------------------------------- |\n| **C++20**    | Engine core                                                   |\n| **Python3**  | Scripting, media conversion, in-game console, code generation |\n| [**Cython**] | Python/C++ Glue code                                          |\n| [**Qt6**]    | Graphical user interface                                      |\n| [**CMake**]  | Build system                                                  |\n| [**OpenGL**] | Rendering, shaders                                            |\n| [**Opus**]   | Audio codec                                                   |\n| [**nyan**]   | Content Configuration and Modding                             |\n| [**Humans**] | Mixing together all of the above                              |\n\n[**Cython**]: https://cython.org/\n[**Qt6**]: https://contribute.qt-project.org/\n[**CMake**]: https://cmake.org/\n[**OpenGL**]: https://www.opengl.org/\n[**Opus**]: https://opus-codec.org/\n[**nyan**]: https://github.com/SFTtech/nyan\n[**Humans**]: https://www.youtube.com/watch?v=fQGbXmkSArs&t=18s\n\nGoals\n-----\n\n* Fully authentic look and feel\n    * This can only be approximated since the behavior of the original game is mostly undocumented,\n    and guessing/experimenting can only get you this close\n    * We will not implement useless artificial limitations (max 30 selectable units...)\n* An easily-moddable content format: [**nyan** yet another notation](https://github.com/SFTtech/nyan)\n* An integrated Python console and API, comparable to [blender](https://www.blender.org/)\n* AI scripting in Python, you can use [machine learning](http://scikit-learn.org/stable/)\n    * here is some [additional literature](http://www.deeplearningbook.org/)\n* Re-creating [free game assets](https://github.com/SFTtech/openage-data)\n* Multiplayer (obviously)\n* Matchmaking and ranking with a [haskell masterserver](https://github.com/SFTtech/openage-masterserver)\n* Optionally, [improvements](/doc/ideas/) over the original game\n* Awesome infrastructure such as our own [Kevin CI service](https://github.com/SFTtech/kevin)\n\nBut beware, for sanity reasons:\n\n* No network compatibility with the original game.\n  You really wanna have the same problems again?\n* No binary compatibility with the original game.\n  A one-way script to convert maps/savegames/missions to openage is planned though.\n\n\nCurrent State of the Project\n----------------------------\n\n**Important notice**: At the moment, \"gameplay\" is basically non-functional.\nWe're implementing the internal game simulation (how units even do anything) with simplicity and extensibility in mind, so we had to get rid of the temporary (but kind of working) previous version.\nWith these changes, we can (finally) actually make use of our converted asset packs and our nyan API!\nWe're working day and night to make gameplay return\\*.\nIf you're interested, we wrote detailed explanations on our blog: [Part 1](https://blog.openage.dev/new-gamestate-2020.html), [Part 2](https://blog.openage.dev/engine-core-modules.html), [Monthly Devlog](https://blog.openage.dev/tag/news.html).\n\n*\\* may not actually be every day and night*\n\n|  Operating System   |                                                                                                       Build status                                                                                                        |\n| :-----------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |\n|     Debian Sid      | [![Kevin CI status](https://cidata.sft.lol/openage/branches/master/status.svg)](/kevinfile) |\n|  Ubuntu 24.04 LTS   |           [![Ubuntu 24.04 build status](https://github.com/SFTTech/openage/actions/workflows/ubuntu-24.04.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/ubuntu-24.04.yml)            |\n|        macOS        |                              [![macOS build status](https://github.com/SFTtech/openage/workflows/macOS-CI/badge.svg)](https://github.com/SFTtech/openage/actions?query=workflow%3AmacOS-CI)                               |\n| Windows Server 2019 | [![Windows Server 2019 build status](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/windows-server-2019.yml) |\n| Windows Server 2022 | [![Windows Server 2022 build status](https://github.com/SFTtech/openage/actions/workflows/windows-server-2022.yml/badge.svg?branch=master)](https://github.com/SFTtech/openage/actions/workflows/windows-server-2022.yml) |\n\n[Todo: Kevin #11]: https://github.com/SFTtech/kevin/issues/11\n\n\nInstallation Packages\n---------------------\n\nThere are many missing parts for an actually working game.\nSo if you \"just wanna play\", [you'll be disappointed](#current-state-of-the-project), unfortunately.\n\nWe strongly recommend building the program from source to get the latest, greatest, and shiniest project state :)\n\n\n* For **Linux** check at [repology](https://repology.org/project/openage/versions) if your distribution has any packages available. Otherwise, you need to build from source.\n  We don't release `*.deb`, `*.rpm`, Flatpak, snap or AppImage packages yet.\n* For **Windows** check our [release page](https://github.com/SFTtech/openage/releases) for the latest installer.\n  Otherwise, you need to build from the source.\n\n* For **macOS** we currently don't have any packages, you need to build from source.\n\nIf you need help, maybe our [troubleshooting guide](/doc/troubleshooting.md) helps you.\n\n\nQuickstart\n----------\n\n* **How do I get this to run on my box?**\n    1. [Clone](https://docs.github.com/repositories/creating-and-managing-repositories/cloning-a-repository) the repo.\n    2. Install dependencies. See [doc/building.md](/doc/building.md#dependency-installation) to get instructions for your favorite platform.\n    3. Build the project:\n   ```\n   ./configure --download-nyan\n   make\n   ```\n\n**Alternative approach:**\nYou can build and run the project using Docker. See [Running with docker](/doc/build_instructions/docker.md) for more details.\n\n* **I compiled everything. Now how do I run it?**\n    * Execute `cd bin && ./run main`.\n    * [The convert script](/doc/media_convert.md) will transform original assets into openage formats, which are a lot saner and more moddable.\n    * Use your brain and react to the things you'll see.\n\n* **Waaaaaah! It...**\n    * segfaults\n    * prints error messages I don't want to read\n    * ate my dog\n\nAll of those are features, not bugs.\n\nTo turn them off, use `./bin/run --dont-segfault --no-errors --dont-eat-dog`.\n\n\nIf this still does not help, try our [troubleshooting guide](/doc/troubleshooting.md), the [contact section](#contact)\nor the [bug tracker](https://github.com/SFTtech/openage/issues).\n\nContributing\n============\n\nYou might ask yourself now \"Sounds cool, but how do I participate\nand ~~get famous~~ contribute useful features?\".\n\nFortunately for you, there is a lot to do and we are very grateful for your help.\n\n## Where do I start?\n\n* **Check the issues** [labelled with `good first issue`](https://github.com/SFTtech/openage/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). These are tasks that you can start right away and don't require much previous knowledge.\n* **Ask us** in the [chat](https://matrix.to/#/#sfttech:matrix.org). Someone there could need\n  help with something.\n* You can also **take the initiative** and fix a bug you found, create an issue for discussion or\n  implement a feature that we never thought of, but always wanted.\n\n\n## Ok, I found something. What now?\n\n* **[Tell us](#contact)**, if you haven't already. Chances are that we have additional information\n  and directions.\n* **[Read the docs](/doc)**. They will answer most \"administrative\"\n  questions like what code style is used and how the engine core parts are connected.\n* **Read the code** and get familiar with the engine component you want to work with.\n* Do not hesitate to **[ask us for help](#contact)** if you do not understand something.\n\n\n## How do I contribute my features/changes?\n\n* Read the **[contributing guide](/doc/contributing.md)**.\n* You can upload work-in-progress (WIP) versions or drafts of your contribution to get feedback or support.\n* Tell us (again) when you want us to review your work.\n\n## I want to help, but I'm not a programmer...\n\nThen openage might be a good reason to become one! We have many issues and tasks for beginners. You\njust have to ask and we'll find something. Alternatively, lurking is also allowed.\n\n----\n\nCheers, happy hecking!\n\n\nDevelopment Process\n-------------------\n\nWhat does openage development look like in practice?\n\n* extensive [synchronization](#contact)!\n* [doc/development.md](/doc/development.md).\n\nHow can I help?\n\n* [doc/contributing.md](/doc/contributing.md).\n\nAll documentation is also in this repo:\n\n* Code documentation is embedded in the sources for Doxygen (see [doc readme](/doc/README.md)).\n* Have a look at the [doc directory](/doc/). This folder tends to get outdated when code changes.\n\n\nLicense\n-------\n\n**GNU GPLv3** or later; see [copying.md](copying.md) and [legal/GPLv3](/legal/GPLv3).\n\nI know that probably nobody is ever gonna look at the `copying.md` file,\nbut if you want to contribute code to openage, please take the time to\nskim through it and add yourself to the authors list.\n"
  },
  {
    "path": "assets/.gitignore",
    "content": "# converted game assets\n/converted/\n"
  },
  {
    "path": "assets/CMakeLists.txt",
    "content": "# Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\nadd_subdirectory(logo/)\nadd_subdirectory(shaders/)\nadd_subdirectory(test/)\nadd_subdirectory(textures/)\n\ninstall(DIRECTORY \"qml\"\n\tDESTINATION \"${ASSET_DIR}\"\n)\n\n# To show QML files in the QtCreator.\nfile(GLOB QML_SRC \"qml/*.qml\")\nadd_custom_target(qtcreator-show-qml SOURCES ${QML_SRC})\n"
  },
  {
    "path": "assets/logo/CMakeLists.txt",
    "content": "install(\n\tFILES \"crown.svg\"\n\tRENAME \"openage.svg\"\n\tDESTINATION \"share/pixmaps/\"\n)\n\ninstall(\n\tFILES \"banner.svg\"\n\tDESTINATION \"${ASSET_DIR}\"\n)\n"
  },
  {
    "path": "assets/qml/.gitignore",
    "content": "*.qmlc\n"
  },
  {
    "path": "assets/qml/Actions.qml",
    "content": "// Copyright 2016-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\t/*\n\t * Calls 'actionMode.act(name)' to execute actions.\n\t */\n\tproperty var actionMode\n\n\t/*\n\t * Use \"act\" properties of these objects to access the Action.\n\t * Action has additional \"iconCheckedSource\" property.\n\t */\n\tproperty alias actionObjects: actionObjectList.children\n\n\tvisible: false\n\n\tExclusiveGroup {\n\t\tid: stanceGrp\n\t}\n\n\tExclusiveGroup {\n\t\tid: noGrp\n\t}\n\n\tItem {\n\t\tid: actionObjectList\n\n\t\tRepeater {\n\t\t\tmodel: OA.ActionsListModel {\n\t\t\t\tid: actionObjectListModel\n\t\t\t\taction_mode: root.actionMode\n\t\t\t}\n\n\t\t\tdelegate: Item {\n\t\t\t\tproperty QtObject act: Action {\n\t\t\t\t\tonTriggered: root.actionMode.act(name)\n\n\t\t\t\t\ticonSource: ico != -1 ? actionObjectListModel.iconsSource + \".\" + ico : \"\"\n\n\t\t\t\t\tproperty url iconCheckedSource: icoChk != -1 ? actionObjectListModel.iconsSource + \".\" + icoChk : \"\"\n\t\t\t\t\tcheckable: icoChk != -1\n\t\t\t\t\texclusiveGroup: icoChk == -1 ? null : (grpID == 1) ? stanceGrp : noGrp\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/qml/ActionsGrid.qml",
    "content": "// Copyright 2016-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\n\nGrid {\n\tid: root\n\n\t/*\n\t * List of actions. Fields:\n\t * 'act' - button action of type 'Action' that may have 'url iconCheckedSource' property\n\t *\n\t * Use 'act.iconSource == \"\"' for gaps.\n\t */\n\tproperty var buttonActions\n\n\tspacing: metricsUnit * 0.5\n\n\tComponent {\n\t\tid: gridElement\n\n\t\tButtonExtruded {\n\t\t\tproperty bool flat\n\n\t\t\twidth: (root.height - root.spacing * (root.rows - 1)) / root.rows\n\t\t\theight: width\n\n\t\t\tvisible: iconSource != \"\"\n\n\t\t\tproperty url iconCheckedSource\n\t\t}\n\t}\n\n\tRepeater {\n\t\tmodel: parent.buttonActions\n\n\t\tdelegate: Loader {\n\t\t\tsourceComponent: gridElement\n\t\t\tactive: typeof act !== \"undefined\"\n\t\t\tonLoaded: {\n\t\t\t\titem.iconSource = Qt.binding(function() { return act.iconSource })\n\t\t\t\titem.action = act\n\n\t\t\t\titem.flat = Qt.binding(function() { return act.iconSourceChecked != \"\" })\n\t\t\t\titem.iconCheckedSource = Qt.binding(function() { return act.iconCheckedSource })\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t * Metric propagation.\n\t */\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tproperty int metricsUnit: metrics ? metrics.unit : fontMetrics.averageCharacterWidth\n}\n"
  },
  {
    "path": "assets/qml/BindsHelp.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nColumn {\n\tproperty var gameControl\n\n\tText {\n\t\tid: dummyText\n\t\tfont.pointSize: 9\n\t}\n\n\tText {\n\t\tfont.pointSize: dummyText.font.pointSize + 3\n\n\t\tcolor: \"white\"\n\t\ttext: gameControl.mode ? gameControl.mode.name : \"No Mode\"\n\t}\n\n\tRepeater {\n\t\tmodel: gameControlObj.mode ? gameControlObj.mode.binds : undefined\n\n\t\tText {\n\t\t\tfont.pointSize: dummyText.font.pointSize\n\t\t\tcolor: \"white\"\n\t\t\ttext: modelData\n\t\t}\n\t}\n\n\tText {\n\t\tfont.pointSize: dummyText.font.pointSize + 3\n\n\t\tcolor: \"white\"\n\t\ttext: \"Global Bindings\"\n\t}\n\n\tRepeater {\n\t\tmodel: OA.Engine.globalBinds\n\n\t\tText {\n\t\t\tfont.pointSize: dummyText.font.pointSize\n\t\t\tcolor: \"white\"\n\t\t\ttext: modelData\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/qml/ButtonExtruded.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nButton {\n\tstyle: ButtonExtrudedStyle {}\n}\n"
  },
  {
    "path": "assets/qml/ButtonExtrudedStyle.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\n/*\n * Additionally accepts 'bool flat' and 'url iconCheckedSource'.\n */\nButtonStyle {\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tproperty int metricsUnit: metrics ? metrics.unit : fontMetrics.averageCharacterWidth\n\n\tbackground: Rectangle {\n\t\tanchors.fill: parent\n\n\t\timplicitWidth: 100\n\t\timplicitHeight: 25\n\n\t\tcolor: control.flat ? \"transparent\" : control.pressed ? \"black\" : \"white\"\n\n\t\tImage {\n\t\t\tanchors.fill: parent\n\n\t\t\tproperty int extrusion: control.flat ? 0 : metricsUnit / 4\n\n\t\t\tanchors.leftMargin: control.pressed ? 0 : extrusion\n\t\t\tanchors.topMargin: anchors.leftMargin\n\t\t\tanchors.rightMargin: control.pressed ? extrusion : 0\n\t\t\tanchors.bottomMargin: anchors.rightMargin\n\n\t\t\tsource: control.checked ? control.iconCheckedSource : control.iconSource\n\t\t\tfillMode: Image.Stretch\n\t\t}\n\t}\n\n\tlabel: Item {\n\t}\n}\n"
  },
  {
    "path": "assets/qml/ButtonFlat.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nButton {\n\tstyle: ButtonFlatStyle {}\n}\n"
  },
  {
    "path": "assets/qml/ButtonFlatStyle.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nButtonStyle {\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tQtObject {\n\t\tid: d\n\t\treadonly property color color: control.hovered || control.checked ? \"yellow\" : \"white\"\n\t}\n\n\tbackground: Rectangle {\n\t\timplicitWidth: control.iconSource ? 0 : 100\n\t\timplicitHeight: control.iconSource ? 0 : 25\n\n\t\tcolor: \"transparent\"\n\n\t\tborder.width:  control.text || control.hovered || !control.iconSource || control.checked ? 1 : 0\n\t\tborder.color: control.checked ? \"yellow\" : \"white\"\n\t\tradius: 4\n\t}\n\n\tlabel: Item {\n\t\timplicitWidth: row.implicitWidth + (control.text ? fontMetrics.averageCharacterWidth * 2 : 0)\n\t\timplicitHeight: row.implicitHeight + (control.text ? fontMetrics.averageCharacterWidth : 0)\n\n\t\tRow {\n\t\t\tid: row\n\t\t\tanchors.centerIn: parent\n\n\t\t\tImage {\n\t\t\t\tsourceSize.width: control.buttonIconWidth\n\t\t\t\tsourceSize.height: control.buttonIconHeight\n\t\t\t\tsource: control.iconSource\n\t\t\t\tanchors.verticalCenter: parent.verticalCenter\n\t\t\t}\n\n\t\t\tText {\n\t\t\t\tid: text\n\n\t\t\t\tcolor: d.color\n\n\t\t\t\ttext: control.text\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/qml/CheckBoxFlat.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nCheckBox {\n\tstyle: CheckBoxFlatStyle {}\n}\n"
  },
  {
    "path": "assets/qml/CheckBoxFlatStyle.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nCheckBoxStyle {\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tQtObject {\n\t\tid: metrics\n\t\treadonly property int unit: fontMetrics.averageCharacterWidth\n\t}\n\n\tQtObject {\n\t\tid: d\n\t\treadonly property color color: control.hovered ? \"yellow\" : \"white\"\n\t}\n\n\tindicator: Rectangle {\n\t\timplicitWidth: metrics.unit * 2\n\t\timplicitHeight: metrics.unit * 2\n\n\t\tcolor: \"transparent\"\n\n\t\tborder.width: 1\n\t\tborder.color: \"white\"\n\t\tradius: 4\n\n\t\tRectangle {\n\t\t\tvisible: control.checked\n\t\t\tcolor: d.color\n\t\t\tradius: 1\n\t\t\tanchors.margins: metrics.unit * .5\n\t\t\tanchors.fill: parent\n\t\t}\n\t}\n\n\tlabel: Text {\n\t\tcolor: d.color\n\t\ttext: control.text\n\t}\n}\n"
  },
  {
    "path": "assets/qml/CreateGameWhenReady.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\n\nimport yay.sfttech.openage 1.0 as OA\n\nOA.GameCreator {\n\tid: root\n\n\tproperty bool enabled: false\n\tproperty var gameControl\n\tproperty int gameControlTargetModeIndex\n\n\tproperty int specState: gameSpec ? gameSpec.state : OA.GameSpec.Null\n\tproperty int gameState: game ? game.state : OA.GameMain.Null\n\n\tonSpecStateChanged: {\n\t\tif (enabled && specState == OA.GameSpec.Ready)\n\t\t\tactivate()\n\t}\n\n\tonGameStateChanged: {\n\t\tif (enabled && gameState == OA.GameMain.Running)\n\t\t\tif (gameControl.modeIndex != -1)\n\t\t\t\tgameControl.modeIndex = gameControlTargetModeIndex\n\t\t\telse\n\t\t\t\tconsole.error(\"CreateGameWhenReady: could not find the desired mode to switch to\")\n\t}\n}\n"
  },
  {
    "path": "assets/qml/GeneratorControl.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Layouts 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\tproperty var generatorParameters\n\tproperty var gameSpec\n\tproperty var game\n\n\timplicitWidth: elements.width\n\timplicitHeight: elements.height\n\n\tColumn {\n\t\tid: elements\n\n\t\tspacing: genActions.spacing\n\n\t\tColumnLayout {\n\t\t\tid: genActions\n\n\t\t\tOA.GameSaver {\n\t\t\t\tid: gameSaver\n\n\t\t\t\tgame: root.game\n\t\t\t\tgeneratorParameters: root.generatorParameters\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\ttext: \"save_game\"\n\t\t\t\tonClicked: gameSaver.activate()\n\t\t\t}\n\n\t\t\tOA.GameCreator {\n\t\t\t\tid: gameCreator\n\n\t\t\t\tgame: root.game\n\t\t\t\tgameSpec: root.gameSpec\n\t\t\t\tgeneratorParameters: root.generatorParameters\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\ttext: \"generate_game\"\n\t\t\t\tonClicked: {\n\t\t\t\t\tgameCreator.activate()\n\t\t\t\t\tgameSaver.clearErrors()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\ttext: \"end_game\"\n\t\t\t\tonClicked: {\n\t\t\t\t\tgame.clear()\n\t\t\t\t\tgameCreator.clearErrors()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\ttext: \"reload_assets\"\n\t\t\t\tonClicked: gameSpec.invalidate()\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\ttext: \"quit\"\n\t\t\t\tonClicked: game.engine.stop()\n\t\t\t}\n\t\t}\n\n\t\tText {\n\t\t\tproperty string errorStringSeparator: gameCreator.errorString && gameSaver.errorString ? \"\\n\" : \"\"\n\t\t\tproperty string errorString: gameCreator.errorString + errorStringSeparator + gameSaver.errorString\n\n\t\t\tcolor: errorString ? \"red\" : \"white\"\n\n\t\t\ttext: if (errorString)\n\t\t\t\t\t\"Error: \" + errorString\n\t\t\t\telse\n\t\t\t\t\tswitch (root.game.state) {\n\t\t\t\t\t\tcase OA.GameMain.Null:\n\t\t\t\t\t\t\t\"Not running\"\n\t\t\t\t\t\t\tbreak\n\n\t\t\t\t\t\tcase OA.GameMain.Running:\n\t\t\t\t\t\t\t\"Running\"\n\t\t\t\t\t\t\tbreak\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\"Unknown\"\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/qml/GeneratorParametersConfiguration.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Layouts 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\tproperty var generatorParameters\n\n\timplicitHeight: parameterTable.height\n\n\tGridLayout {\n\t\tid: parameterTable\n\n\t\tRepeater {\n\t\t\tmodel: root.generatorParameters\n\n\t\t\tText {\n\t\t\t\tLayout.row: index\n\t\t\t\tLayout.column: 0\n\n\t\t\t\tcolor: \"white\"\n\n\t\t\t\ttext: display\n\t\t\t}\n\t\t}\n\n\t\tRepeater {\n\t\t\tmodel: root.generatorParameters\n\n\t\t\tComponent {\n\t\t\t\tid: textField\n\n\t\t\t\tTextFieldFlat {\n\t\t\t\t\tproperty var model\n\n\t\t\t\t\ttext: model.edit\n\t\t\t\t\tonTextChanged: model.edit = text\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tComponent {\n\t\t\t\tid: checkBox\n\n\t\t\t\tCheckBoxFlat {\n\t\t\t\t\tproperty var model\n\n\t\t\t\t\tchecked: model.edit\n\t\t\t\t\tonCheckedChanged: model.edit = checked\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdelegate: Loader {\n\t\t\t\tLayout.row: index\n\t\t\t\tLayout.column: 1\n\n\t\t\t\tsourceComponent: typeof(model.edit) == \"boolean\" ? checkBox : textField\n\n\t\t\t\tonLoaded: item.model = Qt.binding(function() { return model })\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/qml/IngameHud.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Layouts 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\tproperty var actionMode\n\tproperty string playerName\n\tproperty int civIndex\n\n\treadonly property int topStripSubid: 0\n\treadonly property int midStripSubid: 1\n\treadonly property int leftRectSubid: 2\n\treadonly property int rightRectSubid: 3\n\treadonly property int resBaseSubid: 4\n\n\treadonly property string srcPrefix: \"image://by-filename/converted/interface/hud\"\n\treadonly property string pad: \"0000\"\n\treadonly property string srcSuffix: \".slp.png\"\n\tproperty string hudImageSource: srcPrefix + (pad + civIndex).slice(-pad.length) + srcSuffix\n\n\treadonly property string iconsPrefix: \"image://by-filename/converted/interface/\"\n\treadonly property string iconsBorder: \"image://by-filename/converted/interface/53003.slp.png.1\"\n\n\twidth: 1289\n\theight: 960\n\n\tItem {\n\t\tanchors.left: parent.left\n\t\tanchors.right: parent.right\n\t\tanchors.top: parent.top\n\n\t\theight: metricsUnit * 1.5 * 2.5\n\n\t\tImage {\n\t\t\tanchors.fill: parent\n\t\t\tsource: hudImageSource + \".\" + root.topStripSubid\n\n\t\t\tsourceSize.height: parent.height\n\t\t\tfillMode: Image.Tile\n\t\t}\n\n\t\tRowLayout {\n\t\t\tanchors.fill: parent\n\t\t\tanchors.leftMargin: metricsUnit\n\t\t\tanchors.rightMargin: metricsUnit\n\t\t\tanchors.verticalCenter: parent.verticalCenter\n\t\t\tspacing: metricsUnit * 0.7\n\n\t\t\tRow {\n\t\t\t\tspacing: metricsUnit * 0.7\n\n\t\t\t\tComponent {\n\t\t\t\t\tid: resourceIndicator\n\n\t\t\t\t\tRectangle {\n\t\t\t\t\t\tproperty string amount\n\t\t\t\t\t\tproperty int iconIndex\n\t\t\t\t\t\tproperty bool warning\n\n\t\t\t\t\t\twidth: metricsUnit * 1.5 * 6.5\n\t\t\t\t\t\theight: metricsUnit * 1.5 * 1.7\n\n\t\t\t\t\t\tcolor: \"#7FFFFFFF\"\n\n\t\t\t\t\t\tRectangle {\n\t\t\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\t\t\tanchors.rightMargin: metricsUnit * 0.3\n\t\t\t\t\t\t\tanchors.bottomMargin: metricsUnit * 0.3\n\n\t\t\t\t\t\t\tcolor: \"black\"\n\n\t\t\t\t\t\t\tImage {\n\t\t\t\t\t\t\t\tsourceSize.height: parent.height\n\n\t\t\t\t\t\t\t\tsource: hudImageSource + \".\" + (root.resBaseSubid + iconIndex)\n\t\t\t\t\t\t\t\tfillMode: Image.PreserveAspectFit\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tText {\n\t\t\t\t\t\t\t\tanchors.right: parent.right\n\t\t\t\t\t\t\t\tanchors.rightMargin: metricsUnit / 2\n\t\t\t\t\t\t\t\tanchors.verticalCenter: parent.verticalCenter\n\t\t\t\t\t\t\t\ttext: amount\n\n\t\t\t\t\t\t\t\tcolor: \"white\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tRectangle {\n\t\t\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\t\t\tanchors.rightMargin: metricsUnit * 0.3\n\t\t\t\t\t\t\tanchors.bottomMargin: metricsUnit * 0.3\n\n\t\t\t\t\t\t\tvisible: warning\n\n\t\t\t\t\t\t\tcolor: \"#80FFC100\"\n\n\t\t\t\t\t\t\tSequentialAnimation on opacity {\n\t\t\t\t\t\t\t\tloops: Animation.Infinite\n\t\t\t\t\t\t\t\tPropertyAnimation { from: 0; to: 1; duration: 250 }\n\t\t\t\t\t\t\t\tPropertyAnimation { from: 1; to: 0; duration: 250 }\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tRepeater {\n\t\t\t\t\tmodel: OA.Resources {\n\t\t\t\t\t\tactionMode: root.actionMode\n\t\t\t\t\t}\n\n\t\t\t\t\tdelegate: Loader {\n\t\t\t\t\t\tsourceComponent: resourceIndicator\n\n\t\t\t\t\t\tonLoaded: {\n\t\t\t\t\t\t\titem.amount = Qt.binding(function() { return display })\n\t\t\t\t\t\t\titem.iconIndex = Qt.binding(function() { return index })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tLoader {\n\t\t\t\t\tsourceComponent: resourceIndicator\n\n\t\t\t\t\tonLoaded: {\n\t\t\t\t\t\titem.amount = Qt.binding(function() { return root.actionMode.population })\n\t\t\t\t\t\titem.iconIndex = 4\n\t\t\t\t\t\titem.warning = Qt.binding(function() { return root.actionMode.population_warn })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tItem {\n\t\t\t\tLayout.fillWidth: true\n\t\t\t\tLayout.minimumWidth: epoch.implicitWidth\n\n\t\t\t\tRectangle {\n\t\t\t\t\tanchors.centerIn: parent\n\t\t\t\t\twidth: 200\n\t\t\t\t\theight: metricsUnit * 2.5\n\n\t\t\t\t\tcolor: \"black\"\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tid: epoch\n\t\t\t\t\t\tanchors.centerIn: parent\n\n\t\t\t\t\t\tcolor: \"white\"\n\t\t\t\t\t\ttext: root.playerName\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tRepeater {\n\t\t\t\tmodel: 5\n\n\t\t\t\tRectangle {\n\t\t\t\t\twidth: metricsUnit * 1.5 * 5\n\t\t\t\t\theight: metricsUnit * 1.5 * 1.5\n\n\t\t\t\t\tcolor: \"transparent\"\n\t\t\t\t\tborder.width: 1\n\t\t\t\t\tborder.color: \"white\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tItem {\n\t\tanchors.left: parent.left\n\t\tanchors.right: parent.right\n\t\tanchors.bottom: parent.bottom\n\n\t\theight: metricsUnit * 1.5 * 16\n\n\t\tImage {\n\t\t\tid: leftRect\n\n\t\t\tanchors.left: parent.left\n\t\t\tanchors.top: parent.top\n\t\t\tanchors.bottom: parent.bottom\n\n\t\t\twidth: height * 1.63\n\n\t\t\tsource: hudImageSource + \".\" + root.leftRectSubid\n\t\t\tfillMode: Image.Stretch\n\n\t\t\tActionsGrid {\n\t\t\t\tid: actionsGrid\n\n\t\t\t\tanchors.fill: parent\n\t\t\t\tanchors.topMargin: parent.height * 40 / 218\n\t\t\t\tanchors.leftMargin: parent.height * 40 / 218 + metricsUnit * 1.4\n\t\t\t\tanchors.rightMargin: parent.height * 40 / 218 + metricsUnit * 0.5\n\t\t\t\tanchors.bottomMargin: parent.height * 40 / 218 - metricsUnit * 1.4\n\n\t\t\t\tcolumns: 5\n\t\t\t\trows: 3\n\n\t\t\t\tActions {\n\t\t\t\t\tid: actions\n\n\t\t\t\t\tactionMode: root.actionMode\n\t\t\t\t}\n\n\t\t\t\tbuttonActions: actions.actionObjects\n\t\t\t}\n\t\t}\n\n\t\tColumnLayout {\n\t\t\tanchors.left: leftRect.right\n\t\t\tanchors.right: rightRect.left\n\t\t\tanchors.top: parent.top\n\t\t\tanchors.bottom: parent.bottom\n\n\t\t\tspacing: 0\n\n\t\t\tImage {\n\t\t\t\tid: borderStrip\n\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\tsourceSize.height: metricsUnit * 1.4\n\n\t\t\t\tsource: hudImageSource + \".\" + root.midStripSubid\n\t\t\t\tfillMode: Image.Tile\n\t\t\t}\n\n\t\t\tRectangle {\n\t\t\t\tLayout.fillHeight: true\n\t\t\t\tLayout.fillWidth: true\n\n\t\t\t\tcolor: \"#41110d\"\n\n\t\t\t\tPaper {\n\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\ttornBottom: false\n\t\t\t\t}\n\n\t\t\t\tItem {\n\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\tid: selection_single_panel\n\t\t\t\t\tvisible: root.actionMode.selection_size == 1\n\n\t\t\t\t\tImage {\n\t\t\t\t\t\tanchors.top: parent.top\n\t\t\t\t\t\tanchors.topMargin: metricsUnit * 2\n\t\t\t\t\t\tanchors.left: parent.left\n\t\t\t\t\t\tanchors.leftMargin: metricsUnit * 2\n\n\t\t\t\t\t\twidth: 50\n\t\t\t\t\t\theight: 50\n\n\t\t\t\t\t\tid: selected_icon\n\n\t\t\t\t\t\tsource: root.actionMode.selection_icon ? iconsPrefix + root.actionMode.selection_icon : iconsBorder\n\t\t\t\t\t}\n\t\t\t\t\tImage {\n\t\t\t\t\t\tanchors.centerIn: selected_icon\n\n\t\t\t\t\t\twidth: 50\n\t\t\t\t\t\theight: 50\n\n\t\t\t\t\t\tsource: iconsBorder\n\t\t\t\t\t}\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.top: selected_icon.top\n\t\t\t\t\t\tanchors.left: selected_icon.right\n\t\t\t\t\t\tanchors.leftMargin: metricsUnit * 1.2\n\n\t\t\t\t\t\tid: selected_name\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\ttext: root.actionMode.selection_name\n\t\t\t\t\t\tfont.pointSize: 16\n\t\t\t\t\t}\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.verticalCenter: selected_name.verticalCenter\n\t\t\t\t\t\tanchors.left: selected_name.right\n\t\t\t\t\t\tanchors.leftMargin: metricsUnit * 1.2\n\n\t\t\t\t\t\tid: selected_type\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\topacity: 0.8\n\t\t\t\t\t\ttext: root.actionMode.selection_type\n\t\t\t\t\t}\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.top: selected_name.bottom\n\t\t\t\t\t\tanchors.left: selected_name.left\n\t\t\t\t\t\tanchors.topMargin: metricsUnit\n\n\t\t\t\t\t\tid: selected_hp\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\ttext: root.actionMode.selection_hp\n\t\t\t\t\t}\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.top: selected_hp.bottom\n\t\t\t\t\t\tanchors.left: selected_icon.left\n\t\t\t\t\t\tanchors.topMargin: metricsUnit * 2\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\ttext: root.actionMode.selection_attrs\n\t\t\t\t\t}\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.top: parent.top\n\t\t\t\t\t\tanchors.right: parent.right\n\t\t\t\t\t\tanchors.topMargin: metricsUnit * 2\n\t\t\t\t\t\tanchors.rightMargin: metricsUnit * 2\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\ttext: root.actionMode.selection_owner\n\t\t\t\t\t\thorizontalAlignment: Text.AlignRight\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tItem {\n\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\tid: selection_group_panel\n\t\t\t\t\tvisible: root.actionMode.selection_size > 1\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.top: parent.top\n\t\t\t\t\t\tanchors.topMargin: metricsUnit * 2\n\t\t\t\t\t\tanchors.left: parent.left\n\t\t\t\t\t\tanchors.leftMargin: metricsUnit * 2\n\n\t\t\t\t\t\tcolor: \"black\"\n\t\t\t\t\t\ttext: root.actionMode.selection_name\n\t\t\t\t\t\tfont.pointSize: 14\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tItem {\n\t\t\t\t\tanchors.left: parent.left\n\t\t\t\t\tanchors.right: parent.right\n\t\t\t\t\tanchors.bottom: parent.bottom\n\t\t\t\t\tanchors.bottomMargin: metricsUnit * 3\n\t\t\t\t\tvisible: actionMode.ability.length\n\n\t\t\t\t\tText {\n\t\t\t\t\t\tanchors.centerIn: parent\n\n\t\t\t\t\t\ttext: actionMode.ability\n\t\t\t\t\t\tfont.pointSize: 14\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tImage {\n\t\t\tid: rightRect\n\n\t\t\tanchors.right: parent.right\n\t\t\tanchors.top: parent.top\n\t\t\tanchors.bottom: parent.bottom\n\n\t\t\twidth: height * 2.01\n\n\t\t\tsource: hudImageSource + \".\" + root.rightRectSubid\n\t\t\tfillMode: Image.Stretch\n\t\t}\n\t}\n\n\t/*\n\t * Metric propagation.\n\t */\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tproperty int metricsUnit: metrics ? metrics.unit : fontMetrics.averageCharacterWidth\n}\n"
  },
  {
    "path": "assets/qml/Paper.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\n\nItem {\n\tid: root\n\n\t/*\n\t * A paper texture.\n\t */\n\tproperty url paperTextureSource: \"image://by-filename/converted/interface/scr5b.slp.png.0\"\n\n\t/*\n\t * This texture is tiled along the item edges, bottom faces the inside.\n\t * So, alpha should be zero for the pixels where to keep the paper, solid color - where it's torn off.\n\t */\n\tproperty url edgeHmapSource: \"image://by-filename/textures/torn_paper_edge.png.0\"\n\n\t/*\n\t * Which edges are torn off.\n\t */\n\tproperty bool tornTop: true\n\tproperty bool tornBottom: true\n\tproperty bool tornLeft: true\n\tproperty bool tornRight: true\n\n\t/*\n\t * Changing metrics.unit (currently bound to the font) scales all textures.\n\t * So the details on the textures stay exactly the same relatively to metricsUnit.\n\t */\n\tproperty real scaling: 8. / metricsUnit\n\n\ttransform: Scale { xScale: 1. / root.scaling; yScale: 1. / root.scaling}\n\n\tItem {\n\t\tanchors.left: parent.left\n\t\tanchors.top: parent.top\n\n\t\t/*\n\t\t * Do +.5 to avoid being less than parent due to truncation.\n\t\t */\n\t\twidth: (parent.width + .5) * root.scaling\n\t\theight: (parent.height + .5) * root.scaling\n\n\t\tItem {\n\t\t\tanchors.fill: parent\n\n\t\t\t/*\n\t\t\t * Hack, so the child has 'visible' and 'layer.enabled'.\n\t\t\t */\n\t\t\tclip: true\n\n\t\t\tItem {\n\t\t\t\tid: texturedPaper\n\n\t\t\t\t/*\n\t\t\t\t * Hack, so that this item has 'visible' and 'layer.enabled'.\n\t\t\t\t */\n\t\t\t\twidth: parent.width\n\t\t\t\theight: parent.height\n\t\t\t\tx: parent.width\n\n\t\t\t\tlayer.enabled: true\n\n\t\t\t\tImage {\n\t\t\t\t\tanchors.fill: parent\n\t\t\t\t\tsource: root.paperTextureSource\n\t\t\t\t\tfillMode: Image.Tile\n\n\t\t\t\t\t/*\n\t\t\t\t\t * Use an ellipse to darken parts that are far from the center.\n\t\t\t\t\t */\n\t\t\t\t\tlayer.enabled: true\n\t\t\t\t\tlayer.wrapMode: ShaderEffectSource.Repeat\n\t\t\t\t\tlayer.samplerName: \"paperSource\"\n\t\t\t\t\tlayer.effect: ShaderEffect {\n\t\t\t\t\t\tfragmentShader: \"\n\t\t\t\t\t\t\tuniform lowp sampler2D paperSource;\n\t\t\t\t\t\t\tvarying highp vec2 qt_TexCoord0;\n\n\t\t\t\t\t\t\tconst float exposure = .5;\n\t\t\t\t\t\t\tconst float times = 3.;\n\n\t\t\t\t\t\t\tvec3 burntMidtones(vec3 src, float exposure, float times) {\n\t\t\t\t\t\t\t\tfloat factor = 1. + exposure * (.33);\n\t\t\t\t\t\t\t\treturn pow(src.rgb, vec3(factor * times));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvoid main() {\n\t\t\t\t\t\t\t\tvec4 bkg = texture2D(paperSource, qt_TexCoord0);\n\t\t\t\t\t\t\t\tvec3 burnt = burntMidtones(bkg.rgb, exposure, times);\n\n\t\t\t\t\t\t\t\tfloat burnGradient = pow(pow((qt_TexCoord0.x - .5), 2.) + pow((qt_TexCoord0.y - 1.), 2.), 1.) * .9;\n\n\t\t\t\t\t\t\t\tgl_FragColor = vec4(mix(bkg.rgb, burnt, vec3(burnGradient)), 1.);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Inverse QtGraphicalEffects.OpacityMask.\n\t\t */\n\t\tItem {\n\t\t\tanchors.fill: parent\n\n\t\t\tlayer.enabled: true\n\n\t\t\tlayer.effect: ShaderEffect {\n\t\t\t\tproperty variant source: texturedPaper\n\t\t\t\tproperty variant mask: rectWithTornOffEdges\n\n\t\t\t\tfragmentShader: \"\n\t\t\t\t\tuniform sampler2D source;\n\t\t\t\t\tuniform sampler2D mask;\n\t\t\t\t\tvarying highp vec2 qt_TexCoord0;\n\t\t\t\t\tvoid main() {\n\t\t\t\t\t\tgl_FragColor = texture2D(source, qt_TexCoord0) * (1. - texture2D(mask, qt_TexCoord0).a);\n\t\t\t\t\t}\n\t\t\t\t\"\n\t\t\t}\n\t\t}\n\n\t\t/*\n\t\t * Alpha == 1.0 where the edges are torn off.\n\t\t */\n\t\tItem {\n\t\t\tid: rectWithTornOffEdges\n\n\t\t\tanchors.fill: parent\n\t\t\tlayer.enabled: true\n\n\t\t\tImage {\n\t\t\t\tanchors.left: parent.left\n\t\t\t\tanchors.right: parent.right\n\t\t\t\tanchors.top: parent.top\n\n\t\t\t\tsource: root.edgeHmapSource\n\t\t\t\tfillMode: Image.Tile\n\n\t\t\t\tvisible: root.tornTop\n\t\t\t}\n\n\t\t\tImage {\n\t\t\t\tanchors.left: parent.left\n\t\t\t\tanchors.right: parent.right\n\t\t\t\tanchors.top: parent.top\n\n\t\t\t\tsource: root.edgeHmapSource\n\t\t\t\tfillMode: Image.Tile\n\n\t\t\t\ttransform: Rotation {\n\t\t\t\t\torigin.x: rectWithTornOffEdges.width / 2\n\t\t\t\t\torigin.y: rectWithTornOffEdges.height / 2\n\t\t\t\t\tangle: 180\n\t\t\t\t}\n\n\t\t\t\tvisible: root.tornBottom\n\t\t\t}\n\n\t\t\tImage {\n\t\t\t\tanchors.left: parent.left\n\t\t\t\tanchors.leftMargin: parent.height\n\t\t\t\tanchors.top: parent.top\n\n\t\t\t\twidth: parent.height\n\n\t\t\t\tsource: root.edgeHmapSource\n\t\t\t\tfillMode: Image.Tile\n\n\t\t\t\ttransform: Rotation {\n\t\t\t\t\torigin.y: rectWithTornOffEdges.height\n\t\t\t\t\tangle: -90\n\t\t\t\t}\n\n\t\t\t\tvisible: root.tornLeft\n\t\t\t}\n\n\t\t\tImage {\n\t\t\t\tanchors.right: parent.right\n\t\t\t\tanchors.rightMargin: parent.height\n\t\t\t\tanchors.top: parent.top\n\n\t\t\t\twidth: parent.height\n\n\t\t\t\tsource: root.edgeHmapSource\n\t\t\t\tfillMode: Image.Tile\n\n\t\t\t\ttransform: Rotation {\n\t\t\t\t\torigin.y: rectWithTornOffEdges.height\n\t\t\t\t\torigin.x: rectWithTornOffEdges.height\n\t\t\t\t\tangle: 90\n\t\t\t\t}\n\n\t\t\t\tvisible: root.tornRight\n\t\t\t}\n\n\t\t\tvisible: false\n\t\t}\n\t}\n\n\t/*\n\t * Average character width that the paper will adapt to (by scaling its texture).\n\t */\n\t/*\n\t * Metric propagation.\n\t */\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tproperty int metricsUnit: metrics ? metrics.unit : fontMetrics.averageCharacterWidth\n}\n"
  },
  {
    "path": "assets/qml/TextFieldFlat.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nTextField {\n\tstyle: TextFieldFlatStyle {}\n}\n"
  },
  {
    "path": "assets/qml/TextFieldFlatStyle.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Controls.Styles 1.3\n\nTextFieldStyle {\n\ttextColor: \"white\"\n\n\tbackground: Rectangle {\n\t\tcolor: \"transparent\"\n\n\t\tborder.width: 1\n\t\tborder.color: \"white\"\n\t\tradius: 4\n\t}\n}\n"
  },
  {
    "path": "assets/qml/TypePicker.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Layouts 1.1\n\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\t// input\n\tproperty var editorMode\n\tproperty var gameSpec\n\tproperty int iconHeight\n\n\tfunction toggle() {\n\t\tif (!categoryPicker.current && categoryButtons.children.length)\n\t\t\tcategoryButtons.children[0].checked = true\n\t\telse\n\t\t\tfor (var i = 0; i < categoryButtons.children.length; ++i) {\n\t\t\t\tvar item = categoryButtons.children[i]\n\n\t\t\t\tif (item.exclusiveGroup && item.exclusiveGroup == categoryPicker && item.checked) {\n\t\t\t\t\tfor (var j = (i + 1) % categoryButtons.children.length; j != i; j = (j + 1) % categoryButtons.children.length) {\n\t\t\t\t\t\tvar candidate = categoryButtons.children[j]\n\n\t\t\t\t\t\tif (candidate.exclusiveGroup && candidate.exclusiveGroup == categoryPicker) {\n\t\t\t\t\t\t\tcandidate.checked = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t}\n\n\t// output\n\treadonly property int current: selectedType.current ? selectedType.current.thisTypeId : -1\n\tproperty int currentHighlighted: -1\n\n\treadonly property int currentTerrain: selectedTerrain.current ? selectedTerrain.current.thisTerrainId : -1\n\treadonly property bool paintTerrain: categoryPicker.current && categoryPicker.current.text == \"terrain\"\n\n\tColumnLayout {\n\t\tanchors.fill: parent\n\n\t\tRow {\n\t\t\tid: categoryButtons\n\n\t\t\tanchors.horizontalCenter: implicitWidth < parent.width ? parent.horizontalCenter : undefined\n\n\t\t\tExclusiveGroup {\n\t\t\t\tid: categoryPicker\n\t\t\t}\n\n\t\t\tOA.Category {\n\t\t\t\tid: categoryToUse\n\t\t\t\tname: categoryPicker.current && categoryPicker.current.text != \"terrain\" ? categoryPicker.current.text : \"\"\n\t\t\t\teditorMode: root.editorMode\n\t\t\t}\n\n\t\t\tButtonFlat {\n\t\t\t\tcheckable: true\n\t\t\t\texclusiveGroup: categoryPicker\n\n\t\t\t\ttext: \"terrain\"\n\t\t\t}\n\n\t\t\tRepeater {\n\t\t\t\tmodel: editorMode.categories\n\n\t\t\t\tButtonFlat {\n\t\t\t\t\tcheckable: true\n\t\t\t\t\texclusiveGroup: categoryPicker\n\n\t\t\t\t\ttext: modelData\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tRectangle {\n\t\t\tLayout.fillWidth: true\n\t\t\tLayout.fillHeight: true\n\t\t\tLayout.minimumWidth: metricsUnit * 10\n\t\t\tLayout.minimumHeight: metricsUnit * 10\n\n\t\t\tcolor: \"transparent\"\n\n\t\t\tborder.width: 1\n\t\t\tborder.color: \"white\"\n\t\t\tradius: 4\n\n\t\t\tFlickable {\n\t\t\t\tanchors.fill: parent\n\t\t\t\tanchors.margins: metricsUnit / 2\n\n\t\t\t\tboundsBehavior: Flickable.StopAtBounds\n\n\t\t\t\tclip: true\n\t\t\t\tcontentWidth: width\n\t\t\t\tcontentHeight: typeIcons.height\n\n\t\t\t\tFlow {\n\t\t\t\t\tid: typeIcons\n\n\t\t\t\t\tanchors.left: parent.left\n\t\t\t\t\tanchors.right: parent.right\n\n\t\t\t\t\tExclusiveGroup {\n\t\t\t\t\t\tid: selectedType\n\t\t\t\t\t}\n\n\t\t\t\t\tRepeater {\n\t\t\t\t\t\tmodel: categoryPicker.current && categoryPicker.current.text != \"terrain\" ? categoryToUse : undefined\n\n\t\t\t\t\t\tButtonFlat {\n\t\t\t\t\t\t\tcheckable: true\n\t\t\t\t\t\t\texclusiveGroup: selectedType\n\n\t\t\t\t\t\t\tproperty int thisTypeId: typeId\n\n\t\t\t\t\t\t\tproperty int buttonIconHeight: iconHeight\n\t\t\t\t\t\t\ticonSource: \"image://by-graphic-id/\" + display + \".0\"\n\n\t\t\t\t\t\t\tonHoveredChanged: {\n\t\t\t\t\t\t\t\tif (hovered)\n\t\t\t\t\t\t\t\t\troot.currentHighlighted = typeId\n\t\t\t\t\t\t\t\telse if (root.currentHighlighted == typeId)\n\t\t\t\t\t\t\t\t\troot.currentHighlighted = -1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tExclusiveGroup {\n\t\t\t\t\t\tid: selectedTerrain\n\t\t\t\t\t}\n\n\t\t\t\t\tRepeater {\n\t\t\t\t\t\tmodel: categoryPicker.current && categoryPicker.current.text == \"terrain\" ? gameSpec.terrainIdCount : undefined\n\n\t\t\t\t\t\tButtonFlat {\n\t\t\t\t\t\t\tcheckable: true\n\t\t\t\t\t\t\texclusiveGroup: selectedTerrain\n\n\t\t\t\t\t\t\tproperty int thisTerrainId: index\n\n\t\t\t\t\t\t\tproperty int buttonIconHeight: iconHeight\n\t\t\t\t\t\t\ticonSource: \"image://by-terrain-id/\" + index + \".0\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/*\n\t * Metric propagation.\n\t */\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tproperty int metricsUnit: metrics ? metrics.unit : fontMetrics.averageCharacterWidth\n}\n"
  },
  {
    "path": "assets/qml/main.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport QtQuick.Controls 1.1\nimport QtQuick.Layouts 1.1\nimport QtQuick.Controls.Styles 1.3\n\nimport yay.sfttech.livereload 1.0\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\t/*\n\t * Global metric declaration.\n\t */\n\tFontMetrics {\n\t\tid: fontMetrics\n\t}\n\n\tQtObject {\n\t\tid: metrics\n\t\tproperty real scale: 1\n\t\tproperty int unit: fontMetrics.averageCharacterWidth * scale\n\t}\n\n\tOA.GameSpec {\n\t\tid: specObj\n\n\t\t/**\n\t\t * States: Null, Loading, Ready\n\t\t */\n\n\t\t// Start loading assets immediately\n\t\tactive: true\n\n\t\tassetManager: amObj\n\n\t\tLR.tag: \"spec\"\n\t}\n\n\tOA.LegacyAssetManager {\n\t\tid: amObj\n\n\t\tassetDir: OA.MainArgs.assetDir\n\n\t\tengine: OA.Engine\n\n\t\tLR.tag: \"am\"\n\t}\n\n\tOA.GameMain {\n\t\tid: gameObj\n\n\t\t/**\n\t\t * States: Null, Running\n\t\t */\n\n\t\tengine: OA.Engine\n\n\t\tLR.tag: \"game\"\n\t}\n\n\tOA.GameControl {\n\t\tid: gameControlObj\n\n\t\tengine: OA.Engine\n\t\tgame: gameObj\n\n\t\t/**\n\t\t * must be run after the engine is attached\n\t\t */\n\t\tComponent.onCompleted: {\n\t\t\tmodes = [createModeObj, editorModeObj, actionModeObj]\n\t\t\tmodeIndex = 0\n\t\t}\n\n\t\tLR.tag: \"gamecontrol\"\n\t}\n\n\tOA.GeneratorParameters {\n\t\tid: genParamsObj\n\n\t\tLR.tag: \"gen\"\n\t}\n\n\tColumnLayout {\n\t\tid: controls\n\n\t\tanchors.left: parent.left\n\t\tanchors.right: parent.right\n\t\tanchors.top: parent.top\n\n\t\tspacing: fontMetrics.averageCharacterWidth * 2\n\n\t\tstate: gameControlObj.mode ? gameControlObj.mode.name : \"\"\n\n\t\tComponent {\n\t\t\tid: changeMode\n\n\t\t\tButtonFlat {\n\t\t\t\ttext: \"change_mode\"\n\t\t\t\tonClicked: gameControlObj.modeIndex = (gameControlObj.effectiveModeIndex + 1) % gameControlObj.modes.length\n\t\t\t}\n\t\t}\n\n\t\tOA.CreateMode {\n\t\t\tid: createModeObj\n\t\t\tLR.tag: \"createMode\"\n\t\t}\n\n\t\tOA.ActionMode {\n\t\t\tid: actionModeObj\n\t\t\tLR.tag: \"actionMode\"\n\t\t}\n\n\t\tOA.EditorMode {\n\t\t\tid: editorModeObj\n\n\t\t\tcurrentTypeId: typePicker.current\n\t\t\tcurrentTerrainId: typePicker.currentTerrain\n\t\t\tpaintTerrain: typePicker.paintTerrain\n\n\t\t\tonToggle: typePicker.toggle()\n\n\t\t\tLR.tag: \"editorMode\"\n\t\t}\n\n\t\tCreateGameWhenReady {\n\t\t\tenabled: createWhenReady.checked\n\n\t\t\tgame: gameObj\n\t\t\tgameSpec: specObj\n\t\t\tgeneratorParameters: genParamsObj\n\t\t\tgameControl: gameControlObj\n\n\t\t\tgameControlTargetModeIndex: gameControlObj.modes.indexOf(actionModeObj)\n\t\t}\n\n\t\tstates: [\n\t\t\tState {\n\t\t\t\tid: creationMode\n\t\t\t\tname: createModeObj.name\n\n\t\t\t\tproperty list<Item> content: [\n\t\t\t\t\tLoader {\n\t\t\t\t\t\tsourceComponent: changeMode\n\t\t\t\t\t},\n\t\t\t\t\tGeneratorParametersConfiguration {\n\t\t\t\t\t\tgeneratorParameters: genParamsObj\n\t\t\t\t\t},\n\t\t\t\t\tGeneratorControl {\n\t\t\t\t\t\tgeneratorParameters: genParamsObj\n\t\t\t\t\t\tgameSpec: specObj\n\t\t\t\t\t\tgame: gameObj\n\t\t\t\t\t},\n\t\t\t\t\tCheckBoxFlat {\n\t\t\t\t\t\tid: createWhenReady\n\t\t\t\t\t\ttext: \"create_when_ready\"\n\t\t\t\t\t\tvisible: specObj.state == OA.GameSpec.Loading\n\t\t\t\t\t}\n\t\t\t\t]\n\n\t\t\t\tPropertyChanges {\n\t\t\t\t\ttarget: controls\n\t\t\t\t\tchildren: creationMode.content\n\t\t\t\t\tanchors.topMargin: fontMetrics.averageCharacterWidth * 4\n\t\t\t\t\tanchors.leftMargin: fontMetrics.averageCharacterWidth * 2\n\t\t\t\t}\n\t\t\t},\n\t\t\tState {\n\t\t\t\tid: editorMode\n\t\t\t\tname: editorModeObj.name\n\n\t\t\t\tproperty list<Item> content: [\n\t\t\t\t\tLoader {\n\t\t\t\t\t\tsourceComponent: changeMode\n\t\t\t\t\t},\n\t\t\t\t\tTypePicker {\n\t\t\t\t\t\tid: typePicker\n\n\t\t\t\t\t\tLayout.preferredWidth: root.width / 4\n\t\t\t\t\t\tLayout.preferredHeight: root.height / 2\n\n\t\t\t\t\t\ticonHeight: fontMetrics.averageCharacterWidth * 8\n\n\t\t\t\t\t\teditorMode: editorModeObj\n\t\t\t\t\t\tgameSpec: specObj\n\t\t\t\t\t},\n\t\t\t\t\tText {\n\t\t\t\t\t\tcolor: \"white\"\n\t\t\t\t\t\ttext: typePicker.currentHighlighted != -1 ? typePicker.currentHighlighted : \"\"\n\t\t\t\t\t}\n\t\t\t\t]\n\n\t\t\t\tPropertyChanges {\n\t\t\t\t\ttarget: controls\n\t\t\t\t\tchildren: editorMode.content\n\t\t\t\t\tanchors.topMargin: fontMetrics.averageCharacterWidth * 4\n\t\t\t\t\tanchors.leftMargin: fontMetrics.averageCharacterWidth * 2\n\t\t\t\t}\n\t\t\t},\n\t\t\tState {\n\t\t\t\tid: actionMode\n\t\t\t\tname: actionModeObj.name\n\n\t\t\t\tproperty list<Item> content: [\n\t\t\t\t\tIngameHud {\n\t\t\t\t\t\tanchors.fill: root\n\n\t\t\t\t\t\tactionMode: actionModeObj\n\t\t\t\t\t\tplayerName: gameControlObj.currentPlayerName\n\t\t\t\t\t\tcivIndex: gameControlObj.currentCivIndex\n\t\t\t\t\t}\n\t\t\t\t]\n\n\t\t\t\tPropertyChanges {\n\t\t\t\t\ttarget: controls; children: actionMode.content\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n\n\tColumnLayout {\n\t\tanchors.left: parent.left\n\t\tanchors.right: parent.right\n\t\tanchors.top: parent.top\n\t\tanchors.topMargin: fontMetrics.averageCharacterWidth * 4\n\t\tanchors.rightMargin: fontMetrics.averageCharacterWidth * 2\n\n\t\tspacing: fontMetrics.averageCharacterWidth * 2\n\n\t\tBindsHelp {\n\t\t\tLayout.alignment: Qt.AlignRight\n\n\t\t\tgameControl: gameControlObj\n\t\t}\n\t}\n\n\tComponent.onCompleted: {\n\t\tOA.ImageProviderByFilename.gameSpec = specObj\n\t\tOA.ImageProviderById.gameSpec = specObj\n\t\tOA.ImageProviderByTerrainId.gameSpec = specObj\n\t}\n}\n"
  },
  {
    "path": "assets/shaders/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/shaders\"\n\tFILES_MATCHING PATTERN \"*.glsl\"\n)\n"
  },
  {
    "path": "assets/shaders/alphamask.frag.glsl",
    "content": "// alpha masking shader\n//\n// applies an alpha mask texture to a base texture,\n// then draws the masked texture.\n\n// the base and mask texture, base is the plain terrain tile\nuniform sampler2D base_texture;\nuniform sampler2D mask_texture;\n\n// disable blending and show the mask instead\nuniform bool show_mask;\n\n// get those interpolated texture position from vertexshader\nvarying vec2 base_tex_position;\nvarying vec2 mask_tex_position;\n\n\nvoid main() {\n\t// get the texel from the uniform texture.\n\tvec4 base_pixel = texture2D(base_texture, base_tex_position);\n\tvec4 mask_pixel = texture2D(mask_texture, mask_tex_position);\n\n\tfloat factor = 1.0 - mask_pixel.x;\n\n\tvec4 blended_pixel = vec4(base_pixel.r, base_pixel.g, base_pixel.b, base_pixel.a - factor);\n\n\tif (show_mask) {\n\t\tgl_FragColor = mask_pixel;\n\t} else {\n\t\tgl_FragColor = blended_pixel;\n\t}\n}\n"
  },
  {
    "path": "assets/shaders/alphamask.vert.glsl",
    "content": "// vertex shader for applying an alpha mask to a texture\n\n// modelview*projection matrix\nuniform mat4 mvp_matrix;\n\n// the position of this vertex\nattribute vec4 vertex_position;\n\n// send the texture coordinates to the fragmentshader\nattribute vec2 base_tex_coordinates;\nattribute vec2 mask_tex_coordinates;\n\n// send the texture coordinates to the fragmentshader\nvarying vec2 base_tex_position;\nvarying vec2 mask_tex_position;\n\nvoid main(void) {\n\t// transform the position with the mvp matrix\n\tgl_Position = gl_ModelViewProjectionMatrix * vertex_position;\n\n\t// set the fixpoints for the tex coordinates at this vertex\n\tmask_tex_position = mask_tex_coordinates;\n\tbase_tex_position = base_tex_coordinates;\n}\n"
  },
  {
    "path": "assets/shaders/equalsEpsilon.glsl",
    "content": "bool equalsEpsilon(vec4 left, vec4 right, float epsilon) {\n\treturn all(lessThanEqual(abs(left - right), vec4(epsilon)));\n}\n\nbool equalsEpsilon(vec3 left, vec3 right, float epsilon) {\n\treturn all(lessThanEqual(abs(left - right), vec3(epsilon)));\n}\n\nbool equalsEpsilon(vec2 left, vec2 right, float epsilon) {\n\treturn all(lessThanEqual(abs(left - right), vec2(epsilon)));\n}\n\nbool equalsEpsilon(float left, float right, float epsilon) {\n    return (abs(left - right) <= epsilon);\n}\n"
  },
  {
    "path": "assets/shaders/final.frag.glsl",
    "content": "#version 330\n\nin vec2 tex_pos;\n\nout vec4 out_col;\n\nuniform sampler2D tex;\n\nvoid main() {\n\tout_col = texture(tex, tex_pos);\n}\n"
  },
  {
    "path": "assets/shaders/final.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\n\nout vec2 tex_pos;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n\ttex_pos = uv;\n}\n"
  },
  {
    "path": "assets/shaders/hud_drag_select.frag.glsl",
    "content": "#version 330\n\n// Color of the drag rectangle\nuniform vec4 in_col;\n\nlayout(location=0) out vec4 out_col;\n\nvoid main() {\n\tout_col = in_col;\n}\n"
  },
  {
    "path": "assets/shaders/hud_drag_select.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/shaders/identity.vert.glsl",
    "content": "#version 120\n\n// no-transformation texture mapping vertex shader\n\n// the position of this vertex\nattribute vec4 vertex_position;\n\n// interpolated texture coordinates sent to fragment shader\nvarying vec2 tex_position;\n\nvoid main(void) {\n\tgl_Position = vertex_position;\n\n\t// convert from screen coordinates (-1, 1) to texture coordinates (0, 1)\n\ttex_position = (vertex_position.xy + vec2(1.f)) / 2.f;\n}\n"
  },
  {
    "path": "assets/shaders/maptexture.frag.glsl",
    "content": "#version 120\n// total basic standard texture drawing fragment shader\n\n// the texture data\nuniform sampler2D texture;\n\n// interpolated texture coordinates received from vertex shader\nvarying vec2 tex_position;\n\nvoid main (void) {\n\t// this sets the fragment color to the corresponding texel.\n\tgl_FragColor = texture2D(texture, tex_position);\n}\n"
  },
  {
    "path": "assets/shaders/maptexture.vert.glsl",
    "content": "//total basic standard texture mapping vertex shader\n\n//modelview*projection matrix\nuniform mat4 mvp_matrix;\n\n//the position of this vertex\nattribute vec4 vertex_position;\n\n//the texture coordinates assigned to this vertex\nattribute vec2 tex_coordinates;\n\n//interpolated texture coordinates sent to fragment shader\nvarying vec2 tex_position;\n\nvoid main(void) {\n\t//transform the vertex coordinates\n\tgl_Position = gl_ModelViewProjectionMatrix * vertex_position;\n\n\t//pass the fix points for texture coordinates set at this vertex\n\ttex_position = tex_coordinates;\n}\n"
  },
  {
    "path": "assets/shaders/skybox.frag.glsl",
    "content": "#version 330\n\nuniform vec4 in_col;\n\nlayout(location=0) out vec4 out_col;\n\nvoid main() {\n\tout_col = in_col;\n}\n"
  },
  {
    "path": "assets/shaders/skybox.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/shaders/teamcolors.frag.glsl",
    "content": "//team color replacement shader\n//\n//looks for an alpha value specified by 'alpha_marker'\n//and then replaces this pixel with the desired color,\n//tinted for player_number, and using the given color as base.\n\n//the unmodified texture itself\nuniform sampler2D texture;\n\n//the desired player number the final resulting colors\nuniform int player_number;\n\n//the alpha value which marks colors to be replaced\nuniform float alpha_marker;\n\n//color entries for all players and their subcolors\nuniform vec4 player_color[NUM_OF_PLAYER_COLORS];\n\n//interpolated texture coordinates sent from vertex shader\nvarying vec2 tex_position;\n\n//create epsilon environment for float comparison\nconst float EPSILON = 0.001;\n\n//do the lookup in the player color table\n//for a playernumber (red, blue, etc)\n//get the subcolor (brightness variations)\nvec4 get_color(int playernum, int subcolor) {\n\treturn player_color[((playernum-1) * 8) + subcolor];\n}\n\nvoid main() {\n\t//get the texel from the uniform texture.\n\tvec4 pixel = texture2D(texture, tex_position);\n\n\t//check if this texel has an alpha marker, so we can replace it's rgb values.\n\tif (player_number != 1 && equalsEpsilon(pixel[3], alpha_marker, EPSILON)) {\n\t\t//try to find the base color, there are 8 of them.\n\t\tfor(int i = 0; i <= 7; i++) {\n\t\t\tif (equalsEpsilon(vec3(pixel), vec3(player_color[i]), EPSILON)) {\n\t\t\t\t//base color found, now replace it with the same color\n\t\t\t\t//but player_number tinted.\n\t\t\t\tgl_FragColor = get_color(player_number, i);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t//unknown base color gets pink muhahaha\n\t\tpixel = vec4(255.0/255.0, 20.0/255.0, 147.0/255.0, 1.0);\n\t}\n\n\t//else the texel had no marker so we can just draw it without player coloring\n\tgl_FragColor = pixel;\n}\n"
  },
  {
    "path": "assets/shaders/terrain.frag.glsl",
    "content": "#version 330\n\nin vec2 tex_pos;\n\nlayout(location=0) out vec4 out_col;\n\nuniform sampler2D tex;\n\nvoid main()\n{\n    vec4 tex_val = texture(tex, tex_pos);\n\tout_col = tex_val;\n}\n"
  },
  {
    "path": "assets/shaders/terrain.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in vec2 uv;\n\nout vec2 tex_pos;\n\nuniform mat4 model;\n\n// camera parameters for transforming the object position\n// and scaling the subtex to the correct size\nlayout (std140) uniform camera {\n    // view matrix (world to view space)\n    mat4  view;\n    // projection matrix (view to clip space)\n    mat4  proj;\n    // inverse zoom factor (1.0 / zoom)\n    float inv_zoom;\n    // inverse viewport size (1.0 / viewport size)\n    vec2  inv_viewport_size;\n};\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n    tex_pos = vec2(uv.x, 1.0 - uv.y);\n}\n"
  },
  {
    "path": "assets/shaders/texturefont.frag.glsl",
    "content": "varying vec2 tex_position;\nuniform sampler2D texture;\nuniform vec4 color;\n\nvoid main() {\n\t// Glyph's image data is stored in the RED channel of the texture\n\tfloat red = texture2D(texture, tex_position).r;\n\tif (red < 0.01) {\n\t\tdiscard;\n\t}\n\n\tgl_FragColor = color * vec4(1.0f, 1.0f, 1.0f, red);\n}\n"
  },
  {
    "path": "assets/shaders/texturefont.vert.glsl",
    "content": "attribute vec4 vertex_position;\nattribute vec2 tex_coordinates;\nvarying vec2 tex_position;\n\nvoid main() {\n\tgl_Position = gl_ModelViewProjectionMatrix * vertex_position;\n\ttex_position = tex_coordinates;\n}\n"
  },
  {
    "path": "assets/shaders/world2d.frag.glsl",
    "content": "#version 330\n\nin vec2 vert_uv;\n\nlayout(location=0) out vec4 col;\nlayout(location=1) out uint id;\n\nuniform sampler2D tex;\nuniform uint u_id;\n\n// position (top left corner) and size: (x, y, width, height)\nuniform vec4 tile_params;\n\nvec2 uv = vec2(\n\tvert_uv.x * tile_params.z + tile_params.x,\n\tvert_uv.y * tile_params.w + tile_params.y\n);\n\nvoid main() {\n\tvec4 tex_val = texture(tex, uv);\n\tint alpha = int(round(tex_val.a * 255));\n\tswitch (alpha) {\n\t\tcase 0:\n\t\t\tcol = tex_val;\n\t\t\tdiscard;\n\n\t\t\t// do not save the ID\n\t\t\treturn;\n\t\tcase 254:\n\t\t\tcol = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n\t\t\tbreak;\n\t\tcase 252:\n\t\t\tcol = vec4(0.0f, 1.0f, 0.0f, 1.0f);\n\t\t\tbreak;\n\t\tcase 250:\n\t\t\tcol = vec4(0.0f, 0.0f, 1.0f, 1.0f);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tcol = tex_val;\n\t\t\tbreak;\n\t}\n\tid = u_id;\n}\n"
  },
  {
    "path": "assets/shaders/world2d.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 v_position;\nlayout(location=1) in vec2 uv;\n\nout vec2 vert_uv;\n\n// camera parameters for transforming the object position\n// and scaling the subtex to the correct size\nlayout (std140) uniform camera {\n    // view matrix (world to view space)\n    mat4  view;\n    // projection matrix (view to clip space)\n    mat4  proj;\n    // inverse zoom factor (1.0 / zoom)\n    // high zoom = upscale subtex\n    // low zoom = downscale subtex\n    float inv_zoom;\n    // inverse viewport size (1.0 / viewport size)\n    vec2  inv_viewport_size;\n};\n\n// can be used to move the object position in world space _before_\n// it's transformed to clip space\n// this is usually unnecessary because we want to draw the\n// subtex where the object is, so this can be set to the identity matrix\nuniform mat4 model;\n\n// position of the object in world space\nuniform vec3 obj_world_position;\n\n// flip the subtexture horizontally/vertically\nuniform bool flip_x;\nuniform bool flip_y;\n\n// parameters for scaling and moving the subtex\n// to the correct position in clip space\n\n// animation scalefactor\n// scales the vertex positions so that they\n// match the subtex dimensions\n//\n// high animation scale = downscale subtex\n// low animation scale = upscale subtex\nuniform float scale;\n\n// size of the subtex (in pixels)\nuniform vec2 subtex_size;\n\n// offset of the subtex anchor point\n// from the subtex center (in pixels)\n// used to move the subtex so that the anchor point\n// is at the object position\nuniform vec2 anchor_offset;\n\nvoid main() {\n    // translate the position of the object from world space to clip space\n    // this is the position where we want to draw the subtex in 2D\n\tvec4 obj_clip_pos = proj * view * model * vec4(obj_world_position, 1.0);\n\n    // subtex has to be scaled to account for the zoom factor\n    // and the animation scale factor. essentially this is (animation scale / zoom).\n    float zoom_scale = scale * inv_zoom;\n\n    // Scale the subtex vertices\n    // we have to account for the viewport size to get the correct dimensions\n    // and then scale the subtex to the zoom factor to get the correct size\n    vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size;\n\n    // Scale the anchor offset with the same method as above\n    // to get the correct anchor position in the viewport\n    vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size;\n\n    // if the subtex is flipped, we also need to flip the anchor offset\n    // essentially, we invert the coordinates for the flipped axis\n    float anchor_x = float(flip_x) * -1.0 * anchor_scale.x + float(!flip_x) * anchor_scale.x;\n    float anchor_y = float(flip_y) * -1.0 * anchor_scale.y + float(!flip_y) * anchor_scale.y;\n\n    // offset the clip position by the offset of the subtex anchor\n    // imagine this as pinning the subtex to the object position at the subtex anchor point\n    obj_clip_pos += vec4(anchor_x, anchor_y, 0.0, 0.0);\n\n    // create a move matrix for positioning the vertices\n    // uses the vert scale and the transformed object position in clip space\n    mat4 move = mat4(vert_scale.x,   0.0,            0.0,            0.0,\n                     0.0,            vert_scale.y,   0.0,            0.0,\n                     0.0,            0.0,            1.0,            0.0,\n                     obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0);\n\n    // calculate the final vertex position\n    gl_Position = move * vec4(v_position, 0.0, 1.0);\n\n    // if the subtex is flipped, we also need to flip the uv tex coordinates\n    // essentially, we invert the coordinates for the flipped axis\n\n    // !flip_x is default because OpenGL uses bottom-left as its origin\n    float uv_x = float(!flip_x) * uv.x + float(flip_x) * (1.0 - uv.x);\n    float uv_y = float(flip_y) * uv.y + float(!flip_y) * (1.0 - uv.y);\n\n    vert_uv = vec2(uv_x, uv_y);\n}\n"
  },
  {
    "path": "assets/shaders/world3d.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\n\nout vec2 vert_uv;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 0.0, 1.0);\n    vert_uv = vec2(uv.x, 1.0 - uv.y);\n}\n"
  },
  {
    "path": "assets/test/CMakeLists.txt",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\nadd_subdirectory(nyan/)\nadd_subdirectory(qml/)\nadd_subdirectory(shaders/)\nadd_subdirectory(textures/)\n"
  },
  {
    "path": "assets/test/nyan/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/test/nyan\"\n\tFILES_MATCHING PATTERN \"*.nyan\"\n)\n"
  },
  {
    "path": "assets/test/nyan/pong.nyan",
    "content": "!version 1\n\nPongGame():\n    ball : Ball\n    player1 : Player\n    player2 : Player\n\nBall():\n    Color():\n        r : int = 0\n        g : int = 200\n        b : int = 0\n    color : Color = Color\n\nPlayer():\n    lives : int = 3\n    size : int = 200\n\nGameTest(PongGame):\n    ball = Ball\n    player1 = Player\n    player2 = Player\n\nLeftColor<Ball.Color>():\n    r = 0\n    g = 20\n    b = 230\n\nRightColor<Ball.Color>():\n    r = 180\n    g = 40\n    b = 0\n"
  },
  {
    "path": "assets/test/qml/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/test/qml\"\n\tFILES_MATCHING PATTERN \"*.qml\"\n)\n"
  },
  {
    "path": "assets/test/qml/main.qml",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\nimport QtQuick 2.4\nimport yay.sfttech.livereload 1.0\nimport yay.sfttech.openage 1.0 as OA\n\nItem {\n\tid: root\n\n\tRectangle {\n\t\twidth: 200\n\t\theight: 200\n\t\tcolor: \"red\"\n\n\t\tRectangle {\n\t\t\tx: 100\n\t\t\ty: 100\n\t\t\twidth: 100\n\t\t\theight: 100\n\t\t\tcolor: \"blue\"\n\n\t\t\tRectangle {\n\t\t\t\tid: r\n\t\t\t\twidth: 50\n\t\t\t\theight: 50\n\t\t\t\tcolor: \"green\"\n\t\t\t}\n\t\t}\n\n\t\tMouseArea {\n\t\t\tanchors.fill: parent\n\t\t\tonClicked: r.color = \"yellow\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "assets/test/shaders/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/test/shaders\"\n\tFILES_MATCHING PATTERN \"*.glsl\"\n)\n"
  },
  {
    "path": "assets/test/shaders/demo_0_display.frag.glsl",
    "content": "#version 330\n\nout vec4 col;\n\nvoid main() {\n    col = vec4(1.0, 0.4, 0.0, 0.8);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_0_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_1_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_1_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position =  vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_1_obj.frag.glsl",
    "content": "#version 330\n\nin vec2 v_uv;\nuniform sampler2D tex;\nuniform uint u_id;\n\nlayout(location=0) out vec4 col;\nlayout(location=1) out uint id;\n\nvoid main() {\n\tvec4 tex_val = texture(tex, v_uv);\n\tif (tex_val.a == 0) {\n\t\tdiscard;\n\t}\n\tcol = tex_val;\n\tid = u_id;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_1_obj.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nuniform mat4 mv;\nuniform mat4 proj;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = proj * mv * vec4(position, 0.0, 1.0);\n\tv_uv = vec2(uv.x, 1.0 - uv.y);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_2_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_2_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_2_obj.frag.glsl",
    "content": "#version 330\n\nin vec2 v_uv;\nuniform sampler2D tex;\nuniform uint u_id;\n\nlayout(location=0) out vec4 col;\nlayout(location=1) out uint id;\n\nvoid main() {\n\tvec4 tex_val = texture(tex, v_uv);\n\tint alpha = int(round(tex_val.a * 255));\n\tswitch (alpha) {\n\t\tcase 0:\n\t\tdiscard;\n\t\tbreak;\n\t\tcase 254:\n\t\tcol = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n\t\tbreak;\n\t\tcase 252:\n\t\tcol = vec4(0.0f, 1.0f, 0.0f, 1.0f);\n\t\tbreak;\n\t\tcase 250:\n\t\tcol = vec4(0.0f, 0.0f, 1.0f, 1.0f);\n\t\tbreak;\n\t\tdefault:\n\t    col = tex_val;\n\t\tbreak;\n\t}\n\tid = u_id;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_2_obj.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\n\nuniform mat4 mv;\nuniform mat4 proj;\nuniform vec4 offset_tile;\n\nfloat width = offset_tile.y - offset_tile.x;\nfloat height = offset_tile.w - offset_tile.z;\n\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = proj * mv * vec4(position, 0.0, 1.0);\n    v_uv = vec2((uv.x * width) + offset_tile.x, (((1.0 - uv.y) * height) + offset_tile.z));\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_4_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_4_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_4_obj.frag.glsl",
    "content": "#version 330\n\nin vec2 v_uv;\nuniform sampler2D tex;\nuniform uint u_id;\n\nlayout(location=0) out vec4 col;\nlayout(location=1) out uint id;\n\nvoid main() {\n\tvec4 tex_val = texture(tex, v_uv);\n\tint alpha = int(round(tex_val.a * 255));\n\tswitch (alpha) {\n\t\tcase 0:\n\t\tdiscard;\n\t\tbreak;\n\t\tcase 254:\n\t\tcol = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n\t\tbreak;\n\t\tcase 252:\n\t\tcol = vec4(0.0f, 1.0f, 0.0f, 1.0f);\n\t\tbreak;\n\t\tcase 250:\n\t\tcol = vec4(0.0f, 0.0f, 1.0f, 1.0f);\n\t\tbreak;\n\t\tdefault:\n\t    col = tex_val;\n\t\tbreak;\n\t}\n\tid = u_id;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_4_obj.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\n\nuniform mat4 mv;\nuniform mat4 proj;\nuniform vec4 offset_tile;\n\nfloat width = offset_tile.y - offset_tile.x;\nfloat height = offset_tile.w - offset_tile.z;\n\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = proj * mv * vec4(position, 0.0, 1.0);\n    v_uv = vec2((uv.x * width) + offset_tile.x, (((1.0 - uv.y) * height) + offset_tile.z));\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_5_obj.frag.glsl",
    "content": "#version 330\n\nin vec2 tex_pos;\n\nlayout(location=0) out vec4 out_col;\n\nuniform sampler2D tex;\n\nvoid main()\n{\n    vec4 tex_val = texture(tex, tex_pos);\n\tout_col = tex_val;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_5_obj.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in vec2 uv;\n\nout vec2 tex_pos;\n\nuniform mat4 model;\n\nlayout (std140) uniform cam {\n    mat4 view;\n    mat4 proj;\n};\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n    tex_pos = vec2(uv.x, 1.0 - uv.y);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_2d.frag.glsl",
    "content": "#version 330\n\nin vec2 vert_uv;\n\nlayout(location=0) out vec4 col;\n\nuniform sampler2D tex;\n\n// position (top left corner) and size: (x, y, width, height)\nuniform vec4 tile_params;\n\nvec2 uv = vec2(\n\tvert_uv.x * tile_params.z + tile_params.x,\n\tvert_uv.y * tile_params.w + tile_params.y\n);\n\nvoid main() {\n\tvec4 tex_val = texture(tex, uv);\n\tint alpha = int(round(tex_val.a * 255));\n\tswitch (alpha) {\n\t\tcase 0:\n\t\t\tcol = tex_val;\n\t\t\tdiscard;\n\t\tcase 254:\n\t\t\tcol = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n\t\t\tbreak;\n\t\tcase 252:\n\t\t\tcol = vec4(0.0f, 1.0f, 0.0f, 1.0f);\n\t\t\tbreak;\n\t\tcase 250:\n\t\t\tcol = vec4(0.0f, 0.0f, 1.0f, 1.0f);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tcol = tex_val;\n\t\t\tbreak;\n\t}\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_2d.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 v_position;\nlayout(location=1) in vec2 uv;\n\nout vec2 vert_uv;\n\n// camera parameters for transforming the object position\n// and scaling the subtex to the correct size\nlayout (std140) uniform camera {\n    // view matrix (world to view space)\n    mat4  view;\n    // projection matrix (view to clip space)\n    mat4  proj;\n    // inverse zoom factor (1.0 / zoom)\n    // high zoom = upscale subtex\n    // low zoom = downscale subtex\n    float inv_zoom;\n    // inverse viewport size (1.0 / viewport size)\n    vec2  inv_viewport_size;\n};\n\n// position of the object in world space\nuniform vec3 obj_world_position;\n\n// parameters for scaling and moving the subtex\n// to the correct position in clip space\n\n// animation scalefactor\n// scales the vertex positions so that they\n// match the subtex dimensions\n//\n// high animation scale = downscale subtex\n// low animation scale = upscale subtex\nuniform float scale;\n\n// size of the subtex (in pixels)\nuniform vec2 subtex_size;\n\n// offset of the subtex anchor point\n// from the subtex center (in pixels)\n// used to move the subtex so that the anchor point\n// is at the object position\nuniform vec2 anchor_offset;\n\nvoid main() {\n    // translate the position of the object from world space to clip space\n    // this is the position where we want to draw the subtex in 2D\n\tvec4 obj_clip_pos = proj * view * vec4(obj_world_position, 1.0);\n\n    // subtex has to be scaled to account for the zoom factor\n    // and the animation scale factor. essentially this is (animation scale / zoom).\n    float zoom_scale = scale * inv_zoom;\n\n    // Scale the subtex vertices\n    // we have to account for the viewport size to get the correct dimensions\n    // and then scale the subtex to the zoom factor to get the correct size\n    vec2 vert_scale = zoom_scale * subtex_size * inv_viewport_size;\n\n    // Scale the anchor offset with the same method as above\n    // to get the correct anchor position in the viewport\n    vec2 anchor_scale = zoom_scale * anchor_offset * inv_viewport_size;\n\n    // offset the clip position by the offset of the subtex anchor\n    // imagine this as pinning the subtex to the object position at the subtex anchor point\n    obj_clip_pos += vec4(anchor_scale.x, anchor_scale.y, 0.0, 0.0);\n\n    // create a move matrix for positioning the vertices\n    // uses the vert scale and the transformed object position in clip space\n    mat4 move = mat4(vert_scale.x,   0.0,            0.0,            0.0,\n                     0.0,            vert_scale.y,   0.0,            0.0,\n                     0.0,            0.0,            1.0,            0.0,\n                     obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0);\n\n    // calculate the final vertex position\n    gl_Position = move * vec4(v_position, 0.0, 1.0);\n\n    // flip y axis because OpenGL uses bottom-left as its origin\n    float uv_x = uv.x;\n    float uv_y = 1.0 - uv.y;\n\n    vert_uv = vec2(uv_x, uv_y);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_2d_frame.frag.glsl",
    "content": "#version 330\n\nout vec4 outcol;\n\nuniform vec4 incol;\n\nvoid main() {\n    outcol = incol;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_2d_frame.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 v_position;\n\n// camera parameters for transforming the object position\n// and scaling the subtex to the correct size\nlayout (std140) uniform camera {\n    // view matrix (world to view space)\n    mat4  view;\n    // projection matrix (view to clip space)\n    mat4  proj;\n    // inverse zoom factor (1.0 / zoom)\n    float inv_zoom;\n    // inverse viewport size (1.0 / viewport size)\n    vec2  inv_viewport_size;\n};\n\n// position of the object in world space\nuniform vec3 obj_world_position;\n\n// parameters for scaling and moving the subtex\n// to the correct position in clip space\n\n// animation scalefactor\n// scales the vertex positions so that they\n// match the subtex dimensions\n//\n// high animation scale = downscale subtex\n// low animation scale = upscale subtex\nuniform float scale;\n\n// size of the frame (in pixels)\nuniform vec2 frame_size;\n\nvoid main() {\n    // translate the position of the object from world space to clip space\n    // this is the position where we want to draw the subtex in 2D\n\tvec4 obj_clip_pos = proj * view * vec4(obj_world_position, 1.0);\n\n    // subtex has to be scaled to account for the zoom factor\n    // and the animation scale factor. essentially this is (animation scale / zoom).\n    float zoom_scale = scale * inv_zoom;\n\n    // Scale the subtex vertices\n    // we have to account for the viewport size to get the correct dimensions\n    // and then scale the frame to the zoom factor to get the correct size\n    vec2 vert_scale = zoom_scale * frame_size * inv_viewport_size;\n\n    // create a move matrix for positioning the vertices\n    // uses the vert scale and the transformed object position in clip space\n    mat4 move = mat4(vert_scale.x,   0.0,            0.0,            0.0,\n                     0.0,            vert_scale.y,   0.0,            0.0,\n                     0.0,            0.0,            1.0,            0.0,\n                     obj_clip_pos.x, obj_clip_pos.y, obj_clip_pos.z, 1.0);\n\n    // calculate the final vertex position\n    gl_Position = move * vec4(v_position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_2d_frustum_frame.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 v_position;\n\nvoid main() {\n    // flip the y coordinate in OpenGL\n    gl_Position = vec4(v_position.x, v_position.y, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_3d.frag.glsl",
    "content": "#version 330\n\nin vec2 tex_pos;\n\nlayout(location=0) out vec4 out_col;\n\nuniform sampler2D tex;\n\nvoid main()\n{\n    vec4 tex_val = texture(tex, tex_pos);\n\tout_col = tex_val;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_3d.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in vec2 uv;\n\nout vec2 tex_pos;\n\n// camera parameters for transforming the object position\n// and scaling the subtex to the correct size\nlayout (std140) uniform camera {\n    // view matrix (world to view space)\n    mat4  view;\n    // projection matrix (view to clip space)\n    mat4  proj;\n    // inverse zoom factor (1.0 / zoom)\n    float inv_zoom;\n    // inverse viewport size (1.0 / viewport size)\n    vec2  inv_viewport_size;\n};\n\nvoid main() {\n\tgl_Position = proj * view * vec4(position, 1.0);\n    tex_pos = vec2(uv.x, 1.0 - uv.y);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_6_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_7_shader_command.frag.glsl",
    "content": "#version 330\n\nin vec2 tex_coord;\nout vec4 frag_color;\n\nuniform float time;\n\nvoid main() {\n\t// PLACEHOLDER: color\n\t// PLACEHOLDER: alpha\n\tfrag_color = vec4(r, g, b, alpha);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_7_shader_command.vert.glsl",
    "content": "#version 330\n\nin vec2 position;\nout vec2 tex_coord;\n\nvoid main() {\n\ttex_coord = position.xy * 0.5 + 0.5;\n\tgl_Position = vec4(position.xy, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/demo_7_snippets/alpha.snippet",
    "content": "float alpha = (sin(time) + 1.0) * 0.5;\n"
  },
  {
    "path": "assets/test/shaders/demo_7_snippets/color.snippet",
    "content": "float r = 0.5 + 0.5 * sin(time);\nfloat g = 0.5 + 0.5 * sin(time + 2.0);\nfloat b = 0.5 + 0.5 * sin(time + 4.0);\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/test/shaders/pathfinding\"\n\tFILES_MATCHING PATTERN \"*.glsl\"\n)\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl",
    "content": "#version 330\n\nin float v_cost;\n\nout vec4 out_col;\n\nvoid main()\n{\n    if (v_cost == 255.0) {\n        out_col = vec4(0.0, 0.0, 0.0, 1.0);\n        return;\n    }\n    float cost = (v_cost / 256) * 2.0;\n    float red = clamp(cost, 0.0, 1.0);\n    float green = clamp(2.0 - cost, 0.0, 1.0);\n\tout_col = vec4(red, green, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in float cost;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nout float v_cost;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n    v_cost = cost;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position =  vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl",
    "content": "#version 330\n\n/// Flow field value\nin float flow_val;\n\n/// Integration field flags\nin float int_val;\n\nout vec4 outcol;\n\nint WAVEFRONT_BLOCKED = 0x04;\nint LINE_OF_SIGHT = 0x20;\nint PATHABLE = 0x10;\n\nvoid main() {\n    int flow_flags = int(flow_val) & 0xF0;\n    int int_flags = int(int_val);\n    if (bool(int_flags & WAVEFRONT_BLOCKED)) {\n        // wavefront blocked\n        outcol = vec4(0.9, 0.9, 0.9, 1.0);\n        return;\n    }\n\n    if (bool(int_flags & LINE_OF_SIGHT)) {\n        // line of sight\n        outcol = vec4(1.0, 1.0, 1.0, 1.0);\n        return;\n    }\n\n    if (bool(flow_flags & PATHABLE)) {\n        // pathable\n        outcol = vec4(0.7, 0.7, 0.7, 1.0);\n        return;\n    }\n\n    // not pathable\n    outcol = vec4(0.0, 0.0, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec3 position;\nlayout(location=1) in float flow_cell;\nlayout(location=2) in float int_cell;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nout float flow_val;\nout float int_val;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n    flow_val = flow_cell;\n    int_val = int_cell;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_grid.frag.glsl",
    "content": "#version 330\n\nout vec4 out_col;\n\nvoid main()\n{\n\tout_col = vec4(0.0, 0.0, 0.0, 0.3);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_grid.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl",
    "content": "#version 330\n\nin float v_cost;\n\nout vec4 out_col;\n\nvoid main()\n{\n    if (v_cost > 512.0) {\n        out_col = vec4(0.0, 0.0, 0.0, 1.0);\n        return;\n    }\n    float cost = 0.05 * v_cost;\n    float green = clamp(1.0 - cost, 0.0, 1.0);\n\tout_col = vec4(0.75, green, 0.5, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec3 position;\nlayout (location = 1) in float cost;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nout float v_cost;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n    v_cost = cost;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_obj.frag.glsl",
    "content": "#version 330\n\nuniform vec4 color;\n\nout vec4 outcol;\n\nvoid main() {\n    outcol = color;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_obj.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\n\nvoid main() {\n\tgl_Position = vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_vector.frag.glsl",
    "content": "#version 330\n\nuniform vec4 color;\n\nout vec4 outcol;\n\nvoid main() {\n    outcol = color;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_0_vector.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec3 position;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_display.frag.glsl",
    "content": "#version 330\n\nuniform sampler2D color_texture;\n\nin vec2 v_uv;\nout vec4 col;\n\nvoid main() {\n\tcol = texture(color_texture, v_uv);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_display.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\nlayout(location=1) in vec2 uv;\nout vec2 v_uv;\n\nvoid main() {\n\tgl_Position =  vec4(position, 0.0, 1.0);\n\tv_uv = uv;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_grid.frag.glsl",
    "content": "#version 330\n\nout vec4 out_col;\n\nvoid main()\n{\n\tout_col = vec4(0.0, 0.0, 0.0, 0.3);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_grid.vert.glsl",
    "content": "#version 330\n\nlayout (location = 0) in vec2 position;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_obj.frag.glsl",
    "content": "#version 330\n\nuniform vec4 color;\n\nout vec4 outcol;\n\nvoid main() {\n    outcol = color;\n}\n"
  },
  {
    "path": "assets/test/shaders/pathfinding/demo_1_obj.vert.glsl",
    "content": "#version 330\n\nlayout(location=0) in vec2 position;\n\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * view * model * vec4(position, 0.0, 1.0);\n}\n"
  },
  {
    "path": "assets/test/textures/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/test/textures\"\n\tPATTERN \"CMakeLists.txt\" EXCLUDE\n)\n"
  },
  {
    "path": "assets/test/textures/test_animation.sprite",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n# openage sprite definition file\n\nversion 2\n\ntexture 0 \"test_texture.texture\"\n\nscalefactor 1.0\n\nlayer 0 mode=loop position=20 time_per_frame=0.125\n\nangle 0\n\nframe 0 0 0 0 0\nframe 1 0 0 0 1\nframe 2 0 0 0 2\nframe 3 0 0 0 3\nframe 4 0 0 0 4\nframe 5 0 0 0 5\nframe 6 0 0 0 6\nframe 7 0 0 0 7\nframe 8 0 0 0 8\nframe 9 0 0 0 9\nframe 10 0 0 0 10\nframe 11 0 0 0 11\nframe 12 0 0 0 12\nframe 13 0 0 0 13\nframe 14 0 0 0 14\nframe 15 0 0 0 15\nframe 16 0 0 0 16\nframe 17 0 0 0 17\n"
  },
  {
    "path": "assets/test/textures/test_gaben.sprite",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage sprite definition file\n\nversion 2\n\ntexture 0 \"test_gaben.texture\"\n\nscalefactor 0.2\n\nlayer 0 mode=once\n\nangle 0\n\nframe 0 0 0 0 0\n"
  },
  {
    "path": "assets/test/textures/test_gaben.texture",
    "content": "# Copyright 2023-2023 the openage authors.\n# openage texture definition file\n\nversion 1\n\nimagefile \"gaben.png\"\n\nsize 719 1073\n\npxformat rgba8 cbits=True\n\nsubtex 0 0 719 1073 359 1000\n"
  },
  {
    "path": "assets/test/textures/test_missing.sprite",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage sprite definition file\n\nversion 2\n\ntexture 0 \"test_missing.texture\"\n\nscalefactor 1.0\n\nlayer 0 mode=once\n\nangle 0\n\nframe 0 0 0 0 0\n"
  },
  {
    "path": "assets/test/textures/test_missing.texture",
    "content": "# Copyright 2023-2023 the openage authors.\n# openage texture definition file\n\nversion 1\n\nimagefile \"missing.png\"\n\nsize 32 32\n\npxformat rgba8 cbits=True\n\nsubtex 0 0 32 32 16 16\n"
  },
  {
    "path": "assets/test/textures/test_tank.sprite",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage sprite definition file\n\nversion 2\n\ntexture 0 \"test_tank.texture\"\n\nscalefactor 2.0\n\nlayer 0 mode=off\n\nangle 0\nangle 45\nangle 90\nangle 135\nangle 180\nangle 225\nangle 270\nangle 315\n\nframe 0 0 0 0 0\nframe 0 45 0 0 1\nframe 0 90 0 0 2\nframe 0 135 0 0 3\nframe 0 180 0 0 4\nframe 0 225 0 0 5\nframe 0 270 0 0 6\nframe 0 315 0 0 7\n"
  },
  {
    "path": "assets/test/textures/test_tank.texture",
    "content": "# The image file was adapted from the \"German WW2 isometric pixel tanks\" sprite pack,\n# Copyright 2020-2020 by jh2assets.\n#\n# It's licensed under the terms of the Creative Commons Attribution v4.0 International license.\n# (https://creativecommons.org/licenses/by/4.0/)\n#\n# The original version can be found at:\n# https://jimhatama.itch.io/german-ww2-pixel-tanks\n#\n# Modifications by us:\n#   - Merged sprites from the \"german_panzer4\" animation into one spritesheet\n#\n# Copyright 2023-2023 the openage authors.\n# openage texture definition file\n\nversion 1\n\nimagefile \"test_tank.png\"\n\nsize 153 178\n\npxformat rgba8 cbits=False\n\nsubtex 109 101 32 45 16 22\nsubtex 81 55 68 44 34 22\nsubtex 1 40 78 37 39 18\nsubtex 82 1 70 52 35 26\nsubtex 73 101 34 54 17 27\nsubtex 1 79 70 51 35 25\nsubtex 1 1 79 37 38 18\nsubtex 1 132 68 45 34 22\n"
  },
  {
    "path": "assets/test/textures/test_tank_mirrored.sprite",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage sprite definition file\n\nversion 2\n\ntexture 0 \"test_tank.texture\"\n\nscalefactor 2.0\n\nlayer 0 mode=off\n\nangle 0\nangle 45\nangle 90\nangle 135\nangle 180\nangle 225 mirror_from=135\nangle 270 mirror_from=90\nangle 315 mirror_from=45\n\nframe 0 0 0 0 0\nframe 0 45 0 0 1\nframe 0 90 0 0 2\nframe 0 135 0 0 3\nframe 0 180 0 0 4\n"
  },
  {
    "path": "assets/test/textures/test_terrain.terrain",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage terrain definition file\n\nversion 2\n\ntexture 0 \"test_terrain.texture\"\n\nscalefactor 1.0\n\nlayer 0\n\nframe 0 0 0 0\n"
  },
  {
    "path": "assets/test/textures/test_terrain.texture",
    "content": "# Copyright 2023-2023 the openage authors.\n# openage texture definition file\n\nversion 1\n\nimagefile \"test_terrain.png\"\n\nsize 500 500\n\npxformat rgba8 cbits=True\n\nsubtex 0 0 500 500 0 0\n"
  },
  {
    "path": "assets/test/textures/test_terrain2.terrain",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n# openage terrain definition file\n\nversion 2\n\ntexture 0 \"test_terrain2.texture\"\n\nscalefactor 1.0\n\nlayer 0\n\nframe 0 0 0 0\n"
  },
  {
    "path": "assets/test/textures/test_terrain2.texture",
    "content": "# Copyright 2023-2023 the openage authors.\n# openage texture definition file\n\nversion 1\n\nimagefile \"test_terrain2.png\"\n\nsize 500 500\n\npxformat rgba8 cbits=True\n\nsubtex 0 0 500 500 0 0\n"
  },
  {
    "path": "assets/test/textures/test_texture.texture",
    "content": "# The image file was adapted from the \"Elemental Ground Monk\" sprite pack,\n# Copyright 2021-2021 by chierit.\n#\n# It's licensed under the terms of the Creative Commons Attribution v4.0 International license.\n# (https://creativecommons.org/licenses/by/4.0/)\n#\n# The original version can be found at:\n# https://chierit.itch.io/elementals-ground-monk\n#\n# Modifications by us:\n#   - Extracted sprites from the \"3_atk\" animation\n#   - Added an outline to the monk with the openage outline colour command (RGBA: 0,0,0,252)\n#   - Changed orange colour (239,105,47,255) with the openage playercolor colour command (RGBA: 239,105,47,254)\n#\n# Copyright 2021-2021 the openage authors.\n# See copying.md for further legal info.\n\n# openage texture definition file\n\nversion 1\n\nimagefile \"test_texture.png\"\n\nsize 208 160\n\npxformat rgba8 cbits=True\n\nsubtex 0 0 28 39 0 0\nsubtex 28 0 27 37 0 0\nsubtex 55 0 36 38 0 0\nsubtex 91 0 40 36 0 0\nsubtex 91 36 42 37 0 0\nsubtex 0 73 33 42 0 0\nsubtex 33 73 36 46 0 0\nsubtex 69 73 35 37 0 0\nsubtex 131 0 28 33 0 0\nsubtex 133 33 27 45 0 0\nsubtex 104 78 28 46 0 0\nsubtex 0 124 47 31 0 0\nsubtex 160 0 48 41 0 0\nsubtex 160 41 48 41 0 0\nsubtex 132 82 48 41 0 0\nsubtex 47 123 51 37 0 0\nsubtex 0 41 55 31 0 0\nsubtex 132 123 48 31 0 0\n"
  },
  {
    "path": "assets/textures/CMakeLists.txt",
    "content": "install(DIRECTORY \".\"\n\tDESTINATION \"${ASSET_DIR}/textures\"\n\tPATTERN \"CMakeLists.txt\" EXCLUDE\n)\n"
  },
  {
    "path": "assets/textures/torn_paper_edge.docx",
    "content": "# x,y,w,h,cx,cy\n0,0,512,16,0,0\n"
  },
  {
    "path": "buildsystem/CheckCompilerFeatures.cmake",
    "content": "# Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\ninclude(CheckCXXSourceRuns)\n\nif(cxx_thread_local IN_LIST CMAKE_CXX_COMPILE_FEATURES)\n\tset(HAVE_THREAD_LOCAL_STORAGE true)\nelse()\n\tset(HAVE_THREAD_LOCAL_STORAGE false)\nendif()\n\ncheck_cxx_source_runs(\"\n#include <compare>\n#include <concepts>\n\nclass Cat {\npublic:\n\tvirtual int maunz() const { return 42; };\n\n\tauto operator <=>(const Cat &other) const = default;\n};\n\ntemplate <typename T>\nconcept Meowable = std::same_as<T, Cat> || std::derived_from<T, Cat>;\n\ntemplate <Meowable T>\nint meow(const T &cat) {\n\treturn cat.maunz();\n}\n\nclass HouseCat : public Cat {\npublic:\n\tint maunz() const override { return 1337; };\n};\n\nint main() {\n\tHouseCat mrr{};\n\tCat rrarrr{};\n\treturn meow(mrr) > meow(rrarrr) ? 0 : 1;\n}\n\"\nHAVE_REQUIRED_CXX20_SUPPORT\n)\n\nif(NOT HAVE_REQUIRED_CXX20_SUPPORT)\n\tmessage(\"\n\nThe compiler doesn't support required C++20 features:\n  * Concepts\n  * Default comparisons\nThe following versions support these features:\n  * clang++ >= 10\n  * g++ >= 10\n  * Microsoft Visual Studio 2019 >= 16.8\nPlease upgrade your compiler to build openage.\n\n\")\n\tmessage(FATAL_ERROR \"aborting\")\nendif()\n"
  },
  {
    "path": "buildsystem/CheckInSourceBuild.cmake",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\nif(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})\n\tif(ALLOW_IN_SOURCE_BUILD)\n\t\tmessage(\"in-source build\")\n\telse()\n\t\tmessage(\"\n\n\nIn-source builds are disallowed. They are a source of infinite pain,\n- cluttering up the source folder\n- causing cmake to silently refuse creating out-of-source builds\n- overwriting the root Makefile (which contains cleaninsourcebuild)\n\nTo perform an out-of-source build,\n- mkdir bin; cd bin; cmake ..\n- or use ./configure, which will do this automatically\n\nIf you really want to perform an in-source build, use -DALLOW_IN_SOURCE_BUILD=TRUE\n\nYou need to run 'make cleaninsourcebuild' right now, since this (failed)\nattempt already produced traces of an in-source build.\ncmake will attempt in-source-builds instead of regular ones until you clean\nthose traces (remove CMakeCache.txt and CMakeFiles).\n\n\n\n\")\n\t\tmessage(FATAL_ERROR \"aborting\")\n\tendif()\nelse()\n\t# Check if the source directory has a generated file.\n\tif (EXISTS ${CMAKE_SOURCE_DIR}/openage/config.py)\n\t\t# Clean the generated files from the source directory.\n\t\texecute_process(\n\t\t\tCOMMAND git clean -Xf -- libopenage/ openage/\n\t\t\tWORKING_DIRECTORY ${CMAKE_SOURCE_DIR})\n\tendif()\nendif()\n"
  },
  {
    "path": "buildsystem/CheckRuntimeDependencies.cmake",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n# python modules\n# a list of imported modules may be obtained via\n#\n# grep -RE '^ *(import |from [^.])' | cut -d: -f2- | \\\n#     sed 's/^ *//g' | sort -u | grep -v openage\nset(REQUIRED_PYTHON_MODULES \"PIL.Image\" \"PIL.ImageDraw\" \"numpy\" \"pygments\" \"mako.template\" \"toml\" \"lz4\")\n\n# command-line tools\n# example: set(REQUIRED_UTILITIES \"foobar\")\n\n# Checks if the specified python module exists\n#\n#     check_python_module_exists(<module> <exists>)\n#         <module>: The python module.\n#         <exists>: TRUE of FALSE based on the check.\nfunction(check_python_module_exists MODULE EXISTS)\n\tset(STATEMENT \"from importlib import import_module; import_module(\\\"${MODULE}\\\")\")\n\texecute_process(\n\t\tCOMMAND ${PYTHON} -c \"${STATEMENT}\"\n\t\tRESULT_VARIABLE PY_RESULT)\n\n\tif(PY_RESULT EQUAL 0)\n\t\tset(${EXISTS} TRUE PARENT_SCOPE)\n\telse()\n\t\tset(${EXISTS} FALSE PARENT_SCOPE)\n\tendif()\nendfunction()\n\n# loop through all required python modules to find them\nforeach(_PYTHON_MODULE ${REQUIRED_PYTHON_MODULES})\n\tif(\"${PY_MOD_${_PYTHON_MODULE}_EXISTS}\" STREQUAL \"FOUND\")\n\t\tcontinue()\n\tendif()\n\n\tcheck_python_module_exists(${_PYTHON_MODULE} EXISTS)\n\n\tif(EXISTS)\n\t\tmessage(STATUS \"Checking python3 module ${_PYTHON_MODULE} - Success\")\n\t\tset(PY_MOD_${_PYTHON_MODULE}_EXISTS \"FOUND\" CACHE INTERNAL \"Python module availability\")\n\telse()\n\t\tmessage(FATAL_ERROR \"Checking python3 module ${_PYTHON_MODULE} - Not Found\")\n\tendif()\nendforeach()\n\n# loop through all required utilities to find them\nforeach(_UTILITY ${REQUIRED_UTILITIES})\n\tif(\"${UTILITY_${_UTILITY}_EXISTS}\" STREQUAL \"FOUND\")\n\t\tcontinue()\n\tendif()\n\n\tfind_program(${_UTILITY}_EXECUTABLE\n\t\tNAMES \"${_UTILITY}\"\n\t)\n\n\tif(${_UTILITY}_EXECUTABLE)\n\t\tmessage(STATUS \"Checking utility program ${_UTILITY} - Success\")\n\t\tset(UTILITY_${_UTILITY}_EXISTS \"FOUND\" CACHE INTERNAL \"Helper program availability\")\n\telse()\n\t\tmessage(FATAL_ERROR \"Checking utility program ${_UTILITY} - Not Found\")\n\tendif()\nendforeach()\n"
  },
  {
    "path": "buildsystem/DependencyFetch.cmake",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n# Fetch project with given name from the Internet.\n# Basically wraps ExternalProject and does a nested cmake invocation.\n#\n# Usage:\n#   fetch_project(\n#       NAME ${projectname}\n#       [DISABLE_UPDATES]\n#       ${locationspecification...}\n#   )\n#\n# Arguments:\n#   NAME                    -- custom name of the project\n#   DISABLE_UPDATES         -- when set, don't do automatic git updates\n#   locationspecification   -- passed to ExternalProject_Add\n#\n# sets in caller scope:\n#   ${projectname}_SOURCE_DIR     -- subproject source directory\n#   ${projectname}_BINARY_DIR     -- subproject binary directory\n#   ${projectname}_SUBPROJ_DIR    -- everything of the subproject\n#                                 -- (including src and bin directory)\n#\n# You need to specify the location with options of\n# ExternalProject_Add, for example:\n#\n# fetch_project(\n#     NAME yourmom\n#     GIT_REPOSITORY https://github.com/nevergonna/giveyouup\n#     GIT_TAG origin/master\n# )\n#\nfunction(fetch_project)\n\tcmake_parse_arguments(PROJ \"DISABLE_UPDATES\" \"NAME\" \"\" ${ARGN})\n\n\tif(PROJ_NAME STREQUAL \"\")\n\t\tmessage(FATAL_ERROR \"no project name given\")\n\tendif()\n\n\tset(PROJ_DIR \"${CMAKE_BINARY_DIR}/${PROJ_NAME}-external\")\n\tset(PROJ_DL_DIR \"${PROJ_DIR}/dl\")\n\tset(PROJ_SRC_DIR \"${PROJ_DIR}/source\")\n\tset(PROJ_BIN_DIR \"${PROJ_DIR}/bin\")\n\tset(PROJ_STAMP_DIR \"${PROJ_DIR}/stamp\")\n\n\t# CLion creates this file for multiple projects\n\tfile(REMOVE \"${PROJ_DL_DIR}/CMakeCache.txt\")\n\n\t# create the ExternalProject configuration file\n\t# for the nested cmake call.\n\tconfigure_file(\n\t\t\"${BUILDSYSTEM_DIR}/templates/ExternalFetch.cmake.in\"\n\t\t\"${PROJ_DL_DIR}/CMakeLists.txt\"\n\t\t@ONLY\n\t)\n\n\t# run cmake to \"configure\" the external project.\n\t# this prepares the download that will be done in the next step.\n\texecute_process(COMMAND\n\t\t${CMAKE_COMMAND}\n\t\t\"-G${CMAKE_GENERATOR}\"\n\t\t\"-DCMAKE_MAKE_PROGRAM:FILE=${CMAKE_MAKE_PROGRAM}\"\n\t\t.\n\t\tOUTPUT_QUIET\n\t\tRESULT_VARIABLE config_ok\n\t\tWORKING_DIRECTORY \"${PROJ_DL_DIR}\"\n\t)\n\n\tif(config_ok)\n\t\tmessage(FATAL_ERROR \"Failed to set up external project ${PROJ_NAME}\")\n\tendif()\n\n\t# Now actually download the project.\n\t# This does not \"build\" it! The \"build\" is the download execution.\n\texecute_process(COMMAND\n\t\t${CMAKE_COMMAND} --build .\n\t\tOUTPUT_QUIET\n\t\tRESULT_VARIABLE download_ok\n\t\tWORKING_DIRECTORY \"${PROJ_DL_DIR}\"\n\t)\n\n\tif(download_ok)\n\t\tmessage(FATAL_ERROR \"Failed to download external project ${PROJ_NAME}.\")\n\tendif()\n\n\tset(${PROJ_NAME}_SOURCE_DIR \"${PROJ_SRC_DIR}\" PARENT_SCOPE)\n\tset(${PROJ_NAME}_BINARY_DIR \"${PROJ_BIN_DIR}\" PARENT_SCOPE)\n\tset(${PROJ_NAME}_SUBPROJ_DIR \"${PROJ_DIR}\" PARENT_SCOPE)\nendfunction()\n"
  },
  {
    "path": "buildsystem/DetectProjectVersion.cmake",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n# Determines the version of the project\n# stores a full version string with commit hash and number in the variable `PROJECT_VERSION`\n\n# If the project version was already defined or previously retrieved, don't bother.\nif(PROJECT_VERSION)\n\treturn()\nendif()\n\n# if .git exists, try running git describe\nif(IS_DIRECTORY \"${CMAKE_SOURCE_DIR}/.git\")\n\tmessage(STATUS \"Set PROJECT_VERSION from git.\")\n\texecute_process(\n\t\tCOMMAND git describe --tags --long\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t\tRESULT_VARIABLE _RES\n\t\tOUTPUT_VARIABLE PROJECT_VERSION\n\t\tERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE\n\t)\n\n\tif(_RES EQUAL 0)\n\t\tset(USED_GIT_VERSION 1)\n\t\treturn()\n\tendif()\nendif()\n\n# Fallback for downloaded zip's or git unavailability\nset(PROJECT_VERSION_FILE \"${CMAKE_SOURCE_DIR}/openage_version\")\nif(EXISTS ${PROJECT_VERSION_FILE})\n\tfile(STRINGS ${PROJECT_VERSION_FILE} FILE_DESCRIBE_VERSION)\n\n\tSTRING(REGEX REPLACE \"([0-9]+\\\\.[0-9]+\\\\.[0-9]+)\"\n\t\t   \"\\\\1\" PROJECT_VERSION \"${FILE_DESCRIBE_VERSION}\")\nendif()\n\n# Still could not detect the version. Don't worry, raise a warning (shout a curse word?) and move on.\nif(NOT PROJECT_VERSION)\n\tmessage(WARNING \"Could not determine project version.\")\n\tset(PROJECT_VERSION \"0.0\")\nendif()\n"
  },
  {
    "path": "buildsystem/HandleCXXOptions.cmake",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n# sets CXXFLAGS and compiler for the project\n\n#TODO: integrate PGO (profile-guided optimization) build\n\n\ninclude(CheckCXXCompilerFlag)\n\nmacro(set_compiler_version_flags TYPE MINIMAL FLAGS INVERS EQTYPE)\n\tif(${INVERS} CMAKE_CXX_COMPILER_VERSION VERSION_${EQTYPE} ${MINIMAL})\n\t\tif(${TYPE} STREQUAL \"CXX\")\n\t\t\tset(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} ${FLAGS}\")\n\t\telseif(${TYPE} STREQUAL \"EXTRA\")\n\t\t\tset(EXTRA_FLAGS \"${EXTRA_FLAGS} ${FLAGS}\")\n\t\telse()\n\t\t\tmessage(FATAL_ERROR \"Invalid compiler flag type specified!\")\n\t\tendif()\n\tendif()\nendmacro()\n\nmacro(set_compiler_greater_flags TYPE MINIMAL FLAGS)\n\tset_compiler_version_flags(${TYPE} ${MINIMAL} ${FLAGS} NOT LESS)\nendmacro()\n\nmacro(set_compiler_equal_flags TYPE MINIMAL FLAGS)\n\tset_compiler_version_flags(${TYPE} ${MINIMAL} ${FLAGS} \"\" EQUAL)\nendmacro()\n\nmacro(set_compiler_flags TYPE FLAGS)\n\tset_compiler_version_flags(${TYPE} \"0.0\" ${FLAGS} \"\" GREATER)\nendmacro()\n\nmacro(test_compiler_flag_apply TYPE FLAG NAME DONTRUN)\n\tif(NOT ${DONTRUN})\n\t\tcheck_cxx_compiler_flag(\"${FLAG}\" ${NAME})\n\t\tif(${NAME})\n\t\t\tset_compiler_flags(${TYPE} \"${FLAG}\")\n\t\tendif()\n\tendif()\nendmacro()\n\nmacro(set_linker_flags FLAGS)\n\tset(CMAKE_EXE_LINKER_FLAGS \"${CMAKE_EXE_LINKER_FLAGS} ${FLAGS}\")\n\tset(CMAKE_MODULE_LINKER_FLAGS \"${CMAKE_MODULE_LINKER_FLAGS} ${FLAGS}\")\n\tset(CMAKE_SHARED_LINKER_FLAGS \"${CMAKE_SHARED_LINKER_FLAGS} ${FLAGS}\")\n\tset(CMAKE_STATIC_LINKER_FLAGS \"${CMAKE_STATIC_LINKER_FLAGS} ${FLAGS}\")\nendmacro()\n\nmacro(set_cxx_optimize_flags FLAGS)\n\tset(${BUILD_TYPE_CXX_FLAGS} \"${${BUILD_TYPE_CXX_FLAGS}} ${FLAGS}\")\nendmacro()\n\n\nset(CMAKE_EXPORT_COMPILE_COMMANDS ON)\nif(NOT MSVC)\n\tset(EXTRA_FLAGS \"${EXTRA_FLAGS} -Wall -Wextra -pedantic\")\nendif()\n\n# set up gold linker features\nmacro(try_enable_gold_linker)\n\t# Activate ld.gold instead of the default\n\toption(USE_LD_GOLD \"Use GNU gold linker\" ON)\n\tif(USE_LD_GOLD)\n\t\tif(MINGW)\n\t\t\texecute_process(COMMAND ${CMAKE_CXX_COMPILER} -Wl,--major-image-version,0,--minor-image-version,0 -fuse-ld=gold -Wl,--version ERROR_QUIET OUTPUT_VARIABLE LD_VERSION)\n\t\telse()\n\t\t\texecute_process(COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=gold -Wl,--version ERROR_QUIET OUTPUT_VARIABLE LD_VERSION)\n\t\tendif()\n\t\tif(\"${LD_VERSION}\" MATCHES \"GNU gold\")\n\t\t\tset(HAVE_LD_GOLD TRUE)\n\t\t\tset_linker_flags(\"-fuse-ld=gold\")\n\t\telse()\n\t\t\tset(HAVE_LD_GOLD FALSE)\n\t\t\tmessage(WARNING \"GNU gold linker isn't available, using the default system linker.\")\n\t\tendif()\n\tendif()\n\n\t# do splitdebug by default when in debug mode\n\tset(DEBUG_FISSION_DEFAULT OFF)\n\tif(HAVE_LD_GOLD AND CMAKE_BUILD_TYPE STREQUAL \"Debug\")\n\t\tcheck_cxx_compiler_flag(\"-gsplit-dwarf\" HAVE_GSPLIT_DWARF_SUPPORT)\n\t\tif(HAVE_GSPLIT_DWARF_SUPPORT)\n\t\t\tset(DEBUG_FISSION_DEFAULT ON)\n\t\tendif()\n\tendif()\n\n\t# https://gcc.gnu.org/wiki/DebugFission\n\toption(DEBUG_FISSION \"Enable Debug Fission\" DEBUG_FISSION_DEFAULT)\n\tif(DEBUG_FISSION)\n\t\tif(NOT HAVE_LD_GOLD)\n\t\t\tmessage(FATAL_ERROR \"GNU gold linker required for Debug Fission\")\n\t\tendif()\n\t\tset(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} -gsplit-dwarf\")\n\t\tset_linker_flags(\"-Wl,--gdb-index\")\n\tendif()\nendmacro()\n\n# check for compiler versions\nif(\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"GNU\")\n\tset_compiler_greater_flags(\"CXX\" 4.9 \"-fdiagnostics-color=auto\")\n\tset_compiler_greater_flags(\"EXTRA\" 5.0 \"-Wsuggest-override\")\n\ttry_enable_gold_linker()\n\nelseif(\"${CMAKE_CXX_COMPILER_ID}\" MATCHES \"Clang\")\n\tset_compiler_flags(\"EXTRA\" \"-Wno-gnu-statement-expression\")\n\ttry_enable_gold_linker()\n\n\tif(APPLE)\n\t\tset_compiler_flags(\"CXX\" \"-stdlib=libc++\")\n\tendif()\n\nelseif(MSVC)\n\t# Don't worry. You're not alone. If you face an issue, just ask.\n\n\t# Enable multi processor compilation on MSVC\n\tset_compiler_flags(\"CXX\" \"/MP\")\n\n\treturn() # The following flag specifications don't apply.\n\nelse() # \"Intel\", etc..\n\tmessage(WARNING \"Using untested compiler, at least I hope it's free software. Continue on your own, warrior.\")\nendif()\n\n# optional code warnings from include-what-you-use\nif(\"${CXX_INCLUDE_WHAT_YOU_USE}\" STREQUAL \"warn\")\n\tset(CXX_INCLUDE_WHAT_YOU_USE)\nelseif(\"${CXX_INCLUDE_WHAT_YOU_USE}\" STREQUAL \"error\")\n\tset(CXX_INCLUDE_WHAT_YOU_USE \"--error_always\")\nendif()\n\n# optimization settings.\n# TODO: multi-configuration support for xcode, vstudio, ...\n#       the following code just makes sense for single-config\n#       generation, e.g. makefiles.\n#       we'd have to perform the flag generation for other types as well.\n\n# these flags will now be extended by the following code.\nset(CMAKE_CXX_FLAGS_DEBUG \"-g\")\nset(CMAKE_CXX_FLAGS_RELEASE \"-DNDEBUG\")\n\n# If CXX_OPTIMIZATION_LEVEL was not provided, default to auto\nif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"\")\n\tset(CXX_OPTIMIZATION_LEVEL \"auto\")\nendif()\n\nif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"auto\")\n\tif(${CMAKE_BUILD_TYPE} STREQUAL \"Debug\")\n\t\tset(CXX_OPTIMIZATION_LEVEL \"g\")\n\telse()\n\t\tset(CXX_OPTIMIZATION_LEVEL \"3\")\n\tendif()\nendif()\n\nif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"0\")\n\tset_cxx_optimize_flags(\"-O0\")\nelseif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"1\")\n\tset_cxx_optimize_flags(\"-O1\")\nelseif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"2\")\n\tset_cxx_optimize_flags(\"-O2\")\nelseif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"3\")\n\tset_cxx_optimize_flags(\"-O3\")\nelseif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"g\")\n\tif(\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"GNU\")\n\t\tset_cxx_optimize_flags(\"-Og\")\n\telse()\n\t\tset_cxx_optimize_flags(\"-O0\")\n\tendif()\nelseif(\"${CXX_OPTIMIZATION_LEVEL}\" STREQUAL \"max\")\n\tset_cxx_optimize_flags(\"-O3 -march=native\")\n\n\tif(\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"GNU\")\n\t\tinclude(ProcessorCount)\n\t\tProcessorCount(N)\n\t\tif(NOT N EQUAL 0)\n\t\t\tset_cxx_optimize_flags(\"-flto=${N}\")\n\t\t\tset_linker_flags(\"-flto=${N}\")\n\t\tendif()\n\tendif()\nendif()\n\n\n# sanitizing options\n\nif(NOT CXX_SANITIZE_MODE)\n\tset(CXX_SANITIZE_MODE \"none\")\nendif()\n\nif(\"${CXX_SANITIZE_MODE}\" STREQUAL \"none\")\n\t# Do nothing\n\tif(\"${CXX_SANITIZE_FATAL}\")\n\t\tmessage(WARNING \"CXX_SANITIZE_FATAL is only valid when CXX_SANITIZE_MODE is not none\")\n\tendif()\nelse()\n\tset_compiler_flags(\"CXX\" \"-fno-omit-frame-pointer\")\n\tif(\"${CXX_SANITIZE_FATAL}\" AND \"${CMAKE_CXX_COMPILER_ID}\" MATCHES \"Clang\")\n\t\tset_compiler_flags(\"CXX\" \"-fno-sanitize-recover\")\n\tendif()\n\n\tif(\"${CXX_SANITIZE_MODE}\" STREQUAL \"yes\")\n\t\tset_compiler_flags(\"CXX\" \"-fsanitize=address\")\n\t\tset_compiler_flags(\"CXX\" \"-fsanitize=undefined\")\n\telseif(\"${CXX_SANITIZE_MODE}\" STREQUAL \"mem\")\n\t\tif(NOT APPLE)\n\t\t\tset_compiler_flags(\"CXX\" \"-fsanitize=memory\")\n\t\tendif()\n\telseif(\"${CXX_SANITIZE_MODE}\" STREQUAL \"thread\")\n\t\tif(NOT APPLE)\n\t\t\tset_compiler_flags(\"CXX\" \"-fsanitize=thread\")\n\t\tendif()\n\t\tif(\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"GNU\")\n\t\t\tset_compiler_flags(\"CXX\" \"-fPIC\")\n\t\t\tset_compiler_flags(\"CXX\" \"-pie\")\n\t\tendif()\n\telse()\n\t\tmessage(WARNING \"Unknown sanitizer mode provided: ${CXX_SANITIZE_MODE}\")\n\tendif()\nendif()\n"
  },
  {
    "path": "buildsystem/HandlePythonOptions.cmake",
    "content": "# Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n# finds the python interpreter, install destination and extension flags.\n\n# the Python version number requirement is in modules/FindPython_test.cpp\nfind_package(Python ${PYTHON_MIN_VERSION} REQUIRED)\n\nfind_package(Cython ${CYTHON_MIN_VERSION})\nif(NOT CYTHON_FOUND)\n\tmessage(\"Checking for alternative Cython fallback version (>=${CYTHON_MIN_VERSION_FALLBACK} AND <=${CYTHON_MAX_VERSION_FALLBACK})\")\n\tfind_package(Cython ${CYTHON_MIN_VERSION_FALLBACK} QUIET)\n\tif(CYTHON_VERSION VERSION_LESS ${CYTHON_MIN_VERSION} AND CYTHON_VERSION VERSION_GREATER ${CYTHON_MAX_VERSION_FALLBACK})\n\t\tmessage(FATAL_ERROR \"Cython version ${CYTHON_VERSION} is not compatible\")\n\telse()\n\t\tmessage(\"Compatible Cython version ${CYTHON_VERSION} found\")\n\tendif()\nendif()\n\npy_get_config_var(EXT_SUFFIX PYEXT_SUFFIX)\nif(MINGW)\n\tstring(REGEX REPLACE \"dll\" \"pyd\" PYEXT_SUFFIX \"${PYEXT_SUFFIX}\")\nendif()\n\n# This is the only useful thing after cleaning up what python suggests\nif(NOT \"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"MSVC\")\n\tset(PYEXT_CXXFLAGS \"-fwrapv\")\nendif()\n\n\n# numpy deprecated api\n# http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#configuring-the-c-build\nif(CYTHON_VERSION VERSION_GREATER_EQUAL 3)\n\tset(PYEXT_CXXFLAGS \"${PYEXT_CXXFLAGS} -DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION\")\nelse()\n\n\t# suppress #warning about deprecated numpy api\n\tif(\"${CMAKE_CXX_COMPILER_ID}\" STREQUAL \"GNU\")\n\t\tset(PYEXT_CXXFLAGS \"${PYEXT_CXXFLAGS} -Wno-cpp\")\n\telseif(\"${CMAKE_CXX_COMPILER_ID}\" MATCHES \"Clang\")\n\t\tset(PYEXT_CXXFLAGS \"${PYEXT_CXXFLAGS} -Wno-#warnings\")\n\tendif()\nendif()\n\n# silence cython+python3.8 tp_print deprecation warning\n# https://github.com/cython/cython/pull/3201\n# https://github.com/cython/cython/issues/3474\nif(PYTHON_VER VERSION_GREATER_EQUAL 3.8 AND PYTHON_VERSION VERSION_LESS 3.9)\n\tset(PYEXT_CXXCLAGS \"${PYEXT_CXXCLAGS}\" \"-Wno-deprecated-declarations\")\nendif()\n\nset(PYEXT_LIBRARY \"${PYTHON_LIBRARIES}\")\nmessage(\"PYTHON_LIBRARIES: \" \"${PYTHON_LIBRARIES}\")\n#Windows always uses optimized version of Python lib\nif(WIN32 AND \"${CMAKE_BUILD_TYPE}\" STREQUAL \"Debug\")\n\t#get index of string \"optimized\" and increment it by 1 so index points at the path of the optimized lib\n\tlist(FIND PYEXT_LIBRARY \"optimized\" _index)\n\tif(${_index} GREATER -1)\n\t\tMATH(EXPR _index \"${_index}+1\")\n\t\tlist(GET PYEXT_LIBRARY ${_index} PYEXT_LIBRARY)\n\tendif()\n\tmessage(\"force linking to python release lib, instead of debug lib when cythonising\")\n\tset(force_optimized_lib_flag \"--force_optimized_lib\")\nendif()\n\nset(PYEXT_INCLUDE_DIRS \"${PYTHON_INCLUDE_DIRS};${NUMPY_INCLUDE_DIR}\")\n\nif(NOT CMAKE_PY_INSTALL_PREFIX)\n\tif(MSVC)\n\t\tset(CMAKE_PY_INSTALL_PREFIX \"python\")\n\telse()\n\t\t# get site-packages directory, prepended with cmake's install prefix\n\t\tpy_exec(\"import sys, sysconfig, os; print(os.path.join('${CMAKE_INSTALL_PREFIX}', os.path.relpath(sysconfig.get_path('purelib'), os.path.normpath(sys.prefix))))\" PREFIX)\n\t\tset(CMAKE_PY_INSTALL_PREFIX \"${PREFIX}\")\n\tendif()\nendif()\n"
  },
  {
    "path": "buildsystem/__init__.py",
    "content": "# Copyright 2015-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCMake build helpers; see doc/buildsystem.md for further info.\n\nModules in the 'buildsystem' package are not intended to be installed\nwith openage, or used from outside the build process; otherwise, they\nwould be in the 'openage' package.\n\"\"\"\n"
  },
  {
    "path": "buildsystem/check_py_file_list.py",
    "content": "# Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTests whether the files listed via add_py_module are consistent with the\n.py files in the openage directory.\n\"\"\"\n\nimport argparse\nimport os\nimport sys\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    cli = argparse.ArgumentParser()\n    cli.add_argument('py_file_list', help=(\n        \"semicolon-separated list of listed .py files\"\n    ))\n    cli.add_argument('py_module_dir', help=(\n        \"directory containing the python module to check\"\n    ))\n    cli.add_argument('-v', '--verbose', action='store_true', help=(\n        \"produce verbose output\"\n    ))\n    args = cli.parse_args()\n\n    openage_dir = os.path.realpath(args.py_module_dir)\n\n    listed = set()\n    with open(args.py_file_list, encoding='utf8') as fileobj:\n        for filename in fileobj.read().strip().split(';'):\n            filepath = os.path.realpath(os.path.normpath(filename))\n            if filepath.startswith(openage_dir):\n                listed.add(filepath)\n            elif args.verbose:\n                print(\"Ignoring \" + filepath + \" outside \" + openage_dir)\n\n    if args.verbose:\n        print(\"Files listed in \" + args.py_file_list + \":\",\n              *sorted(listed), sep='\\n\\t')\n\n    actual = set()\n    for dirname, _, files in os.walk(openage_dir):\n        dirname = os.path.realpath(os.path.abspath(dirname))\n        for filename in files:\n            if filename.endswith('.py'):\n                actual.add(os.path.join(dirname, filename))\n\n    if args.verbose:\n        print(\"Files available:\", *sorted(actual), sep='\\n\\t')\n\n    success = True\n    for filename in sorted(actual - listed):\n        success = False\n        print(\"file was not listed via add_py_module: \" +\n              os.path.relpath(filename, openage_dir))\n\n    for filename in sorted(listed - actual):\n        success = False\n        print(\"file was listed via add_py_module but does not exist: \" +\n              os.path.relpath(filename, openage_dir))\n\n    if success:\n        return 0\n\n    return 1\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "buildsystem/codecompliance/__init__.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCode compliance checker module\n\nIntegrated into the root Makefile.\n\"\"\"\n"
  },
  {
    "path": "buildsystem/codecompliance/__main__.py",
    "content": "# Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n\"\"\"\nEntry point for the code compliance checker.\n\"\"\"\n\nimport argparse\nimport importlib\nimport os\nimport shutil\nimport subprocess\nimport sys\n\nfrom .util import log_setup\n\n\ndef parse_args():\n    \"\"\" Returns the raw argument namespace. \"\"\"\n\n    cli = argparse.ArgumentParser()\n    check_types = cli.add_mutually_exclusive_group()\n    check_types.add_argument(\"--fast\", action=\"store_true\",\n                             help=\"do all checks that can be performed quickly\")\n    check_types.add_argument(\"--merge\", action=\"store_true\",\n                             help=\"do all checks that are required before merges to master\")\n    check_types.add_argument(\"--all\", action=\"store_true\",\n                             help=\"do all checks, even the really slow ones\")\n\n    cli.add_argument(\"--only-changed-files\", metavar='GITREF',\n                     help=(\"slow checks are only done on files that have \"\n                           \"changed since GITREF.\"))\n    cli.add_argument(\"--authors\", action=\"store_true\",\n                     help=(\"check whether all git authors are in copying.md. \"\n                           \"repo must be a git repository.\"))\n    cli.add_argument(\"--clang-tidy\", action=\"store_true\",\n                     help=(\"Check the C++ code with clang-tidy. Make sure you have build the \"\n                           \"project with ./configure --clang-tidy or have set \"\n                           \"CMAKE_CXX_CLANG_TIDY for your CMake build.\"))\n    cli.add_argument(\"--cppstyle\", action=\"store_true\",\n                     help=\"check the cpp code style\")\n    cli.add_argument(\"--cython\", action=\"store_true\",\n                     help=\"check if cython is turned off\")\n    cli.add_argument(\"--headerguards\", action=\"store_true\",\n                     help=\"check all header guards\")\n    cli.add_argument(\"--legal\", action=\"store_true\",\n                     help=\"check whether all sourcefiles have legal headers\")\n    cli.add_argument(\"--filemodes\", action=\"store_true\",\n                     help=(\"check whether files in the repo have the \"\n                           \"correct access bits (-> 0644) \"))\n    cli.add_argument(\"--pylint\", action=\"store_true\",\n                     help=\"run pylint on the python code\")\n    cli.add_argument(\"--pystyle\", action=\"store_true\",\n                     help=(\"check whether the python code complies with \"\n                           \"(a selected subset of) pep8.\"))\n    cli.add_argument(\"--textfiles\", action=\"store_true\",\n                     help=\"check text files for whitespace issues\")\n    cli.add_argument(\"--test-git-change-years\", action=\"store_true\",\n                     help=(\"when doing legal checks, test whether the \"\n                           \"copyright year matches the git history.\"))\n\n    cli.add_argument(\"--fix\", action=\"store_true\",\n                     help=\"try to automatically fix the found issues\")\n\n    cli.add_argument(\"-v\", \"--verbose\", action=\"count\", default=0,\n                     help=\"increase program verbosity\")\n    cli.add_argument(\"-q\", \"--quiet\", action=\"count\", default=0,\n                     help=\"decrease program verbosity\")\n\n    args = cli.parse_args()\n    process_args(args, cli.error)\n\n    return args\n\n\ndef process_args(args, error):\n    \"\"\"\n    Sanitizes the given argument namespace, modifying it in the process.\n\n    Calls error (with a string argument) in case of errors.\n    \"\"\"\n    # this method is very flat; artificially nesting it would be bullshit.\n    # pylint: disable=too-many-branches\n\n    # set up log level\n    log_setup(args.verbose - args.quiet)\n\n    if args.fast or args.merge or args.all:\n        # enable \"fast\" tests\n        args.authors = True\n        args.cppstyle = True\n        args.cython = True\n        args.headerguards = True\n        args.legal = True\n        args.filemodes = True\n        args.textfiles = True\n\n    if args.merge or args.all:\n        # enable tests that are required before merging to master\n        args.pystyle = True\n        args.pylint = True\n        args.test_git_change_years = True\n\n    if args.all:\n        # enable tests that take a bit longer\n        args.clang_tidy = True\n\n    if not any((args.headerguards, args.legal, args.authors, args.pystyle,\n                args.cppstyle, args.cython, args.test_git_change_years,\n                args.pylint, args.filemodes, args.textfiles, args.clang_tidy)):\n        error(\"no checks were specified\")\n\n    has_git = bool(shutil.which('git'))\n    is_git_repo = os.path.exists('.git')\n\n    if args.only_changed_files and not all((has_git, is_git_repo)):\n        error(\"can not check only changed files: git is required\")\n\n    if args.authors:\n        if not all((has_git, is_git_repo)):\n            # non-fatal fail\n            print(\"can not check author list for compliance: git is required\")\n            args.authors = False\n\n    if args.test_git_change_years:\n        if not args.legal:\n            error(\"--test-git-change-years may only be passed with --legal\")\n\n        if not all((has_git, is_git_repo)):\n            error(\"--test-git-change-years requires git\")\n\n    if args.pystyle:\n        if not importlib.util.find_spec('pep8') and \\\n           not importlib.util.find_spec('pycodestyle'):\n\n            error(\"pep8 or pycodestyle python module \"\n                  \"required for style checking\")\n\n    if args.pylint:\n        if not importlib.util.find_spec('pylint'):\n            error(\"pylint python module required for linting\")\n\n    if args.clang_tidy:\n        if not shutil.which('clang-tidy'):\n            error(\"--clang-tidy requires clang-tidy to be installed\")\n\n\ndef get_changed_files(gitref):\n    \"\"\"\n    return a list of changed files\n    \"\"\"\n    invocation = ['git', 'diff', '--name-only', '--diff-filter=ACMRTUXB',\n                  gitref]\n\n    try:\n        file_list = subprocess.check_output(invocation)\n\n    except subprocess.CalledProcessError as exc:\n        raise RuntimeError(\n            \"could not determine list of recently-changed files with git\"\n        ) from exc\n\n    return set(file_list.decode('ascii').strip().split('\\n'))\n\n\ndef main(args):\n    \"\"\"\n    Takes an argument namespace as returned by parse_args.\n\n    Calls find_all_issues(main args, list of files to consider)\n\n    Returns True if no issues were found.\n    \"\"\"\n    if args.only_changed_files:\n        check_files = get_changed_files(args.only_changed_files)\n    else:\n        check_files = None\n\n    auto_fixes = []\n    fixes_possible = False\n\n    issues_count = 0\n    for title, text, apply_fix in find_all_issues(args, check_files):\n        issues_count += 1\n        print(f\"\\x1b[33;1mWARNING\\x1b[m {title}: {text}\")\n\n        if apply_fix:\n            fixes_possible = True\n\n            if args.fix:\n                print(\"        This will be fixed automatically.\")\n                auto_fixes.append(apply_fix)\n            else:\n                print(\"        This can be fixed automatically.\")\n\n        # nicely seperate warnings\n        print()\n\n    if args.fix and auto_fixes:\n        print(f\"\\x1b[33;1mApplying {len(auto_fixes):d} automatic fixes...\\x1b[m\")\n\n        for auto_fix in auto_fixes:\n            print(auto_fix())\n            issues_count -= 1\n\n        print()\n\n    if issues_count > 0:\n        plural = \"s\" if issues_count > 1 else \"\"\n\n        if args.fix and auto_fixes:\n            remainfound = f\"remain{plural}\"\n        else:\n            remainfound = (\"were\" if issues_count > 1 else \"was\") + \" found\"\n\n        print(f\"==> \\x1b[33;1m{issues_count} issue{plural}\\x1b[m {remainfound}.\")\n\n        if not args.fix and fixes_possible:\n            print(\"When invoked with --fix, I can try \"\n                  \"to automatically resolve some of the issues.\\n\")\n\n    return issues_count == 0\n\n\ndef find_all_issues(args, check_files=None):\n    \"\"\"\n    Invokes all the individual issue checkers, and yields their returned\n    issues.\n\n    If check_files is not None, all other files are ignored during the\n    more resource-intense checks.\n    That is, check_files is the set of files to verify.\n\n    Yields tuples of (title, text) that are displayed as warnings.\n    \"\"\"\n\n    # pylint: disable=too-many-function-args, no-value-for-parameter\n    # no-value-for-parameter has to be used because pylint is dumb\n\n    if args.headerguards:\n        from .headerguards import find_issues\n        yield from find_issues('libopenage')\n\n    if args.authors:\n        from .authors import find_issues\n        yield from find_issues()\n\n    if args.pystyle:\n        from .pystyle import find_issues\n        yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty'))\n\n    if args.cython:\n        from buildsystem.codecompliance.cython import find_issues\n        yield from find_issues(check_files, ('openage',))\n\n    if args.cppstyle:\n        from .cppstyle import find_issues\n        yield from find_issues(check_files, ('libopenage',))\n\n    if args.pylint:\n        from .pylint import find_issues\n        yield from find_issues(check_files, ('openage', 'buildsystem', 'etc/gdb_pretty'))\n\n    if args.textfiles:\n        from .textfiles import find_issues\n        yield from find_issues(\n            ('openage', 'libopenage', 'buildsystem', 'doc', 'legal', 'etc/gdb_pretty'),\n            ('.pxd', '.pyx', '.pxi', '.py',\n             '.h', '.cpp', '.template',\n             '', '.txt', '.md', '.conf',\n             '.cmake', '.in', '.yml', '.supp', '.desktop'))\n\n    if args.legal:\n        from .legal import find_issues\n        yield from find_issues(check_files,\n                               ('openage', 'buildsystem', 'libopenage', 'etc/gdb_pretty'),\n                               args.test_git_change_years)\n\n    if args.filemodes:\n        from .modes import find_issues\n        yield from find_issues(check_files, ('openage', 'buildsystem',\n                                             'libopenage', 'etc/gdb_pretty'))\n    if args.clang_tidy:\n        from .clangtidy import find_issues\n        yield from find_issues(check_files, ('libopenage', ))\n\n\nif __name__ == '__main__':\n    if main(parse_args()):\n        sys.exit(0)\n    else:\n        sys.exit(1)\n"
  },
  {
    "path": "buildsystem/codecompliance/authors.py",
    "content": "# Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks whether all authors are properly listed in copying.md.\n\"\"\"\n\nimport re\n\nimport logging\n\nfrom .util import Strlazy\n\n\ndef deobfuscate_email(string):\n    \"\"\"\n    Should reveal the original email address passed into obfuscate_email\n\n    deobfuscate_email('first dawt last+tag à gmail dawt com')\n    = 'first.last+tag@gmail.com'\n    \"\"\"\n    replacements = {\n        ' dawt ': '.',\n        ' à ': '@'\n    }\n\n    for key, value in replacements.items():\n        string = string.replace(key, value)\n\n    return string\n\n\ndef get_author_emails_copying_md():\n    \"\"\"\n    yields all emails from the author table in copying.md\n\n    they must be part of a line like\n\n    |     name     |    nick    |    email    |\n    \"\"\"\n    with open(\"copying.md\", encoding='utf8') as fobj:\n        for line in fobj:\n            match = re.match(r\"^.*\\|[^|]*\\|[^|]*\\|([^|]+)\\|.*$\", line)\n            if not match:\n                continue\n\n            email = match.group(1).strip()\n            if 'à' in email:\n                email = deobfuscate_email(email)\n\n            if not any(email.startswith(prefix) for prefix in (\"E-Mail\", \"-\" * 15))\\\n               and '@' not in email:\n                raise ValueError(f\"no @ or à was found in email: {email}\")\n\n            yield email\n\n\ndef get_author_emails_git_shortlog(exts):\n    \"\"\"\n    yields emails of all authors that have authored any of the files ending\n    in exts (plus their templates)\n\n    parses the output of git shortlog -sne\n    \"\"\"\n    from subprocess import Popen, PIPE\n\n    invocation = ['git', 'shortlog', '-sne', '--']\n    for ext in exts:\n        invocation.append(f\"*{ext}\")\n        invocation.append(f\"*{ext}.in\")\n        invocation.append(f\"*{ext}.template\")\n\n    with Popen(invocation, stdout=PIPE) as invoc:\n        output = invoc.communicate()[0]\n\n    for line in output.decode('utf-8', errors='replace').split('\\n'):\n        match = re.match(\"^ +[0-9]+\\t[^<]*\\\\<(.*)\\\\>$\", line)\n        if match:\n            yield match.group(1).lower()\n\n\ndef find_issues():\n    \"\"\"\n    compares the output of git shortlog -sne to the authors table in copying.md\n\n    prints all discrepancies, and returns False if one is detected.\n    \"\"\"\n    relevant_exts = ('.cpp', '.h', '.py', '.pyi', '.pyx', '.cmake',\n                     '.qml')\n\n    copying_md_emails = set(get_author_emails_copying_md())\n    logging.debug(\"scanned authors in copying.md:\\n%s\",\n                  Strlazy(lambda: f\"{chr(10).join(sorted(copying_md_emails))}\"))\n\n    git_shortlog_emails = set(get_author_emails_git_shortlog(relevant_exts))\n    logging.debug(\"scanned authors from git shortlog:\\n%s\",\n                  Strlazy(lambda: f\"{chr(10).join(sorted(git_shortlog_emails))}\"))\n\n    # look for git emails that are unlisted in copying.md\n    for email in git_shortlog_emails - copying_md_emails:\n        if email in {'coop@sft.mx', '?'}:\n            continue\n\n        yield (\n            \"author inconsistency\",\n            f\"{email}\\n\\temail appears in git log, but not in copying.md or .mailmap\",\n            None\n        )\n"
  },
  {
    "path": "buildsystem/codecompliance/clangtidy.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks clang-tidy errors on cpp files\n\"\"\"\n\nimport subprocess\nfrom .cppstyle import filter_file_list\nfrom .util import findfiles\n\n\ndef find_issues(check_files, dirnames):\n    \"\"\"\n    Invoke clang-tidy to check C++ files for issues.\n    Yields issues found by clang-tidy in real-time.\n    \"\"\"\n    # Specify the checks to include\n    # 4 checks we focus on\n    checks_to_include = [\n        'clang-analyzer-*',\n        'bugprone-*',\n        'concurrency-*',\n        'performance-*'\n    ]\n    # Create the checks string\n    checks = ', '.join(checks_to_include)\n\n    # Invocation command\n    invocation = ['clang-tidy', f'-checks=-*,{checks}']\n\n    # Use utility functions from util.py and cppstyle.py\n    if check_files is not None:\n        filenames = list(filter_file_list(check_files, dirnames))\n    else:\n        filenames = list(filter_file_list(findfiles(dirnames), dirnames))\n\n    if not filenames:\n        print(\"No files to check.\")\n        return  # No files to check\n\n    for filename in filenames:\n        # Run clang-tidy for each file\n        print(f\"Starting clang-tidy check on file: {filename}\")\n        try:\n            with subprocess.Popen(\n                invocation + [filename],\n                stdout=subprocess.PIPE,\n                stderr=subprocess.PIPE,\n                text=True\n            ) as process:\n                # Stream output in real-time\n                while True:\n                    output = process.stdout.readline()\n                    if output:\n                        yield (\"clang-tidy output\", output.strip(), None)\n                    elif process.poll() is not None:\n                        break\n\n                # Capture remaining errors (if any)\n                for error_line in process.stderr:\n                    yield (\"clang-tidy error\", error_line.strip(), None)\n\n        # Handle exception\n        except subprocess.SubprocessError as exc:\n            yield (\n                \"clang-tidy error\",\n                f\"An error occurred while running clang-tidy on {filename}: {str(exc)}\",\n                None\n            )\n"
  },
  {
    "path": "buildsystem/codecompliance/cppstyle.py",
    "content": "# Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks some code style rules for cpp files.\n\"\"\"\n\nimport re\n\nfrom .util import findfiles, readfile, issue_str_line\n\n# spaces missing in `if () {` and `for`, `while`, ...\nMISSING_SPACES_RE = re.compile(\n    # on of the folowing, the first group is used for the column where\n    # the pointer is going to show\n    r\"(?:\"\n    # a ) folowed by a { without a space between\n    r\"(?:\\)(\\{))|\"\n    # a if/for/while folowed by a ( without a space between\n    r\"(?:\\s+(?:if|for|while)(\\())\"\n    r\")\"\n)\n\n# extra spaces before a ;\nEXTRA_SPACES_RE = re.compile(\n    # on of the folowing, the first group is used for the column where\n    # the pointer is going to show\n    r\"(?:\"\n    # a space before a \";\"\n    r\"(?:(\\s+);)\"\n    r\")\"\n)\n\n# indentation fails looking at a file\nINDENT_FAIL_RE = re.compile(\n    # leading whitespace fail\n    r\"(?:\"\n    r\"(\\n\\t*[ ]+\\t+)\"         # \\n tab* space+ tab+\n    r\")\"\n)\n\n# we need both because we look at the file first.\n# indentation fails when looking at a line\nINDENT_FAIL_LINE_RE = re.compile(\n    # leading whitespace fail\n    r\"(?:\"\n    r\"(^\\t*[ ]+\\t+)\"          # tab* space+ tab+\n    r\")\"\n)\n\n\ndef filter_file_list(check_files, dirnames):\n    \"\"\"\n    Yields all those files in check_files that are in one of the directories\n    and end in '.cpp' or '.h' and some other conditions.\n    \"\"\"\n    for filename in check_files:\n        if not (filename.endswith('.cpp') or filename.endswith('.h')):\n            continue\n\n        if filename.endswith('.gen.h') or filename.endswith('.gen.cpp'):\n            # TODO all this for now, until someone fixes the codegen.\n            continue\n\n        if any(filename.startswith(dirname) for dirname in dirnames):\n            yield filename\n\n\ndef find_issues(check_files, dirnames):\n    \"\"\"\n    Finds all issues in the given directories (filtered by check_files).\n    \"\"\"\n\n    if check_files is not None:\n        filenames = filter_file_list(check_files, dirnames)\n    else:\n        filenames = filter_file_list(findfiles(dirnames), dirnames)\n\n    for filename in filenames:\n        data = readfile(filename)\n        analyse_each_line = False\n\n        if MISSING_SPACES_RE.search(data) or\\\n           EXTRA_SPACES_RE.search(data) or\\\n           INDENT_FAIL_RE.search(data):\n\n            analyse_each_line = True\n\n        # if there are possible issues perform a per line analysis\n        if analyse_each_line:\n            yield from find_issues_with_lines(data, filename)\n\n\ndef find_issues_with_lines(data, filename):\n    \"\"\"\n    Checks a file for issues per line\n    \"\"\"\n\n    for num, line in enumerate(data.splitlines(True), start=1):\n\n        match = MISSING_SPACES_RE.search(line)\n        if match:\n            start = match.start(1) + match.start(2)\n            end = start + 1\n            yield issue_str_line(\"Missing space\",\n                                 filename, line, num,\n                                 (start, end))\n\n        match = EXTRA_SPACES_RE.search(line)\n        if match:\n            yield issue_str_line(\"Extra space\",\n                                 filename, line, num,\n                                 (match.start(1), match.end(1)))\n\n        match = INDENT_FAIL_LINE_RE.search(line)\n        if match:\n            yield issue_str_line(\"Wrong indentation\",\n                                 filename, line, num,\n                                 (match.start(1), match.end(1)))\n"
  },
  {
    "path": "buildsystem/codecompliance/cython.py",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nVerifies that Cython directives for profiling are deactivated.\n\"\"\"\n\nimport re\n\nfrom buildsystem.codecompliance.util import issue_str_line\n\nfrom .util import findfiles, readfile\n\n\nGLOBAL_PROFILE_DIREC = re.compile((\n    # global profiling directive for a file\n    r\"^(# cython: .*(profile=True|linetrace=True).*\\n)\"\n))\n\nFUNC_PROFILE_DIREC = re.compile((\n    # profiling for single functions\n    r\"@cython\\.profile\\(True\\)\"\n))\n\n\ndef filter_file_list(check_files, dirnames):\n    \"\"\"\n    Yields all those files in check_files that are in one of the directories\n    and end in '.py'x.\n    \"\"\"\n    for filename in check_files:\n        if not filename.endswith('.pyx'):\n            continue\n\n        if any(filename.startswith(dirname) for dirname in dirnames):\n            yield filename\n\n\ndef find_issues(check_files, dirnames):\n    \"\"\"\n    Finds all issues in the given directories (filtered by check_files).\n    \"\"\"\n    if check_files:\n        filenames = filter_file_list(check_files, dirnames)\n\n    else:\n        filenames = findfiles(dirnames, ('.pyx',))\n\n    for filename in filenames:\n        data = readfile(filename)\n\n        for num, line in enumerate(data.splitlines(True), start=1):\n            match = GLOBAL_PROFILE_DIREC.match(line)\n            if match:\n                yield issue_str_line(\"cython profiling activated in header\",\n                                     filename, line, num,\n                                     (match.start(1), match.end(1)))\n\n            match = FUNC_PROFILE_DIREC.search(line)\n            if match:\n                yield issue_str_line(\"cython function profiling activated in file\",\n                                     filename, line, num,\n                                     (match.start(0), match.end(0)))\n"
  },
  {
    "path": "buildsystem/codecompliance/headerguards.py",
    "content": "# Copyright 2014-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nVerifies the guard macros of all C++ header files.\n\"\"\"\n\nimport re\n\nfrom .util import findfiles, readfile\n\n\nclass HeaderIssue(Exception):\n    \"\"\" Some issue was detected with the Header guard. \"\"\"\n\n\nGUARD_RE = re.compile((\n    # allow any number of comments or empty lines\n    \"^(\\\\n|(#|//).*\\\\n)*\"\n\n    # the header guard\n    \"#pragma once\\n\"\n))\n\nNO_GUARD_REQUIRED_RE = re.compile((\n    # allow any number of comments or empty lines\n    \"^(\\\\n|(#|//).*\\\\n)*\"\n\n    # require comment \"has no header guard\"\n    \"(#|//) has no header guard:\"\n))\n\n\ndef find_issues(dirname):\n    \"\"\"\n    checks all headerguards in header files in the cpp folders.\n    \"\"\"\n    for fname in findfiles((dirname,), ('.h',)):\n        try:\n            data = readfile(fname)\n\n            if NO_GUARD_REQUIRED_RE.match(data):\n                # this file needs no header guard, for the reason\n                # detailed in the comment.\n                continue\n\n            match = GUARD_RE.match(data)\n            if not match:\n                raise HeaderIssue(\"No valid header guard found (e.g. #pragma once)\")\n\n        except HeaderIssue as exc:\n            yield (f\"header guard issue in {fname}\", exc.args[0], None)\n"
  },
  {
    "path": "buildsystem/codecompliance/legal.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks the legal headers of all files.\n\"\"\"\n\nfrom datetime import date\nimport re\nfrom subprocess import Popen, PIPE\n\nfrom .util import findfiles, readfile, writefile, has_ext, SHEBANG\n\n\nOPENAGE_AUTHORS = (\n    \"Copyright (?P<crstart>\\\\d{4})-(?P<crend>\\\\d{4}) the openage authors\\\\.\"\n)\nOPENAGE_AUTHORTEMPLATE = (\n    \"Copyright {crstart}-{crend} the openage authors.\"\n)\n\nNATIVELEGALHEADER = re.compile(\n    \"^\"\n    # Allow shebang line, followed by an optional empty line.\n    \"(\" + SHEBANG + \")?\"\n\n    # Next line must be the copyright line.\n    \"(#|//) \" + OPENAGE_AUTHORS + \" See copying\\\\.md for legal info\\\\.\\n\"\n)\n\nTHIRDPARTYLEGALHEADER = re.compile(\n    \"^\"\n    # 3rd-party copyright/license\n    \"(#|//) This file (was (taken|adapted)|contains (data|code)) from .*\\n\"\n    \"(#|//) Copyright \\\\d{4}-\\\\d{4} .*\\n\"\n    \"(#|//) .*license.*\\n\"\n\n    # any number of lines containing further 3rd-party copyright info\n    \"((#|//) .*\\\\n)*\"\n\n    # the openage copyright\n    \"(#|//) (Modifications|Other (data|code)|Everything else) \" +\n    OPENAGE_AUTHORS + \"\\n\"\n    \"(#|//) See copying\\\\.md for further legal info\\\\.\\n\")\n\n# Empty files (consisting of only comments) don't require a legal header.\nEMPTYFILE = re.compile(\"^(((#|//) .*)?\\n)*$\")\n\n# cython-generated files\nCYTHONGENERATED = re.compile(\"^[^\\\\n]*(Generated by Cython |failed Cython compilation.)\")\n\n\n# all those files will be checked.\nEXTENSIONS_REQUIRING_LEGAL_HEADERS = {\n    '.h', '.cpp', '.py', '.pyx', '.pxi', '.cmake', '.h.in',\n    '.cpp.in', '.py.in', '.h.template', '.cpp.template',\n    '.py.template', '.qml'\n}\n\n\ndef get_git_change_year(filename):\n    \"\"\" Returns git-log's opinion on when the file was last changed. \"\"\"\n\n    invocation = [\n        'git', 'log', '-1', '--format=%ad', '--date=short', '--no-merges', '--',\n        filename\n    ]\n\n    with Popen(invocation, stdout=PIPE) as proc:\n        output = proc.communicate()[0].decode('utf-8', errors='ignore').strip()\n\n        if proc.returncode != 0 or not output:\n            # git doesn't know about the file\n            return None\n\n    return int(output[:4])\n\n\ndef match_legalheader(data):\n    \"\"\"\n    Tests whether data matches any of the regular expressions,\n    and returns a tuple of (matching header regex, match).\n    \"\"\"\n    for hdr in (NATIVELEGALHEADER,\n                THIRDPARTYLEGALHEADER,\n                EMPTYFILE,\n                CYTHONGENERATED):\n\n        match = re.match(hdr, data)\n        if match is not None:\n            return hdr, match\n\n    raise ValueError(\"no match found\")\n\n\ndef create_year_fix(filename, file_content, expected_end_year,\n                    found_start_year, headertype):\n    \"\"\"\n    Create a function that, when called, fixes the copyright header.\n    \"\"\"\n\n    # check if a fix can be created\n    if headertype not in {NATIVELEGALHEADER, THIRDPARTYLEGALHEADER}:\n        return None\n\n    def year_fix_function():\n        \"\"\"\n        Store the file with correct copyright years.\n        \"\"\"\n\n        fixed_file, success = re.subn(\n            OPENAGE_AUTHORS,\n            OPENAGE_AUTHORTEMPLATE.format(crstart=found_start_year,\n                                          crend=expected_end_year),\n            file_content\n        )\n\n        if not success:\n            raise ValueError(\"copyright year fix did not suceeed\")\n\n        writefile(filename, fixed_file)\n\n        return f\"Copyright for {filename} was fixed.\"\n\n    return year_fix_function\n\n\ndef test_headers(check_files, paths, git_change_years, third_party_files):\n    \"\"\" Tests all in-sourcefile legal headers. \"\"\"\n\n    if not git_change_years:\n        print(\"warning: I won't check if the copyright matches the git history.\")\n        print(\"         Run with --test-git-change-years to enable the check.\")\n\n    # determine all uncommited files from git.\n    # those definitely need the current year in the copyright message.\n    with Popen(['git', 'diff', '--name-only', 'HEAD'], stdout=PIPE) as proc:\n        uncommited = set(proc.communicate()[0].decode('ascii').strip().split('\\n'))\n\n    current_calendar_year = date.today().year\n\n    for filename in findfiles(paths, EXTENSIONS_REQUIRING_LEGAL_HEADERS):\n        try:\n            file_content = readfile(filename)\n            headertype, match = match_legalheader(file_content)\n        except ValueError:\n            yield (\n                \"Legal header missing or invalid\",\n                (filename + \"\\nSee copying.md for a template\"),\n                None\n            )\n            continue\n\n        if headertype is THIRDPARTYLEGALHEADER:\n            third_party_files.add(filename)\n\n        try:\n            found_start_year = int(match.group('crstart'))\n            found_end_year = int(match.group('crend'))\n        except IndexError:\n            # this header type has/needs no copyright years\n            # (e.g. empty file)\n            continue\n\n        expected_end_year = None\n        if filename in uncommited:\n            expected_end_year = current_calendar_year\n        elif git_change_years:\n            if check_files is None or filename in check_files:\n                expected_end_year = get_git_change_year(filename)\n\n        if expected_end_year is None:\n            continue\n\n        if found_end_year != expected_end_year:\n\n            fix = create_year_fix(\n                filename,\n                file_content,\n                expected_end_year,\n                found_start_year,\n                headertype\n            )\n\n            yield (\n                \"Bad copyright year\",\n                (filename + \"\\n\" +\n                 f\"\\tExpected {expected_end_year}\\n\" +\n                 f\"\\tFound    {found_end_year}\"),\n                fix\n            )\n\n\ndef find_issues(check_files, paths, git_change_years=False):\n    \"\"\"\n    Tests all source files for the required legal headers.\n    \"\"\"\n\n    third_party_files = set()\n\n    yield from test_headers(\n        check_files, paths, git_change_years, third_party_files)\n\n    # test whether all third-party files are listed in copying.md\n    listed_files = set()\n    for line in readfile('copying.md').split('\\n'):\n        match = re.match(\"^ - `([^`]+)`.*$\", line)\n        if not match:\n            continue\n\n        filename = match.group(1)\n        listed_files.add(filename)\n\n    # file listed, but has no 3rd-party header?\n    for filename in sorted(listed_files - third_party_files):\n        if has_ext(filename, EXTENSIONS_REQUIRING_LEGAL_HEADERS):\n            yield (\n                \"third-party file listing issue\",\n                (f\"{filename}\\n\\tlisted in copying.md, but has no \"\n                 \"third-party license header.\"),\n                None\n            )\n\n    # file has 3rd-party header, but is not listed?\n    for filename in sorted(third_party_files - listed_files):\n        yield (\n            \"third-party file listing issue\",\n            (f\"{filename}\\n\\thas a third-party license header, but isn't \"\n             \"listed in copying.md\"),\n            None\n        )\n"
  },
  {
    "path": "buildsystem/codecompliance/modes.py",
    "content": "# Copyright 2016-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks the mode of all files and prevents executable source files.\n\"\"\"\n\nimport re\nimport pathlib\nimport stat\n\nfrom .util import findfiles, SHEBANG\n\n\nSHEBANG_RE = re.compile(\"^\" + SHEBANG)\n\nEXTENSIONS_NO_X_BIT = {\n    '.h', '.cpp', '.py', '.pyx', '.pxi', '.cmake', '.h.in',\n    '.cpp.in', '.py.in', '.h.template', '.cpp.template',\n    '.py.template', '.qml'\n}\n\nEXTENSIONS_SHEBANG_XBIT = {\n    '.sh', '.py'\n}\n\n\ndef check_mode(filename):\n    \"\"\"\n    Test if the the file has no executable bit set.\n    \"\"\"\n\n    path = pathlib.Path(filename)\n    filemode = path.stat().st_mode\n\n    x_ok = False\n\n    if filemode & (stat.S_IXGRP | stat.S_IXOTH | stat.S_IXUSR):\n\n        if path.suffix in EXTENSIONS_SHEBANG_XBIT:\n            # if the file is allowed to have a shebang,\n            # allow its executable bit if it actually has a shebang\n            with path.open(encoding='utf-8') as file:\n                firstline = file.readline()\n\n                if SHEBANG_RE.match(firstline):\n                    x_ok = True\n\n        if not x_ok:\n            raise ValueError(f'file {filename} is executable')\n\n\ndef find_issues(check_files, paths):\n    \"\"\"\n    Check all source files for their required filesystem bits.\n    \"\"\"\n\n    for filename in findfiles(paths, EXTENSIONS_NO_X_BIT):\n        if check_files and filename not in check_files:\n            continue\n\n        try:\n            check_mode(filename)\n        except ValueError as exc:\n            yield (\"wrong file access bits\", str(exc), None)\n            continue\n"
  },
  {
    "path": "buildsystem/codecompliance/pylint.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks the Python modules with pylint.\n\"\"\"\n\nfrom pylint import lint\n\nfrom .pystyle import filter_file_list\nfrom .util import findfiles\n\n\ndef find_pyx_modules(dirnames):\n    \"\"\" Yields the names of all .pyx modules. \"\"\"\n    for pyx_file in findfiles(dirnames, [\".pyx\"]):\n        yield pyx_file.replace('/', '.')[:-len(\".pyx\")]\n\n\ndef find_issues(check_files, dirnames):\n    \"\"\" Invokes the external utility. \"\"\"\n\n    invocation = ['--rcfile=etc/pylintrc', '--reports=n']\n\n    from multiprocessing import cpu_count\n    invocation.append(f\"--jobs={cpu_count():d}\")\n\n    if check_files is None:\n        invocation.extend(dirnames)\n    else:\n        check_files = list(filter_file_list(check_files, dirnames))\n        if not check_files:\n            return\n        invocation.extend(check_files)\n\n    try:\n        lint.Run(invocation)\n    except SystemExit as exc:\n        error_count = exc.args[0]\n        if error_count != 0:\n            if check_files is None:\n                msg = f\"python code is noncompliant: {error_count:d}\"\n            else:\n                msg = (\"false positives may result from not checking the \"\n                       \"entire codebase\")\n\n            yield \"linting issue\", msg, None\n"
  },
  {
    "path": "buildsystem/codecompliance/pystyle.py",
    "content": "# Copyright 2014-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks PEP8 compliance, with some exceptions.\n\"\"\"\n\ntry:\n    from pycodestyle import StyleGuide\nexcept ImportError:\n    from pep8 import StyleGuide\n\n\n# these errors will be ignored by pep8\nIGNORE_ERRORS = (\n    \"E221\",  # multiple spaces before operator\n    \"E241\",  # multiple spaces after ','\n    \"E251\",  # unexpected spaces around keyword / parameter equals\n    \"E501\",  # line too long\n)\n\n\ndef filter_file_list(check_files, dirnames):\n    \"\"\"\n    Yields all those files in check_files that are in one of the directories\n    and end in '.py'.\n    \"\"\"\n    for filename in check_files:\n        if not filename.endswith('.py'):\n            continue\n\n        if any(filename.startswith(dirname) for dirname in dirnames):\n            yield filename\n\n\ndef find_issues(check_files, dirnames):\n    \"\"\"\n    Finds all issues in the given directories (filtered by check_files).\n    \"\"\"\n    checker = StyleGuide()\n    checker.options.ignore = IGNORE_ERRORS\n\n    filenames = dirnames\n    if check_files is not None:\n        filenames = filter_file_list(check_files, dirnames)\n\n    report = checker.check_files(filenames)\n\n    if report.messages:\n        yield (\"style issue\", \"python code violates pep8\", None)\n"
  },
  {
    "path": "buildsystem/codecompliance/textfiles.py",
    "content": "# Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n\"\"\"\nChecks some general whitespace rules and the encoding for text files.\n\"\"\"\n\nimport re\n\nfrom .util import findfiles, readfile, has_ext, issue_str_line, BADUTF8FILES\n\nTRAIL_WHITESPACE_RE = re.compile((\n    # trailing whitespace\n    r\"( |\\t)+\\n\"\n))\n\nIMMEDIATE_TODO_RE = re.compile(\n    \"as\" + \"df\",\n    re.IGNORECASE  # pylint: disable=no-member\n)\n\n\ndef find_issues(dirnames, exts):\n    \"\"\"\n    Checks all files ending in exts in dirnames.\n    \"\"\"\n    for filename in findfiles(dirnames, exts):\n        data = readfile(filename)\n        analyse_each_line = False\n\n        if filename.endswith('.gen.h') or filename.endswith('.gen.cpp'):\n            # TODO all this for now, until someone fixes the codegen.\n            continue\n\n        if filename.startswith('openage/') and filename.endswith('.cpp'):\n            # allow issues for Cython-generated files.\n            continue\n\n        if '\\r\\n' in data:\n            yield \"Windows EOL format\", filename, None\n\n        if data.endswith('\\n\\n'):\n            yield \"Trailing newline at file end\", filename, None\n\n        if data and not data.endswith('\\n'):\n            yield \"File does not end in '\\\\n'\", filename, None\n\n        if has_ext(filename, ('.py', '.pyx', '.pxd')):\n            if '\\t' in data:\n                yield \"File contains tabs\", filename, None\n\n        if TRAIL_WHITESPACE_RE.search(data) or IMMEDIATE_TODO_RE.search(data):\n            analyse_each_line = True\n\n        # If there are possible issues, perform an in-depth analysis.\n        if analyse_each_line:\n            yield from find_issues_with_lines(filename)\n\n    for filename in BADUTF8FILES:\n        yield \"Not valid UTF-8\", filename\n\n\ndef find_issues_with_lines(filename):\n    \"\"\"\n    Checks a file for issues per line.\n    \"\"\"\n    data = readfile(filename)\n\n    for num, line in enumerate(data.splitlines(True), start=1):\n\n        match = TRAIL_WHITESPACE_RE.search(line)\n        if match:\n            yield issue_str_line(\"Trailing whitespace\", filename, line, num,\n                                 (match.start(1), match.end(1)))\n\n        match = IMMEDIATE_TODO_RE.search(line)\n        if match:\n            yield issue_str_line(\"Found 'as\"\n                                 \"df', indicating an immediate TODO\",\n                                 filename, line, num,\n                                 (match.start(), match.end()))\n"
  },
  {
    "path": "buildsystem/codecompliance/util.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSome utilities.\n\"\"\"\n\nimport logging\nimport os\n\n\nSHEBANG = \"#!/.*\\n(#?\\n)?\"\n\nFILECACHE = {}\nBADUTF8FILES = set()\n\n\ndef log_setup(setting, default=1):\n    \"\"\"\n    Perform setup for the logger.\n    Run before any logging.log thingy is called.\n\n    if setting is 0: the default is used, which is WARNING.\n    else: setting + default is used.\n    \"\"\"\n\n    levels = (logging.ERROR, logging.WARNING, logging.INFO,\n              logging.DEBUG, logging.NOTSET)\n\n    factor = clamp(default + setting, 0, len(levels) - 1)\n    level = levels[factor]\n\n    logging.basicConfig(level=level, format=\"[%(asctime)s] %(message)s\")\n    logging.captureWarnings(True)\n\n\ndef clamp(number, smallest, largest):\n    \"\"\" return number but limit it to the inclusive given value range \"\"\"\n    return max(smallest, min(number, largest))\n\n\nclass Strlazy:\n    # pylint: disable=too-few-public-methods\n    \"\"\"\n    to be used like this: logging.debug(\"rolf %s\", strlazy(lambda: do_something()))\n    so do_something is only called when the debug message is actually printed\n    do_something could also be an f-string.\n    \"\"\"\n    def __init__(self, fun):\n        self.fun = fun\n\n    def __str__(self):\n        return self.fun()\n\n\ndef has_ext(fname, exts):\n    \"\"\"\n    Returns true if fname ends in any of the extensions in ext.\n    \"\"\"\n    for ext in exts:\n        if ext == '':\n            if os.path.splitext(fname)[1] == '':\n                return True\n        elif fname.endswith(ext):\n            return True\n\n    return False\n\n\ndef readfile(filename):\n    \"\"\"\n    reads the file, and returns it as a str object.\n\n    if the file has already been read in the past,\n    returns it from the cache.\n    \"\"\"\n    if filename not in FILECACHE:\n        with open(filename, 'rb') as fileobj:\n            data = fileobj.read()\n\n        try:\n            data = data.decode('utf-8')\n        except UnicodeDecodeError:\n            data = data.decode('utf-8', errors='replace')\n            BADUTF8FILES.add(filename)\n\n        FILECACHE[filename] = data\n\n    return FILECACHE[filename]\n\n\ndef writefile(filename, new_content):\n    \"\"\"\n    writes the file and update it in the cache.\n    \"\"\"\n    if filename in BADUTF8FILES:\n        raise ValueError(f\"{filename}: cannot write due to utf8-errors.\")\n\n    with open(filename, 'w', encoding='utf8') as fileobj:\n        fileobj.write(new_content)\n\n    FILECACHE[filename] = new_content\n\n\ndef findfiles(paths, exts=None):\n    \"\"\"\n    yields all files in paths with names ending in an ext from exts.\n\n    If exts is None, all extensions are accepted.\n\n    hidden dirs and files are ignored.\n    \"\"\"\n    for path in paths:\n        for filename in os.listdir(path):\n            if filename.startswith('.'):\n                continue\n\n            filename = os.path.join(path, filename)\n\n            if os.path.isdir(filename):\n                yield from findfiles((filename,), exts)\n                continue\n\n            if exts is None or has_ext(filename, exts):\n                yield filename\n\n\ndef issue_str(title, filename, fix=None):\n    \"\"\"\n    Creates a formated (title, text) desciption of an issue.\n\n    TODO use this function and issue_str_line for all issues, so the format\n    can be easily changed (exta text, colors, etc)\n    \"\"\"\n    return (title, filename, fix)\n\n\n# gread hint, pylint. thank you so much.\n# pylint: disable=too-many-arguments\ndef issue_str_line(title, filename, line, line_number, highlight, fix=None):\n    \"\"\"\n    Creates a formated (title, text) desciption of an issue with information\n    about the location in the file.\n    line:        line content\n    line_number: line id in the file\n    highlight:   a tuple of (start, end), where\n        start:   match start in the line\n        end:     match end in the line\n    \"\"\"\n\n    start, end = highlight\n    start += 1\n    line = line.replace(\"\\n\", \"\").replace(\"\\t\", \" \")\n\n    return (\n        title,\n        (\n            filename + \"\\n\"\n            \"\\tline: \" + str(line_number) + \"\\n\"   # line number\n            \"\\tat:   '\" + line + \"'\\n\"             # line content\n            \"\\t      \" + (' ' * start) +           # mark position with ^\n            \"\\x1b[32;1m^\" + (\"~\" * (end - start)) + \"\\x1b[m\"\n        ),\n        fix\n    )\n"
  },
  {
    "path": "buildsystem/codegen.cmake",
    "content": "# Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n# set CODEGEN_SCU_FILE to the absolute path to SCU file\nmacro(get_codegen_scu_file)\n\tset(CODEGEN_SCU_FILE \"${CMAKE_BINARY_DIR}/libopenage/codegen_scu.gen.cpp\")\nendmacro()\n\nfunction(codegen_run)\n\t# this function must be called once all required assets have been created, but before the executable is finalized.\n\n\t# make sure this function gets called only once\n\tget_property(codegen_run GLOBAL PROPERTY SFT_CODEGEN_HAS_BEEN_RUN)\n\tif(codegen_run)\n\t\tmessage(FATAL_ERROR \"codegen has already been run\")\n\tendif()\n\tset_property(GLOBAL PROPERTY SFT_CODEGEN_HAS_BEEN_RUN 1)\n\n\tset(CODEGEN_INVOCATION\n\t\t\"${PYTHON}\" -m openage codegen\n\t\t\"--input-dir=${CMAKE_SOURCE_DIR}\"\n\t\t\"--output-dir=${CMAKE_BINARY_DIR}\"\n\t\t\"--generated-list-file=${CMAKE_BINARY_DIR}/codegen_generated_files\"\n\t\t\"--depend-list-file=${CMAKE_BINARY_DIR}/codegen_depends\")\n\n\texecute_process(COMMAND\n\t\t${CODEGEN_INVOCATION} --mode=dryrun\n\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t\tRESULT_VARIABLE COMMAND_RESULT\n\t)\n\n\tif(NOT ${COMMAND_RESULT} EQUAL 0)\n\t\tmessage(FATAL_ERROR \"failed to get target list from codegen invocation\")\n\tendif()\n\n\tFILE(READ \"${CMAKE_BINARY_DIR}/codegen_generated_files\" CODEGEN_TARGET_FILES)\n\tFILE(READ \"${CMAKE_BINARY_DIR}/codegen_depends\" CODEGEN_DEPENDS)\n\tSTRING(REGEX REPLACE \"\\n\" \";\" CODEGEN_TARGET_FILES ${CODEGEN_TARGET_FILES})\n\tSTRING(REGEX REPLACE \"\\n\" \";\" CODEGEN_DEPENDS ${CODEGEN_DEPENDS})\n\n\t# as the codegen creates all files at once,\n\t# let the buildsystem only depend on this single dummy file.\n\t# otherwise the codegen invocation would be done for each generated source.\n\tset(CODEGEN_TIMEFILE \"${CMAKE_BINARY_DIR}/codegen_timefile\")\n\n\tadd_custom_command(\n\t\tOUTPUT \"${CODEGEN_TIMEFILE}\"\n\t\tBYPRODUCTS ${CODEGEN_TARGET_FILES}\n\t\tCOMMAND ${CODEGEN_INVOCATION} --mode=codegen \"--touch-file-on-cache-change=${CMAKE_CURRENT_LIST_FILE}\" --force-rerun-on-generated-list-change\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E touch \"${CODEGEN_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t\tDEPENDS ${CODEGEN_DEPENDS}\n\t\tCOMMENT \"openage.codegen: generating c++ code\"\n\t)\n\n\tadd_custom_target(cppgen\n\t\tDEPENDS \"${CODEGEN_TIMEFILE}\"\n\t)\n\n\tadd_custom_target(cleancodegen\n\t\tCOMMAND ${CODEGEN_INVOCATION} --mode=clean\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E remove \"${CODEGEN_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t)\n\n\tget_codegen_scu_file()\n\tget_filename_component(CODEGEN_SCU_DIR ${CODEGEN_SCU_FILE} DIRECTORY)\n\n\tset(CODEGEN_SCU_CONTENT \"// Generated by codegen.\\n\\n\")\n\tlist(SORT CODEGEN_TARGET_FILES)\n\tforeach(target ${CODEGEN_TARGET_FILES})\n\t\tif(\"${target}\" MATCHES \"\\\\.cpp$\")\n\t\t\tfile(RELATIVE_PATH target_rel ${CODEGEN_SCU_DIR} ${target})\n\t\t\tstring(APPEND CODEGEN_SCU_CONTENT \"#include \\\"${target_rel}\\\"\\n\")\n\t\tendif()\n\tendforeach()\n\n\twrite_on_change(${CODEGEN_SCU_FILE} \"${CODEGEN_SCU_CONTENT}\")\nendfunction()\n"
  },
  {
    "path": "buildsystem/compilepy.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCompiles python modules with cpython to pyc/pyo files.\n\"\"\"\n\nimport argparse\nimport importlib.util\nimport os\nimport py_compile\nimport shutil\nimport sys\n\n\ndef clone_file_to_dir(sourcefile, input_dir, output_dir):\n    \"\"\"\n    Make a copy of sourcefile from input_dir to output_dir\n    Try hardlinking first; on failure fallback to copy.\n    Caveat: source files already in output_dir are not cloned.\n\n    :return: the path of file created in output_dir\n    \"\"\"\n    if os.path.samefile(input_dir, output_dir) or sourcefile.startswith(output_dir):\n        return sourcefile\n\n    relsrcpath = os.path.relpath(sourcefile, input_dir)\n    targetfile = os.path.join(output_dir, relsrcpath)\n\n    if os.path.exists(targetfile) and os.path.samefile(sourcefile, targetfile):\n        return targetfile\n\n    os.makedirs(os.path.dirname(targetfile), exist_ok=True)\n\n    # try to hardlink\n    try:\n        os.link(sourcefile, targetfile)\n        return targetfile\n    except OSError:\n        pass\n\n    # we don't try to symlink, because then Python may \"intelligently\"\n    # determine the actual location, and refuse to work.\n\n    # fallback to copying the file\n    shutil.copy(sourcefile, targetfile)\n\n    return targetfile\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"pymodule_list_file\", help=(\n        \"semicolon-separated list of all modules that shall be compiled\"\n    ))\n    cli.add_argument(\"input_dir\", help=(\n        \"base directory where files from the above list are in.\"\n    ))\n    cli.add_argument(\"output_dir\", help=(\n        \"base directory where output files will be created.\"\n    ))\n    cli.add_argument(\"--print-output-paths-only\", action=\"store_true\", help=(\n        \"print the paths of the compiled output files and exit\"\n    ))\n    args = cli.parse_args()\n\n    with open(args.pymodule_list_file, encoding='utf8') as fileobj:\n        modules = fileobj.read().strip().split(';')\n        if modules == ['']:\n            modules = []\n\n    if not os.path.isdir(args.output_dir):\n        cli.error(f\"not a directory: '{args.output_dir}'\")\n\n    all_output_files = []\n    to_compile = []\n\n    for module in modules:\n        # the module is required in the output directory\n        sourcefile = clone_file_to_dir(module, args.input_dir, args.output_dir)\n        outputfile = importlib.util.cache_from_source(sourcefile)\n        all_output_files.append(outputfile)\n\n        if os.path.exists(outputfile):\n            if os.path.getmtime(outputfile) >= os.path.getmtime(sourcefile):\n                continue\n\n            os.remove(outputfile)\n\n        to_compile.append((sourcefile, outputfile))\n\n    if args.print_output_paths_only:\n        print(';'.join(all_output_files))\n        sys.exit(0)\n\n    maxwidth = len(str(len(to_compile)))\n    for idx, (module, outputfile) in enumerate(to_compile):\n        try:\n            print(f\"[{idx+1:{maxwidth}}/{len(to_compile)}] \"\n                  f\"Compiling {module} to {outputfile}\")\n            py_compile.compile(module, cfile=outputfile, doraise=True)\n\n        except py_compile.PyCompileError as exc:\n            print(f\"FAILED to compile '{exc.file}':\")\n            print(exc.msg)\n            sys.exit(1)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "buildsystem/cpp.cmake",
    "content": "# Copyright 2014-2018 the openage authors. See copying.md for legal info.\n\n# declare a new 'empty' executable file.\n# you need to use add_sources to add source files to it, and finalize_binary to finalize it.\n# then you can add libraries, include dirs, etc.\nfunction(declare_binary target_name output_name type)\n\t# create the executable\n\tif(type STREQUAL \"executable\")\n\t\tadd_executable(\"${target_name}\" ${sources})\n\telseif(type STREQUAL \"library\")\n\t\tadd_library(\"${target_name}\" SHARED ${sources})\n\tendif()\n\n\tset_target_properties(\"${target_name}\" PROPERTIES OUTPUT_NAME \"${output_name}\")\n\n\tset_property(GLOBAL APPEND PROPERTY SFT_BINARIES \"${target_name}\")\n\n\t# process further arguments\n\tforeach(flag ${ARGN})\n\t\tif(flag STREQUAL \"allow_no_undefined\")\n\t\t\tif(NOT type STREQUAL \"library\")\n\t\t\t\tmessage(FATAL_ERROR \"finalize_binary flag 'allow_no_undefined' is only valid for libraries!\")\n\t\t\tendif()\n\n\t\t\tif(NOT \"${CMAKE_CXX_FLAGS}\" MATCHES \"-fsanitize\" AND NOT MSVC)\n\t\t\t\tif(APPLE)\n\t\t\t\t\tset(link_flag \"-undefined,error\")\n\t\t\t\telse()\n\t\t\t\t\tset(link_flag \"--no-undefined\")\n\t\t\t\tendif()\n\t\t\t\tset_target_properties(\"${target_name}\" PROPERTIES\n\t\t\t\t\tCOMPILE_FLAGS \"${EXTRA_FLAGS}\"\n\t\t\t\t\tLINK_FLAGS \"-Wl,${link_flag}\"\n\t\t\t\t)\n\t\t\tendif()\n\t\telse()\n\t\t\tmessage(FATAL_ERROR \"declare_binary encountered unknown flag: ${flag}\")\n\t\tendif()\n\tendforeach()\nendfunction()\n\n\n# add source files to a binary\n#\n# by default, the filename is interpreted as relative to the current directory\n# to specify absolute filenames, write ABSOLUTE\n# to specify a file that will be auto-generated, write GENERATED, all files after\n# this modifier are marked as generated.\nfunction(add_sources target_name)\n\tset(generated FALSE)\n\n\tget_property(binary_list GLOBAL PROPERTY SFT_BINARIES)\n\tlist(FIND binary_list \"${target_name}\" index)\n\tif(index EQUAL -1)\n\t\tmessage(FATAL_ERROR \"attempting to add source to unknown binary ${target_name}\")\n\tendif()\n\n\tforeach(source ${ARGN})\n\t\tif(source STREQUAL GENERATED)\n\t\t\tset(generated TRUE)\n\t\telse()\n\t\t\tif(NOT IS_ABSOLUTE \"${source}\")\n\t\t\t\tset(source \"${CMAKE_CURRENT_SOURCE_DIR}/${source}\")\n\t\t\tendif()\n\t\t\tfile(TO_CMAKE_PATH \"${source}\" source)\n\n\t\t\t# add all sources as private, otherwise _ALL SOURCES_\n\t\t\t# would be compiled again for each library that links against\n\t\t\t# the $target_name\n\t\t\ttarget_sources(\"${target_name}\" PRIVATE \"${source}\")\n\n\t\t\tif(generated)\n\t\t\t\tset_source_files_properties(\"${source}\" PROPERTIES GENERATED ON)\n\t\t\t\tset_property(GLOBAL PROPERTY SFT_BINARY_HAS_GENERATED_SRCS_${target_name} TRUE)\n\t\t\tendif()\n\t\tendif()\n\tendforeach()\nendfunction()\n\n\n# finalize the executable definition\nfunction(finalize_binary target_name)\n\tget_property(has_generated_sources GLOBAL PROPERTY SFT_BINARY_HAS_GENERATED_SRCS_${target_name})\n\n\t# make the binary depend on codegen iff it has any generated files\n\t#if(has_generated_sources)\n\t#\tadd_dependencies(\"${target_name}\" codegen)\n\t#endif()\n\n\tpretty_print_target(\"cpp\" \"${target_name}\" \"[${source_file_count} sources]\")\nendfunction()\n"
  },
  {
    "path": "buildsystem/cythonize.py",
    "content": "#!/usr/bin/env python3\n#\n# Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n\"\"\"\nRuns Cython on all modules that were listed via add_cython_module.\n\"\"\"\n\nimport argparse\nimport os\nimport sys\nfrom contextlib import redirect_stdout\nfrom multiprocessing import cpu_count\nfrom pathlib import Path\n\nfrom Cython.Build import cythonize\n\n\nclass LineFilter:\n    \"\"\" Proxy for a stream (default stdout) to filter out whole unwanted lines \"\"\"\n    # pylint: disable=too-few-public-methods\n    def __init__(self, stream=None, filters=None):\n        self.stream = stream or sys.stdout\n        self.filters = filters or []\n        self.buf = \"\"\n\n    def __getattr__(self, attr_name):\n        return getattr(self.stream, attr_name)\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.stream.write(self.buf)\n        self.buf = \"\"\n\n    def write(self, data):\n        \"\"\"\n        Writes to output stream, buffered line-wise,\n        omitting lines given in to constructor in filter\n        \"\"\"\n        self.buf += data\n        lines = self.buf.split('\\n')\n        for line in lines[:-1]:\n            if not any(f(line) for f in self.filters):\n                self.stream.write(line + '\\n')\n        self.buf = lines[-1]\n\n\nclass CythonFilter(LineFilter):\n    \"\"\" Filters output of cythonize for useless warnings \"\"\"\n    # pylint: disable=too-few-public-methods\n    def __init__(self):\n        filters = [\n            lambda x: 'put \"# distutils: language=c++\" in your .pyx or .pxd file(s)' in x,\n            lambda x: x.startswith('Compiling ') and x.endswith(' because it changed.')\n        ]\n        super().__init__(filters=filters)\n\n\ndef read_list_from_file(filename):\n    \"\"\" Reads a semicolon-separated list of file entires \"\"\"\n    with open(filename, encoding='utf8') as fileobj:\n        data = fileobj.read().strip()\n\n    return [Path(filename).resolve() for filename in data.split(';')]\n\n\ndef remove_if_exists(filename):\n    \"\"\" Deletes the file (if it exists) \"\"\"\n    if filename.is_file():\n        print(filename.relative_to(os.getcwd()))\n        filename.unlink()\n\n\ndef cythonize_wrapper(modules, force_optimized_lib = False, **kwargs):\n    \"\"\" Calls cythonize, filtering useless warnings \"\"\"\n    bin_dir, bin_modules = kwargs['build_dir'], []\n    src_dir, src_modules = Path.cwd(), []\n\n    for module in modules:\n        if Path(bin_dir) in module.parents:\n            bin_modules.append(str(module.relative_to(bin_dir)))\n        else:\n            src_modules.append(str(module.relative_to(src_dir)))\n\n    with CythonFilter() as cython_filter:\n        with redirect_stdout(cython_filter):\n            if src_modules:\n                cythonize(src_modules, **kwargs)\n                if sys.platform == 'win32' and force_optimized_lib:\n                    win_use_optimized_lib_python(src_modules, bin_dir)\n\n            if bin_modules:\n                os.chdir(bin_dir)\n                cythonize(bin_modules, **kwargs)\n                if sys.platform == 'win32' and force_optimized_lib:\n                    win_use_optimized_lib_python(bin_modules, bin_dir)\n                os.chdir(src_dir)\n\n\ndef win_use_optimized_lib_python(modules, path):\n    \"\"\"\n    Add an #ifdef statement in cythonized .cpp files to temporarily undefine _DEBUG before\n    #include \"Python.h\"\n\n    This function modifies the generated C++ files from Cython to prevent linking to\n    the debug version of the Python library on Windows. The debug version of the\n    Python library cannot import Python libraries that contain extension modules.\n    see: https://github.com/python/cpython/issues/127619 (To unserstand the problem)\n    see: https://stackoverflow.com/a/59566420 (To understand the soloution)\n    \"\"\"\n\n    for module in modules:\n        module = str(module)\n        if path:\n            module = path + \"\\\\\" + module\n        module = module.removesuffix(\".py\").removesuffix(\".pyx\")\n        module = module + \".cpp\"\n        with open(module, \"r\", encoding='utf8') as file:\n            text = file.read()\n            if not text.count(\"OPENAGE: UNDEF_DEBUG_INSERTED\"):\n                text = text.replace(\n                    '#include \"Python.h\"',\n                    (\n                        \"\\n\\n// OPENAGE: UNDEF_DEBUG_INSERTED\\n\"\n                        \"// Avoid linking to the debug version of the Python library on Windows\\n\\n\"\n                        \"#ifdef _DEBUG\\n\"\n                        \"#define _DEBUG_WAS_DEFINED\\n\"\n                        \"#undef _DEBUG\\n#endif\\n\"\n                        \"#include \\\"Python.h\\\"\\n\"\n                        \"#ifdef _DEBUG_WAS_DEFINED\\n\"\n                        \"#define _DEBUG\\n\"\n                        \"#undef _DEBUG_WAS_DEFINED\\n\"\n                        \"#endif\\n\\n\"\n                        \"// OPENAGE: UNDEF_DEBUG_INSERTED\\n\\n\"\n                    ),\n                    1\n                )\n        with open(module, \"w\", encoding='utf8') as file:\n            file.write(text)\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"module_list\", help=(\n        \"Module list file (semicolon-separated).\"\n    ))\n    cli.add_argument(\"embedded_module_list\", help=(\n        \"Embedded module list file (semicolon-separated).\\n\"\n        \"Modules in this list are compiled with the --embed option.\"\n    ))\n    cli.add_argument(\"depends_list\", help=(\n        \"Dependency list file (semicolon-separated).\\n\"\n        \"Contains all .pxd and other files that may get included.\\n\"\n        \"Used to verify that all dependencies are properly listed \"\n        \"in the CMake build configuration.\"\n    ))\n    cli.add_argument(\"--clean\", action=\"store_true\", help=(\n        \"Clean compilation results and exit.\"\n    ))\n    cli.add_argument(\"--build-dir\", help=(\n        \"Build output directory to generate the cpp files in.\"\n        \"note: this is also added for module search path.\"\n    ))\n    cli.add_argument(\"--memcleanup\", type=int, default=0, help=(\n        \"Generate memory cleanup code to make valgrind happy:\\n\"\n        \"0: nothing, 1+: interned objects,\\n\"\n        \"2+: cdef globals, 3+: types objects\"\n    ))\n    cli.add_argument(\"--threads\", type=int, default=cpu_count(),\n                     help=\"number of compilation threads to use\")\n    cli.add_argument(\"--force_optimized_lib\", action=\"store_true\",\n                     help= \"edit compiled .cpp files to link to optimized version of python libary\")\n    args = cli.parse_args()\n\n    # cython emits warnings on using absolute paths to modules\n    # https://github.com/cython/cython/issues/2323\n    modules = read_list_from_file(args.module_list)\n    embedded_modules = read_list_from_file(args.embedded_module_list)\n    depends = set(read_list_from_file(args.depends_list))\n\n    if args.clean:\n        for module in modules + embedded_modules:\n            rel_module = module.relative_to(Path.cwd())\n            build_module = args.build_dir / rel_module\n            remove_if_exists(build_module.with_suffix('.cpp'))\n            remove_if_exists(build_module.with_suffix('.html'))\n        sys.exit(0)\n\n    from Cython.Compiler import Options\n    Options.annotate = True\n    Options.fast_fail = True\n    Options.generate_cleanup_code = args.memcleanup\n    Options.cplus = 1\n\n    # build cython modules (emits shared libraries)\n    cythonize_args = {\n        'compiler_directives': {'language_level': 3},\n        'build_dir': args.build_dir,\n        'include_path': [args.build_dir],\n        'nthreads': args.threads\n    }\n\n    # this is deprecated, but still better than\n    # writing funny lines at the head of each file.\n    cythonize_args['language'] = 'c++'\n\n    cythonize_wrapper(modules, args.force_optimized_lib, **cythonize_args)\n\n    # build standalone executables that embed the py interpreter\n    Options.embed = \"main\"\n\n    cythonize_wrapper(embedded_modules, args.force_optimized_lib, **cythonize_args)\n\n    # verify depends\n    from Cython.Build.Dependencies import _dep_tree\n\n    depend_failed = False\n    # TODO figure out a less hacky way of getting the depends out of Cython\n    # pylint: disable=no-member, protected-access\n    for module, files in _dep_tree.__cimported_files_cache.items():\n        for filename in files:\n            if not filename.startswith('.'):\n                # system include starts with /\n                continue\n\n            if os.path.realpath(os.path.abspath(filename)) not in depends:\n                print(\"\\x1b[31mERR\\x1b[m unlisted dependency: \" + filename)\n                depend_failed = True\n\n    if depend_failed:\n        sys.exit(1)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "buildsystem/doxygen.cmake",
    "content": "# Copyright 2014-2016 the openage authors. See copying.md for legal info.\n\n# Doxygen integration\n\n# enable doxygen for all given folder names\nfunction(doxygen_init)\n\tfind_package(Doxygen)\n\tif(DOXYGEN_FOUND)\n\t\tfind_file(DOT dot HINTS \"/usr/bin/dot\")\n\n\t\tif(NOT ${DOT} STREQUAL \"DOT-NOTFOUND\")\n\t\t\tset(DOT_EXECUTABLE \"${DOT}\" PARENT_SCOPE)\n\t\t\tset(HAVE_DOT \"YES\" PARENT_SCOPE)\n\t\telse()\n\t\t\tset(DOT_EXECUTABLE \"/bin/false\" PARENT_SCOPE)\n\t\t\tset(HAVE_DOT \"NO\" PARENT_SCOPE)\n\t\t\tmessage(WARNING \"graphviz dot couldn't be found, you won't have cool graphs in the docs.\")\n\t\tendif()\n\tendif()\n\n\tset(DOXYGEN_EXECUTABLE \"${DOXYGEN_EXECUTABLE}\" PARENT_SCOPE)\n\tset(DOXYGEN_FOUND \"${DOXYGEN_FOUND}\" PARENT_SCOPE)\nendfunction()\n\nfunction(doxygen_configure)\n\tif(DOXYGEN_FOUND)\n\t\t# add doc target\n\t\tadd_custom_target(doc\n\t\t\t\"${DOXYGEN_EXECUTABLE}\" \"${CMAKE_BINARY_DIR}/Doxyfile\"\n\t\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t\t\tCOMMENT \"generating docs (via Doxygen)\"\n\t\t\tVERBATIM\n\t\t)\n\n\t\t# create doc folder name list\n\t\tforeach(folder ${ARGN})\n\t\t\tset(DOXYGEN_SCAN_FOLDERS \"${DOXYGEN_SCAN_FOLDERS} \\\"${CMAKE_CURRENT_SOURCE_DIR}/${folder}\\\"\")\n\t\tendforeach()\n\n\t\t# adapt doxygen config\n\t\tconfigure_file(\"${BUILDSYSTEM_DIR}/templates/Doxyfile.in\" \"${CMAKE_BINARY_DIR}/Doxyfile\" @ONLY)\n\telse()\n\t\t# add no-op doc target\n\t\tadd_custom_target(doc\n\t\t\tCOMMAND echo \"When you configured the project, Doxygen could not be found.\"\n\t\t\tCOMMAND echo \"Install Doxygen and `./configure` again, then try to generate docs.\"\n\t\t\tCOMMAND false\n\t\t\tVERBATIM\n\t\t)\n\t\tmessage(WARNING \"doxygen couldn't be found, you won't be able to generate docs\")\n\tendif()\nendfunction()\n\ndoxygen_init()\n"
  },
  {
    "path": "buildsystem/doxygen_custom.css",
    "content": "/*******************************************************\n * Make footer black and content white\n */\n\nbody {\n\tbackground-color: #445;\n}\n\ndiv.contents {\n\tbackground-color: white;\n\tpadding-top: 10px;\n\tpadding-left: 12px;\n\tpadding-right: 8px;\n\tpadding-bottom: 1em;\n\tmargin: 0;\n}\n\n\n/*******************************************************\n * Header\n */\n\n#titlearea {\n\tpadding: 1em 0;\n\tbackground-color: white;\n\tborder-bottom: 2px solid #5373B4;\n}\n\n#projectlogo {\n\tpadding-left: 1em;\n\ttext-align: left;\n}\n\n#projectlogo img {\n\theight: 6em;\n}\n\n#projectname {\n\tvisibility: hidden; /* Hide project name, it's already on the logo */\n\ttext-align: right;\n\tpadding-right: .3em;\n}\n\n#projectnumber {\n\tvisibility: visible; /* Show openage version, because this is a children of\n\t                        #projectname and I've made that invisible */\n}\n\n#projectalign {\n\twidth: 100%; /* So I can align the version number to the right */\n}\n\n\n/*******************************************************\n * Main navbar\n */\n\n.sm {\n\tbackground-image: none;\n\tbackground-color: black;\n}\n\n.sm a {\n\tbackground-image: none;\n\tbackground-color: black;\n\tcolor: white;\n\ttext-shadow: 0 1px 1px black;\n\tborder: none;\n}\n\n.sm a.highlighted,\n.sm a:hover,\n.sm a:active,\n.sm a:focus {\n\tbackground-image: url(\"tab_a.png\");\n\tbackground-repeat: repeat;\n\ttext-shadow: 0 1px 1px black;\n\tborder: none;\n}\n\n.sm ul a.highlighted,\n.sm ul a:hover,\n.sm ul a:active,\n.sm ul a:focus {\n\tbackground-image: none;\n\tbackground-color: #ccc;\n\ttext-shadow: none;\n\tborder: none;\n\tcolor: inherit;\n}\n\n/*\n * Those tiny arrows are instead a 0px box with a 4px border, that triangle it's\n * the one border, the remaining borders are transparent\n */\n.sm a span.sub-arrow {\n\tborder-color: white transparent transparent transparent;\n}\n\n.sm ul a span.sub-arrow {\n\tborder-color: transparent transparent transparent black;\n}\n\n.sm ul a.highlighted span.sub-arrow {\n\tborder-color: transparent transparent transparent white;\n}\n\n\n/*******************************************************\n * Hierarchy navbar\n */\n\n.navpath ul {\n\tbackground-color: #445;\n\tbackground-image: none;\n\n\tborder: 0;\n}\n\n.navpath ul li.navelem {\n\tcolor: white;\n}\n\n.navpath ul li.navelem a {\n\tcolor: #eee;\n\ttext-shadow: 0px 1px 1px black;\n}\n\n.navpath ul li.navelem a:hover {\n\tcolor: white;\n\ttext-decoration: underline;\n}\n\n/*******************************************************\n * Directory tables\n */\n\ntable.directory tr.even {\n\tbackground-color: #eee;\n}\n\ntable.directory td.entry {\n\tcolor: #444;\n}\n\ntable.directory td.desc {\n\tborder-left: 2px solid white;\n}\n\n/* Icons looked off */\n.iconfclosed,\n.iconfopen,\n.icondoc {\n\tmargin-top: 3px;\n}\n\n\n/*******************************************************\n * Footer\n */\n\nhr.footer {\n\tdisplay: none;\n}\n\naddress.footer {\n\t/*background-color: black;*/\n\tcolor: white;\n\tpadding: 1em;\n\tborder-top: 2px solid #aac;\n}\n\ndiv.contents {\n\tbackground-color: white;\n\tpadding-top: 10px;\n\tpadding-left: 12px;\n\tpadding-right: 8px;\n\tpadding-bottom: 1em;\n\tmargin: 0;\n}\n\n\n/*******************************************************\n * Content\n */\n\ndiv.textblock {\n\tmax-width: 900px;\n}\n\n/*\n * Single-line code blocks\n */\npre.fragment {\n\tpadding: .7em;\n\tborder: none;\n\tbackground-color: #f2f2f5;\n}\n\n/*\n * Multiple-line code blocks\n */\ndiv.fragment {\n\tborder: none;\n\tbackground-color: #f2f2f5;\n\tpadding: .7em;\n}\n\ndiv.fragment div.line {\n\tpadding: .5em;\n\ttext-indent: 0;\n}\n\ndiv.fragment div.line span.lineno {\n\tpadding: .5em;\n\ttext-indent: 0;\n\tborder-right: 2px solid #aac;\n}\n\n/*\n * Inline code\n */\ncode {\n\tbackground-color: #eee;\n\tpadding: 0.3em;\n\tmargin: 0;\n\tfont-size: 85%;\n\tborder-radius: 3px;\n}\n\n/*\n * Tables\n */\ntable.doxtable {\n\tborder-bottom: 1px solid #9CAFD4;\n}\n\ntable.doxtable th {\n\tbackground-color: #374F7F;\n\tcolor: white;\n\tborder: none;\n}\n\ntable.doxtable tr:nth-child(even) {\n\tbackground-color: #f2f2f5;\n}\n\ntable.doxtable tr:nth-child(odd) {\n\tbackground-color: white;\n}\n\ntable.doxtable td {\n\tborder: none;\n}\n\n/* Index letters (those in a black box) */\ndiv.ah, span.ah {\n\tbackground-color: black;\n\tbackground-image: none;\n\tborder: none;\n}\n\n/* Index bar */\ndiv.qindex {\n\tborder: 1px solid #bac8e4;\n\tborder-left: 0;\n\tborder-right: 0;\n\tpadding: .7em 0;\n\tbackground-color: white;\n}\n"
  },
  {
    "path": "buildsystem/inplacemodules.py",
    "content": "# Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nInstalls the Python extension modules that were created in the configuration\nspecific subdirectory of the build directory to the places where they would\nbe expected.\n\nAttemts to use OS facilities such as hardlinking, and falls back to copying.\n\"\"\"\n\nimport argparse\nimport os\nfrom pathlib import Path\nimport shutil\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"module_list_file\", help=(\n        \"semicolon-separated list of all modules that shall be installed \"\n        \"in-place.\"\n    ))\n    cli.add_argument(\"configuration\", help=(\n        \"the build configuration like Debug or Release\"\n    ))\n    cli.add_argument(\"--clean\", action=\"store_true\", help=(\n        \"remove instead of creating\"\n    ))\n    args = cli.parse_args()\n\n    with open(args.module_list_file, encoding='utf8') as fileobj:\n        modules = fileobj.read().strip().split(';')\n        if modules == ['']:\n            modules = []\n\n    for module in modules:\n        sourcefile = Path(module)\n\n        if args.configuration in sourcefile.parts:\n            # If `sourcefile` has a configuration component, remove it.\n            file_parts = list(sourcefile.parts)\n            file_parts.remove(args.configuration)\n            targetfile = Path(*file_parts)\n        else:\n            continue\n\n        if targetfile.exists():\n            if args.clean:\n                print(targetfile)\n                targetfile.unlink()\n                continue\n\n            if targetfile.stat().st_mtime >= sourcefile.stat().st_mtime:\n                continue\n\n            targetfile.unlink()\n\n        if args.clean:\n            continue\n\n        # try to hardlink\n        try:\n            os.link(sourcefile, targetfile)\n            continue\n        except OSError:\n            pass\n\n        # we don't try to symlink, because then Python may \"intelligently\"\n        # determine the actual location, and refuse to work.\n\n        # fallback to copying the file\n        shutil.copy(sourcefile, targetfile)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "buildsystem/modules/DownloadCache.cmake",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n# DownloadCache, as the name suggests, downloads a file and\n# if it succeeds, caches it for subsequent runs.\n\n# function args:\n# * url: the source URL to download the file from.\n# * download_path: the destination file path.\nfunction(download_cache url download_path)\n\tset(download_stamp_path \"${download_path}.stamp\")\n\tif(EXISTS \"${download_stamp_path}\")\n\t\tmessage(STATUS \"using cached ${download_path}\")\n\t\treturn()\n\tendif()\n\tfile(DOWNLOAD \"${url}\" \"${download_path}\"\n\t\tSTATUS download_status\n\t\tSHOW_PROGRESS\n\t)\n\tlist(GET download_status 0 status_code)\n\tif(NOT status_code EQUAL 0)\n\t\tlist(GET dowload_status 1 reason)\n\t\tmessage(FATAL_ERROR \"Failed to download from ${url}: ${reason}\")\n\tendif()\n\tfile(WRITE \"${download_stamp_path}\" \"\")\nendfunction()\n"
  },
  {
    "path": "buildsystem/modules/FindCython.cmake",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n# This module defines:\n#\n#  CYTHON_FOUND\n#  CYTHON_VERSION          - e.g. '0.23dev'\n#  CYTHON                  - invocation (using python)\n#\n# It depends on:\n#\n#  PYTHON\n\nset(CYTHON \"${PYTHON} -m cython\")\npy_exec(\"import cython; print(cython.__version__)\" CYTHON_VERSION)\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(Cython VERSION_VAR CYTHON_VERSION REQUIRED_VARS CYTHON)\n"
  },
  {
    "path": "buildsystem/modules/FindEpoxy.cmake",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\nfind_path(EPOXY_INCLUDE_DIRS epoxy/gl.h\n\tHINTS\n\t$ENV{EPOXY_DIR}\n\tPATH_SUFFIXES include src\n\tPATHS\n\t/usr/include\n\t/usr/local/include\n\t/sw/include\n\t/opt/local/include\n\t/usr/freeware/include\n)\n\nfind_library(EPOXY_LIBRARIES\n\tNAMES epoxy\n\tHINTS\n\t$ENV{EPOXY_DIR}\n\tPATH_SUFFIXES lib64 lib\n\tPATHS\n\t/usr/lib\n\t/usr/local/lib\n\t/sw\n\t/usr/freeware\n)\n\ninclude(\"FindPackageHandleStandardArgs\")\nfind_package_handle_standard_args(Epoxy DEFAULT_MSG EPOXY_LIBRARIES EPOXY_INCLUDE_DIRS)\n\nmark_as_advanced(EPOXY_LIBRARIES EPOXY_INCLUDE_DIRS)\n"
  },
  {
    "path": "buildsystem/modules/FindGCCBacktrace.cmake",
    "content": "# Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n# This module defines:\n#\n#  GCCBacktrace_FOUND           - whether libbacktrace was found\n#  GCCBacktrace_INCLUDE_DIRS    - The include dirs for libbacktrace\n#  GCCBacktrace_LIBRARIES       - The list of libraries\n\nfind_path(GCCBacktrace_INCLUDE_DIR \"backtrace.h\")\nfind_library(GCCBacktrace_LIBRARY\n\tNAMES\n\t\t\"backtrace\"\n\tPATHS\n\t\t/usr/lib\n\t\t/usr/local/lib\n\t\t/sw\n\t\t/usr/freeware\n)\n\ninclude(CheckCXXSourceCompiles)\nlist(APPEND CMAKE_REQUIRED_INCLUDES ${GCCBacktrace_INCLUDE_DIR})\nlist(APPEND CMAKE_REQUIRED_LIBRARIES ${GCCBacktrace_LIBRARY})\nset(CMAKE_REQUIRED_QUIET TRUE)\nCHECK_CXX_SOURCE_COMPILES(\"\n#include <iostream>\n#include <backtrace.h>\n\nint callback(void * /* unused */, uintptr_t pc) {\n\tstd::cout << (void *) pc << std::endl;\n\treturn 0;\n}\n\nint main() {\n\tstruct backtrace_state *state = nullptr;\n\tstate = backtrace_create_state(nullptr, false, nullptr, nullptr);\n\tbacktrace_simple(state, 0, callback, nullptr, nullptr);\n\treturn 0;\n}\n\" HAVE_GCC_BACKTRACE)\n\nif(NOT HAVE_GCC_BACKTRACE)\n\tunset(GCCBacktrace_INCLUDE_DIR)\n\tunset(GCCBacktrace_LIBRARY)\nelse()\n\tset(GCCBacktrace_LIBRARIES ${GCCBacktrace_LIBRARY})\n\tset(GCCBacktrace_INCLUDE_DIRS ${GCCBacktrace_INCLUDE_DIR})\nendif()\n\n# handle the QUIETLY and REQUIRED arguments and set BACKTRACE_FOUND to TRUE if\n# all listed variables are TRUE\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(\n\tGCCBacktrace\n\tFOUND_VAR GCCBacktrace_FOUND\n\tREQUIRED_VARS GCCBacktrace_LIBRARIES GCCBacktrace_INCLUDE_DIRS)\n\nmark_as_advanced(GCCBacktrace_LIBRARIES GCCBacktrace_INCLUDE_DIRS)\n"
  },
  {
    "path": "buildsystem/modules/FindGPerfTools.cmake",
    "content": "# This file was taken from VAST,\n# Copyright 2014-2014 Matthias Vallentin.\n# It's licensed under the terms of the 3-clause BSD license.\n# Modifications Copyright 2014-2020 the openage authors.\n# See copying.md for further legal info.\n\n# Tries to find Gperftools.\n#\n# Usage of this module as follows:\n#\n#     find_package(Gperftools)\n#\n# Variables used by this module, they can change the default behaviour and need\n# to be set before calling find_package:\n#\n#  Gperftools_ROOT_DIR  Set this variable to the root installation of\n#                       Gperftools if the module has problems finding\n#                       the proper installation path.\n#\n# Variables defined by this module:\n#\n#  GPERFTOOLS_FOUND              System has Gperftools libs/headers\n#  GPERFTOOLS_LIBRARIES          The Gperftools libraries (tcmalloc & profiler)\n#  GPERFTOOLS_TCMALLOC           The tcmalloc library\n#  GPERFTOOLS_PROFILER           The profiler library\n#  GPERFTOOLS_INCLUDE_DIR        The location of Gperftools headers\n\nfind_library(GPERFTOOLS_TCMALLOC\n\tNAMES tcmalloc\n\tHINTS ${Gperftools_ROOT_DIR}/lib)\n\nfind_library(GPERFTOOLS_PROFILER\n\tNAMES profiler\n\tHINTS ${Gperftools_ROOT_DIR}/lib)\n\nfind_library(GPERFTOOLS_TCMALLOC_AND_PROFILER\n\tNAMES tcmalloc_and_profiler\n\tHINTS ${Gperftools_ROOT_DIR}/lib)\n\nfind_path(GPERFTOOLS_INCLUDE_DIR\n\tNAMES gperftools/heap-profiler.h\n\tHINTS ${Gperftools_ROOT_DIR}/include)\n\nset(GPERFTOOLS_LIBRARIES ${GPERFTOOLS_TCMALLOC_AND_PROFILER})\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(\n\tGPerfTools\n\tDEFAULT_MSG\n\tGPERFTOOLS_LIBRARIES\n\tGPERFTOOLS_INCLUDE_DIR)\n\nmark_as_advanced(\n\tGperftools_ROOT_DIR\n\tGPERFTOOLS_TCMALLOC\n\tGPERFTOOLS_PROFILER\n\tGPERFTOOLS_TCMALLOC_AND_PROFILER\n\tGPERFTOOLS_LIBRARIES\n\tGPERFTOOLS_INCLUDE_DIR)\n"
  },
  {
    "path": "buildsystem/modules/FindHarfBuzz.cmake",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n# FindHarfBuzz\n# ---------\n#\n# Locate HarfBuzz, the awesome text shaping library.\n#\n# The module defines the following variables:\n#\n# ::\n#\n#    HarfBuzz_FOUND - Found HarfBuzz library\n#    HarfBuzz_INCLUDE_DIRS - HarfBuzz include directories\n#    HarfBuzz_LIBRARIES - The libraries needed to use HarfBuzz\n#    HarfBuzz_VERSION_STRING - the version of HarfBuzz found\n#\n# Example Usage:\n#\n# ::\n#\n#     find_package(HarfBuzz REQUIRED)\n#     include_directories(${HarfBuzz_INCLUDE_DIRS})\n#\n# ::\n#\n#     add_executable(foo foo.cc)\n#     target_link_libraries(foo ${HarfBuzz_LIBRARIES})\n\nfind_path(HarfBuzz_INCLUDE_DIR harfbuzz/hb.h\n\tHINTS $ENV{HARFBUZZ_DIR}\n\tPATHS\n\t\t/usr/include\n\t\t/usr/local/include\n\t\t/sw/include\n\t\t/opt/local/include\n\t\t/usr/freeware/include\n)\n\nfind_library(HarfBuzz_LIBRARY\n\tNAMES harfbuzz libharfbuzz\n\tHINTS $ENV{HARFBUZZ_DIR}\n\tPATH_SUFFIXES lib64 lib\n\tPATHS\n\t\t/usr/lib\n\t\t/usr/local/lib\n\t\t/sw\n\t\t/usr/freeware\n)\n\nif(HarfBuzz_INCLUDE_DIR)\n\tset(HarfBuzz_VERSION_FILE \"${HarfBuzz_INCLUDE_DIR}/harfbuzz/hb-version.h\")\n\tif(EXISTS \"${HarfBuzz_VERSION_FILE}\")\n\t\tfile(STRINGS \"${HarfBuzz_VERSION_FILE}\" hb_version_str\n\t\t     REGEX \"^#define[\\t ]+HB_VERSION_STRING[\\t ]+\\\".*\\\"\")\n\n\t\tstring(REGEX REPLACE \"^#define[\\t ]+HB_VERSION_STRING[\\t ]+\\\"([^\\\"]*)\\\".*\" \"\\\\1\"\n\t\t       HarfBuzz_VERSION_STRING \"${hb_version_str}\")\n\t\tunset(hb_version_str)\n\tendif()\n\tunset(HarfBuzz_VERSION_FILE)\nendif()\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(HarfBuzz\n\tFOUND_VAR HarfBuzz_FOUND\n\tREQUIRED_VARS HarfBuzz_LIBRARY HarfBuzz_INCLUDE_DIR\n\tVERSION_VAR HarfBuzz_VERSION_STRING\n)\n\nif(HarfBuzz_FOUND)\n\tset(HarfBuzz_INCLUDE_DIRS \"${HarfBuzz_INCLUDE_DIR}\")\n\tset(HarfBuzz_LIBRARIES \"${HarfBuzz_LIBRARY}\")\nendif()\n\nmark_as_advanced(HarfBuzz_INCLUDE_DIR HarfBuzz_LIBRARY)\n"
  },
  {
    "path": "buildsystem/modules/FindInotify.cmake",
    "content": "# Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n# This module defines\n#\n# INOTIFY_INCLUDE_DIR\n# INOTIFY_FOUND\n\nfind_path(INOTIFY_INCLUDE_DIR sys/inotify.h HINTS /usr/include/${CMAKE_LIBRARY_ARCHITECTURE})\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(Inotify DEFAULT_MSG INOTIFY_INCLUDE_DIR)\n\nmark_as_advanced(INOTIFY_INCLUDE_DIR)\n"
  },
  {
    "path": "buildsystem/modules/FindOgg.cmake",
    "content": "# Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n# - Find ogg library\n# Find the native Ogg headers and library.\n# This module defines\n#  OGG_INCLUDE_DIRS   - where to find ogg/ogg.h etc.\n#  OGG_LIBRARIES      - List of libraries when using libogg\n#  OGG_FOUND          - True if ogg is found.\n\nfind_path(OGG_INCLUDE_DIR\n\tNAMES ogg/ogg.h\n\tDOC \"Ogg include directory\"\n)\n\nfind_library(OGG_LIBRARY\n\tNAMES ogg\n\tDOC \"Path to ogg library\"\n)\n\n# handle the QUIETLY and REQUIRED arguments and set OGG_FOUND to TRUE if\n# all listed variables are TRUE\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(Ogg DEFAULT_MSG OGG_LIBRARY OGG_INCLUDE_DIR)\n\nmark_as_advanced(OGG_INCLUDE_DIR OGG_LIBRARY)\n\n# export the variables\nset(OGG_LIBRARIES \"${OGG_LIBRARY}\")\nset(OGG_INCLUDE_DIRS \"${OGG_INCLUDE_DIR}\")\n"
  },
  {
    "path": "buildsystem/modules/FindOpusfile.cmake",
    "content": "# This file was taken from Unvanquished,\n# Copyright 2000-2009 Kitware, Inc., Insight Software Consortium\n# It's licensed under the terms of the 3-clause OpenBSD license.\n# Modifications Copyright 2014-2020 the openage authors.\n# See copying.md for further legal info.\n\n# - Find opus library\n# Find the native Opus headers and libraries.\n# This module defines\n#  OPUS_INCLUDE_DIRS   - where to find opus/opus.h, opus/opusfile.h, etc\n#  OPUS_LIBRARIES      - List of libraries when using libopus\n#  OPUSFILE_FOUND      - True if opus is found.\n\n# find the opusfile header, defines our api.\nfind_path(OPUSFILE_INCLUDE_DIR\n\tNAMES opus/opusfile.h\n\tDOC \"Opusfile include directory\"\n)\nmark_as_advanced(OPUSFILE_INCLUDE_DIR)\n\n# find the opus header\nfind_path(OPUS_INCLUDE_DIR\n\tNAMES opus/opus.h\n\tDOC \"Opus include directory\"\n)\nmark_as_advanced(OPUS_INCLUDE_DIR)\n\n# look for libopusfile, the highlevel container-aware api.\nfind_library(OPUSFILE_LIBRARY\n\tNAMES opusfile\n\tDOC \"Path to OpusFile library\"\n)\nmark_as_advanced(OPUSFILE_LIBRARY)\n\n# find libopus, the core codec component.\nfind_library(OPUS_LIBRARY\n\tNAMES opus\n\tDOC \"Path to Opus library\"\n)\nmark_as_advanced(OPUS_LIBRARY)\n\n\n# handle the QUIETLY and REQUIRED arguments and set OPUS_FOUND to TRUE if\n# all listed variables are TRUE\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(Opusfile DEFAULT_MSG OPUSFILE_LIBRARY OPUS_LIBRARY OPUS_INCLUDE_DIR)\n\n# export the variables\nset(OPUS_LIBRARIES \"${OPUSFILE_LIBRARY}\" \"${OPUS_LIBRARY}\")\nset(OPUS_INCLUDE_DIRS \"${OPUSFILE_INCLUDE_DIR}\" \"${OPUS_INCLUDE_DIR}\" \"${OPUS_INCLUDE_DIR}/opus\")\n"
  },
  {
    "path": "buildsystem/modules/FindPython.cmake",
    "content": "# Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n# Find Python\n# ~~~~~~~~~~~\n#\n# Find the Python interpreter, and related info.\n#\n# This is a wrapper around FindPython3.cmake,\n# which sets many more variables:\n# https://cmake.org/cmake/help/latest/module/FindPython3.html\n#\n# This file defines the following variables:\n#\n# PYTHON_FOUND        - True when python was found.\n# PYTHON              - The full path to the Python interpreter.\n# PYTHON_INCLUDE_DIRS - Include path for Python extensions.\n# PYTHON_LIBRARIES    - Library and Linker options for Python extensions.\n#\n# Also defines py_exec and py_get_config_var.\n\n###############################################################\n\nset(PYTHON_USED_VERSION \"${PYTHON_MIN_VERSION}\")\n\n# You can manually pass the directory to an interpreter\n# by defining PYTHON_DIR or passing -DPYTHON_DIR=\"<DIRECTORY>\"\n# to CMake. It's used as a hint where to look at\n# https://cmake.org/cmake/help/latest/module/FindPython3.html#hints\nif(PYTHON_DIR)\n\tset(Python3_ROOT_DIR \"${PYTHON_DIR}\")\n\nelseif(Python3_ROOT_DIR OR Python3_EXECUTABLE)\n\t# python paths given directly\n\nelse()\n\t# when there are multiple pythons, preferrably use the version of\n\t# the default `python3` executable.\n\texecute_process(\n\t\tCOMMAND \"python3\" -c \"import platform; print(platform.python_version())\"\n\t\tOUTPUT_VARIABLE PYVER_OUTPUT\n\t\tRESULT_VARIABLE PYVER_RETVAL\n\t)\n\n\t# if a system version exists, check if it's compatible with our min requirements\n\tif(PYVER_RETVAL EQUAL 0)\n\t\tstring(REGEX MATCH \"^[0-9]+\\\\.[0-9]+\" PYTHON_USED_VERSION \"${PYVER_OUTPUT}\")\n\n\t\tif(PYTHON_USED_VERSION VERSION_GREATER_EQUAL PYTHON_MIN_VERSION)\n\t\t\t# set EXACT so we get the system version from find_package\n\t\t\tset(need_exact_version \"EXACT\")\n\n\t\telse()\n\t\t\t# search for alternatives if version doesn't fulfill min requirements\n\t\t\tset(PYTHON_USED_VERSION \"${PYTHON_MIN_VERSION}\")\n\n\t\tendif()\n\tendif()\nendif()\n###############################################################\n\n# Never use the Windows Registry to find python\nset(Python3_FIND_REGISTRY \"NEVER\")\n\n# use cmake's FindPython3 to locate library and interpreter\nfind_package(Python3 ${PYTHON_USED_VERSION} ${need_exact_version} COMPONENTS Interpreter Development NumPy)\n\n# python version string to cpython api test in modules/FindPython_test.cpp\n# we use the solution from CPython's header (see https://github.com/SFTtech/openage/issues/1438#issuecomment-1036311012):\n# define PY_VERSION_HEX ((PY_MAJOR_VERSION << 24) | \\\n#                       (PY_MINOR_VERSION << 16) | \\\n#                       (PY_MICRO_VERSION <<  8) | \\\n#                       (PY_RELEASE_LEVEL <<  4) | \\\n#                       (PY_RELEASE_SERIAL << 0))\nmath(EXPR BIT_SHIFT_HEX \"${Python3_VERSION_MAJOR} << 24 | ${Python3_VERSION_MINOR} << 16\" OUTPUT_FORMAT HEXADECIMAL)\nset(PYTHON_MIN_VERSION_HEX \"${BIT_SHIFT_HEX}\")\n\n\n# there's a static_assert that tests the Python version.\n# that way, we verify the interpreter and the library version.\n# (the interpreter provided us the library location)\n\nif(WIN32 AND \"${CMAKE_BUILD_TYPE}\" STREQUAL \"Debug\")\n\tset(TEMP_CMAKE_TRY_COMPILE_CONFIGURATION ${CMAKE_TRY_COMPILE_CONFIGURATION})\n\tset(CMAKE_TRY_COMPILE_CONFIGURATION \"Release\")\nendif()\n\ntry_compile(PYTHON_TEST_RESULT\n\t\"${CMAKE_BINARY_DIR}\"\n\tSOURCES \"${CMAKE_CURRENT_LIST_DIR}/FindPython_test.cpp\"\n\tLINK_LIBRARIES Python3::Python\n\tCXX_STANDARD ${CMAKE_CXX_STANDARD}\n\tCOMPILE_DEFINITIONS \"-DTARGET_VERSION=${PYTHON_MIN_VERSION_HEX}\"\n\tOUTPUT_VARIABLE PYTHON_TEST_OUTPUT\n)\n\nif(WIN32 AND \"${CMAKE_BUILD_TYPE}\" STREQUAL \"Debug\")\n\tset(CMAKE_TRY_COMPILE_CONFIGURATION ${TEMP_CMAKE_TRY_COMPILE_CONFIGURATION})\nendif()\n\n\nif(NOT PYTHON_TEST_RESULT)\n\tmessage(STATUS \"!! No suitable Python interpreter was found !!\n\nWe need a Python interpreter >= ${PYTHON_MIN_VERSION} that is shipped with libpython and header files.\nYou can tell the python find module where your python is installed:\n  Executable location: -DPython3_EXECUTABLE=/path/to/executable\n  Root directory: -DPython3_ROOT_DIR=/directory/of/executable/\n\nWe tried with Python ${PYTHON_FIND_VERSION}, but test compilation failed:\n\n${PYTHON_TEST_OUTPUT}\n\")\n\tmessage(FATAL_ERROR \"No suitable Python was found!\")\n\nelseif(PYTHON_TEST_RESULT)\n\t# Interfacing\n\t# Python.cmake vars <= Python3.cmake vars\n\tset(PYTHON ${Python3_EXECUTABLE} CACHE FILEPATH \"Location of the Python interpreter\" FORCE)\n\tset(PYTHON_FOUND ${Python3_Interpreter_FOUND})\n\tset(PYTHON_LIBRARIES ${Python3_LIBRARIES} CACHE STRING \"Linker invocation for the Python library\" FORCE)\n\tset(PYTHON_INCLUDE_DIRS ${Python3_INCLUDE_DIRS} CACHE PATH \"Location of the Python include dir\" FORCE)\n\tset(PYTHON_VERSION_STRING ${Python3_VERSION})\n\n\t# Numpy.cmake vars <= Python3.cmake vars\n\tset(NUMPY_FOUND ${Python3_NumPy_FOUND})\n\tset(NUMPY_VERSION ${Python3_NumPy_VERSION})\n\tset(NUMPY_INCLUDE_DIR ${Python3_NumPy_INCLUDE_DIRS} CACHE STRING \"Linker invocation for the NumPy library\" FORCE)\n\n\tinclude(FindPackageHandleStandardArgs)\n\tfind_package_handle_standard_args(Python REQUIRED_VARS PYTHON PYTHON_INCLUDE_DIRS PYTHON_LIBRARIES)\nendif()\n\nunset(PYTHON_TEST_RESULT)\nunset(PYTHON_TEST_OUTPUT)\n\n\n# helper functions\n\nfunction(py_exec STATEMENTS RESULTVAR)\n\t# executes some python statement(s), and returns the result in RESULTVAR.\n\t# aborts with a fatal error on error.\n\t# no single quotes are allowed in STATEMENTS.\n\texecute_process(\n\t\tCOMMAND \"${PYTHON}\" -c \"${STATEMENTS}\"\n\t\tOUTPUT_VARIABLE PY_OUTPUT\n\t\tRESULT_VARIABLE PY_RETVAL\n\t)\n\n\tif(NOT PY_RETVAL EQUAL 0)\n\t\tmessage(FATAL_ERROR \"failed:\\n${PYTHON} -c '${STATEMENTS}'\\n${PY_OUTPUT}\")\n\tendif()\n\n\tstring(STRIP \"${PY_OUTPUT}\" PY_OUTPUT_STRIPPED)\n\n\tset(\"${RESULTVAR}\" \"${PY_OUTPUT_STRIPPED}\" PARENT_SCOPE)\nendfunction()\n\n\nfunction(py_get_config_var VAR RESULTVAR)\n\t# uses py_exec to determine a config var as in sysconfig.get_config_var().\n\tpy_exec(\n\t\t\"from sysconfig import get_config_var; print(get_config_var('${VAR}'))\"\n\t\tRESULT\n\t)\n\n\tset(\"${RESULTVAR}\" \"${RESULT}\" PARENT_SCOPE)\nendfunction()\n"
  },
  {
    "path": "buildsystem/modules/FindPython_test.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include <Python.h>\n\nint main() {\n\t// set by preprocessor:\n\tconstexpr uint64_t min_ver = TARGET_VERSION;\n\n\tstatic_assert(PY_VERSION_HEX >= min_ver, \"unsuitable: \" PY_VERSION);\n\n\tPy_Initialize();\n\tPy_Finalize();\n}\n"
  },
  {
    "path": "buildsystem/options.cmake",
    "content": "# Copyright 2014-2018 the openage authors. See copying.md for legal info.\n\n# logs whether the option NAME is enabled\n# sets WITH_${VARNAME} to HAVE\n# errors if WANT_${VARNAME} conflicts with HAVE\nfunction(have_config_option NAME VARNAME HAVE)\n\tset(WANT \"${WANT_${VARNAME}}\")\n\tset(WITH_${VARNAME} \"${HAVE}\" PARENT_SCOPE)\n\n\tif(HAVE)\n\t\tset_property(GLOBAL APPEND PROPERTY SFT_CONFIG_OPTIONS_ENABLED \"${NAME}\")\n\n\t\tif(NOT WANT)\n\t\t\tmessage(FATAL_ERROR \"${NAME}: WANT_${VARNAME}=${WANT}, but WITH_${VARNAME}=${HAVE}\")\n\t\tendif()\n\telse()\n\t\tset_property(GLOBAL APPEND PROPERTY SFT_CONFIG_OPTIONS_DISABLED \"${NAME}\")\n\n\t\tif(WANT STREQUAL \"if_available\")\n\t\t\tmessage(STATUS \"optional dependency is unavailable: ${NAME}\")\n\t\telseif(WANT)\n\t\t\tmessage(FATAL_ERROR \"${NAME}: WANT_${VARNAME}=${WANT}, but WITH_${VARNAME}=${HAVE}\")\n\t\tendif()\n\tendif()\nendfunction()\n\nfunction(print_config_options)\n\tget_property(enabled_opts GLOBAL PROPERTY SFT_CONFIG_OPTIONS_ENABLED)\n\tget_property(disabled_opts GLOBAL PROPERTY SFT_CONFIG_OPTIONS_DISABLED)\n\n\tmessage(\"enabled options:\")\n\tif(enabled_opts)\n\t\tforeach(opt ${enabled_opts})\n\t\t\tmessage(\"\\t${opt}\")\n\t\tendforeach()\n\telse()\n\t\tmessage(\"\\t<none>\")\n\tendif()\n\n\tmessage(\"\")\n\n\tmessage(\"disabled options:\")\n\tif(disabled_opts)\n\t\tforeach(opt ${disabled_opts})\n\t\t\tmessage(\"\\t${opt}\")\n\t\tendforeach()\n\telse()\n\t\tmessage(\"\\t<none>\")\n\tendif()\n\n\tmessage(\"\")\nendfunction()\n\nfunction(get_config_option_string)\n\tget_property(enabled_opts GLOBAL PROPERTY SFT_CONFIG_OPTIONS_ENABLED)\n\n\tif(enabled_opts)\n\t\tLIST(GET enabled_opts 0 CONFIG_OPTION_STRING)\n\t\tLIST(REMOVE_AT enabled_opts 0)\n\n\t\tforeach(opt ${enabled_opts})\n\t\t\tset(CONFIG_OPTION_STRING \"${CONFIG_OPTION_STRING}, ${opt}\")\n\t\tendforeach()\n\telse()\n\t\tset(CONFIG_OPTION_STRING \"< no options enabled >\")\n\tendif()\n\n\tset(CONFIG_OPTION_STRING \"${CONFIG_OPTION_STRING}\" PARENT_SCOPE)\nendfunction()\n"
  },
  {
    "path": "buildsystem/pxdgen.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nAuto-generates PXD files from annotated C++ headers.\n\nInvoked via cmake during the regular build process.\n\"\"\"\n\nimport argparse\nimport os\nfrom pathlib import Path\nimport re\nimport sys\n\nfrom pygments.token import Token\nfrom pygments.lexers import get_lexer_for_filename\n\n\nCWD = os.getcwd()\n\n\nclass ParserError(Exception):\n    \"\"\"\n    Represents a fatal parsing error in PXDGenerator.\n    \"\"\"\n\n    def __init__(self, filename, lineno, message):\n        super().__init__(f\"{filename}:{lineno} {message}\")\n\n\nclass PXDGenerator:\n    \"\"\"\n    Represents, and performs, a single conversion of a C++ header file to a\n    PXD file.\n\n    @param filename:\n        input (C++ header) file name. is opened and read.\n        the output filename is the same, but with .pxd instead of .h.\n    \"\"\"\n\n    def __init__(self, filename):\n        self.filename = filename\n\n        self.warnings = []\n\n        # current parsing state (not valid until self.parse() is called)\n        self.stack, self.lineno, self.annotations = None, None, None\n\n    def parser_error(self, message, lineno=None):\n        \"\"\"\n        Returns a ParserError object for this generator, at the current line.\n        \"\"\"\n        if lineno is None:\n            lineno = self.lineno\n\n        return ParserError(self.filename, lineno, message)\n\n    def tokenize(self):\n        \"\"\"\n        Tokenizes the input file.\n\n        Yields (tokentype, val) pairs, where val is a string.\n\n        The concatenation of all val strings is equal to the input file's\n        content.\n        \"\"\"\n        # contains all namespaces and other '{' tokens\n        self.stack = []\n        # current line number\n        self.lineno = 1\n\n        # we're using the pygments lexer (mainly because that was the first\n        # google hit for 'python c++ lexer', and it's fairly awesome to use)\n\n        lexer = get_lexer_for_filename('.cpp')\n\n        with open(self.filename, encoding='utf8') as infile:\n            code = infile.read()\n\n        for token, val in lexer.get_tokens(code):\n            # ignore whitespaces\n            yield token, val\n            self.lineno += val.count('\\n')\n\n    def handle_singleline_comment(self, val):\n        \"\"\"\n        Breaks down a '//'-style single-line comment, and passes the result\n        to handle_comment()\n\n        @param val:\n            the comment text, as string, including the '//'\n        \"\"\"\n        try:\n            val = re.match('^// (.*)$', val).group(1)\n        except AttributeError as ex:\n            raise self.parser_error(\"invalid single-line comment\") from ex\n\n        self.handle_comment(val)\n\n    def handle_multiline_comment(self, val):\n        \"\"\"\n        Breaks down a '/* */'-style multi-line comment, and passes the result\n        to handle_comment()\n\n        @param val:\n            the comment text, as string, including the '/*' and '*/'\n        \"\"\"\n        try:\n            # pylint: disable=no-member\n            val = re.match(r\"^/\\*(.*)\\*/$\", val, re.DOTALL).group(1)\n        except AttributeError as ex:\n            raise self.parser_error(\"invalid multi-line comment\") from ex\n\n        # for a comment '/* foo\\n * bar\\n */', val is now 'foo\\n * bar\\n '\n        # however, we'd prefer ' * foo\\n * bar'\n        val = ' * ' + val.rstrip()\n        # actually, we'd prefer [' * foo', ' * bar'].\n        lines = val.split('\\n')\n\n        comment_lines = []\n        for idx, line in enumerate(lines):\n            try:\n                line = re.match(r'^ \\*( (.*))?$', line).group(2) or \"\"\n            except AttributeError as ex:\n                raise self.parser_error(\"invalid multi-line comment line\",\n                                        idx + self.lineno) from ex\n\n            # if comment is still empty, don't append anything\n            if comment_lines or line.strip() != \"\":\n                comment_lines.append(line)\n\n        self.handle_comment('\\n'.join(comment_lines).rstrip())\n\n    def handle_comment(self, val):\n        \"\"\"\n        Handles any comment, with its format characters removed,\n        extracting the pxd annotation\n        \"\"\"\n\n        annotations = re.findall(r\"pxd:\\s(.*?)(:pxd|$)\",\n                                 val, re.DOTALL)   # pylint: disable=no-member\n        annotations = [annotation[0] for annotation in annotations]\n\n        if not annotations:\n            raise self.parser_error(\"comment contains no valid pxd annotation\")\n\n        for annotation in annotations:\n            # remove empty lines at end\n            annotation = annotation.rstrip()\n\n            annotation_lines = annotation.split('\\n')\n            for idx, line in enumerate(annotation_lines):\n                if line.strip() != \"\":\n                    # we've found the first non-empty annotation line\n                    self.add_annotation(annotation_lines[idx:])\n                    break\n            else:\n                raise self.parser_error(\"pxd annotation is empty:\\n\" + val)\n\n    def add_annotation(self, annotation_lines):\n        \"\"\"\n        Adds a (current namespace, pxd annotation) tuple to self.annotations.\n        \"\"\"\n        if \"{\" in self.stack:\n            raise self.parser_error(\"PXD annotation is brace-enclosed\")\n        if not self.stack:\n            namespace = None\n        else:\n            namespace = \"::\".join(self.stack)\n\n        self.annotations.append((namespace, annotation_lines))\n\n    def handle_token(self, token, val):\n        \"\"\"\n        Handles one token while the parser is in its regular state.\n\n        Returns the new state integer.\n        \"\"\"\n        # accept any token here\n        if token == Token.Keyword and val == 'namespace':\n            # advance to next state on 'namespace'\n            return 1\n\n        if (token, val) == (Token.Punctuation, '{'):\n            # we're in a struct/class/...\n            self.stack.append('{')\n\n        elif (token, val) == (Token.Punctuation, '}'):\n            try:\n                # leave the struct/class/...\n                self.stack.pop()\n            except IndexError as ex:\n                raise self.parser_error(\"unmatched '}'\") from ex\n\n        elif token == Token.Comment.Single and 'pxd:' in val:\n            self.handle_singleline_comment(val)\n\n        elif token == Token.Comment.Multiline and 'pxd:' in val:\n            self.handle_multiline_comment(val)\n\n        else:\n            # we don't care about all those other tokens\n            pass\n\n        return 0\n\n    def parse(self):\n        \"\"\"\n        Parses the input file.\n\n        Internally calls self.tokenize().\n\n        Adds all found PXD annotations to self.annotations,\n        together with info about the namespace in which they were encountered.\n        \"\"\"\n\n        def handle_state_0(self, token, val, namespace_parts):\n            del namespace_parts\n            return self.handle_token(token, val)\n\n        def handle_state_1(self, token, val, namespace_parts):\n            # we're inside a namespace definition; expect Token.Name\n            # TODO: pygments 2.9 correctly reports Token.Name.Namespace\n            #       we can require this version eventually and change the condition\n            if token not in Token.Name:\n                raise self.parser_error(\n                    \"expected identifier after 'namespace'\")\n            namespace_parts.append(val)\n            return 2\n\n        def handle_state_2(self, token, val, namespace_parts):\n            # either :: or {\n            # the former is for nested namespaces,\n            # the latter ends the namespace\n            if (token, val) == (Token.Operator, ':'):\n                return 3\n\n            if (token, val) != (Token.Punctuation, '{'):\n                raise self.parser_error(\"expected '{' or '::' after \"\n                                        f\"'namespace {self.stack[-1]}'\")\n            # { found, so fill the stack\n            self.stack.append('::'.join(namespace_parts))\n            namespace_parts.clear()\n            return 0\n\n        def handle_state_3(self, token, val, namespace_parts):\n            del namespace_parts\n            if (token, val) == (Token.Operator, ':'):\n                # now, a name must follow\n                return 1\n\n            raise self.parser_error(\"nested namespaces are separated \"\n                                    \"with '::'\")\n\n        transitions = {0: handle_state_0,\n                       1: handle_state_1,\n                       2: handle_state_2,\n                       3: handle_state_3}\n\n        self.annotations = []\n        state = 0\n\n        # to build up nested namespaces\n        namespace_parts = []\n\n        for token, val in self.tokenize():\n            # ignore whitespaces\n            if token in Token.Text and not val.strip():\n                continue\n\n            try:\n                state = transitions[state](self, token, val, namespace_parts)\n\n            except KeyError as exp:\n                raise ValueError(\"reached invalid state in pxdgen\") from exp\n\n        if self.stack:\n            raise self.parser_error(\"expected '}', but found EOF\")\n\n    def get_pxd_lines(self):\n        \"\"\"\n        calls self.parse() and processes the pxd annotations to pxd code lines.\n        \"\"\"\n\n        from datetime import datetime\n        year = datetime.now().year\n        yield (f\"# Copyright 2013-{year} the openage authors. \"\n               \"See copying.md for legal info.\")\n\n        yield \"\"\n\n        yield (\"# Auto-generated from annotations in \" +\n               self.filename.name)\n\n        yield \"# \" + str(self.filename)\n\n        self.parse()\n\n        # namespace of the previous pxd annotation\n        previous_namespace = None\n\n        for namespace, annotation_lines in self.annotations:\n            yield \"\"\n\n            if namespace != previous_namespace:\n                yield \"\"\n\n            if namespace:\n                prefix = \"    \"\n\n                if namespace != previous_namespace:\n                    yield (\n                        \"cdef extern \"\n                        \"from r\\\"\" + self.filename.as_posix() + \"\\\" \"\n                        \"namespace \\\"\" + namespace + \"\\\" \"\n                        \"nogil\"\n                        \":\"\n                    )\n            else:\n                prefix = \"\"\n\n            for annotation in annotation_lines:\n                annotation = self.postprocess_annotation_line(annotation)\n                if annotation:\n                    yield prefix + annotation\n                else:\n                    # don't emit a line that consists of just the prefix\n                    # (avoids trailing whitespace)\n                    yield \"\"\n\n            previous_namespace = namespace\n\n        yield \"\"\n\n    def postprocess_annotation_line(self, annotation):\n        \"\"\"\n        Post-processes each individual annotation line, applying hacks and\n        testing it, etc.\n\n        See libopenage/pyinterface/hacks.h for documentation on the individual\n        hacks.\n        \"\"\"\n        annotation = annotation.rstrip()\n        if annotation.endswith(';'):\n            self.warnings.append(\n                \"cython declaration ends in ';', \"\n                \"what have you done?\")\n\n        if annotation.endswith(')'):\n            self.warnings.append(\n                \"mark the function as 'except +' or \"\n                \"'noexcept':\\n\" + annotation)\n\n        elif annotation.endswith('noexcept'):\n            annotation = annotation[:-8].rstrip()\n\n        if 'cdef ' in annotation:\n            self.warnings.append(\n                \"there's no need to use 'cdef' in PXD annotations:\\n\" +\n                annotation)\n\n        return annotation\n\n    def generate(self, pxdfile, ignore_timestamps=False, print_warnings=True):\n        \"\"\"\n        reads the input file and writes the output file.\n        the output file is updated only if its content will change.\n\n        on parsing failure, raises ParserError.\n        \"\"\"\n        if not ignore_timestamps and os.path.exists(pxdfile):\n            # skip the file if the timestamp is up to date\n            if os.path.getmtime(self.filename) <= os.path.getmtime(pxdfile):\n                return False\n\n        result = \"\\n\".join(self.get_pxd_lines())\n\n        if os.path.exists(pxdfile):\n            with open(pxdfile, encoding='utf8') as outfile:\n                if outfile.read() == result:\n                    # don't write the file if the content is up to date\n                    return False\n\n        if not pxdfile.parent.is_dir():\n            pxdfile.parent.mkdir()\n\n        with pxdfile.open('w', encoding='utf8') as outfile:\n            if pxdfile.is_absolute():\n                printpath = pxdfile\n            else:\n                printpath = os.path.relpath(pxdfile, CWD)\n            print(f\"\\x1b[36mpxdgen: generate {printpath}\\x1b[0m\")\n\n            outfile.write(result)\n\n        if print_warnings and self.warnings:\n            print(f\"\\x1b[33;1mWARNING\\x1b[m pxdgen[{self.filename}]:\")\n            for warning in self.warnings:\n                print(warning)\n\n        return True\n\n\ndef parse_args():\n    \"\"\"\n    pxdgen command-line interface.\n\n    designed to allow both manual and automatic (via CMake) usage.\n    \"\"\"\n    cli = argparse.ArgumentParser()\n\n    cli.add_argument('files', nargs='*', metavar='HEADERFILE',\n                     help=\"input files (usually cpp .h files).\")\n    cli.add_argument('--file-list',\n                     help=(\"a file containing a semicolon-separated \"\n                           \"list of input files.\"))\n    cli.add_argument('--ignore-timestamps', action='store_true',\n                     help=(\"force generating even if the output file is already\"\n                           \" up to date\"))\n    cli.add_argument('--output-dir',\n                     help=(\"build directory corresponding to the CWD to write\"\n                           \" the generated file(s) in.\"))\n    cli.add_argument('-v', '--verbose', action=\"store_true\",\n                     help=\"increase logging verbosity\")\n\n    args = cli.parse_args()\n\n    if args.file_list:\n        with open(args.file_list, encoding='utf8') as flist:\n            file_list = flist.read().strip().split(';')\n    else:\n        file_list = []\n\n    from itertools import chain\n    args.all_files = list(chain(args.files, file_list))\n\n    return args\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    args = parse_args()\n    cppname = \"libopenage\"\n    cppdir = Path(cppname).absolute()\n    out_cppdir = Path(args.output_dir) / cppname\n\n    if args.verbose:\n        hdr_count = len(args.all_files)\n        plural = \"s\" if hdr_count > 1 else \"\"\n\n        print(f\"extracting pxd information from {hdr_count} header{plural}...\")\n\n    for filename in args.all_files:\n        filename = Path(filename).resolve()\n        if cppdir not in filename.parents:\n            print(f\"pxdgen source file is not in {cppdir!r}: {filename!r}\")\n            sys.exit(1)\n\n        # join out_cppdir with relative path from cppdir\n        pxdfile_relpath = filename.with_suffix('.pxd').relative_to(cppdir)\n        pxdfile = out_cppdir / pxdfile_relpath\n\n        if args.verbose:\n            print(f\"creating '{pxdfile}' for '{filename}':\")\n\n        generator = PXDGenerator(filename)\n\n        result = generator.generate(\n            pxdfile,\n            ignore_timestamps=args.ignore_timestamps,\n            print_warnings=True\n        )\n\n        if args.verbose and not result:\n            print(\"nothing done.\")\n\n        # create empty __init__.py in all parent directories.\n        # Cython requires this; else it won't find the .pxd files.\n        for dirname in pxdfile_relpath.parents:\n            template = out_cppdir / dirname / \"__init__\"\n            for extension in (\"py\", \"pxd\"):\n                initfile = template.with_suffix(\".\" + extension)\n                if not initfile.exists():\n                    print(\"\\x1b[36mpxdgen: create package index \"\n                          f\"{initfile.relative_to(args.output_dir)}\\x1b[0m\")\n\n                    initfile.touch()\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "buildsystem/python.cmake",
    "content": "# Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n# provides macros for defining python extension modules and pxdgen sources.\n# and a 'finalize' function that must be called in the end.\n\nfunction(python_init)\n\t# filled by pxdgen; written to bin/py/pxdgen_sources for use by pxdgen.py.\n\tset_property(GLOBAL PROPERTY SFT_PXDGEN_SOURCES)\n\n\t# filled by add_cython_modules. used by cythonize.py.\n\tset_property(GLOBAL PROPERTY SFT_CYTHON_MODULES)\n\tset_property(GLOBAL PROPERTY SFT_CYTHON_MODULES_EMBED)\n\n\t# list of pxd files added manually\n\t# filled by add_pxds only. for use as depends list for cythonize.py.\n\tset_property(GLOBAL PROPERTY SFT_PXD_FILES)\n\n\t# filled by pxdgen. for use as output list for the pxdgen target.\n\tset_property(GLOBAL PROPERTY SFT_GENERATED_PXD_FILES)\n\n\t# filled with all .py filenames; used for installing.\n\tset_property(GLOBAL PROPERTY SFT_PY_FILES)\n\n\t# filled with the relative source dirs of the above py files.\n\tset_property(GLOBAL PROPERTY SFT_PY_FILES_RELSRCDIRS)\n\n\t# filled with all .py filenames that should not be installed.\n\tset_property(GLOBAL PROPERTY SFT_PY_FILES_NOINSTALL)\n\n\t# filled with all .py filenames that are installed to the elf binary directory.\n\tset_property(GLOBAL PROPERTY SFT_PY_FILES_BININSTALL)\n\n\t# filled with replacement names that are used for installing each entry in SFT_PY_FILES\n\t# special value __nope is used if no replacement should be done.\n\t# thank you cmake for not providing a dict\n\t# where i could just look the original name up to get the replacement name.\n\tset_property(GLOBAL PROPERTY SFT_PY_FILES_INSTALLNAMES)\n\n\t# filled with all cython module target names; used for in-place creation.\n\tset_property(GLOBAL PROPERTY SFT_CYTHON_MODULE_TARGETS)\nendfunction()\n\n\nfunction(cython_get_target_name RESULT_VAR)\n\tfile(RELATIVE_PATH REL_CURRENT_SOURCE_DIR\n\t\t\"${CMAKE_SOURCE_DIR}\"\n\t\t\"${CMAKE_CURRENT_SOURCE_DIR}\")\n\n\tget_filename_component(OUTPUTNAME \"${source}\" NAME_WE)\n\n\tset(TARGETNAME \"${REL_CURRENT_SOURCE_DIR}/${OUTPUTNAME}\")\n\tstring(REPLACE \"/\" \"_\" TARGETNAME \"${TARGETNAME}\")\n\n\tset(\"${RESULT_VAR}\" \"${TARGETNAME}\" PARENT_SCOPE)\nendfunction()\n\n\nfunction(cython_modname_sanitize RESULT_VAR SOURCE)\n\tif(NOT IS_ABSOLUTE \"${SOURCE}\")\n\t\tset(SOURCE \"${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE}\")\n\tendif()\n\n\tif(NOT \"${SOURCE}\" MATCHES \".*\\\\.pyx?$\")\n\t\tmessage(FATAL_ERROR \"non-.py/.pyx file given: ${SOURCE}\")\n\tendif()\n\n\tset(\"${RESULT_VAR}\" \"${SOURCE}\" PARENT_SCOPE)\nendfunction()\n\n\nfunction(add_cython_modules)\n\t# adds a new module for cython compilation, by relative filename.\n\t# synoposis:\n\t# add_cython_modules(\n\t#    test.pyx\n\t#    EMBED __main__.pyx\n\t#    STANDALONE EMBED foo/bar.pyx\n\t#    NOINSTALL STANDALONE foo/test.pyx\n\t# )\n\t#\n\t# test.pyx is compiled to a shared library linked against PYEXT_LINK_LIBRARY\n\t# __main__.pyx is compiled to a executable with embedded python interpreter,\n\t# linked against libpython and PYEXT_LINK_LIBRARY.\n\t# foo/bar.pyx is compiled to a executable with embedded python interpreter,\n\t# linked only against libpython.\n\t# foo/test.pyx is compiled to a shared library linked against nothing, and will\n\t# not be installed.\n\t#\n\t# PYEXT_LINK_LIBRARY is linked to all non-standalone cython modules,\n\t# and is used e.g. to inject compiler options\n\n\tfile(RELATIVE_PATH REL_CURRENT_SOURCE_DIR\n\t\t\"${CMAKE_SOURCE_DIR}\"\n\t\t\"${CMAKE_CURRENT_SOURCE_DIR}\")\n\n\tset(EMBED_NEXT FALSE)\n\tset(STANDALONE_NEXT FALSE)\n\tset(NOINSTALL_NEXT FALSE)\n\tforeach(source ${ARGN})\n\t\tif(source STREQUAL \"EMBED\")\n\t\t\tset(EMBED_NEXT TRUE)\n\t\telseif(source STREQUAL \"STANDALONE\")\n\t\t\tset(STANDALONE_NEXT TRUE)\n\t\telseif(source STREQUAL \"NOINSTALL\")\n\t\t\tset(NOINSTALL_NEXT TRUE)\n\t\telse()\n\t\t\tcython_modname_sanitize(source \"${source}\")\n\n\t\t\t# construct some hopefully unique target name\n\t\t\tcython_get_target_name(TARGETNAME \"${source}\")\n\n\t\t\tget_filename_component(OUTPUTNAME \"${source}\" NAME_WE)\n\t\t\tset(CPPNAME \"${CMAKE_CURRENT_BINARY_DIR}/${OUTPUTNAME}.cpp\")\n\t\t\tset_source_files_properties(\"${CPPNAME}\" PROPERTIES GENERATED ON)\n\n\t\t\t# generate the pretty module name\n\t\t\tset(PRETTY_MODULE_NAME \"${REL_CURRENT_SOURCE_DIR}/${OUTPUTNAME}\")\n\t\t\tstring(REPLACE \"/\" \".\" PRETTY_MODULE_NAME \"${PRETTY_MODULE_NAME}\")\n\t\t\tstring(REGEX REPLACE \"^\\\\.\" \"\" PRETTY_MODULE_NAME \"${PRETTY_MODULE_NAME}\")\n\t\t\tset(PRETTY_MODULE_PROPERTIES \"\")\n\n\t\t\tif(EMBED_NEXT)\n\t\t\t\tset(PRETTY_MODULE_PROPERTIES \"${PRETTY_MODULE_PROPERTIES} [embedded interpreter]\")\n\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_CYTHON_MODULES_EMBED \"${source}\")\n\t\t\t\tadd_executable(\"${TARGETNAME}\" \"${CPPNAME}\")\n\n\t\t\t\tif(MINGW)\n\t\t\t\t\tset_target_properties(\"${TARGETNAME}\" PROPERTIES LINK_FLAGS \"-municode\")\n\t\t\t\tendif()\n\n\t\t\t\ttarget_link_libraries(\"${TARGETNAME}\" PRIVATE ${PYEXT_LIBRARY})\n\t\t\telse()\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_CYTHON_MODULES \"${source}\")\n\t\t\t\tadd_library(\"${TARGETNAME}\" MODULE \"${CPPNAME}\")\n\n\t\t\t\tset_target_properties(\"${TARGETNAME}\" PROPERTIES\n\t\t\t\t\tPREFIX \"\"\n\t\t\t\t\tSUFFIX \"${PYEXT_SUFFIX}\"\n\t\t\t\t)\n\n\t\t\t\tif(WIN32)\n\t\t\t\t\ttarget_link_libraries(\"${TARGETNAME}\" PRIVATE ${PYEXT_LIBRARY})\n\t\t\t\tendif()\n\t\t\tendif()\n\n\t\t\tif(NOINSTALL_NEXT)\n\t\t\t\tset(PRETTY_MODULE_PROPERTIES \"${PRETTY_MODULE_PROPERTIES} [noinstall]\")\n\t\t\telse()\n\t\t\t\tinstall(\n\t\t\t\t\tTARGETS \"${TARGETNAME}\"\n\t\t\t\t\tDESTINATION \"${CMAKE_PY_INSTALL_PREFIX}/${REL_CURRENT_SOURCE_DIR}\"\n\t\t\t\t)\n\t\t\tendif()\n\n\t\t\tset_target_properties(\"${TARGETNAME}\" PROPERTIES\n\t\t\t\tCOMPILE_FLAGS \"${PYEXT_CXXFLAGS}\"\n\t\t\t\tINCLUDE_DIRECTORIES \"${PYEXT_INCLUDE_DIRS}\"\n\t\t\t\tOUTPUT_NAME \"${OUTPUTNAME}\"\n\t\t\t)\n\n\t\t\tif (STANDALONE_NEXT)\n\t\t\t\tset(PRETTY_MODULE_PROPERTIES \"${PRETTY_MODULE_PROPERTIES} [standalone]\")\n\t\t\telse()\n\t\t\t\tset_target_properties(\"${TARGETNAME}\" PROPERTIES LINK_DEPENDS_NO_SHARED 1)\n\t\t\t\ttarget_link_libraries(\"${TARGETNAME}\" PRIVATE \"${PYEXT_LINK_LIBRARY}\")\n\t\t\tendif()\n\n\t\t\t# Since this module is not embedded with python interpreter,\n\t\t\t# Mac OS X requires a link flag to resolve undefined symbols\n\t\t\tif(NOT EMBED_NEXT AND APPLE)\n\t\t\t\tset_target_properties(\"${TARGETNAME}\" PROPERTIES LINK_FLAGS \"-undefined dynamic_lookup\" )\n\t\t\tendif()\n\n\t\t\tadd_dependencies(\"${TARGETNAME}\" cythonize)\n\n\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_CYTHON_MODULE_TARGETS \"${TARGETNAME}\")\n\n\t\t\tpretty_print_target(\"cython module\" \"${PRETTY_MODULE_NAME}\" \"${PRETTY_MODULE_PROPERTIES}\")\n\n\t\t\t# Reset the flags before processing the next cython module\n\t\t\tset(EMBED_NEXT FALSE)\n\t\t\tset(NOINSTALL_NEXT FALSE)\n\t\t\tset(STANDALONE_NEXT FALSE)\n\t\tendif()\n\tendforeach()\nendfunction()\n\n\nfunction(pyext_link_libraries SOURCE)\n\t# link a cython module to some libraries\n\t#\n\t# synoposis:\n\t# pyext_link_libraries(\n\t#    somemodule.pyx\n\t#    PNG::PNG\n\t#    ${OPUS_LIBRARIES}\n\t# )\n\tcython_modname_sanitize(source \"${SOURCE}\")\n\tcython_get_target_name(TARGETNAME \"${SOURCE}\")\n\n\tforeach(library ${ARGN})\n\t\ttarget_link_libraries(\"${TARGETNAME}\" PRIVATE \"${library}\")\n\tendforeach()\nendfunction()\n\n\nfunction(pyext_include_directories SOURCE)\n\t# build a cython module with given include dirs\n\t#\n\t# synoposis:\n\t# pyext_include_directories(\n\t#    somemodule.pyx\n\t#    ${OPUS_INCLUDE_DIRS}\n\t# )\n\tcython_modname_sanitize(source \"${SOURCE}\")\n\tcython_get_target_name(TARGETNAME \"${SOURCE}\")\n\n\tforeach(directory ${ARGN})\n\t\ttarget_include_directories(\"${TARGETNAME}\" PRIVATE \"${directory}\")\n\tendforeach()\nendfunction()\n\n\nfunction(pxdgen)\n\t# add a C++ header file as pxdgen source file\n\tforeach(source ${ARGN})\n\t\tif(NOT IS_ABSOLUTE \"${source}\")\n\t\t\tset(source \"${CMAKE_CURRENT_SOURCE_DIR}/${source}\")\n\t\tendif()\n\n\t\tget_filename_component(source_ext \"${source}\" EXT)\n\t\tif(NOT \"${source_ext}\" STREQUAL \".h\")\n\t\t\tmessage(FATAL_ERROR \"non-.h file given to pxdgen: ${source}\")\n\t\tendif()\n\n\t\t# TODO: change if multiple files with the same name are supported in a directory\n\t\tget_filename_component(source_name_without_ext \"${source}\" NAME_WE)\n\t\tset(PXDNAME \"${CMAKE_CURRENT_BINARY_DIR}/${source_name_without_ext}.pxd\")\n\t\tset_source_files_properties(\"${PXDNAME}\" PROPERTIES GENERATED ON)\n\n\t\tset_property(GLOBAL APPEND PROPERTY SFT_PXDGEN_SOURCES \"${source}\")\n\t\tset_property(GLOBAL APPEND PROPERTY SFT_GENERATED_PXD_FILES \"${PXDNAME}\")\n\tendforeach()\nendfunction()\n\n\nfunction(add_pxds)\n\t# add a .pxd or other additional Cython source\n\tforeach(source ${ARGN})\n\t\tif(NOT IS_ABSOLUTE \"${source}\")\n\t\t\tset(source \"${CMAKE_CURRENT_SOURCE_DIR}/${source}\")\n\t\tendif()\n\n\t\tif(NOT \"${source}\" MATCHES \".*\\\\.px[id]$\")\n\t\t\tmessage(FATAL_ERROR \"non-pxd/pxi file given to add_pxds: ${source}\")\n\t\tendif()\n\n\t\tset_property(GLOBAL APPEND PROPERTY SFT_PXD_FILES \"${source}\")\n\tendforeach()\nendfunction()\n\n\nfunction(add_py_modules)\n\t# add a .py file for installing\n\t#\n\t# add_py_module (\n\t#    somefile.py\n\t#    NOINSTALL otherfile.py          # do not install this file\n\t#    BININSTALL wtf.py               # install to $PREFIX/bin/wtf.py\n\t#    BININSTALL lol.py AS rofl       # install lol.py to $PREFIX/bin/rofl\n\t# )\n\t#\n\t# because cmake is such a nice language,\n\t# the handling of the above is quite painful.\n\n\tfile(RELATIVE_PATH REL_CURRENT_SOURCE_DIR \"${CMAKE_SOURCE_DIR}\" \"${CMAKE_CURRENT_SOURCE_DIR}\")\n\n\tset(NOINSTALL_NEXT FALSE)\n\tset(BININSTALL_NEXT FALSE)\n\tset(AS_NEXT FALSE)\n\tset(IN_BININSTALL FALSE)\n\tforeach(source ${ARGN})\n\t\t# keyword parsing so the next arg knows\n\t\t# what the previous keyword was.\n\t\tif(source STREQUAL \"NOINSTALL\")\n\t\t\tset(NOINSTALL_NEXT TRUE)\n\t\telseif(source STREQUAL \"BININSTALL\")\n\t\t\tset(BININSTALL_NEXT TRUE)\n\t\telseif(source STREQUAL \"AS\")\n\t\t\tset(AS_NEXT TRUE)\n\t\telse()\n\t\t\tif(NOT ${source} MATCHES \".*\\\\.py$\" AND NOT AS_NEXT)\n\t\t\t\tmessage(FATAL_ERROR \"non-Python file given to add_py_modules: ${source}\")\n\t\t\tendif()\n\n\t\t\tif(IN_BININSTALL AND NOT AS_NEXT)\n\t\t\t\t# if after the source file for /bin installation no AS\n\t\t\t\t# follows, normal source listings follow.\n\t\t\t\tset(IN_BININSTALL FALSE)\n\t\t\tendif()\n\n\t\t\tif(NOT IS_ABSOLUTE \"${source}\" AND NOT AS_NEXT)\n\t\t\t\t# make the source absolute, except when source is the \"AS $replacementname\"\n\t\t\t\tset(source \"${CMAKE_CURRENT_SOURCE_DIR}/${source}\")\n\t\t\tendif()\n\n\t\t\tif(NOINSTALL_NEXT OR BININSTALL_NEXT)\n\t\t\t\t# if source should not be installed or be installed to bin/\n\t\t\t\t# it is excluded from the big python installation list\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES_NOINSTALL \"${source}\")\n\t\t\t\tset(NOINSTALL_NEXT FALSE)\n\t\t\tendif()\n\n\t\t\tif(BININSTALL_NEXT)\n\t\t\t\t# if the file is to be installed to bin/\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES_BININSTALL \"${source}\")\n\t\t\t\tset(BININSTALL_NEXT FALSE)\n\t\t\t\tset(IN_BININSTALL TRUE)\n\t\t\tendif()\n\n\t\t\tif(AS_NEXT)\n\t\t\t\t# the replacement name must be after the BININSTALL definition\n\t\t\t\tif(NOT IN_BININSTALL)\n\t\t\t\t\tmessage(FATAL_ERROR \"you used AS without BININSTALL!\")\n\t\t\t\tendif()\n\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES_INSTALLNAMES \"${source}\")\n\t\t\telse()\n\t\t\t\t# in all cases except the \"AS $newname\",\n\t\t\t\t# add the python file to the to-compile list.\n\n\t\t\t\tif(\"${REL_CURRENT_SOURCE_DIR}\" STREQUAL \"\")\n\t\t\t\t\tset(REL_CURRENT_SOURCE_DIR \"./\")\n\t\t\t\tendif()\n\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES \"${source}\")\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES_RELSRCDIRS \"${REL_CURRENT_SOURCE_DIR}\")\n\t\t\tendif()\n\n\t\t\tif(NOT IN_BININSTALL)\n\t\t\t\t# don't replace the install name if we're in the \"AS $name\"\n\t\t\t\t# or no AS followed the BININSTALL\n\n\t\t\t\tset_property(GLOBAL APPEND PROPERTY SFT_PY_FILES_INSTALLNAMES \"__nope\")\n\t\t\tendif()\n\n\t\t\tif(AS_NEXT)\n\t\t\t\t# reset the alias and bin-install flags\n\t\t\t\t# so they count only for one (the current) source file.\n\t\t\t\tset(AS_NEXT FALSE)\n\t\t\t\tset(IN_BININSTALL FALSE)\n\t\t\tendif()\n\t\tendif()\n\tendforeach()\nendfunction()\n\n\nfunction(python_finalize)\n\t# pxdgen (.h -> .pxd)\n\n\tget_property(pxdgen_sources GLOBAL PROPERTY SFT_PXDGEN_SOURCES)\n\twrite_on_change(\"${CMAKE_BINARY_DIR}/py/pxdgen_sources\" \"${pxdgen_sources}\")\n\tset(PXDGEN_TIMEFILE \"${CMAKE_BINARY_DIR}/py/pxdgen_timefile\")\n\tadd_custom_command(OUTPUT \"${PXDGEN_TIMEFILE}\"\n\t\tCOMMAND \"${PYTHON}\" -m buildsystem.pxdgen\n\t\t--file-list \"${CMAKE_BINARY_DIR}/py/pxdgen_sources\"\n\t\t--output-dir \"${CMAKE_BINARY_DIR}\"\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E touch \"${PXDGEN_TIMEFILE}\"\n\t\tDEPENDS ${pxdgen_sources} \"${CMAKE_BINARY_DIR}/py/pxdgen_sources\"\n\t\tCOMMENT \"pxdgen: generating .pxd files from headers\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t)\n\tadd_custom_target(pxdgen ALL DEPENDS \"${PXDGEN_TIMEFILE}\")\n\n\n\t# cythonize (.pyx -> .cpp)\n\n\tget_property(cython_modules GLOBAL PROPERTY SFT_CYTHON_MODULES)\n\twrite_on_change(\"${CMAKE_BINARY_DIR}/py/cython_modules\" \"${cython_modules}\")\n\tget_property(cython_modules_embed GLOBAL PROPERTY SFT_CYTHON_MODULES_EMBED)\n\twrite_on_change(\"${CMAKE_BINARY_DIR}/py/cython_modules_embed\" \"${cython_modules_embed}\")\n\tget_property(pxd_list GLOBAL PROPERTY SFT_PXD_FILES)\n\tget_property(generated_pxd_list GLOBAL PROPERTY SFT_GENERATED_PXD_FILES)\n\twrite_on_change(\"${CMAKE_BINARY_DIR}/py/pxd_list\" \"${pxd_list};${generated_pxd_list}\")\n\tset(CYTHONIZE_TIMEFILE \"${CMAKE_BINARY_DIR}/py/cythonize_timefile\")\n\tadd_custom_command(OUTPUT \"${CYTHONIZE_TIMEFILE}\"\n\t\t# remove unneeded files from the previous builds (if any)\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E remove -f\n\t\t\t\"${CMAKE_BINARY_DIR}/__init__.py\"\n\t\t\t\"${CMAKE_BINARY_DIR}/__init__.pxd\"\n\t\tCOMMAND \"${PYTHON}\" -m buildsystem.cythonize\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules\"\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules_embed\"\n\t\t\"${CMAKE_BINARY_DIR}/py/pxd_list\"\n\t\t${force_optimized_lib_flag}\n\t\t\"--build-dir\" \"${CMAKE_BINARY_DIR}\"\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E touch \"${CYTHONIZE_TIMEFILE}\"\n\t\tDEPENDS\n\t\t\"${PXDGEN_TIMEFILE}\"\n\t\t${cython_modules}\n\t\t${cython_modules_embed}\n\t\t${pxd_list}\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules\"\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules_embed\"\n\t\t\"${CMAKE_BINARY_DIR}/py/pxd_list\"\n\t\tCOMMENT \"cythonize.py: compiling .pyx files to .cpp\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t)\n\tadd_custom_target(cythonize ALL DEPENDS \"${CYTHONIZE_TIMEFILE}\")\n\n\n\t# py compile (.py -> .pyc)\n\n\tget_property(py_files GLOBAL PROPERTY SFT_PY_FILES)\n\tget_property(py_files_srcdirs GLOBAL PROPERTY SFT_PY_FILES_RELSRCDIRS)\n\tget_property(py_files_noinstall GLOBAL PROPERTY SFT_PY_FILES_NOINSTALL)\n\tget_property(py_files_bininstall GLOBAL PROPERTY SFT_PY_FILES_BININSTALL)\n\tget_property(py_install_names GLOBAL PROPERTY SFT_PY_FILES_INSTALLNAMES)\n\n\twrite_on_change(\"${CMAKE_BINARY_DIR}/py/py_files\" \"${py_files}\")\n\tset(COMPILEPY_TIMEFILE \"${CMAKE_BINARY_DIR}/py/compilepy_timefile\")\n\tset(COMPILEPY_INVOCATION\n\t\t\"${PYTHON}\" -m buildsystem.compilepy\n\t\t\"${CMAKE_BINARY_DIR}/py/py_files\"\n\t\t\"${CMAKE_SOURCE_DIR}\"\n\t\t\"${CMAKE_BINARY_DIR}\"\n\t)\n\n\t# determine the compiled file name for all source files\n\texecute_process(COMMAND\n\t\t${COMPILEPY_INVOCATION} \"--print-output-paths-only\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t\tOUTPUT_VARIABLE py_compiled_files\n\t\tRESULT_VARIABLE COMMAND_RESULT\n\t)\n\tif(NOT ${COMMAND_RESULT} EQUAL 0)\n\t\tmessage(FATAL_ERROR \"failed to get output list from compilepy invocation\")\n\tendif()\n\tstring(STRIP \"${py_compiled_files}\" py_compiled_files)\n\n\tadd_custom_command(OUTPUT \"${COMPILEPY_TIMEFILE}\"\n\t\tCOMMAND ${COMPILEPY_INVOCATION}\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E touch \"${COMPILEPY_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t\tDEPENDS \"${CMAKE_BINARY_DIR}/py/py_files\" ${py_files}\n\t\tCOMMENT \"compiling .py files to .pyc files\"\n\t)\n\tadd_custom_target(compilepy ALL DEPENDS \"${COMPILEPY_TIMEFILE}\")\n\n\tlist(LENGTH py_files py_files_count)\n\tmath(EXPR py_files_count_range \"${py_files_count} - 1\")\n\n\tforeach(idx RANGE ${py_files_count_range})\n\t\tlist(GET py_files ${idx} pyfile)\n\t\tlist(GET py_compiled_files ${idx} pycfile)\n\t\tlist(GET py_files_srcdirs ${idx} pyrelsrcdir)\n\t\tlist(GET py_install_names ${idx} pyinstallname)\n\t\tfile(TO_CMAKE_PATH ${pycfile} pycfile)\n\n\t\tlist(FIND py_files_noinstall \"${pyfile}\" do_install)\n\t\tlist(FIND py_files_bininstall \"${pyfile}\" do_bininstall)\n\n\t\tif(do_install LESS 0)\n\t\t\tinstall(\n\t\t\t\tFILES \"${pyfile}\"\n\t\t\t\tDESTINATION  \"${CMAKE_PY_INSTALL_PREFIX}/${pyrelsrcdir}\"\n\t\t\t)\n\t\t\tinstall(\n\t\t\t\tFILES \"${pycfile}\"\n\t\t\t\tDESTINATION \"${CMAKE_PY_INSTALL_PREFIX}/${pyrelsrcdir}/__pycache__\"\n\t\t\t)\n\t\telseif(do_bininstall GREATER -1)\n\t\t\t# install the python file to the elf binary dir\n\n\t\t\tif(\"${pyinstallname}\" STREQUAL \"__nope\")\n\t\t\t\tinstall(\n\t\t\t\t\tPROGRAMS \"${pyfile}\"\n\t\t\t\t\tDESTINATION \"${CMAKE_INSTALL_BINDIR}\"\n\t\t\t\t)\n\t\t\telse()\n\t\t\t\t# optionally, rename the file:\n\t\t\t\tinstall(\n\t\t\t\t\tPROGRAMS \"${pyfile}\"\n\t\t\t\t\tRENAME \"${pyinstallname}\"\n\t\t\t\t\tDESTINATION \"${CMAKE_INSTALL_BINDIR}\"\n\t\t\t\t)\n\t\t\tendif()\n\t\tendif()\n\tendforeach()\n\n\t##################################################\n\t# code generation\n\n\tcodegen_run()\n\n\t# inplace module install (bin/module.so -> module.so)\n\t# MSVC, Xcode (multi-config generators) produce outputs\n\t# in a directory different from `CMAKE_CURRENT_BINARY_DIR`.\n\t# link/copy the output files as required for the cython modules.\n\n\tget_property(cython_module_targets GLOBAL PROPERTY SFT_CYTHON_MODULE_TARGETS)\n\tset(cython_module_files_expr)\n\tforeach(cython_module_target ${cython_module_targets})\n\t\tlist(APPEND cython_module_files_expr \"$<TARGET_FILE:${cython_module_target}>\")\n\tendforeach()\n\n\tset(INPLACEMODULES_LISTFILE \"${CMAKE_BINARY_DIR}/py/inplace_module_list_$<CONFIG>\")\n\tfile(GENERATE\n\t\tOUTPUT ${INPLACEMODULES_LISTFILE}\n\t\tCONTENT \"${cython_module_files_expr}\"\n\t)\n\tset(INPLACEMODULES_INVOCATION\n\t\t\"${PYTHON}\" -m buildsystem.inplacemodules\n\t\t${INPLACEMODULES_LISTFILE}\n\t\t\"$<CONFIG>\"\n\t)\n\tset(INPLACEMODULES_TIMEFILE \"${CMAKE_BINARY_DIR}/py/inplacemodules_timefile\")\n\tadd_custom_command(OUTPUT \"${INPLACEMODULES_TIMEFILE}\"\n\t\tCOMMAND ${INPLACEMODULES_INVOCATION}\n\t\tDEPENDS\n\t\t${INPLACEMODULES_LISTFILE} ${cython_module_targets}\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E touch \"${INPLACEMODULES_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t\tCOMMENT \"creating in-place modules\"\n\t)\n\tadd_custom_target(inplacemodules ALL DEPENDS \"${INPLACEMODULES_TIMEFILE}\")\n\n\n\t# cleaning of all in-sourcedir stuff\n\n\tadd_custom_target(cleancython\n\t\tCOMMAND ${INPLACEMODULES_INVOCATION} --clean\n\t\tCOMMAND \"${PYTHON}\" -m buildsystem.cythonize --clean\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules\"\n\t\t\"${CMAKE_BINARY_DIR}/py/cython_modules_embed\"\n\t\t\"${CMAKE_BINARY_DIR}/py/pxd_list\"\n\t\t\"--build-dir\" \"${CMAKE_BINARY_DIR}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t)\n\tadd_custom_command(TARGET cleancython POST_BUILD\n\t\t# general deleters to catch files that have already been un-listed.\n\t\tCOMMAND find openage -name \"'*.cpp'\" -type f -print -delete\n\t\tCOMMAND find openage -name \"'*.html'\" -type f -print -delete\n\t\tCOMMAND find openage -name \"'*.so'\" -type f -print -delete\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E remove \"${CYTHONIZE_TIMEFILE}\"\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E remove \"${INPLACEMODULES_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t)\n\n\tadd_custom_target(cleanpxdgen\n\t\tCOMMAND find libopenage -name \"'*.pxd'\" -type f -print -delete\n\t\tCOMMAND find libopenage -name \"__init__.py\" -type f -print -delete\n\t\tCOMMAND \"${CMAKE_COMMAND}\" -E remove \"${PXDGEN_TIMEFILE}\"\n\t\tWORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n\t)\n\n\n\t# check for any unlisted .py files, and error.\n\n\texecute_process(\n\t\tCOMMAND \"${PYTHON}\" -m buildsystem.check_py_file_list\n\t\t\"${CMAKE_BINARY_DIR}/py/py_files\"\n\t\t\"${CMAKE_SOURCE_DIR}/openage\"\n\t\tWORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n\t\tRESULT_VARIABLE res\n\t)\n\tif(NOT res EQUAL 0)\n\t\tmessage(FATAL_ERROR \".py file listing inconsistent\")\n\tendif()\nendfunction()\n\npython_init()\n"
  },
  {
    "path": "buildsystem/scripts/EmbedFont.cmake",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\nif(NOT forward_variables)\n\tmessage(FATAL_ERROR \"CMake configuration variables not available. Please include(ForwardVariables.cmake)\")\nendif()\n\ninclude(\"${buildsystem_dir}/modules/DownloadCache.cmake\")\nset(dejavu_dir \"${downloads_dir}/DejaVu\")\nset(dejavu_zip \"dejavu-fonts-ttf-2.37.tar.bz2\")\nset(dejavu_zip_download_path \"${dejavu_dir}/${dejavu_zip}\")\n# TODO: disable downloading for CI builds. <Need CI first>\ndownload_cache(\n\t\"https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/${dejavu_zip}\"\n\t\"${dejavu_zip_download_path}\"\n)\n\nexecute_process(COMMAND\n\t\"${CMAKE_COMMAND}\" -E tar xf \"${dejavu_zip_download_path}\"\n\tWORKING_DIRECTORY \"${dejavu_dir}\"\n)\n\nfile(GLOB_RECURSE dejavu_fonts \"${dejavu_dir}/DejaVuSerif*.ttf\")\nfile(COPY ${dejavu_fonts} DESTINATION \"${CMAKE_INSTALL_PREFIX}/share/fonts\")\n\nfile(GLOB_RECURSE dejavu_conf \"${dejavu_dir}/*dejavu-serif.conf\")\nfile(COPY ${dejavu_conf} DESTINATION \"${CMAKE_INSTALL_PREFIX}/${py_install_prefix}/fonts/conf.d\")\n"
  },
  {
    "path": "buildsystem/scripts/EmbedPython.cmake",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\nif(NOT forward_variables)\n\tmessage(FATAL_ERROR \"CMake configuration variables not available. Please include(ForwardVariables.cmake)\")\nendif()\n\ninclude(\"${buildsystem_dir}/modules/DownloadCache.cmake\")\nif(sizeof_void_p EQUAL 8)\n\tset(py_arch \"amd64\")\nelse()\n\tset(py_arch \"win32\")\nendif()\nset(python_embed_zip \"python-${py_version}-embed-${py_arch}.zip\")\nset(python_zip_download_path \"${downloads_dir}/${python_embed_zip}\")\n# TODO: disable downloading for CI builds. <Need CI first>\ndownload_cache(\n\t\"https://www.python.org/ftp/python/${py_version}/${python_embed_zip}\"\n\t\"${python_zip_download_path}\"\n)\n\nexecute_process(COMMAND\n\t\"${CMAKE_COMMAND}\" -E tar xf \"${python_zip_download_path}\"\n\tWORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/${py_install_prefix}\"\n)\n\nexecute_process(COMMAND\n\t\"${PYTHON}\" \"${buildsystem_dir}/scripts/copy_modules.py\"\n\tnumpy PIL pyreadline3 readline\n\t\"${CMAKE_INSTALL_PREFIX}/${py_install_prefix}\"\n)\n"
  },
  {
    "path": "buildsystem/scripts/EmbedWinDependencies.cmake",
    "content": "# Copyright 2017-2020 the openage authors. See copying.md for legal info.\n\nif(NOT forward_variables)\n\tmessage(FATAL_ERROR \"CMake configuration variables not available. Please include(ForwardVariables.cmake)\")\nendif()\n\nget_filename_component(cl_directory \"${cmake_linker}\" DIRECTORY)\nfind_program(dumpbin dumpbin.exe PATHS \"${cl_directory}\")\nif(NOT dumpbin)\n\tmessage(SEND_ERROR \"Cannot locate dumpbin.\")\n\treturn()\nendif()\n\n# Paths for applocal and powershell\nset(APPLOCAL_SCRIPT \"${vcpkg_dir}/../../scripts/buildsystems/msbuild/applocal.ps1\")\nset(POWERSHELL_COMMAND \"$ENV{SYSTEMROOT}/System32/WindowsPowerShell/v1.0/powershell.exe\")\n\n# function to wrap the dependency resolving and copying logic on Windows.\n# Inspired by `applocal.ps1` from vcpkg which recursively descends into the\n# dependency tree of a binary and copies each to the target directory.\nfunction(resolve binary)\n\tget_filename_component(bin_dir \"${binary}\" DIRECTORY)\n\texecute_process(COMMAND \"${dumpbin}\" /DEPENDENTS \"${binary}\"\n\t\tRESULT_VARIABLE dump_result\n\t\tOUTPUT_VARIABLE dump_output\n\t)\n\tif(NOT dump_result EQUAL 0)\n\t\tmessage(SEND_ERROR \"dumpbin returned ${dump_result}.\")\n\t\treturn()\n\tendif()\n\tstring(REGEX MATCHALL \"    [^\\\\\\n]+\\\\.dll\" dll_list ${dump_output})\n\tforeach(item ${dll_list})\n\t\tstring(STRIP \"${item}\" dll_name)\n\t\tset(dll_src_path \"${vcpkg_dir}/bin/${dll_name}\")\n\t\tset(dll_dest_path \"${bin_dir}/${dll_name}\")\n\t\tif(NOT EXISTS \"${dll_dest_path}\" AND EXISTS \"${dll_src_path}\")\n\t\t\tmessage(STATUS \"Copying ${dll_name}\")\n\t\t\tfile(COPY \"${dll_src_path}\" DESTINATION \"${bin_dir}\")\n\t\t\tresolve(\"${dll_dest_path}\")\n\t\tendif()\n\tendforeach()\nendfunction()\n\n\n# windeployqt\nif(use_windeployqt AND windeployqt)\n\tforeach(file ${CMAKE_INSTALL_MANIFEST_FILES})\n\t\tif((file MATCHES \"\\\\.dll$\") AND NOT (\"${file}\" STREQUAL \"nyan.dll\"))\n\t\t\tresolve(\"${file}\")\n\t\t\tif(windeployqt)\n\t\t\t\tmessage(STATUS \"Calling windeployqt with ${file}.\")\n\t\t\t\texecute_process(COMMAND \"${CMAKE_COMMAND}\" -E env \"PATH=${vcpkg_dir}/bin;$ENV{PATH}\"\n\t\t\t\t\t\"${windeployqt}\"\n\t\t\t\t\t--qmldir \"${CMAKE_INSTALL_PREFIX}/${asset_dir}/qml\"\n\t\t\t\t\t--dir \"${CMAKE_INSTALL_PREFIX}/bin/qml\"\n\t\t\t\t\t--plugindir \"${CMAKE_INSTALL_PREFIX}/bin/plugins\"\n\t\t\t\t\t--libdir \"${CMAKE_INSTALL_PREFIX}/bin\"\n\t\t\t\t\t--list \"target\"\n\t\t\t\t\t--verbose 2\n\t\t\t\t\t\"${file}\"\n\t\t\t\t)\n\t\t\tendif()\n\t\tendif()\n\tendforeach()\nendif()\n\n# applocal\nif((EXISTS ${POWERSHELL_COMMAND}) AND (EXISTS ${APPLOCAL_SCRIPT}))\n\tforeach(file ${CMAKE_INSTALL_MANIFEST_FILES})\n\t\tif(file MATCHES \"\\\\.dll$\")\n\t\t\tmessage(STATUS \"Calling applocal.ps1 with ${file}.\")\n\t\t\texecute_process(COMMAND \"${POWERSHELL_COMMAND}\" \"${APPLOCAL_SCRIPT}\"\n\t\t\t\t\t-targetBinary \"${file}\"\n\t\t\t\t\t-installedDir \"${vcpkg_dir}/bin\"\n\t\t\t\t\t-tlogFile \"${LOGFILES_DIR}/applocal.log\"\n\t\t\t\t\t-copiedFilesLog \"${LOGFILES_DIR}/applocal_copied_files.log\"\n\t\t\t\t\tERROR_FILE \"${LOGFILES_DIR}/applocal_error.log\"\n\t\t\t\t\tOUTPUT_FILE \"${LOGFILES_DIR}/applocal_output.log\"\n\t\t\t)\n\t\tendif()\n\tendforeach()\nendif()\n\n# Copy qt.conf to python-directory\nfile(COPY \"${buildsystem_dir}/templates/qt.conf\"\n\tDESTINATION \"${CMAKE_INSTALL_PREFIX}/${py_install_prefix}\"\n)\n\n# The fonts directory defaults to the same directory as the executable.\n# In this case, it is python.exe at `py_install_prefix`.\nfile(COPY \"${vcpkg_dir}/tools/fontconfig/fonts\"\n\tDESTINATION \"${CMAKE_INSTALL_PREFIX}/${py_install_prefix}\"\n)\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/EmbedFont.cmake\")\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/EmbedPython.cmake\")\n"
  },
  {
    "path": "buildsystem/scripts/copy_modules.py",
    "content": "# Copyright 2017-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCopies the specified modules to the required directory.\nUsed for packaging the python dependencies.\n\"\"\"\n\nimport argparse\nimport importlib\nimport importlib.abc\nimport importlib.util\nimport os\nimport shutil\nimport sys\n\n\ndef copy_module(name, destination):\n    \"\"\"Copy the importable module 'name' to the 'destination' directory\"\"\"\n    loader = importlib.util.find_spec(name).loader\n    if not isinstance(loader, importlib.abc.FileLoader):\n        sys.exit(f\"Loader for module {name} is not handled\")\n\n    print(f'Copying \"{name}\" to \"{destination}\"')\n    filename = loader.get_filename(name)\n    if loader.is_package(name):\n        pkgdir, _ = os.path.split(filename)\n        shutil.copytree(pkgdir, os.path.join(destination, name))\n    else:\n        shutil.copy2(filename, destination)\n\n\ndef main():\n    \"\"\" CLI entry point \"\"\"\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"pymodule_name\", nargs='+', help=(\n        \"list of all modules that shall be copied\"\n    ))\n    cli.add_argument(\"dest_dir\", help=(\n        \"destination directory where modules will be copied\"\n    ))\n    args = cli.parse_args()\n    for module in args.pymodule_name:\n        copy_module(module, args.dest_dir)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "buildsystem/templates/Doxyfile.in",
    "content": "# Doxyfile 1.8.12\n\n# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) for a project.\n#\n# All text after a double hash (##) is considered a comment and is placed in\n# front of the TAG it is preceding.\n#\n# All text after a single hash (#) is considered a comment and will be ignored.\n# The format is:\n# TAG = value [value, ...]\n# For lists, items can also be appended using:\n# TAG += value [value, ...]\n# Values that contain spaces should be placed between quotes (\\\" \\\").\n\n#---------------------------------------------------------------------------\n# Project related configuration options\n#---------------------------------------------------------------------------\n\n# This tag specifies the encoding used for all characters in the config file\n# that follow. The default is UTF-8 which is also the encoding used for all text\n# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv\n# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv\n# for the list of possible encodings.\n# The default value is: UTF-8.\n\nDOXYFILE_ENCODING      = UTF-8\n\n# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by\n# double-quotes, unless you are using Doxywizard) that should identify the\n# project for which the documentation is generated. This name is used in the\n# title of most generated pages and in a few other places.\n# The default value is: My Project.\n\nPROJECT_NAME           = @PROJECT_NAME@\n\n# The PROJECT_NUMBER tag can be used to enter a project or revision number. This\n# could be handy for archiving the generated documentation or if some version\n# control system is used.\n\nPROJECT_NUMBER         = @PROJECT_VERSION@\n\n# Using the PROJECT_BRIEF tag one can provide an optional one line description\n# for a project that appears at the top of each page and should give viewer a\n# quick idea about the purpose of the project. Keep the description short.\n\nPROJECT_BRIEF          =\n\n# With the PROJECT_LOGO tag one can specify a logo or an icon that is included\n# in the documentation. The maximum height of the logo should not exceed 55\n# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy\n# the logo to the output directory.\n\nPROJECT_LOGO           = @CMAKE_SOURCE_DIR@/assets/logo/banner.png\n\n# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path\n# into which the generated documentation will be written. If a relative path is\n# entered, it will be relative to the location where doxygen was started. If\n# left blank the current directory will be used.\n\nOUTPUT_DIRECTORY       = doc\n\n# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-\n# directories (in 2 levels) under the output directory of each output format and\n# will distribute the generated files over these directories. Enabling this\n# option can be useful when feeding doxygen a huge amount of source files, where\n# putting all generated files in the same directory would otherwise causes\n# performance problems for the file system.\n# The default value is: NO.\n\nCREATE_SUBDIRS         = YES\n\n# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII\n# characters to appear in the names of generated files. If set to NO, non-ASCII\n# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode\n# U+3044.\n# The default value is: NO.\n\nALLOW_UNICODE_NAMES    = NO\n\n# The OUTPUT_LANGUAGE tag is used to specify the language in which all\n# documentation generated by doxygen is written. Doxygen will use this\n# information to generate all constant output in the proper language.\n# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,\n# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),\n# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,\n# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),\n# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,\n# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,\n# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,\n# Ukrainian and Vietnamese.\n# The default value is: English.\n\nOUTPUT_LANGUAGE        = English\n\n# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member\n# descriptions after the members that are listed in the file and class\n# documentation (similar to Javadoc). Set to NO to disable this.\n# The default value is: YES.\n\nBRIEF_MEMBER_DESC      = YES\n\n# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief\n# description of a member or function before the detailed description\n#\n# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the\n# brief descriptions will be completely suppressed.\n# The default value is: YES.\n\nREPEAT_BRIEF           = YES\n\n# This tag implements a quasi-intelligent brief description abbreviator that is\n# used to form the text in various listings. Each string in this list, if found\n# as the leading text of the brief description, will be stripped from the text\n# and the result, after processing the whole list, is used as the annotated\n# text. Otherwise, the brief description is used as-is. If left blank, the\n# following values are used ($name is automatically replaced with the name of\n# the entity):The $name class, The $name widget, The $name file, is, provides,\n# specifies, contains, represents, a, an and the.\n\nABBREVIATE_BRIEF       =\n\n# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then\n# doxygen will generate a detailed section even if there is only a brief\n# description.\n# The default value is: NO.\n\nALWAYS_DETAILED_SEC    = NO\n\n# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all\n# inherited members of a class in the documentation of that class as if those\n# members were ordinary class members. Constructors, destructors and assignment\n# operators of the base classes will not be shown.\n# The default value is: NO.\n\nINLINE_INHERITED_MEMB  = NO\n\n# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path\n# before files name in the file list and in the header files. If set to NO the\n# shortest path that makes the file name unique will be used\n# The default value is: YES.\n\nFULL_PATH_NAMES        = YES\n\n# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.\n# Stripping is only done if one of the specified strings matches the left-hand\n# part of the path. The tag can be used to show relative paths in the file list.\n# If left blank the directory from which doxygen is run is used as the path to\n# strip.\n#\n# Note that you can specify absolute paths here, but also relative paths, which\n# will be relative from the directory where doxygen is started.\n# This tag requires that the tag FULL_PATH_NAMES is set to YES.\n\nSTRIP_FROM_PATH        =\n\n# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the\n# path mentioned in the documentation of a class, which tells the reader which\n# header file to include in order to use a class. If left blank only the name of\n# the header file containing the class definition is used. Otherwise one should\n# specify the list of include paths that are normally passed to the compiler\n# using the -I flag.\n\nSTRIP_FROM_INC_PATH    =\n\n# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but\n# less readable) file names. This can be useful is your file systems doesn't\n# support long names like on DOS, Mac, or CD-ROM.\n# The default value is: NO.\n\nSHORT_NAMES            = NO\n\n# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the\n# first line (until the first dot) of a Javadoc-style comment as the brief\n# description. If set to NO, the Javadoc-style will behave just like regular Qt-\n# style comments (thus requiring an explicit @brief command for a brief\n# description.)\n# The default value is: NO.\n\nJAVADOC_AUTOBRIEF      = YES\n\n# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first\n# line (until the first dot) of a Qt-style comment as the brief description. If\n# set to NO, the Qt-style will behave just like regular Qt-style comments (thus\n# requiring an explicit \\brief command for a brief description.)\n# The default value is: NO.\n\nQT_AUTOBRIEF           = YES\n\n# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a\n# multi-line C++ special comment block (i.e. a block of //! or /// comments) as\n# a brief description. This used to be the default behavior. The new default is\n# to treat a multi-line C++ comment block as a detailed description. Set this\n# tag to YES if you prefer the old behavior instead.\n#\n# Note that setting this tag to YES also means that rational rose comments are\n# not recognized any more.\n# The default value is: NO.\n\nMULTILINE_CPP_IS_BRIEF = NO\n\n# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the\n# documentation from any documented member that it re-implements.\n# The default value is: YES.\n\nINHERIT_DOCS           = YES\n\n# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new\n# page for each member. If set to NO, the documentation of a member will be part\n# of the file/class/namespace that contains it.\n# The default value is: NO.\n\nSEPARATE_MEMBER_PAGES  = NO\n\n# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen\n# uses this value to replace tabs by spaces in code fragments.\n# Minimum value: 1, maximum value: 16, default value: 4.\n\nTAB_SIZE               = 8\n\n# This tag can be used to specify a number of aliases that act as commands in\n# the documentation. An alias has the form:\n# name=value\n# For example adding\n# \"sideeffect=@par Side Effects:\\n\"\n# will allow you to put the command \\sideeffect (or @sideeffect) in the\n# documentation, which will result in a user-defined paragraph with heading\n# \"Side Effects:\". You can put \\n's in the value part of an alias to insert\n# newlines.\n\nALIASES                =\n\n# This tag can be used to specify a number of word-keyword mappings (TCL only).\n# A mapping has the form \"name=value\". For example adding \"class=itcl::class\"\n# will allow you to use the command class in the itcl::class meaning.\n\nTCL_SUBST              =\n\n# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources\n# only. Doxygen will then generate output that is more tailored for C. For\n# instance, some of the names that are used will be different. The list of all\n# members will be omitted, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_FOR_C  = NO\n\n# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or\n# Python sources only. Doxygen will then generate output that is more tailored\n# for that language. For instance, namespaces will be presented as packages,\n# qualified scopes will look different, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_JAVA   = NO\n\n# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran\n# sources. Doxygen will then generate output that is tailored for Fortran.\n# The default value is: NO.\n\nOPTIMIZE_FOR_FORTRAN   = NO\n\n# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL\n# sources. Doxygen will then generate output that is tailored for VHDL.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_VHDL   = NO\n\n# Doxygen selects the parser to use depending on the extension of the files it\n# parses. With this tag you can assign which parser to use for a given\n# extension. Doxygen has a built-in mapping, but you can override or extend it\n# using this tag. The format is ext=language, where ext is a file extension, and\n# language is one of the parsers supported by doxygen: IDL, Java, Javascript,\n# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:\n# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:\n# Fortran. In the later case the parser tries to guess whether the code is fixed\n# or free formatted code, this is the default for Fortran type files), VHDL. For\n# instance to make doxygen treat .inc files as Fortran files (default is PHP),\n# and .f files as C (default is Fortran), use: inc=Fortran f=C.\n#\n# Note: For files without extension you can use no_extension as a placeholder.\n#\n# Note that for custom extensions you also need to set FILE_PATTERNS otherwise\n# the files are not read by doxygen.\n\nEXTENSION_MAPPING      =\n\n# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments\n# according to the Markdown format, which allows for more readable\n# documentation. See http://daringfireball.net/projects/markdown/ for details.\n# The output of markdown processing is further processed by doxygen, so you can\n# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in\n# case of backward compatibilities issues.\n# The default value is: YES.\n\nMARKDOWN_SUPPORT       = YES\n\n# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up\n# to that level are automatically included in the table of contents, even if\n# they do not have an id attribute.\n# Note: This feature currently applies only to Markdown headings.\n# Minimum value: 0, maximum value: 99, default value: 0.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nTOC_INCLUDE_HEADINGS   = 0\n\n# When enabled doxygen tries to link words that correspond to documented\n# classes, or namespaces to their corresponding documentation. Such a link can\n# be prevented in individual cases by putting a % sign in front of the word or\n# globally by setting AUTOLINK_SUPPORT to NO.\n# The default value is: YES.\n\nAUTOLINK_SUPPORT       = YES\n\n# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want\n# to include (a tag file for) the STL sources as input, then you should set this\n# tag to YES in order to let doxygen match functions declarations and\n# definitions whose arguments contain STL classes (e.g. func(std::string);\n# versus func(std::string) {}). This also make the inheritance and collaboration\n# diagrams that involve STL classes more complete and accurate.\n# The default value is: NO.\n\nBUILTIN_STL_SUPPORT    = NO\n\n# If you use Microsoft's C++/CLI language, you should set this option to YES to\n# enable parsing support.\n# The default value is: NO.\n\nCPP_CLI_SUPPORT        = NO\n\n# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:\n# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen\n# will parse them like normal C++ but will assume all classes use public instead\n# of private inheritance when no explicit protection keyword is present.\n# The default value is: NO.\n\nSIP_SUPPORT            = NO\n\n# For Microsoft's IDL there are propget and propput attributes to indicate\n# getter and setter methods for a property. Setting this option to YES will make\n# doxygen to replace the get and set methods by a property in the documentation.\n# This will only work if the methods are indeed getting or setting a simple\n# type. If this is not the case, or you want to show the methods anyway, you\n# should set this option to NO.\n# The default value is: YES.\n\nIDL_PROPERTY_SUPPORT   = YES\n\n# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC\n# tag is set to YES then doxygen will reuse the documentation of the first\n# member in the group (if any) for the other members of the group. By default\n# all members of a group must be documented explicitly.\n# The default value is: NO.\n\nDISTRIBUTE_GROUP_DOC   = NO\n\n# If one adds a struct or class to a group and this option is enabled, then also\n# any nested class or struct is added to the same group. By default this option\n# is disabled and one has to add nested compounds explicitly via \\ingroup.\n# The default value is: NO.\n\nGROUP_NESTED_COMPOUNDS = NO\n\n# Set the SUBGROUPING tag to YES to allow class member groups of the same type\n# (for instance a group of public functions) to be put as a subgroup of that\n# type (e.g. under the Public Functions section). Set it to NO to prevent\n# subgrouping. Alternatively, this can be done per class using the\n# \\nosubgrouping command.\n# The default value is: YES.\n\nSUBGROUPING            = YES\n\n# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions\n# are shown inside the group in which they are included (e.g. using \\ingroup)\n# instead of on a separate page (for HTML and Man pages) or section (for LaTeX\n# and RTF).\n#\n# Note that this feature does not work in combination with\n# SEPARATE_MEMBER_PAGES.\n# The default value is: NO.\n\nINLINE_GROUPED_CLASSES = NO\n\n# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions\n# with only public data fields or simple typedef fields will be shown inline in\n# the documentation of the scope in which they are defined (i.e. file,\n# namespace, or group documentation), provided this scope is documented. If set\n# to NO, structs, classes, and unions are shown on a separate page (for HTML and\n# Man pages) or section (for LaTeX and RTF).\n# The default value is: NO.\n\nINLINE_SIMPLE_STRUCTS  = NO\n\n# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or\n# enum is documented as struct, union, or enum with the name of the typedef. So\n# typedef struct TypeS {} TypeT, will appear in the documentation as a struct\n# with name TypeT. When disabled the typedef will appear as a member of a file,\n# namespace, or class. And the struct will be named TypeS. This can typically be\n# useful for C code in case the coding convention dictates that all compound\n# types are typedef'ed and only the typedef is referenced, never the tag name.\n# The default value is: NO.\n\nTYPEDEF_HIDES_STRUCT   = NO\n\n# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This\n# cache is used to resolve symbols given their name and scope. Since this can be\n# an expensive process and often the same symbol appears multiple times in the\n# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small\n# doxygen will become slower. If the cache is too large, memory is wasted. The\n# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range\n# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536\n# symbols. At the end of a run doxygen will report the cache usage and suggest\n# the optimal cache size from a speed point of view.\n# Minimum value: 0, maximum value: 9, default value: 0.\n\nLOOKUP_CACHE_SIZE      = 0\n\n#---------------------------------------------------------------------------\n# Build related configuration options\n#---------------------------------------------------------------------------\n\n# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in\n# documentation are documented, even if no documentation was available. Private\n# class members and static file members will be hidden unless the\n# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.\n# Note: This will also disable the warnings about undocumented members that are\n# normally produced when WARNINGS is set to YES.\n# The default value is: NO.\n\nEXTRACT_ALL            = YES\n\n# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will\n# be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIVATE        = YES\n\n# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal\n# scope will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PACKAGE        = NO\n\n# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be\n# included in the documentation.\n# The default value is: NO.\n\nEXTRACT_STATIC         = YES\n\n# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined\n# locally in source files will be included in the documentation. If set to NO,\n# only classes defined in header files are included. Does not have any effect\n# for Java sources.\n# The default value is: YES.\n\nEXTRACT_LOCAL_CLASSES  = YES\n\n# This flag is only useful for Objective-C code. If set to YES, local methods,\n# which are defined in the implementation section but not in the interface are\n# included in the documentation. If set to NO, only methods in the interface are\n# included.\n# The default value is: NO.\n\nEXTRACT_LOCAL_METHODS  = NO\n\n# If this flag is set to YES, the members of anonymous namespaces will be\n# extracted and appear in the documentation as a namespace called\n# 'anonymous_namespace{file}', where file will be replaced with the base name of\n# the file that contains the anonymous namespace. By default anonymous namespace\n# are hidden.\n# The default value is: NO.\n\nEXTRACT_ANON_NSPACES   = NO\n\n# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all\n# undocumented members inside documented classes or files. If set to NO these\n# members will be included in the various overviews, but no documentation\n# section is generated. This option has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_MEMBERS     = NO\n\n# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all\n# undocumented classes that are normally visible in the class hierarchy. If set\n# to NO, these classes will be included in the various overviews. This option\n# has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_CLASSES     = NO\n\n# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend\n# (class|struct|union) declarations. If set to NO, these declarations will be\n# included in the documentation.\n# The default value is: NO.\n\nHIDE_FRIEND_COMPOUNDS  = NO\n\n# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any\n# documentation blocks found inside the body of a function. If set to NO, these\n# blocks will be appended to the function's detailed documentation block.\n# The default value is: NO.\n\nHIDE_IN_BODY_DOCS      = NO\n\n# The INTERNAL_DOCS tag determines if documentation that is typed after a\n# \\internal command is included. If the tag is set to NO then the documentation\n# will be excluded. Set it to YES to include the internal documentation.\n# The default value is: NO.\n\nINTERNAL_DOCS          = NO\n\n# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file\n# names in lower-case letters. If set to YES, upper-case letters are also\n# allowed. This is useful if you have classes or files whose names only differ\n# in case and if your file system supports case sensitive file names. Windows\n# and Mac users are advised to set this option to NO.\n# The default value is: system dependent.\n\nCASE_SENSE_NAMES       = YES\n\n# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with\n# their full class and namespace scopes in the documentation. If set to YES, the\n# scope will be hidden.\n# The default value is: NO.\n\nHIDE_SCOPE_NAMES       = NO\n\n# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will\n# append additional text to a page's title, such as Class Reference. If set to\n# YES the compound reference will be hidden.\n# The default value is: NO.\n\nHIDE_COMPOUND_REFERENCE= NO\n\n# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of\n# the files that are included by a file in the documentation of that file.\n# The default value is: YES.\n\nSHOW_INCLUDE_FILES     = YES\n\n# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each\n# grouped member an include statement to the documentation, telling the reader\n# which file to include in order to use the member.\n# The default value is: NO.\n\nSHOW_GROUPED_MEMB_INC  = NO\n\n# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include\n# files with double quotes in the documentation rather than with sharp brackets.\n# The default value is: NO.\n\nFORCE_LOCAL_INCLUDES   = NO\n\n# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the\n# documentation for inline members.\n# The default value is: YES.\n\nINLINE_INFO            = YES\n\n# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the\n# (detailed) documentation of file and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order.\n# The default value is: YES.\n\nSORT_MEMBER_DOCS       = YES\n\n# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief\n# descriptions of file, namespace and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order. Note that\n# this will also influence the order of the classes in the class list.\n# The default value is: NO.\n\nSORT_BRIEF_DOCS        = NO\n\n# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the\n# (brief and detailed) documentation of class members so that constructors and\n# destructors are listed first. If set to NO the constructors will appear in the\n# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.\n# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief\n# member documentation.\n# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting\n# detailed member documentation.\n# The default value is: NO.\n\nSORT_MEMBERS_CTORS_1ST = NO\n\n# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy\n# of group names into alphabetical order. If set to NO the group names will\n# appear in their defined order.\n# The default value is: NO.\n\nSORT_GROUP_NAMES       = NO\n\n# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by\n# fully-qualified names, including namespaces. If set to NO, the class list will\n# be sorted only by class name, not including the namespace part.\n# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.\n# Note: This option applies only to the class list, not to the alphabetical\n# list.\n# The default value is: NO.\n\nSORT_BY_SCOPE_NAME     = NO\n\n# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper\n# type resolution of all parameters of a function it will reject a match between\n# the prototype and the implementation of a member function even if there is\n# only one candidate or it is obvious which candidate to choose by doing a\n# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still\n# accept a match between prototype and implementation in such cases.\n# The default value is: NO.\n\nSTRICT_PROTO_MATCHING  = NO\n\n# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo\n# list. This list is created by putting \\todo commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TODOLIST      = YES\n\n# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test\n# list. This list is created by putting \\test commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TESTLIST      = YES\n\n# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug\n# list. This list is created by putting \\bug commands in the documentation.\n# The default value is: YES.\n\nGENERATE_BUGLIST       = YES\n\n# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)\n# the deprecated list. This list is created by putting \\deprecated commands in\n# the documentation.\n# The default value is: YES.\n\nGENERATE_DEPRECATEDLIST= YES\n\n# The ENABLED_SECTIONS tag can be used to enable conditional documentation\n# sections, marked by \\if <section_label> ... \\endif and \\cond <section_label>\n# ... \\endcond blocks.\n\nENABLED_SECTIONS       =\n\n# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the\n# initial value of a variable or macro / define can have for it to appear in the\n# documentation. If the initializer consists of more lines than specified here\n# it will be hidden. Use a value of 0 to hide initializers completely. The\n# appearance of the value of individual variables and macros / defines can be\n# controlled using \\showinitializer or \\hideinitializer command in the\n# documentation regardless of this setting.\n# Minimum value: 0, maximum value: 10000, default value: 30.\n\nMAX_INITIALIZER_LINES  = 30\n\n# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at\n# the bottom of the documentation of classes and structs. If set to YES, the\n# list will mention the files that were used to generate the documentation.\n# The default value is: YES.\n\nSHOW_USED_FILES        = YES\n\n# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This\n# will remove the Files entry from the Quick Index and from the Folder Tree View\n# (if specified).\n# The default value is: YES.\n\nSHOW_FILES             = YES\n\n# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces\n# page. This will remove the Namespaces entry from the Quick Index and from the\n# Folder Tree View (if specified).\n# The default value is: YES.\n\nSHOW_NAMESPACES        = YES\n\n# The FILE_VERSION_FILTER tag can be used to specify a program or script that\n# doxygen should invoke to get the current version for each file (typically from\n# the version control system). Doxygen will invoke the program by executing (via\n# popen()) the command command input-file, where command is the value of the\n# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided\n# by doxygen. Whatever the program writes to standard output is used as the file\n# version. For an example see the documentation.\n\nFILE_VERSION_FILTER    =\n\n# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed\n# by doxygen. The layout file controls the global structure of the generated\n# output files in an output format independent way. To create the layout file\n# that represents doxygen's defaults, run doxygen with the -l option. You can\n# optionally specify a file name after the option, if omitted DoxygenLayout.xml\n# will be used as the name of the layout file.\n#\n# Note that if you run doxygen from a directory containing a file called\n# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE\n# tag is left empty.\n\nLAYOUT_FILE            =\n\n# The CITE_BIB_FILES tag can be used to specify one or more bib files containing\n# the reference definitions. This must be a list of .bib files. The .bib\n# extension is automatically appended if omitted. This requires the bibtex tool\n# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.\n# For LaTeX the style of the bibliography can be controlled using\n# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the\n# search path. See also \\cite for info how to create references.\n\nCITE_BIB_FILES         =\n\n#---------------------------------------------------------------------------\n# Configuration options related to warning and progress messages\n#---------------------------------------------------------------------------\n\n# The QUIET tag can be used to turn on/off the messages that are generated to\n# standard output by doxygen. If QUIET is set to YES this implies that the\n# messages are off.\n# The default value is: NO.\n\nQUIET                  = NO\n\n# The WARNINGS tag can be used to turn on/off the warning messages that are\n# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES\n# this implies that the warnings are on.\n#\n# Tip: Turn warnings on while writing the documentation.\n# The default value is: YES.\n\nWARNINGS               = YES\n\n# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate\n# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: YES.\n\nWARN_IF_UNDOCUMENTED   = NO\n\n# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for\n# potential errors in the documentation, such as not documenting some parameters\n# in a documented function, or documenting parameters that don't exist or using\n# markup commands wrongly.\n# The default value is: YES.\n\nWARN_IF_DOC_ERROR      = YES\n\n# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that\n# are documented, but have no documentation for their parameters or return\n# value. If set to NO, doxygen will only warn about wrong or incomplete\n# parameter documentation, but not about the absence of documentation.\n# The default value is: NO.\n\nWARN_NO_PARAMDOC       = NO\n\n# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when\n# a warning is encountered.\n# The default value is: NO.\n\nWARN_AS_ERROR          = NO\n\n# The WARN_FORMAT tag determines the format of the warning messages that doxygen\n# can produce. The string should contain the $file, $line, and $text tags, which\n# will be replaced by the file and line number from which the warning originated\n# and the warning text. Optionally the format may contain $version, which will\n# be replaced by the version of the file (if it could be obtained via\n# FILE_VERSION_FILTER)\n# The default value is: $file:$line: $text.\n\nWARN_FORMAT            = \"$file:$line: $text\"\n\n# The WARN_LOGFILE tag can be used to specify a file to which warning and error\n# messages should be written. If left blank the output is written to standard\n# error (stderr).\n\nWARN_LOGFILE           =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the input files\n#---------------------------------------------------------------------------\n\n# The INPUT tag is used to specify the files and/or directories that contain\n# documented source files. You may enter file names like myfile.cpp or\n# directories like /usr/src/myproject. Separate the files or directories with\n# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING\n# Note: If this tag is empty the current directory is searched.\n\nINPUT                  = @DOXYGEN_SCAN_FOLDERS@\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses\n# libiconv (or the iconv built into libc) for the transcoding. See the libiconv\n# documentation (see: http://www.gnu.org/software/libiconv) for the list of\n# possible encodings.\n# The default value is: UTF-8.\n\nINPUT_ENCODING         = UTF-8\n\n# If the value of the INPUT tag contains directories, you can use the\n# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and\n# *.h) to filter out the source-files in the directories.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# read by doxygen.\n#\n# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,\n# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,\n# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,\n# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,\n# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.\n\nFILE_PATTERNS          = *.cpp \\\n                         *.h \\\n                         *.py \\\n                         *.pyx \\\n                         *.pxd \\\n                         *.in \\\n                         *.md\n\n# The RECURSIVE tag can be used to specify whether or not subdirectories should\n# be searched for input files as well.\n# The default value is: NO.\n\nRECURSIVE              = YES\n\n# The EXCLUDE tag can be used to specify files and/or directories that should be\n# excluded from the INPUT source files. This way you can easily exclude a\n# subdirectory from a directory tree whose root is specified with the INPUT tag.\n#\n# Note that relative paths are relative to the directory from which doxygen is\n# run.\n\nEXCLUDE                =\n\n# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or\n# directories that are symbolic links (a Unix file system feature) are excluded\n# from the input.\n# The default value is: NO.\n\nEXCLUDE_SYMLINKS       = NO\n\n# If the value of the INPUT tag contains directories, you can use the\n# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude\n# certain files from those directories.\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories for example use the pattern */test/*\n\nEXCLUDE_PATTERNS       =\n\n# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names\n# (namespaces, classes, functions, etc.) that should be excluded from the\n# output. The symbol name can be a fully qualified name, a word, or if the\n# wildcard * is used, a substring. Examples: ANamespace, AClass,\n# AClass::ANamespace, ANamespace::*Test\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories use the pattern */test/*\n\nEXCLUDE_SYMBOLS        =\n\n# The EXAMPLE_PATH tag can be used to specify one or more files or directories\n# that contain example code fragments that are included (see the \\include\n# command).\n\nEXAMPLE_PATH           =\n\n# If the value of the EXAMPLE_PATH tag contains directories, you can use the\n# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and\n# *.h) to filter out the source-files in the directories. If left blank all\n# files are included.\n\nEXAMPLE_PATTERNS       =\n\n# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be\n# searched for input files to be used with the \\include or \\dontinclude commands\n# irrespective of the value of the RECURSIVE tag.\n# The default value is: NO.\n\nEXAMPLE_RECURSIVE      = NO\n\n# The IMAGE_PATH tag can be used to specify one or more files or directories\n# that contain images that are to be included in the documentation (see the\n# \\image command).\n\nIMAGE_PATH             =\n\n# The INPUT_FILTER tag can be used to specify a program that doxygen should\n# invoke to filter for each input file. Doxygen will invoke the filter program\n# by executing (via popen()) the command:\n#\n# <filter> <input-file>\n#\n# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the\n# name of an input file. Doxygen will then use the output that the filter\n# program writes to standard output. If FILTER_PATTERNS is specified, this tag\n# will be ignored.\n#\n# Note that the filter must not add or remove lines; it is applied before the\n# code is scanned, but not when the output code is generated. If lines are added\n# or removed, the anchors will not be placed correctly.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nINPUT_FILTER           =\n\n# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern\n# basis. Doxygen will compare the file name with each pattern and apply the\n# filter if there is a match. The filters are a list of the form: pattern=filter\n# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how\n# filters are used. If the FILTER_PATTERNS tag is empty or if none of the\n# patterns match the file name, INPUT_FILTER is applied.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nFILTER_PATTERNS        =\n\n# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using\n# INPUT_FILTER) will also be used to filter the input files that are used for\n# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).\n# The default value is: NO.\n\nFILTER_SOURCE_FILES    = NO\n\n# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file\n# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and\n# it is also possible to disable source filtering for a specific pattern using\n# *.ext= (so without naming a filter).\n# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.\n\nFILTER_SOURCE_PATTERNS =\n\n# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that\n# is part of the input, its contents will be placed on the main page\n# (index.html). This can be useful if you have a project on for instance GitHub\n# and want to reuse the introduction page also for the doxygen output.\n\nUSE_MDFILE_AS_MAINPAGE = @CMAKE_SOURCE_DIR@/README.md\n\n#---------------------------------------------------------------------------\n# Configuration options related to source browsing\n#---------------------------------------------------------------------------\n\n# If the SOURCE_BROWSER tag is set to YES then a list of source files will be\n# generated. Documented entities will be cross-referenced with these sources.\n#\n# Note: To get rid of all source code in the generated output, make sure that\n# also VERBATIM_HEADERS is set to NO.\n# The default value is: NO.\n\nSOURCE_BROWSER         = YES\n\n# Setting the INLINE_SOURCES tag to YES will include the body of functions,\n# classes and enums directly into the documentation.\n# The default value is: NO.\n\nINLINE_SOURCES         = NO\n\n# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any\n# special comment blocks from generated source code fragments. Normal C, C++ and\n# Fortran comments will always remain visible.\n# The default value is: YES.\n\nSTRIP_CODE_COMMENTS    = YES\n\n# If the REFERENCED_BY_RELATION tag is set to YES then for each documented\n# function all documented functions referencing it will be listed.\n# The default value is: NO.\n\nREFERENCED_BY_RELATION = NO\n\n# If the REFERENCES_RELATION tag is set to YES then for each documented function\n# all documented entities called/used by that function will be listed.\n# The default value is: NO.\n\nREFERENCES_RELATION    = NO\n\n# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set\n# to YES then the hyperlinks from functions in REFERENCES_RELATION and\n# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will\n# link to the documentation.\n# The default value is: YES.\n\nREFERENCES_LINK_SOURCE = YES\n\n# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the\n# source code will show a tooltip with additional information such as prototype,\n# brief description and links to the definition and documentation. Since this\n# will make the HTML file larger and loading of large files a bit slower, you\n# can opt to disable this feature.\n# The default value is: YES.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nSOURCE_TOOLTIPS        = YES\n\n# If the USE_HTAGS tag is set to YES then the references to source code will\n# point to the HTML generated by the htags(1) tool instead of doxygen built-in\n# source browser. The htags tool is part of GNU's global source tagging system\n# (see http://www.gnu.org/software/global/global.html). You will need version\n# 4.8.6 or higher.\n#\n# To use it do the following:\n# - Install the latest version of global\n# - Enable SOURCE_BROWSER and USE_HTAGS in the config file\n# - Make sure the INPUT points to the root of the source tree\n# - Run doxygen as normal\n#\n# Doxygen will invoke htags (and that will in turn invoke gtags), so these\n# tools must be available from the command line (i.e. in the search path).\n#\n# The result: instead of the source browser generated by doxygen, the links to\n# source code will now point to the output of htags.\n# The default value is: NO.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nUSE_HTAGS              = NO\n\n# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a\n# verbatim copy of the header file for each class for which an include is\n# specified. Set to NO to disable this.\n# See also: Section \\class.\n# The default value is: YES.\n\nVERBATIM_HEADERS       = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the alphabetical class index\n#---------------------------------------------------------------------------\n\n# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all\n# compounds will be generated. Enable this if the project contains a lot of\n# classes, structs, unions or interfaces.\n# The default value is: YES.\n\nALPHABETICAL_INDEX     = YES\n\n# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in\n# which the alphabetical index list will be split.\n# Minimum value: 1, maximum value: 20, default value: 5.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nCOLS_IN_ALPHA_INDEX    = 5\n\n# In case all classes in a project start with a common prefix, all classes will\n# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag\n# can be used to specify a prefix (or a list of prefixes) that should be ignored\n# while generating the index headers.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nIGNORE_PREFIX          =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the HTML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output\n# The default value is: YES.\n\nGENERATE_HTML          = YES\n\n# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_OUTPUT            = html\n\n# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each\n# generated HTML page (for example: .htm, .php, .asp).\n# The default value is: .html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FILE_EXTENSION    = .html\n\n# The HTML_HEADER tag can be used to specify a user-defined HTML header file for\n# each generated HTML page. If the tag is left blank doxygen will generate a\n# standard header.\n#\n# To get valid HTML the header file that includes any scripts and style sheets\n# that doxygen needs, which is dependent on the configuration options used (e.g.\n# the setting GENERATE_TREEVIEW). It is highly recommended to start with a\n# default header using\n# doxygen -w html new_header.html new_footer.html new_stylesheet.css\n# YourConfigFile\n# and then modify the file new_header.html. See also section \"Doxygen usage\"\n# for information on how to generate the default header that doxygen normally\n# uses.\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. For a description\n# of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_HEADER            =\n\n# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each\n# generated HTML page. If the tag is left blank doxygen will generate a standard\n# footer. See HTML_HEADER for more information on how to generate a default\n# footer and what special commands can be used inside the footer. See also\n# section \"Doxygen usage\" for information on how to generate the default footer\n# that doxygen normally uses.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FOOTER            =\n\n# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style\n# sheet that is used by each HTML page. It can be used to fine-tune the look of\n# the HTML output. If left blank doxygen will generate a default style sheet.\n# See also section \"Doxygen usage\" for information on how to generate the style\n# sheet that doxygen normally uses.\n# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as\n# it is more robust and this tag (HTML_STYLESHEET) will in the future become\n# obsolete.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_STYLESHEET        =\n\n# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# cascading style sheets that are included after the standard style sheets\n# created by doxygen. Using this option one can overrule certain style aspects.\n# This is preferred over using HTML_STYLESHEET since it does not replace the\n# standard style sheet and is therefore more robust against future updates.\n# Doxygen will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list). For an example see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_STYLESHEET  = \"@CMAKE_SOURCE_DIR@/buildsystem/doxygen_custom.css\"\n\n# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the HTML output directory. Note\n# that these files will be copied to the base HTML output directory. Use the\n# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these\n# files. In the HTML_STYLESHEET file, use the file name only. Also note that the\n# files will be copied as-is; there are no commands or markers available.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_FILES       =\n\n# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen\n# will adjust the colors in the style sheet and background images according to\n# this color. Hue is specified as an angle on a colorwheel, see\n# http://en.wikipedia.org/wiki/Hue for more information. For instance the value\n# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300\n# purple, and 360 is red again.\n# Minimum value: 0, maximum value: 359, default value: 220.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_HUE    = 220\n\n# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors\n# in the HTML output. For a value of 0 the output will use grayscales only. A\n# value of 255 will produce the most vivid colors.\n# Minimum value: 0, maximum value: 255, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_SAT    = 100\n\n# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the\n# luminance component of the colors in the HTML output. Values below 100\n# gradually make the output lighter, whereas values above 100 make the output\n# darker. The value divided by 100 is the actual gamma applied, so 80 represents\n# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not\n# change the gamma.\n# Minimum value: 40, maximum value: 240, default value: 80.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_GAMMA  = 80\n\n# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML\n# page will contain the date and time when the page was generated. Setting this\n# to YES can help to show when doxygen was last run and thus if the\n# documentation is up to date.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_TIMESTAMP         = YES\n\n# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML\n# documentation will contain sections that can be hidden and shown after the\n# page has loaded.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_SECTIONS  = YES\n\n# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries\n# shown in the various tree structured indices initially; the user can expand\n# and collapse entries dynamically later on. Doxygen will expand the tree to\n# such a level that at most the specified number of entries are visible (unless\n# a fully collapsed tree already exceeds this amount). So setting the number of\n# entries 1 will produce a full collapsed tree by default. 0 is a special value\n# representing an infinite number of entries and will result in a full expanded\n# tree by default.\n# Minimum value: 0, maximum value: 9999, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_INDEX_NUM_ENTRIES = 100\n\n# If the GENERATE_DOCSET tag is set to YES, additional index files will be\n# generated that can be used as input for Apple's Xcode 3 integrated development\n# environment (see: http://developer.apple.com/tools/xcode/), introduced with\n# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a\n# Makefile in the HTML output directory. Running make will produce the docset in\n# that directory and running make install will install the docset in\n# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at\n# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html\n# for more information.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_DOCSET        = NO\n\n# This tag determines the name of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# The default value is: Doxygen generated docs.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDNAME        = \"Doxygen generated docs\"\n\n# This tag specifies a string that should uniquely identify the documentation\n# set bundle. This should be a reverse domain-name style string, e.g.\n# com.mycompany.MyDocSet. Doxygen will append .docset to the name.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_BUNDLE_ID       = org.doxygen.Project\n\n# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify\n# the documentation publisher. This should be a reverse domain-name style\n# string, e.g. com.mycompany.MyDocSet.documentation.\n# The default value is: org.doxygen.Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_ID    = org.doxygen.Publisher\n\n# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.\n# The default value is: Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_NAME  = Publisher\n\n# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three\n# additional HTML index files: index.hhp, index.hhc, and index.hhk. The\n# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop\n# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on\n# Windows.\n#\n# The HTML Help Workshop contains a compiler that can convert all HTML output\n# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML\n# files are now used as the Windows 98 help format, and will replace the old\n# Windows help format (.hlp) on all Windows platforms in the future. Compressed\n# HTML files also contain an index, a table of contents, and you can search for\n# words in the documentation. The HTML workshop also contains a viewer for\n# compressed HTML files.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_HTMLHELP      = NO\n\n# The CHM_FILE tag can be used to specify the file name of the resulting .chm\n# file. You can add a path in front of the file if the result should not be\n# written to the html output directory.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_FILE               =\n\n# The HHC_LOCATION tag can be used to specify the location (absolute path\n# including file name) of the HTML help compiler (hhc.exe). If non-empty,\n# doxygen will try to run the HTML help compiler on the generated index.hhp.\n# The file has to be specified with full path.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nHHC_LOCATION           =\n\n# The GENERATE_CHI flag controls if a separate .chi index file is generated\n# (YES) or that it should be included in the master .chm file (NO).\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nGENERATE_CHI           = NO\n\n# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)\n# and project file content.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_INDEX_ENCODING     =\n\n# The BINARY_TOC flag controls whether a binary table of contents is generated\n# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it\n# enables the Previous and Next buttons.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nBINARY_TOC             = NO\n\n# The TOC_EXPAND flag can be set to YES to add extra items for group members to\n# the table of contents of the HTML help documentation and to the tree view.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nTOC_EXPAND             = NO\n\n# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and\n# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that\n# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help\n# (.qch) of the generated HTML documentation.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_QHP           = NO\n\n# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify\n# the file name of the resulting .qch file. The path specified is relative to\n# the HTML output folder.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQCH_FILE               =\n\n# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help\n# Project output. For more information please see Qt Help Project / Namespace\n# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_NAMESPACE          = org.doxygen.Project\n\n# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt\n# Help Project output. For more information please see Qt Help Project / Virtual\n# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-\n# folders).\n# The default value is: doc.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_VIRTUAL_FOLDER     = doc\n\n# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom\n# filter to add. For more information please see Qt Help Project / Custom\n# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-\n# filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_NAME   =\n\n# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the\n# custom filter to add. For more information please see Qt Help Project / Custom\n# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-\n# filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_ATTRS  =\n\n# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this\n# project's filter section matches. Qt Help Project / Filter Attributes (see:\n# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_SECT_FILTER_ATTRS  =\n\n# The QHG_LOCATION tag can be used to specify the location of Qt's\n# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the\n# generated .qhp file.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHG_LOCATION           =\n\n# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be\n# generated, together with the HTML files, they form an Eclipse help plugin. To\n# install this plugin and make it available under the help contents menu in\n# Eclipse, the contents of the directory containing the HTML and XML files needs\n# to be copied into the plugins directory of eclipse. The name of the directory\n# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.\n# After copying Eclipse needs to be restarted before the help appears.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_ECLIPSEHELP   = NO\n\n# A unique identifier for the Eclipse help plugin. When installing the plugin\n# the directory name containing the HTML and XML files should also have this\n# name. Each documentation set should have its own identifier.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.\n\nECLIPSE_DOC_ID         = org.doxygen.Project\n\n# If you want full control over the layout of the generated HTML pages it might\n# be necessary to disable the index and replace it with your own. The\n# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top\n# of each HTML page. A value of NO enables the index and the value YES disables\n# it. Since the tabs in the index contain the same information as the navigation\n# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nDISABLE_INDEX          = NO\n\n# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index\n# structure should be generated to display hierarchical information. If the tag\n# value is set to YES, a side panel will be generated containing a tree-like\n# index structure (just like the one that is generated for HTML Help). For this\n# to work a browser that supports JavaScript, DHTML, CSS and frames is required\n# (i.e. any modern browser). Windows users are probably better off using the\n# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can\n# further fine-tune the look of the index. As an example, the default style\n# sheet generated by doxygen has an example that shows how to put an image at\n# the root of the tree instead of the PROJECT_NAME. Since the tree basically has\n# the same information as the tab index, you could consider setting\n# DISABLE_INDEX to YES when enabling this option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_TREEVIEW      = NO\n\n# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that\n# doxygen will group on one line in the generated HTML documentation.\n#\n# Note that a value of 0 will completely suppress the enum values from appearing\n# in the overview section.\n# Minimum value: 0, maximum value: 20, default value: 4.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nENUM_VALUES_PER_LINE   = 4\n\n# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used\n# to set the initial width (in pixels) of the frame in which the tree is shown.\n# Minimum value: 0, maximum value: 1500, default value: 250.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nTREEVIEW_WIDTH         = 250\n\n# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to\n# external symbols imported via tag files in a separate window.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nEXT_LINKS_IN_WINDOW    = NO\n\n# Use this tag to change the font size of LaTeX formulas included as images in\n# the HTML documentation. When you change the font size after a successful\n# doxygen run you need to manually remove any form_*.png images from the HTML\n# output directory to force them to be regenerated.\n# Minimum value: 8, maximum value: 50, default value: 10.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_FONTSIZE       = 10\n\n# Use the FORMULA_TRANPARENT tag to determine whether or not the images\n# generated for formulas are transparent PNGs. Transparent PNGs are not\n# supported properly for IE 6.0, but are supported on all modern browsers.\n#\n# Note that when changing this option you need to delete any form_*.png files in\n# the HTML output directory before the changes have effect.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_TRANSPARENT    = YES\n\n# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see\n# http://www.mathjax.org) which uses client side Javascript for the rendering\n# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX\n# installed or if you want to formulas look prettier in the HTML output. When\n# enabled you may also need to install MathJax separately and configure the path\n# to it using the MATHJAX_RELPATH option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nUSE_MATHJAX            = NO\n\n# When MathJax is enabled you can set the default output format to be used for\n# the MathJax output. See the MathJax site (see:\n# http://docs.mathjax.org/en/latest/output.html) for more details.\n# Possible values are: HTML-CSS (which is slower, but has the best\n# compatibility), NativeMML (i.e. MathML) and SVG.\n# The default value is: HTML-CSS.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_FORMAT         = HTML-CSS\n\n# When MathJax is enabled you need to specify the location relative to the HTML\n# output directory using the MATHJAX_RELPATH option. The destination directory\n# should contain the MathJax.js script. For instance, if the mathjax directory\n# is located at the same level as the HTML output directory, then\n# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax\n# Content Delivery Network so you can quickly see the result without installing\n# MathJax. However, it is strongly recommended to install a local copy of\n# MathJax from http://www.mathjax.org before deployment.\n# The default value is: http://cdn.mathjax.org/mathjax/latest.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest\n\n# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax\n# extension names that should be enabled during MathJax rendering. For example\n# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_EXTENSIONS     =\n\n# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces\n# of code that will be used on startup of the MathJax code. See the MathJax site\n# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an\n# example see the documentation.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_CODEFILE       =\n\n# When the SEARCHENGINE tag is enabled doxygen will generate a search box for\n# the HTML output. The underlying search engine uses javascript and DHTML and\n# should work on any modern browser. Note that when using HTML help\n# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)\n# there is already a search function so this one should typically be disabled.\n# For large projects the javascript based search engine can be slow, then\n# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to\n# search using the keyboard; to jump to the search box use <access key> + S\n# (what the <access key> is depends on the OS and browser, but it is typically\n# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down\n# key> to jump into the search results window, the results can be navigated\n# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel\n# the search. The filter options can be selected when the cursor is inside the\n# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>\n# to select a filter and <Enter> or <escape> to activate or cancel the filter\n# option.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSEARCHENGINE           = YES\n\n# When the SERVER_BASED_SEARCH tag is enabled the search engine will be\n# implemented using a web server instead of a web client using Javascript. There\n# are two flavors of web server based searching depending on the EXTERNAL_SEARCH\n# setting. When disabled, doxygen will generate a PHP script for searching and\n# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing\n# and searching needs to be provided by external tools. See the section\n# \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSERVER_BASED_SEARCH    = NO\n\n# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP\n# script for searching. Instead the search results are written to an XML file\n# which needs to be processed by an external indexer. Doxygen will invoke an\n# external search engine pointed to by the SEARCHENGINE_URL option to obtain the\n# search results.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see: http://xapian.org/).\n#\n# See the section \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH        = NO\n\n# The SEARCHENGINE_URL should point to a search engine hosted by a web server\n# which will return the search results when EXTERNAL_SEARCH is enabled.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see: http://xapian.org/). See the section \"External Indexing and\n# Searching\" for details.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHENGINE_URL       =\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed\n# search data is written to a file for indexing by an external tool. With the\n# SEARCHDATA_FILE tag the name of this file can be specified.\n# The default file is: searchdata.xml.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHDATA_FILE        = searchdata.xml\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the\n# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is\n# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple\n# projects and redirect the results back to the right project.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH_ID     =\n\n# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen\n# projects other than the one defined by this configuration file, but that are\n# all added to the same external search index. Each project needs to have a\n# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of\n# to a relative location where the documentation can be found. The format is:\n# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTRA_SEARCH_MAPPINGS  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the LaTeX output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.\n# The default value is: YES.\n\nGENERATE_LATEX         = YES\n\n# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: latex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_OUTPUT           = latex\n\n# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be\n# invoked.\n#\n# Note that when enabling USE_PDFLATEX this option is only used for generating\n# bitmaps for formulas in the HTML output, but not in the Makefile that is\n# written to the output directory.\n# The default file is: latex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_CMD_NAME         = latex\n\n# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate\n# index for LaTeX.\n# The default file is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nMAKEINDEX_CMD_NAME     = makeindex\n\n# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nCOMPACT_LATEX          = NO\n\n# The PAPER_TYPE tag can be used to set the paper type that is used by the\n# printer.\n# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x\n# 14 inches) and executive (7.25 x 10.5 inches).\n# The default value is: a4.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPAPER_TYPE             = a4\n\n# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names\n# that should be included in the LaTeX output. The package can be specified just\n# by its name or with the correct syntax as to be used with the LaTeX\n# \\usepackage command. To get the times font for instance you can specify :\n# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}\n# To use the option intlimits with the amsmath package you can specify:\n# EXTRA_PACKAGES=[intlimits]{amsmath}\n# If left blank no extra packages will be included.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nEXTRA_PACKAGES         =\n\n# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the\n# generated LaTeX document. The header should contain everything until the first\n# chapter. If it is left blank doxygen will generate a standard header. See\n# section \"Doxygen usage\" for information on how to let doxygen write the\n# default header to a separate file.\n#\n# Note: Only use a user-defined header if you know what you are doing! The\n# following commands have a special meaning inside the header: $title,\n# $datetime, $date, $doxygenversion, $projectname, $projectnumber,\n# $projectbrief, $projectlogo. Doxygen will replace $title with the empty\n# string, for the replacement values of the other commands the user is referred\n# to HTML_HEADER.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HEADER           =\n\n# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the\n# generated LaTeX document. The footer should contain everything after the last\n# chapter. If it is left blank doxygen will generate a standard footer. See\n# LATEX_HEADER for more information on how to generate a default footer and what\n# special commands can be used inside the footer.\n#\n# Note: Only use a user-defined footer if you know what you are doing!\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_FOOTER           =\n\n# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# LaTeX style sheets that are included after the standard style sheets created\n# by doxygen. Using this option one can overrule certain style aspects. Doxygen\n# will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_STYLESHEET =\n\n# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the LATEX_OUTPUT output\n# directory. Note that the files will be copied as-is; there are no commands or\n# markers available.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_FILES      =\n\n# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is\n# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will\n# contain links (just like the HTML output) instead of page references. This\n# makes the output suitable for online browsing using a PDF viewer.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPDF_HYPERLINKS         = YES\n\n# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate\n# the PDF file directly from the LaTeX files. Set this option to YES, to get a\n# higher quality PDF documentation.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nUSE_PDFLATEX           = YES\n\n# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode\n# command to the generated LaTeX files. This will instruct LaTeX to keep running\n# if errors occur, instead of asking the user for help. This option is also used\n# when generating formulas in HTML.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BATCHMODE        = NO\n\n# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the\n# index chapters (such as File Index, Compound Index, etc.) in the output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HIDE_INDICES     = NO\n\n# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source\n# code with syntax highlighting in the LaTeX output.\n#\n# Note that which sources are shown also depends on other settings such as\n# SOURCE_BROWSER.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_SOURCE_CODE      = YES\n\n# The LATEX_BIB_STYLE tag can be used to specify the style to use for the\n# bibliography, e.g. plainnat, or ieeetr. See\n# http://en.wikipedia.org/wiki/BibTeX and \\cite for more info.\n# The default value is: plain.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BIB_STYLE        = plain\n\n# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated\n# page will contain the date and time when the page was generated. Setting this\n# to NO can help when comparing the output of multiple runs.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_TIMESTAMP        = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the RTF output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The\n# RTF output is optimized for Word 97 and may not look too pretty with other RTF\n# readers/editors.\n# The default value is: NO.\n\nGENERATE_RTF           = NO\n\n# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: rtf.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_OUTPUT             = rtf\n\n# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nCOMPACT_RTF            = NO\n\n# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will\n# contain hyperlink fields. The RTF file will contain links (just like the HTML\n# output) instead of page references. This makes the output suitable for online\n# browsing using Word or some other Word compatible readers that support those\n# fields.\n#\n# Note: WordPad (write) and others do not support links.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_HYPERLINKS         = NO\n\n# Load stylesheet definitions from file. Syntax is similar to doxygen's config\n# file, i.e. a series of assignments. You only have to provide replacements,\n# missing definitions are set to their default value.\n#\n# See also section \"Doxygen usage\" for information on how to generate the\n# default style sheet that doxygen normally uses.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_STYLESHEET_FILE    =\n\n# Set optional variables used in the generation of an RTF document. Syntax is\n# similar to doxygen's config file. A template extensions file can be generated\n# using doxygen -e rtf extensionFile.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_EXTENSIONS_FILE    =\n\n# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code\n# with syntax highlighting in the RTF output.\n#\n# Note that which sources are shown also depends on other settings such as\n# SOURCE_BROWSER.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_SOURCE_CODE        = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the man page output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for\n# classes and files.\n# The default value is: NO.\n\nGENERATE_MAN           = NO\n\n# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it. A directory man3 will be created inside the directory specified by\n# MAN_OUTPUT.\n# The default directory is: man.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_OUTPUT             = man\n\n# The MAN_EXTENSION tag determines the extension that is added to the generated\n# man pages. In case the manual section does not start with a number, the number\n# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is\n# optional.\n# The default value is: .3.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_EXTENSION          = .3\n\n# The MAN_SUBDIR tag determines the name of the directory created within\n# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by\n# MAN_EXTENSION with the initial . removed.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_SUBDIR             =\n\n# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it\n# will generate one additional man file for each entity documented in the real\n# man page(s). These additional files only source the real man page, but without\n# them the man command would be unable to find the correct page.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_LINKS              = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the XML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that\n# captures the structure of the code including all documentation.\n# The default value is: NO.\n\nGENERATE_XML           = NO\n\n# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: xml.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_OUTPUT             = xml\n\n# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program\n# listings (including syntax highlighting and cross-referencing information) to\n# the XML output. Note that enabling this will significantly increase the size\n# of the XML output.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_PROGRAMLISTING     = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the DOCBOOK output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files\n# that can be used to generate PDF.\n# The default value is: NO.\n\nGENERATE_DOCBOOK       = NO\n\n# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.\n# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in\n# front of it.\n# The default directory is: docbook.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_OUTPUT         = docbook\n\n# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the\n# program listings (including syntax highlighting and cross-referencing\n# information) to the DOCBOOK output. Note that enabling this will significantly\n# increase the size of the DOCBOOK output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_PROGRAMLISTING = NO\n\n#---------------------------------------------------------------------------\n# Configuration options for the AutoGen Definitions output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an\n# AutoGen Definitions (see http://autogen.sf.net) file that captures the\n# structure of the code including all documentation. Note that this feature is\n# still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_AUTOGEN_DEF   = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the Perl module output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module\n# file that captures the structure of the code including all documentation.\n#\n# Note that this feature is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_PERLMOD       = NO\n\n# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary\n# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI\n# output from the Perl module output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_LATEX          = NO\n\n# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely\n# formatted so it can be parsed by a human reader. This is useful if you want to\n# understand what is going on. On the other hand, if this tag is set to NO, the\n# size of the Perl module output will be much smaller and Perl will parse it\n# just the same.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_PRETTY         = YES\n\n# The names of the make variables in the generated doxyrules.make file are\n# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful\n# so different doxyrules.make files included by the same Makefile don't\n# overwrite each other's variables.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_MAKEVAR_PREFIX =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the preprocessor\n#---------------------------------------------------------------------------\n\n# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all\n# C-preprocessor directives found in the sources and include files.\n# The default value is: YES.\n\nENABLE_PREPROCESSING   = YES\n\n# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names\n# in the source code. If set to NO, only conditional compilation will be\n# performed. Macro expansion can be done in a controlled way by setting\n# EXPAND_ONLY_PREDEF to YES.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nMACRO_EXPANSION        = NO\n\n# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then\n# the macro expansion is limited to the macros specified with the PREDEFINED and\n# EXPAND_AS_DEFINED tags.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_ONLY_PREDEF     = NO\n\n# If the SEARCH_INCLUDES tag is set to YES, the include files in the\n# INCLUDE_PATH will be searched if a #include is found.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSEARCH_INCLUDES        = YES\n\n# The INCLUDE_PATH tag can be used to specify one or more directories that\n# contain include files that are not input files but should be processed by the\n# preprocessor.\n# This tag requires that the tag SEARCH_INCLUDES is set to YES.\n\nINCLUDE_PATH           =\n\n# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard\n# patterns (like *.h and *.hpp) to filter out the header-files in the\n# directories. If left blank, the patterns specified with FILE_PATTERNS will be\n# used.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nINCLUDE_FILE_PATTERNS  =\n\n# The PREDEFINED tag can be used to specify one or more macro names that are\n# defined before the preprocessor is started (similar to the -D option of e.g.\n# gcc). The argument of the tag is a list of macros of the form: name or\n# name=definition (no spaces). If the definition and the \"=\" are omitted, \"=1\"\n# is assumed. To prevent a macro definition from being undefined via #undef or\n# recursively expanded use the := operator instead of the = operator.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nPREDEFINED             =\n\n# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this\n# tag can be used to specify a list of macro names that should be expanded. The\n# macro definition that is found in the sources will be used. Use the PREDEFINED\n# tag if you want to use a different macro definition that overrules the\n# definition found in the source code.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_AS_DEFINED      =\n\n# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will\n# remove all references to function-like macros that are alone on a line, have\n# an all uppercase name, and do not end with a semicolon. Such function macros\n# are typically used for boiler-plate code, and will confuse the parser if not\n# removed.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSKIP_FUNCTION_MACROS   = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to external references\n#---------------------------------------------------------------------------\n\n# The TAGFILES tag can be used to specify one or more tag files. For each tag\n# file the location of the external documentation should be added. The format of\n# a tag file without this location is as follows:\n# TAGFILES = file1 file2 ...\n# Adding location for the tag files is done as follows:\n# TAGFILES = file1=loc1 \"file2 = loc2\" ...\n# where loc1 and loc2 can be relative or absolute paths or URLs. See the\n# section \"Linking to external documentation\" for more information about the use\n# of tag files.\n# Note: Each tag file must have a unique name (where the name does NOT include\n# the path). If a tag file is not located in the directory in which doxygen is\n# run, you must also specify the path to the tagfile here.\n\nTAGFILES               =\n\n# When a file name is specified after GENERATE_TAGFILE, doxygen will create a\n# tag file that is based on the input files it reads. See section \"Linking to\n# external documentation\" for more information about the usage of tag files.\n\nGENERATE_TAGFILE       =\n\n# If the ALLEXTERNALS tag is set to YES, all external class will be listed in\n# the class index. If set to NO, only the inherited external classes will be\n# listed.\n# The default value is: NO.\n\nALLEXTERNALS           = NO\n\n# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed\n# in the modules index. If set to NO, only the current project's groups will be\n# listed.\n# The default value is: YES.\n\nEXTERNAL_GROUPS        = YES\n\n# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in\n# the related pages index. If set to NO, only the current project's pages will\n# be listed.\n# The default value is: YES.\n\nEXTERNAL_PAGES         = YES\n\n# The PERL_PATH should be the absolute path and name of the perl script\n# interpreter (i.e. the result of 'which perl').\n# The default file (with absolute path) is: /usr/bin/perl.\n\nPERL_PATH              = /usr/bin/perl\n\n#---------------------------------------------------------------------------\n# Configuration options related to the dot tool\n#---------------------------------------------------------------------------\n\n# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram\n# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to\n# NO turns the diagrams off. Note that this option also works with HAVE_DOT\n# disabled, but it is recommended to install and use dot, since it yields more\n# powerful graphs.\n# The default value is: YES.\n\nCLASS_DIAGRAMS         = YES\n\n# You can define message sequence charts within doxygen comments using the \\msc\n# command. Doxygen will then run the mscgen tool (see:\n# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the\n# documentation. The MSCGEN_PATH tag allows you to specify the directory where\n# the mscgen tool resides. If left empty the tool is assumed to be found in the\n# default search path.\n\nMSCGEN_PATH            =\n\n# You can include diagrams made with dia in doxygen documentation. Doxygen will\n# then run dia to produce the diagram and insert it in the documentation. The\n# DIA_PATH tag allows you to specify the directory where the dia binary resides.\n# If left empty dia is assumed to be found in the default search path.\n\nDIA_PATH               =\n\n# If set to YES the inheritance and collaboration graphs will hide inheritance\n# and usage relations if the target is undocumented or is not a class.\n# The default value is: YES.\n\nHIDE_UNDOC_RELATIONS   = NO\n\n# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is\n# available from the path. This tool is part of Graphviz (see:\n# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent\n# Bell Labs. The other options in this section have no effect if this option is\n# set to NO\n# The default value is: NO.\n\nHAVE_DOT               = @HAVE_DOT@\n\n# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed\n# to run in parallel. When set to 0 doxygen will base this on the number of\n# processors available in the system. You can set it explicitly to a value\n# larger than 0 to get control over the balance between CPU load and processing\n# speed.\n# Minimum value: 0, maximum value: 32, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NUM_THREADS        = 0\n\n# When you want a differently looking font in the dot files that doxygen\n# generates you can specify the font name using DOT_FONTNAME. You need to make\n# sure dot is able to find the font, which can be done by putting it in a\n# standard location or by setting the DOTFONTPATH environment variable or by\n# setting DOT_FONTPATH to the directory containing the font.\n# The default value is: Helvetica.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTNAME           = Helvetica\n\n# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of\n# dot graphs.\n# Minimum value: 4, maximum value: 24, default value: 10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTSIZE           = 10\n\n# By default doxygen will tell dot to use the default font as specified with\n# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set\n# the path where dot can find it using this tag.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTPATH           =\n\n# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for\n# each documented class showing the direct and indirect inheritance relations.\n# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCLASS_GRAPH            = YES\n\n# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a\n# graph for each documented class showing the direct and indirect implementation\n# dependencies (inheritance, containment, and class references variables) of the\n# class with other documented classes.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCOLLABORATION_GRAPH    = YES\n\n# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for\n# groups, showing the direct groups dependencies.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGROUP_GRAPHS           = YES\n\n# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and\n# collaboration diagrams in a style similar to the OMG's Unified Modeling\n# Language.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LOOK               = YES\n\n# If the UML_LOOK tag is enabled, the fields and methods are shown inside the\n# class node. If there are many fields or methods and many nodes the graph may\n# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the\n# number of items for each type to make the size more manageable. Set this to 0\n# for no limit. Note that the threshold may be exceeded by 50% before the limit\n# is enforced. So when you set the threshold to 10, up to 15 fields may appear,\n# but if the number exceeds 15, the total amount of fields shown is limited to\n# 10.\n# Minimum value: 0, maximum value: 100, default value: 10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LIMIT_NUM_FIELDS   = 50\n\n# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and\n# collaboration graphs will show the relations between templates and their\n# instances.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nTEMPLATE_RELATIONS     = YES\n\n# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to\n# YES then doxygen will generate a graph for each documented file showing the\n# direct and indirect include dependencies of the file with other documented\n# files.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDE_GRAPH          = YES\n\n# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are\n# set to YES then doxygen will generate a graph for each documented file showing\n# the direct and indirect include dependencies of the file with other documented\n# files.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDED_BY_GRAPH      = YES\n\n# If the CALL_GRAPH tag is set to YES then doxygen will generate a call\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable call graphs for selected\n# functions only using the \\callgraph command. Disabling a call graph can be\n# accomplished by means of the command \\hidecallgraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALL_GRAPH             = YES\n\n# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable caller graphs for selected\n# functions only using the \\callergraph command. Disabling a caller graph can be\n# accomplished by means of the command \\hidecallergraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALLER_GRAPH           = YES\n\n# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical\n# hierarchy of all classes instead of a textual one.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGRAPHICAL_HIERARCHY    = YES\n\n# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the\n# dependencies a directory has on other directories in a graphical way. The\n# dependency relations are determined by the #include relations between the\n# files in the directories.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDIRECTORY_GRAPH        = YES\n\n# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images\n# generated by dot. For an explanation of the image formats see the section\n# output formats in the documentation of the dot tool (Graphviz (see:\n# http://www.graphviz.org/)).\n# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order\n# to make the SVG files visible in IE 9+ (other browsers do not have this\n# requirement).\n# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,\n# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and\n# png:gdiplus:gdiplus.\n# The default value is: png.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_IMAGE_FORMAT       = svg\n\n# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to\n# enable generation of interactive SVG images that allow zooming and panning.\n#\n# Note that this requires a modern browser other than Internet Explorer. Tested\n# and working are Firefox, Chrome, Safari, and Opera.\n# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make\n# the SVG files visible. Older versions of IE do not have SVG support.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINTERACTIVE_SVG        = YES\n\n# The DOT_PATH tag can be used to specify the path where the dot tool can be\n# found. If left blank, it is assumed the dot tool can be found in the path.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_PATH               = @DOT_EXECUTABLE@\n\n# The DOTFILE_DIRS tag can be used to specify one or more directories that\n# contain dot files that are included in the documentation (see the \\dotfile\n# command).\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOTFILE_DIRS           =\n\n# The MSCFILE_DIRS tag can be used to specify one or more directories that\n# contain msc files that are included in the documentation (see the \\mscfile\n# command).\n\nMSCFILE_DIRS           =\n\n# The DIAFILE_DIRS tag can be used to specify one or more directories that\n# contain dia files that are included in the documentation (see the \\diafile\n# command).\n\nDIAFILE_DIRS           =\n\n# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the\n# path where java can find the plantuml.jar file. If left blank, it is assumed\n# PlantUML is not used or called during a preprocessing step. Doxygen will\n# generate a warning when it encounters a \\startuml command in this case and\n# will not generate output for the diagram.\n\nPLANTUML_JAR_PATH      =\n\n# When using plantuml, the specified paths are searched for files specified by\n# the !include statement in a plantuml block.\n\nPLANTUML_INCLUDE_PATH  =\n\n# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes\n# that will be shown in the graph. If the number of nodes in a graph becomes\n# larger than this value, doxygen will truncate the graph, which is visualized\n# by representing a node as a red box. Note that doxygen if the number of direct\n# children of the root node in a graph is already larger than\n# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that\n# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.\n# Minimum value: 0, maximum value: 10000, default value: 50.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_GRAPH_MAX_NODES    = 500\n\n# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs\n# generated by dot. A depth value of 3 means that only nodes reachable from the\n# root by following a path via at most 3 edges will be shown. Nodes that lay\n# further from the root node will be omitted. Note that setting this option to 1\n# or 2 may greatly reduce the computation time needed for large code bases. Also\n# note that the size of a graph can be further restricted by\n# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.\n# Minimum value: 0, maximum value: 1000, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nMAX_DOT_GRAPH_DEPTH    = 0\n\n# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent\n# background. This is disabled by default, because dot on Windows does not seem\n# to support this out of the box.\n#\n# Warning: Depending on the platform used, enabling this option may lead to\n# badly anti-aliased labels on the edges of a graph (i.e. they become hard to\n# read).\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_TRANSPARENT        = NO\n\n# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output\n# files in one run (i.e. multiple -o and -T options on the command line). This\n# makes dot run faster, but since only newer versions of dot (>1.8.10) support\n# this, this feature is disabled by default.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_MULTI_TARGETS      = NO\n\n# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page\n# explaining the meaning of the various boxes and arrows in the dot generated\n# graphs.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGENERATE_LEGEND        = YES\n\n# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot\n# files that are used to generate the various graphs.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_CLEANUP            = YES\n"
  },
  {
    "path": "buildsystem/templates/ExternalFetch.cmake.in",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n# @AUTOGEN_WARNING@\n\n# Template file because the cmake ExternalProject can not download projects\n# in the configure phase.\n# -> in the configure phase, run cmake again to download the project.\n#\n# this is the file used to tell the nested cmake run what to do.\n\ncmake_minimum_required(VERSION 3.6)\n\nproject(@PROJ_NAME@-external LANGUAGES)\n\ninclude(ExternalProject)\nExternalProject_Add(@PROJ_NAME@-fetch\n\tPREFIX              \"@PROJ_DIR@\"\n\tSOURCE_DIR          \"@PROJ_SRC_DIR@\"\n\tBINARY_DIR          \"@PROJ_BIN_DIR@\"\n\tSTAMP_DIR           \"@PROJ_STAMP_DIR@\"\n\tCONFIGURE_COMMAND   \"\"\n\tBUILD_COMMAND       \"\"\n\tINSTALL_COMMAND     \"\"\n\tTEST_COMMAND        \"\"\n\tUPDATE_DISCONNECTED \"@PROJ_DISABLE_UPDATES@\"\n\t@PROJ_UNPARSED_ARGUMENTS@\n)\n\n# silence target-reached status messages\nset_property(GLOBAL PROPERTY RULE_MESSAGES OFF)\nset_property(GLOBAL PROPERTY TARGET_MESSAGES OFF)\n"
  },
  {
    "path": "buildsystem/templates/ForwardVariables.cmake.in",
    "content": "# Copyright 2017-2020 the openage authors. See copying.md for legal info.\n\n# @AUTOGEN_WARNING@\n\n# This script passes (forwards) variables generated at CMake configuration\n# to be made available in CPack - install(SCRIPT ...) scripts.\n\n# NOTE: the following variables should never be forwarded:\n#  - `CMAKE_INSTALL_PREFIX`: defined separately by CPack; should not be overwritten.\n\n# The following variables are defined at cmake generation time\nset(asset_dir \"@ASSET_DIR@\")\nset(buildsystem_dir \"@BUILDSYSTEM_DIR@\")\nset(cmake_linker \"@CMAKE_LINKER@\")\nset(downloads_dir \"@CMAKE_BINARY_DIR@/downloads\")\nset(logfiles_dir \"@CMAKE_BINARY_DIR@/logfiles\")\nset(python \"@PYTHON@\")\nset(py_install_prefix \"@CMAKE_PY_INSTALL_PREFIX@\")\nset(py_version \"@PYTHON_VERSION_STRING@\")\nset(sizeof_void_p @CMAKE_SIZEOF_VOID_P@)\nset(vcpkg_dir \"@vcpkg_dir@\")\nset(windeployqt \"@windeployqt@\")\n\n# Use windeploy for packaging qt-prebuilt, standard value '1' for windeploy, '0' for vcpkg\nset(use_windeployqt 1)\n\n# the following denotes inclusion of this script.\nset(forward_variables 1)\n"
  },
  {
    "path": "buildsystem/templates/openage.bat.in",
    "content": "@rem Copyright 2017-2017 the openage authors. See copying.md for legal info.\n@echo off\n\nrem @AUTOGEN_WARNING@\n\nset INST_DIR=%CD%\nset PATH=%INST_DIR%\\@CMAKE_INSTALL_BINDIR@;\nset XDG_DATA_HOME=%INST_DIR%\\share\n\ncall \"%INST_DIR%\\@CMAKE_PY_INSTALL_PREFIX@\\python.exe\" -m openage\n"
  },
  {
    "path": "buildsystem/templates/qt.conf",
    "content": "[Paths]\nPlugins = ..\\\\bin\\\\plugins\nQml2Imports = ..\\\\bin\\\\qml\n"
  },
  {
    "path": "buildsystem/util.cmake",
    "content": "# Copyright 2014-2016 the openage authors. See copying.md for legal info.\n\nset(AUTOGEN_WARNING \"warning: auto-generated file by cmake. look at the template file instead.\")\n\nfunction(pretty_print_target type targetname targetproperties)\n\t# pretty-prints a new target; tries to group target types to make stuff look more clean.\n\tget_property(previous_type GLOBAL PROPERTY SFT_TARGETPRINTER_CURRENT_TYPE)\n\tif(NOT \"${type}\" STREQUAL \"${previous_type}\")\n\t\tmessage(\"\")\n\t\tmessage(\"${type}\")\n\tendif()\n\tset_property(GLOBAL PROPERTY SFT_TARGETPRINTER_CURRENT_TYPE \"${type}\")\n\n\tstring(STRIP \"${targetname}\" targetname)\n\tstring(STRIP \"${targetproperties}\" targetproperties)\n\n\tstring(LENGTH \"${targetname}\" targetnamelength)\n\tforeach(tmp RANGE ${targetnamelength} 50)\n\t\tset(targetname \"${targetname} \")\n\tendforeach()\n\n\tset(pretty_message \"${targetname} ${targetproperties}\")\n\tstring(STRIP \"${pretty_message}\" pretty_message)\n\tmessage(\"\\t${pretty_message}\")\nendfunction()\n\n\nfunction(write_on_change filename content)\n\t# write the content to filename if it is different than\n\t# what's already in that file (if exists)\n\t# if the file doesn't exist, create it.\n\n\tunset(current_content)\n\n\tif(EXISTS ${filename})\n\t\tfile(READ ${filename} current_content)\n\tendif()\n\n\tif(NOT current_content STREQUAL content)\n\t\tfile(WRITE \"${filename}\" \"${content}\")\n\tendif()\nendfunction()\n"
  },
  {
    "path": "cfg/.gitignore",
    "content": "asset_location\n"
  },
  {
    "path": "cfg/CMakeLists.txt",
    "content": "# Copyright 2017-2021 the openage authors. See copying.md for legal info.\n\ninstall(FILES\n\t\"${CMAKE_CURRENT_SOURCE_DIR}/keybinds.oac\"\n\tDESTINATION \"${GLOBAL_CONFIG_DIR}\"\n)\n\ninstall(DIRECTORY\n\t\"${CMAKE_CURRENT_SOURCE_DIR}/converter\"\n\tDESTINATION \"${GLOBAL_CONFIG_DIR}/\"\n)\n"
  },
  {
    "path": "cfg/converter/games/aoc/version_hashes.toml",
    "content": "# version hashes of Age of Empires 2: The Conqueror's\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[age2_x1]\npaths = [\"age2_x1/age2_x1.exe\"]\n\n  [age2_x1.map]\n  d02ef2f4935c5ef568eaa873442b4b2b547067ed8e3572758bbbbee8 = \"1.0c\"\n  4568f37f309c0a89fa9e84a0c0f6c61b0f394c28adc82c0b3f2203b3 = \"1.0c\"\n\n[empires2_x1_p1dat]\npaths = [\"Data/empires2_x1_p1.dat\"]\n\n  [empires2_x1_p1dat.map]\n  6aab4c7468c3d319ec00f577835dc7799b70ffab1de6bfd25ac227b5 = \"1.0c\"\n"
  },
  {
    "path": "cfg/converter/games/aoc_demo/version_hashes.toml",
    "content": "# version hashes of Age of Empires 2: The Conqueror's Demo\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[age2_x1t]\npaths = [\"age2_x1t.exe\"]\n\n  [age2_x1t.map]\n  d02ef2f4935c5ef568eaa873442b4b2b547067ed8e3572758bbbbee8 = \"1.0c\"\n  4568f37f309c0a89fa9e84a0c0f6c61b0f394c28adc82c0b3f2203b3 = \"1.0c\"\n\n[empires2_x1dat]\npaths = [\"Data/empires2_x1.dat\"]\n\n  [empires2_x1dat.map]\n  6aab4c7468c3d319ec00f577835dc7799b70ffab1de6bfd25ac227b5 = \"1.0c\"\n"
  },
  {
    "path": "cfg/converter/games/aok/version_hashes.toml",
    "content": "# version hashes of Age of Empires 2: Age of Kings\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[empires2]\npaths = [\"empires2.exe\"]\n\n  [empires2.map]\n  0425273b541b8405325775a4bae48d078db7444a94888943148b4e1a = \"2.0a\"\n\n[empires2dat]\npaths = [\"data/empires2.dat\"]\n\n  [empires2dat.map]\n  f7a9d214e16182747bd1c3e3302398740609413407aeffd32a0acdd8 = \"2.0a\"\n"
  },
  {
    "path": "cfg/converter/games/de1/version_hashes.toml",
    "content": "# version hashes of Age of Empires 1: Definitive Edition (Steam)\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[AoEDE_s]\npaths = [\"AoEDE_s.exe\", \"AoEDE.exe\"]\n\n  [AoEDE_s.map]\n  3356c013f2cb732601e1acf286654e4360d7e7304a092bdbeaa63d48 = \"steam\"\n\n[empiresdat]\npaths = [\"Data/empires.dat\"]\n\n  [empiresdat.map]\n  4d1f3cb7ce20074bfa23445c62755b95e4ec193b41507d5196921033 = \"steam\"\n"
  },
  {
    "path": "cfg/converter/games/de2/version_hashes.toml",
    "content": "# version hashes of Age of Empires 2: Definitive Edition\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[AoE2DE_s]\npaths = [\"AoE2DE_s.exe\", \"AoE2DE.exe\"]\n\n  [AoE2DE_s.map]\n  01ff7cc7819e4f77be059c8efc6c07472dd1a58f429621e937328750 = \"36906\"\n\n[empires2_x2_p1dat]\npaths = [\"resources/_common/dat/empires2_x2_p1.dat\"]\n\n  [empires2_x2_p1dat.map]\n  cf13c553c78df192da1af957d1519870db15c9dbca314b245a5abe6f = \"36906\"\n"
  },
  {
    "path": "cfg/converter/games/game_editions.toml",
    "content": "# game edition config file for openage\n\nfile_version = \"1.1\"\n\n[AOC]\nname            = \"Age of Empires 2: The Conqueror's\"\ngame_edition_id = \"AOC\"\nsubfolder       = \"aoc\"\nsupport         = \"yes\"\ntargetmod       = [\"aoe2_base\", \"aoe2_base_graphics\"]\nexpansions      = []\n\n  [AOC.mediapaths]\n  datfile   = [\"data/empires2_x1_p1.dat\"]\n  gamedata  = [\"data/gamedata_x1_p1.drs\"]\n  graphics  = [\"data/graphics.drs\"]\n  language  = [\"language.dll\", \"language_x1.dll\", \"language_x1_p1.dll\"]\n  palettes  = [\"data/interfac.drs\"]\n  sounds    = [\"data/sounds.drs\", \"data/sounds_x1.drs\"]\n  interface = [\"data/interfac.drs\"]\n  terrain   = [\"data/terrain.drs\"]\n  blend     = [\"data/blendomatic.dat\"]\n\n  [AOC.installpaths]\n  linux = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II\",\n    # Lutris\n    \"~/Games/age-of-empires-ii-the-conquerors\",\n  ]\n  macos = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II\",\n  ]\n  windows = [\n    \"C:/Program Files/Microsoft Games/Age of Empires II\",\n    \"C:/Program Files (x86)/Microsoft Games/Age of Empires II\",\n  ]\n\n  [AOC.targetmods.aoe2_base]\n  version         = \"0.5.1\"\n  versionstr      = \"1.0c\"\n  min_api_version = \"0.5.0\"\n\n\n[AOCDEMO]\nname            = \"Age of Empires 2: The Conqueror's Trial Version\"\ngame_edition_id = \"AOCDEMO\"\nsubfolder       = \"aoc_demo\"\nsupport         = \"yes\"\ntargetmod       = [\"trial_base\", \"trial_graphics\"]\nexpansions      = []\n\n  [AOCDEMO.mediapaths]\n  datfile   = [\"data/empires2_x1.dat\"]\n  gamedata  = [\"data/gamedata_x1.drs\"]\n  graphics  = [\"data/graphics.drs\"]\n  language  = [\"language.dll\", \"language_x1.dll\"]\n  palettes  = [\"data/interfac.drs\"]\n  sounds    = [\"data/sounds_x1.drs\"]\n  interface = [\"data/interfac.drs\"]\n  terrain   = [\"data/Terrain.drs\"]\n  blend     = [\"data/blendomatic.dat\"]\n\n  [AOCDEMO.targetmods.trial_base]\n  version         = \"0.5.1\"\n  versionstr      = \"Trial\"\n  min_api_version = \"0.5.0\"\n\n\n[AOK]\nname            = \"Age of Empires 2: Age of Kings\"\ngame_edition_id = \"AOK\"\nsubfolder       = \"aok\"\nsupport         = \"nope\"\ntargetmod       = []\nexpansions      = []\n\n  [AOK.mediapaths]\n  datfile   = [\"data/empires2.dat\"]\n  gamedata  = [\"data/gamedata.drs\"]\n  graphics  = [\"data/graphics.drs\"]\n  palettes  = [\"data/interfac.drs\"]\n  sounds    = [\"data/sounds.drs\"]\n  interface = [\"data/interfac.drs\"]\n  terrain   = [\"data/terrain.drs\"]\n\n  [AOK.installpaths]\n  linux = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II\",\n    # Lutris\n    \"~/Games/age-of-empires-ii-the-conquerors\",\n  ]\n  macos = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires II\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires II\",\n  ]\n  windows = [\n    \"C:/Program Files/Microsoft Games/Age of Empires II\",\n    \"C:/Program Files (x86)/Microsoft Games/Age of Empires II\",\n  ]\n\n  [AOK.targetmods]\n\n\n[AOE1DE]\nname            = \"Age of Empires 1: Definitive Edition (Steam)\"\ngame_edition_id = \"AOE1DE\"\nsubfolder       = \"de1\"\nsupport         = \"yes\"\ntargetmod       = [\"de1_base\", \"de1_base_graphics\"]\nexpansions      = []\n\n  [AOE1DE.mediapaths]\n  datfile = [\"Data/empires.dat\"]\n  graphics = [\"Assets/SLP/\"]\n  palettes = [\"Assets/Palettes/\"]\n  sounds = [\"Assets/Sounds/\"]\n  interface = [\"Data/DRS/interfac.drs\", \"Data/DRS/interfac_x1.drs\"]\n  language = [\n    \"Data/Localization/br/strings.txt\",\n    \"Data/Localization/de/strings.txt\",\n    \"Data/Localization/en/strings.txt\",\n    \"Data/Localization/es/strings.txt\",\n    \"Data/Localization/fr/strings.txt\",\n    \"Data/Localization/hi/strings.txt\",\n    \"Data/Localization/it/strings.txt\",\n    \"Data/Localization/jp/strings.txt\",\n    \"Data/Localization/ko/strings.txt\",\n    \"Data/Localization/mx/strings.txt\",\n    \"Data/Localization/ru/strings.txt\",\n    \"Data/Localization/vi/strings.txt\",\n    \"Data/Localization/zhs/strings.txt\",\n    \"Data/Localization/zht/strings.txt\",\n  ]\n  terrain = [\"Assets/SLP/\"]\n\n  [AOE1DE.installpaths]\n  linux = [\"~/.steam/steam/steamapps/common/AoEDE\"]\n  macos = [\"~/.steam/steam/steamapps/common/AoEDE\"]\n  windows = [\n    \"C:/Program Files/Steam/steamapps/common/AoEDE\",\n    \"C:/Program Files (x86)/Steam/steamapps/common/AoEDE\",\n  ]\n\n  [AOE1DE.targetmods.de1_base]\n  version         = \"0.5.1\"\n  versionstr      = \"1.0a\"\n  min_api_version = \"0.5.0\"\n\n\n[ROR]\nname            = \"Age of Empires 1: Rise of Rome\"\ngame_edition_id = \"ROR\"\nsubfolder       = \"ror\"\nsupport         = \"yes\"\ntargetmod       = [\"aoe1_base\", \"aoe1_base_graphics\"]\nexpansions      = []\n\n  [ROR.mediapaths]\n  datfile   = [\"data2/empires.dat\"]\n  graphics  = [\"data/graphics.drs\", \"data2/graphics.drs\"]\n  palettes  = [\"data/Interfac.drs\", \"data2/Interfac.drs\"]\n  sounds    = [\"data/sounds.drs\", \"data2/sounds.drs\"]\n  interface = [\"data/Interfac.drs\", \"data2/Interfac.drs\"]\n  language  = [\"language.dll\", \"languagex.dll\"]\n  terrain   = [\"data/Terrain.drs\"]\n  border    = [\"data/Border.drs\"]\n\n  [ROR.installpaths]\n  linux = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires\",\n    # Lutris\n    \"~/Games/age-of-empires\",\n  ]\n  macos = [\n    \"~/.wine/drive_c/Program Files/Microsoft Games/Age of Empires\",\n    \"~/.wine/drive_c/Program Files (x86)/Microsoft Games/Age of Empires\",\n  ]\n  windows = [\n    \"C:/Program Files/Microsoft Games/Age of Empires\",\n    \"C:/Program Files (x86)/Microsoft Games/Age of Empires\",\n  ]\n\n  [ROR.targetmods.aoe1_base]\n  version         = \"0.5.1\"\n  versionstr      = \"1.0a\"\n  min_api_version = \"0.5.0\"\n\n\n[HDEDITION]\nname            = \"Age of Empires 2: HD Edition\"\ngame_edition_id = \"HDEDITION\"\nsubfolder       = \"hd\"\nsupport         = \"yes\"\ntargetmod       = [\"hd_base\", \"hd_base_graphics\"]\nexpansions      = []\n\n  [HDEDITION.mediapaths]\n  datfile = [\"resources/_common/dat/empires2_x1_p1.dat\"]\n  gamedata = [\"resources/_common/drs/gamedata_x1/\"]\n  graphics = [\"resources/_common/drs/graphics/\"]\n  palettes = [\"resources/_common/drs/interface/\"]\n  sounds = [\"resources/_common/drs/sounds/\"]\n  interface = [\"resources/_common/drs/interface/\"]\n  language = [\n    \"resources/br/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/de/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/en/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/es/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/fr/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/it/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/jp/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/ko/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/nl/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/ru/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/zh/strings/key-value/key-value-strings-utf8.txt\",\n  ]\n  terrain = [\"resources/_common/terrain/textures/\"]\n\n  [HDEDITION.installpaths]\n  linux = [\"~/.steam/steam/steamapps/common/Age2HD\"]\n  macos = [\"~/.steam/steam/steamapps/common/Age2HD\"]\n  windows = [\n    \"C:/Program Files/Steam/steamapps/common/Age2HD\",\n    \"C:/Program Files (x86)/Steam/steamapps/common/Age2HD\",\n  ]\n\n  [HDEDITION.targetmods.hd_base]\n  version         = \"0.5.1\"\n  versionstr      = \"5.8\"\n  min_api_version = \"0.5.0\"\n\n\n[AOE2DE]\nname            = \"Age of Empires 2: Definitive Edition\"\ngame_edition_id = \"AOE2DE\"\nsubfolder       = \"de2\"\nsupport         = \"yes\"\ntargetmod       = [\"de2_base\", \"de2_base_graphics\"]\nexpansions      = []\n\n  [AOE2DE.mediapaths]\n  datfile = [\"resources/_common/dat/empires2_x2_p1.dat\"]\n  gamedata = [\"resources/_common/drs/gamedata_x2/\"]\n  graphics = [\"resources/_common/drs/graphics/\"]\n  language = [\n    \"resources/br/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/de/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/en/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/es/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/fr/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/hi/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/it/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/jp/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/ko/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/ms/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/mx/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/ru/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/tr/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/tw/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/vi/strings/key-value/key-value-strings-utf8.txt\",\n    \"resources/zh/strings/key-value/key-value-strings-utf8.txt\",\n  ]\n  palettes = [\"resources/_common/palettes/\"]\n  sounds = [\"wwise/\"]\n  interface = [\"resources/_common/drs/interface/\"]\n  terrain = [\"resources/_common/terrain/textures/\"]\n\n  [AOE2DE.installpaths]\n  linux = [\"~/.steam/steam/steamapps/common/AoE2DE\"]\n  macos = [\"~/.steam/steam/steamapps/common/AoE2DE\"]\n  windows = [\n    \"C:/Program Files/Steam/steamapps/common/AoE2DE\",\n    \"C:/Program Files (x86)/Steam/steamapps/common/AoE2DE\",\n  ]\n\n  [AOE2DE.targetmods.de2_base]\n  version         = \"0.6.0\"\n  versionstr      = \"Update 118476+\"\n  min_api_version = \"0.5.0\"\n\n\n[SWGB]\nname            = \"Star Wars: Galactic Battlegrounds\"\ngame_edition_id = \"SWGB\"\nsubfolder       = \"swgb\"\nsupport         = \"yes\"\ntargetmod       = [\"swgb_base\", \"swgb_base_graphics\"]\nexpansions      = [\"SWGB_CC\"]\n\n  [SWGB.mediapaths]\n  datfile   = [\"Game/Data/GENIE.DAT\"]\n  gamedata  = [\"Game/Data/GAMEDATA.DRS\"]\n  graphics  = [\"Game/Data/GRAPHICS.DRS\"]\n  language  = [\"Game/language.dll\"]\n  palettes  = [\"Game/Data/INTERFAC.DRS\"]\n  sounds    = [\"Game/Data/SOUNDS.DRS\"]\n  interface = [\"Game/Data/INTERFAC.DRS\"]\n  terrain   = [\"Game/Data/TERRAIN.DRS\"]\n  blend     = [\"Game/Data/blendomatic.dat\"]\n\n  [SWGB.installpaths]\n  linux = [\n    \"~/.wine/drive_c/GOG Games/Star Wars - Galactic Battlegrounds\",\n    \"~/.steam/steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga\",\n    # Lutris\n    \"~/Games/gog/star-wars-galactic-battlegrounds-saga\",\n  ]\n  macos = [\n    \"~/.wine/drive_c/GOG Games/Star Wars - Galactic Battlegrounds\",\n    \"~/.steam/steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga\",\n  ]\n  windows = [\n    \"C:/GOG Games/Star Wars - Galactic Battlegrounds\",\n    \"C:/Program Files (x86)/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga\",\n    \"C:/Program Files/Steam/steamapps/common/STAR WARS - Galactic Battlegrounds Saga\",\n  ]\n\n  [SWGB.targetmods.swgb_base]\n  version         = \"0.5.1\"\n  versionstr      = \"1.1-gog4\"\n  min_api_version = \"0.5.0\"\n"
  },
  {
    "path": "cfg/converter/games/game_expansions.toml",
    "content": "# game expansion config file for openage\n\nfile_version = \"1.0\"\n\n[AFRI_KING]\nname            = \"African Kingdoms (HD)\"\ngame_edition_id = \"AFRI_KING\"\nsubfolder       = \"hd_ak\"\nsupport         = \"nope\"\ntargetmod       = [\"aoe2-ak\", \"aoe2-ak-graphics\"]\n\n  [AFRI_KING.mediapaths]\n  graphics  = [\"resources/_common/slp/\"]\n  sounds    = [\"resources/_common/sound/\"]\n  interface = [\"resources/_common/drs/interface/\"]\n  terrain   = [\"resources/_common/terrain/\"]\n\n  [AFRI_KING.targetmods]\n\n\n[SWGB_CC]\nname            = \"Clone Campaigns\"\ngame_edition_id = \"SWGB_CC\"\nsubfolder       = \"swgb_cc\"\nsupport         = \"yes\"\ntargetmod       = [\"swgb-cc\", \"swgb-cc-graphics\"]\n\n  [SWGB_CC.mediapaths]\n  datfile   = [\"Game/Data/genie_x1.dat\"]\n  gamedata  = [\"Game/Data/genie_x1.dat\"]\n  graphics  = [\"Game/Data/graphics_x1.drs\"]\n  language  = [\"Game/language_x1.dll\"]\n  palettes  = [\"Game/Data/interfac_x1.drs\"]\n  sounds    = [\"Game/Data/sounds_x1.drs\"]\n  interface = [\"Game/Data/interfac_x1.drs\"]\n  terrain   = [\"Game/Data/terrain_x1.drs\"]\n\n  [SWGB_CC.targetmods]\n"
  },
  {
    "path": "cfg/converter/games/hd/version_hashes.toml",
    "content": "# version hashes of Age of Empires 2: HD Edition\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[\"AoK HD\"]\npaths = [\"AoK HD.exe\"]\n\n  [\"AoK HD\".map]\n  73a068f353efd50e058762ca090062600a7ca50e435fe55a6ca87efa = \"5.8\"\n\n[empires2_x1_p1dat]\npaths = [\"resources/_common/dat/empires2_x1_p1.dat\"]\n\n  [empires2_x1_p1dat.map]\n  91f7370b75b2fadf238b25f6fdd2f43a88a1f01ea022cd6086e44f43 = \"5.8\"\n"
  },
  {
    "path": "cfg/converter/games/hd_ak/version_hashes.toml",
    "content": "# version hashes of African Kingdoms (HD)\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[empires2_x2_p1dat]\npaths = [\"resources/_common/dat/empires2_x2_p1.dat\"]\n\n  [empires2_x2_p1dat.map]\n  b2a69980a1ef39e68de2be1771392858cb639a9f2797373ab1649acf = \"5.8\"\n"
  },
  {
    "path": "cfg/converter/games/hd_fgt/version_hashes.toml",
    "content": "# version hashes of Forgotten Empires (HD)\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[empires2_x2_p1dat]\npaths = [\"resources/_common/dat/empires2_x2_p1.dat\"]\n\n  [empires2_x2_p1dat.map]\n  b2a69980a1ef39e68de2be1771392858cb639a9f2797373ab1649acf = \"5.8\"\n"
  },
  {
    "path": "cfg/converter/games/hd_raj/version_hashes.toml",
    "content": "# version hashes of Rise of the Rajas (HD)\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[empires2_x2_p1dat]\npaths = [\"resources/_common/dat/empires2_x2_p1.dat\"]\n\n  [empires2_x2_p1dat.map]\n  b2a69980a1ef39e68de2be1771392858cb639a9f2797373ab1649acf = \"5.8\"\n"
  },
  {
    "path": "cfg/converter/games/ror/version_hashes.toml",
    "content": "# version hashes of Age of Empires 1: Rise of Rome\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[EMPIRESX]\npaths = [\"EMPIRESX.EXE\"]\n\n  [EMPIRESX.map]\n  bd55a288136ea5457c61fbc794e3e4a4ac526b1d53809f1b16762119 = \"1.0B\"\n\n[empiresdat]\npaths = [\"data2/empires.dat\"]\n\n  [empiresdat.map]\n  0376368ce4c78be31596dea7b76f592b2352f6b5c3aca222b7939b4a = \"1.0B\"\n"
  },
  {
    "path": "cfg/converter/games/swgb/version_hashes.toml",
    "content": "# version hashes of Star Wars: Galactic Battlegrounds\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[Battlegrounds]\npaths = [\"Game/Battlegrounds.exe\"]\n\n  [Battlegrounds.map]\n  0a5e2b7828b5b0a45cb6dfbf52e3becb2021e97259ba23a5cee94230 = \"GOG\"\n\n[GENIE]\npaths = [\"Game/Data/GENIE.DAT\"]\n\n  [GENIE.map]\n  f1f641f201dac9d7663df4cf1ece7a60a78770a09e70e202399505d9 = \"GOG\"\n"
  },
  {
    "path": "cfg/converter/games/swgb_cc/version_hashes.toml",
    "content": "# version hashes of Clone Campaigns\n\nfile_version = \"2.0\"\nhash_algo = \"SHA3-256\"\n\n[battlegrounds_x1]\npaths = [\"Game/battlegrounds_x1.exe\"]\n\n  [battlegrounds_x1.map]\n  fb722624bae8a4626925926d5709e1397766dd3ee2dcd1df7849ad27 = \"GOG\"\n\n[genie_x1dat]\npaths = [\"Game/Data/genie_x1.dat\"]\n\n  [genie_x1dat.map]\n  036a16aefd1a66f5bad121786e66b8cc9542cbe88b43c4eb8f73eadb = \"GOG\""
  },
  {
    "path": "cfg/keybinds.oac",
    "content": "# Copyright 2016-2016 the openage authors. See copying.md for legal info.\n\nset TOGGLE_CONSOLE `\nset START_GAME Return\nset STOP_GAME Shift Escape\nset TOGGLE_HUD F1\nset SCREENSHOT F2\nset TOGGLE_DEBUG_OVERLAY F3\nset TOGGLE_DEBUG_GRID F4\nset QUICK_SAVE F5\nset QUICK_LOAD F9\nset TOGGLE_PROFILER F12\nset TOGGLE_BLENDING Space \nset TOGGLE_CONSTRUCT_MODE m\nset TOGGLE_UNIT_DEBUG p\nset TRAIN_OBJECT t\nset ENABLE_BUILDING_PLACEMENT y\nset DISABLE_SET_ABILITY Ctrl z\nset SET_ABILITY_MOVE Ctrl x\nset SET_ABILITY_GATHER Ctrl c\nset SET_ABILITY_GARRISON g\nset SPAWN_VILLAGER v\nset KILL_UNIT Delete\nset BUILD_MENU q\nset BUILD_MENU_MIL w\nset CANCEL Escape\nset BUILDING_HOUS q\nset BUILDING_MILL w\nset BUILDING_MINE e\nset BUILDING_SMIL r\nset BUILDING_DOCK t\nset BUILDING_FARM a\nset BUILDING_BLAC s\nset BUILDING_MRKT d\nset BUILDING_CRCH f\nset BUILDING_UNIV g\nset BUILDING_RTWC z\nset BUILDING_WNDR x\nset BUILDING_BRKS q\nset BUILDING_ARRG w\nset BUILDING_STBL e\nset BUILDING_SIWS r\nset BUILDING_WCTWX t\nset BUILDING_WALL a\nset BUILDING_WALL2 s\nset BUILDING_WCTW d\nset BUILDING_WCTW4 f\nset BUILDING_GTCA2 g\nset BUILDING_CSTL z\nset SWITCH_TO_PLAYER_1 1\nset SWITCH_TO_PLAYER_2 2\nset SWITCH_TO_PLAYER_3 3\nset SWITCH_TO_PLAYER_4 4\nset SWITCH_TO_PLAYER_5 5\nset SWITCH_TO_PLAYER_6 6\nset SWITCH_TO_PLAYER_7 7\nset SWITCH_TO_PLAYER_8 8\nset UP_ARROW Up\nset DOWN_ARROW Down\nset LEFT_ARROW Left\nset RIGHT_ARROW Right\nset PAINT_TERRAIN MOUSE 1\nset FORWARD WHEEL 1\nset BACK WHEEL -1\nset SELECT MOUSE 1\nset INCREASE_SELECTION Shift MOUSE 1\nset BUILD MOUSE 1\nset KEEP_BUILDING Shift MOUSE 1\nset ORDER_SELECT MOUSE 3\nset BEGIN_SELECTION MOUSE_DOWN 1\nset END_SELECTION MOUSE_UP 1\n"
  },
  {
    "path": "changelog.md",
    "content": "# Changelog\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\nIndividual changelogs for the individual engine parts are stored in the [doc/changelogs](doc/changelogs) folder.\n"
  },
  {
    "path": "configure",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2013-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nopenage autocancer-like cmake frontend.\n\nTogether with the Makefile, ./configure provides an autotools-like build\nexperience. For more info, see --help and doc/buildsystem.\n\"\"\"\n\nimport argparse\nimport os\nimport shlex\nimport shutil\nimport subprocess\nimport sys\n\nif sys.version_info < (3, 9):\n    print(\"openage requires Python 3.9 or higher\")\n    exit(1)\n\n\n# argparsing\nDESCRIPTION = \"\"\"./configure is a convenience script:\nit creates the build directory,  symlinks it,\nand invokes cmake for an out-of-source build.\n\nNobody is stopping you from skipping ./configure and our Makefile,\nand using CMake directly (e.g. when packaging, or using an IDE).\nFor your convenience, ./configure even prints the direct CMake invocation!\"\"\"\n\nEPILOG = \"\"\"environment variables like CXX, CXXFLAGS, LDFLAGS are honored, \\\nbut overwritten by command-line arguments.\"\"\"\n\n\ndef getenv(*varnames, default=\"\"):\n    \"\"\"\n    fetches an environment variable.\n    tries all given varnames until it finds an existing one.\n    if none fits, returns default.\n    \"\"\"\n    for var in varnames:\n        if var in os.environ:\n            return os.environ[var]\n\n    return default\n\n\ndef getenv_bool(varname):\n    \"\"\"\n    fetches a \"boolean\" environment variable.\n    \"\"\"\n    value = os.environ.get(varname)\n    if isinstance(value, str):\n        if value.lower() in {\"0\", \"false\", \"no\", \"off\", \"n\"}:\n            value = False\n\n    return bool(value)\n\n\n# available optional features\n# this defines the default activation for those:\n# if_available: enable if it was found\n# True:         enable feature\n# False:        disable feature\n# This 3-state activation allows distros to control the features definitively\n# but independent compilations may still have autodetection.\nOPTIONS = {\n    \"backtrace\": \"if_available\",\n    \"inotify\": \"if_available\",\n    \"opengl\": \"if_available\",\n    \"vulkan\": \"if_available\",\n    \"gperftools-tcmalloc\": False,\n    \"gperftools-profiler\": \"if_available\",\n    \"ncurses\": \"if_available\"\n}\n\n\ndef features(args, parser):\n    \"\"\"\n    Enable or disable optional features.\n    If a feature is not explicitly enabled/disabled,\n    the defaults below will be used.\n    \"\"\"\n\n    def sanitize_option_name(option):\n        \"\"\" Check if the given feature exists \"\"\"\n        if option not in OPTIONS:\n            parser.error(\"unknown feature: '{}'.\\n\"\n                         \"available features:\\n   {}\".format(\n                             option, '\\n   '.join(OPTIONS)))\n\n    options = OPTIONS.copy()\n\n    if args.with_:\n        for arg in args.with_:\n            sanitize_option_name(arg)\n            options[arg] = True\n\n    if args.without:\n        for arg in args.without:\n            sanitize_option_name(arg)\n            options[arg] = False\n\n    return options\n\n\ndef build_type(args):\n    \"\"\" Set the cmake build type \"\"\"\n    mode = args.mode\n    if mode == 'debug':\n        ret = 'Debug'\n    elif mode == 'release':\n        ret = 'Release'\n    elif mode == 'relwithdebinfo':\n        ret = 'RelWithDebInfo'\n    elif mode == 'minsizerel':\n        ret = 'MinSizeRel'\n\n    return {\n        \"build_type\": ret\n    }\n\n\ndef get_compiler(args, parser):\n    \"\"\"\n    Compute the compiler executable name\n    \"\"\"\n\n    # determine compiler binaries from args.compiler\n    if args.compiler:\n        # map alias -> actual compiler\n        aliases = {\n            \"clang\": \"clang++\",\n            \"gcc\": \"g++\",\n        }\n\n        cxxver = args.compiler.split('-', maxsplit=1)\n        cxx = cxxver[0]\n\n        # try to replace aliases\n        if cxx in aliases:\n            cxx = aliases[cxx]\n\n        # we had a version suffix with e.g. -1.2.3\n        if len(cxxver) == 2:\n            cxx += \"-\" + cxxver[1]\n\n    else:\n        # CXX has not been specified\n        if sys.platform.startswith('darwin'):\n            cxx = 'clang++'\n        else:\n            # default to gnu compiler suite\n            cxx = 'g++'\n\n    # test whether the specified compiler actually exists\n    if not shutil.which(cxx):\n        parser.error('could not find c++ compiler executable: %s' % cxx)\n\n    return {\n        \"cxx_compiler\": cxx,\n        \"cxx_flags\": args.flags,\n        \"exe_linker_flags\": args.ldflags,\n        \"module_linker_flags\": args.ldflags,\n        \"shared_linker_flags\": args.ldflags,\n    }\n\n\ndef get_install_prefixes(args):\n    \"\"\"\n    Determine the install prefix configuration.\n    \"\"\"\n\n    ret = {\n        \"install_prefix\": args.prefix,\n    }\n\n    if args.py_prefix is not None:\n        ret[\"py_install_prefix\"] = args.py_prefix\n\n    return ret\n\n\ndef bindir_creation(args, defines):\n    \"\"\"\n    configuration for the sanitizer addons for gcc and clang.\n    \"\"\"\n\n    def sanitize_for_filename(txt, fallback='-'):\n        \"\"\"\n        sanitizes a string for safe usage in a filename\n        \"\"\"\n\n        def yieldsanitizedchars():\n            \"\"\" generator for sanitizing the output folder name \"\"\"\n            # False if the previous char was regular.\n            fallingback = True\n            for char in txt:\n                if char == fallback and fallingback:\n                    fallingback = False\n                elif char.isalnum() or char in \"+-_,\":\n                    fallingback = False\n                    yield char\n                elif not fallingback:\n                    fallingback = True\n                    yield fallback\n\n        return \"\".join(yieldsanitizedchars())\n\n    bindir = \".bin/%s-%s-%s\" % (\n        sanitize_for_filename(defines[\"cxx_compiler\"]),\n        sanitize_for_filename(args.mode),\n        sanitize_for_filename(\"-O%s -sanitize=%s\" % (\n            args.optimize, args.sanitize)))\n\n    if not args.dry_run:\n        os.makedirs(bindir, exist_ok=True)\n\n    def forcesymlink(linkto, name):\n        \"\"\" similar in function to ln -sf \"\"\"\n        if args.dry_run:\n            return\n\n        try:\n            os.unlink(name)\n        except FileNotFoundError:\n            pass\n\n        os.symlink(linkto, name)\n\n    # create the build dir and symlink it to 'bin'\n    forcesymlink(bindir, 'bin')\n\n    return bindir\n\n\ndef invoke_cmake(args, bindir, defines, options):\n    \"\"\"\n    run cmake.\n    \"\"\"\n\n    # the project root directory contains this configure file.\n    project_root = os.path.dirname(os.path.realpath(__file__))\n\n    # calculate cmake invocation from defines dict\n    invocation = [args.cmake_binary]\n    maxkeylen = max(len(k) for k in defines)\n    for key, val in sorted(defines.items()):\n        print('%s | %s' % (key.rjust(maxkeylen), val))\n\n        if key in ('cxx_compiler', ):\n            # work around this cmake 'feature':\n            # when run in an existing build directory, if CXX is given,\n            # all other arguments are ignored... this is retarded.\n            if os.path.exists(os.path.join(bindir, 'CMakeCache.txt')):\n                continue\n\n        invocation.append('-DCMAKE_%s=%s' % (key.upper(), shlex.quote(val)))\n\n    if args.ninja:\n        invocation.extend(['-G', 'Ninja'])\n\n    if args.ccache:\n        invocation.append('-DENABLE_CCACHE=ON')\n\n    if args.clang_tidy:\n        invocation.append('-DENABLE_CLANG_TIDY=ON')\n\n    if args.download_nyan:\n        invocation.append(\"-DDOWNLOAD_NYAN=YES\")\n\n    cxx_options = dict()\n    if args.iwyu:\n        cxx_options[\"CXX_INCLUDE_WHAT_YOU_USE\"] = args.iwyu\n\n        invocation.append('-DWANT_IWYU=true')\n\n    cxx_options[\"CXX_OPTIMIZATION_LEVEL\"] = args.optimize\n    cxx_options[\"CXX_SANITIZE_MODE\"] = args.sanitize\n    cxx_options[\"CXX_SANITIZE_FATAL\"] = args.sanitize_fatal\n    for key, val in sorted(cxx_options.items()):\n        invocation.append('-D%s=%s' % (key, val))\n\n    print(\"\\nconfig options:\\n\")\n\n    maxkeylen = max(len(k) for k in options)\n    for key, val in sorted(options.items()):\n        print('%s | %s' % (key.rjust(maxkeylen), val))\n\n        invocation.append('-DWANT_%s=%s' % (\n            key.upper().replace('-', '_'), val))\n\n    for raw_cmake_arg in args.raw_cmake_args:\n        if raw_cmake_arg == \"--\":\n            continue\n\n        invocation.append(raw_cmake_arg)\n\n    invocation.append(project_root)\n\n    # look for traces of an in-source build\n\n    if os.path.isfile('CMakeCache.txt'):\n        print(\"\\nwarning: found traces of an in-source build.\")\n        print(\"CMakeCache.txt was deleted to make building possible.\")\n        print(\"run 'make cleaninsourcebuild' to fully wipe the traces.\")\n        os.remove('CMakeCache.txt')\n\n    # switch to build directory\n    print('\\nbindir:\\n%s/\\n' % os.path.join(project_root, bindir))\n    if not args.dry_run:\n        os.chdir(bindir)\n\n    # invoke cmake\n    try:\n        print('invocation:\\n%s\\n' % ' '.join(invocation))\n        if args.dry_run:\n            exit(0)\n        else:\n            print(\"(now running cmake:)\\n\")\n            exit(subprocess.call(invocation))\n    except FileNotFoundError:\n        print(\"cmake was not found\")\n        exit(1)\n\n\ndef main(args, parser):\n    \"\"\"\n    Compose the cmake invocation.\n    Basically does what many distro package managers do as well.\n    \"\"\"\n\n    try:\n        subprocess.call(['cowsay', '--', DESCRIPTION])\n        print(\"\")\n    except (FileNotFoundError, PermissionError):\n        print(DESCRIPTION)\n        print(\"\")\n\n    defines = {}\n\n    options = features(args, parser)\n    defines.update(build_type(args))\n    defines.update(get_compiler(args, parser))\n    defines.update(get_install_prefixes(args))\n\n    bindir = bindir_creation(args, defines)\n    invoke_cmake(args, bindir, defines, options)\n\n\ndef parse_args():\n    \"\"\" argument parsing \"\"\"\n\n    cli = argparse.ArgumentParser(\n        description=DESCRIPTION,\n        epilog=EPILOG,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n\n    cli.add_argument(\"--mode\", \"-m\",\n                     choices=[\"debug\", \"release\", \"relwithdebinfo\", \"minsizerel\"],\n                     default=getenv(\"BUILDMODE\", default=\"debug\"),\n                     help=\"controls cmake build mode\")\n    cli.add_argument(\"--optimize\", \"-O\",\n                     choices=[\"auto\", \"0\", \"1\", \"g\", \"2\", \"3\", \"max\"],\n                     default=getenv(\"OPTIMIZE\", default=\"auto\"),\n                     help=(\"controls optimization-related flags. \" +\n                           \"is set according to mode if 'auto'. \" +\n                           \"conflicts with --flags\"))\n    cli.add_argument(\"--sanitize\",\n                     choices=[\"none\", \"yes\", \"mem\", \"thread\"],\n                     default=getenv(\"SANITIZER\", default=\"none\"),\n                     help=(\"enable one of those (run-time) code sanitizers.\"\n                           \"'yes' enables the address and \"\n                           \"undefined sanitizers.\"))\n    cli.add_argument(\"--sanitize-fatal\", action='store_true',\n                     default=getenv_bool(\"SANITIZER_FATAL\"),\n                     help=\"With --sanitize, stop execution on first problem.\")\n    cli.add_argument(\"--compiler\", \"-c\",\n                     default=getenv(\"CXX\"),\n                     help=\"c++ compiler executable, default=$ENV[CXX]\")\n    cli.add_argument(\"--iwyu\",\n                     choices=[\"warn\", \"error\"],\n                     default=None,\n                     help=\"use include-what-you-use tool to check \"\n                          \"for unnecessary imports\")\n    cli.add_argument(\"--with\", action='append', dest='with_', metavar='OPTION',\n                     help=\"enable optional functionality. \"\n                          \"for a list of available features, \"\n                          \"use --list-options\")\n    cli.add_argument(\"--without\", action='append', metavar='OPTION',\n                     help=\"disable optional functionality. \"\n                          \"for a list of available features, \"\n                          \"use --list-options\")\n    cli.add_argument(\"--list-options\", action=\"store_true\",\n                     help=\"list available optional feature switches\")\n    cli.add_argument(\"--flags\", \"-f\",\n                     default=getenv(\"CXXFLAGS\", \"CCFLAGS\", \"CFLAGS\"),\n                     help=\"compiler flags\")\n    cli.add_argument(\"--ldflags\", \"-l\",\n                     default=getenv(\"LDFLAGS\"),\n                     help=\"linker flags\")\n    cli.add_argument(\"--prefix\", \"-p\", default=\"/usr/local\",\n                     help=\"installation directory prefix\")\n    cli.add_argument(\"--py-prefix\", default=None,\n                     help=\"python module installation directory prefix\")\n    cli.add_argument(\"--dry-run\", action='store_true',\n                     help=\"just print the cmake invocation without calling it\")\n    cli.add_argument(\"--cmake-binary\", default=\"cmake\",\n                     help=\"path to the cmake binary\")\n    cli.add_argument(\"--ninja\", action=\"store_true\",\n                     help=\"use ninja instead of GNU make\")\n    cli.add_argument(\"--ccache\", action=\"store_true\",\n                     help=\"activate using the ccache compiler cache\")\n    cli.add_argument(\"--clang-tidy\", action=\"store_true\",\n                     help=\"emit clang-tidy analysis messages\")\n    cli.add_argument(\"--download-nyan\", action=\"store_true\",\n                     help=\"enable automatic download of the nyan project\")\n\n    # arguments after -- are used as raw cmake args\n    cli.add_argument('raw_cmake_args', nargs=argparse.REMAINDER, default=[],\n                     help=\"all args after ' -- ' are passed directly to cmake\")\n\n    args = cli.parse_args()\n\n    if args.sanitize == 'none' and args.sanitize_fatal:\n        cli.error('--sanitize-fatal only valid with --sanitize')\n\n    if args.list_options:\n        header = \"{} | Default state\".format(\"Optional features:\".ljust(25))\n        print(\"{}\\n{}\".format(header, \"-\" * len(header)))\n        for option, state in sorted(OPTIONS.items()):\n            state_str = (state if not isinstance(state, bool)\n                         else (\"on\" if state else \"off\"))\n            print(\"{} | {}\".format(option.ljust(25), state_str))\n        exit(0)\n\n    return args, cli\n\n\nif __name__ == \"__main__\":\n    main(*parse_args())\n"
  },
  {
    "path": "copying.md",
    "content": "Any file in this project that doesn't state otherwise, and isn't listed as an\nexception below, is Copyright 2013-2021 The openage authors, and licensed\nunder the terms of the GNU General Public License Version 3, or\n(at your option) any later version (\"GPL3+\").\nA copy of the license can be found in [legal/GPLV3](/legal/GPLv3).\n\n_the openage authors_ are:\n\n| Full name                   | aliases                     | E-Mail                                            |\n|-----------------------------|-----------------------------|---------------------------------------------------|\n| Jonas Jelten                | TheJJ                       | jj à sft dawt lol                                 |\n| Michael Enßlin              | mic_e                       | michael à ensslin dawt cc                         |\n| Andre Kupka                 | freakout                    | kupka à in dawt tum dawt de                       |\n| Frank Schmidt               | gellardo                    | rubiccuber à googlemail dawt com                  |\n| Markus Otto                 | zuntrax                     | otto à fs dawt tum dawt de                        |\n| Sascha Vincent Kurowski     | svkurowski                  | svkurowski à gmail dawt com                       |\n| James Mintram               | JimmyJazz                   | jamesmintram à gmail dawt com                     |\n| Martin McGrath              | MartinMcGrath               | mcgrath dawt martin à gmail dawt com              |\n| Renée Kooi                  | goto-bus-stop               | renee à kooi dawt me                              |\n| Markus Elfring              | elfring                     | elfring à users dawt sourceforge dawt net         |\n| Jimmy Berry                 | boombatower                 | jimmy à boombatower dawt com                      |\n| João Roque                  | joaoroque                   | joaoroque à gmail dawt com                        |\n| Julius Michaelis            | jcaesar                     | gitter à liftm dawt de                            |\n| Katharina Bogad             | mistressofjellyfish         | delirium à hacked dawt xyz                        |\n| Oliver Fawcett-Griffiths    | ollyfg                      | olly à ollyfg dawt com                            |\n| Ross Murray                 | rossmurray                  | rm à egoorb dawt com                              |\n| Alexandre Arpin             | AlexandreArpin              | arpin dawt alexandre à gmail dawt com             |\n| Henry Snoek                 | snoek09                     | snoek09 à gmail dawt com                          |\n| Gabriel Scherer             | gasche                      | gasche dawt dylc à gmail dawt com                 |\n| Austin Eyler                | awestin1                    | awestin1 à gmail dawt com                         |\n| Francisco Demartino         | franciscod                  | demartino dawt francisco à gmail dawt com         |\n| Peter Piwowarski            | oldlaptop                   | oldlaptop654 à aol dawt com                       |\n| Charles Pigott              | LordAro                     | charlespigott à googlemail dawt com               |\n| Andrew Eikum                | ColdPie1                    | coldpies à gmail dawt com                         |\n| Michael Sebastiyan          | BugExplorer                 | sebastiyan dawt michael à outlook dawt com        |\n| Adam Miartus                | miartad                     | adam dawt miartus à gmail dawt com                |\n| Benoît Legat                | blegat                      | benoit dawt legat à gmail dawt com                |\n| James Hagborg               | blucoat                     | jameshagborg à gmail dawt com                     |\n| Prashanth Jonnala           | jprashanth                  | prashanth dawt neo à gmail dawt com               |\n| Jonathan Remnant            | Jon0                        | jono4728 à gmail dawt com                         |\n| Sam Schetterer              | schets                      | samschet à gmail dawt com                         |\n| Georg Kilzer                | leper                       | leper à wildfiregames dawt com                    |\n| Florian Erler               | ethon                       | ethon à ethon dawt cc                             |\n| Michał Janiszewski          | janisozaur                  | janisozaur+openage à gmail dawt com               |\n| Lautaro Nahuel De León      | lndl                        | laudleon à gmail dawt com                         |\n| Robin Kreis                 | rkreis                      | r dawt kreis à uni-bremen dawt de                 |\n| Shion Ryuu                  | shion                       | shionryuu à outlook dawt com                      |\n| Jonas Borchelt              | riotjones                   | jonasbr-github à bellatrix dawt uberspace dawt de |\n| Jon Gelderloos              | jgelderloos                 | jgelderloos à gmail dawt com                      |\n| Emmanuel Gil Peyrot         | Link Mauve                  | linkmauve à linkmauve dawt fr                     |\n| Danilo Bargen               | dbrgn                       | mail à dbrgn dawt ch                              |\n| Niklas Fiekas               | niklasf                     | niklas dawt fiekas à backscattering dawt de       |\n| Charles Gould               | charlesrgould               | charles dawt r dawt gould à gmail dawt com        |\n| Wilco Kusee                 | detrumi                     | wilcokusee à gmail dawt com                       |\n| Sreejith R                  | sreejithr                   | sreejith dawt r44 à gmail dawt com                |\n| Jens Feodor Nielsen         | jfeo                        | xws747 à alumni dawt ku dawt dk                   |\n| Franz-Niclas Muschter       | fm                          | fm à stusta dawt net                              |\n| Valentin Gagarin            | frickler01                  | valentin à fricklerhandwerk dawt de               |\n| Emmanouil Kampitakis        | madonius                    | emmanouil à kampitakis dawt de                    |\n| Thomas Oltmann              | tomolt                      | thomas dawt oltmann dawt hhg à gmail dawt com     |\n| Miguel Kasparick            | miguellissimo               | miguellissimo à gmail dawt com                    |\n| Darren Strash               | darrenstrash                | darren dawt strash à gmail dawt com               |\n| Kyle Robbertze              | paddatrapper                | paddatrapper à gmail dawt com                     |\n| Jonathan Biegert            | azrdev                      | azrdev à qrdn dawt de                             |\n| Hadrien Mary                | hadim                       | hadrien dawt mary à gmail dawt com                |\n| Sachin Kelkar               | s4chin                      | sachinkel19 à gmail dawt com                      |\n| Camillo Dell'mour           | spjoe                       | cdellmour à gmail dawt com                        |\n| Timothee Behety             | tim2000                     | tim dawt behety à gmail dawt com                  |\n| Vyacheslav Davydov          | tombouctou                  | vissi à vissi dawt su                             |\n| Lyle Nel                    | lyle-nel                    | pt20100938 à gmail dawt com                       |\n| Michael Kilby               | kilbyjmichael               | kilbyjmichael à gmail dawt com                    |\n| Michal Kováč                | mirelon                     | miso à github dawt ksp dawt sk                    |\n| Patrik Stutz                | VanCoding                   | patrik dawt stutz à gmail dawt com                |\n| James McMurray              | jamesmcm                    | jamesmcm03 à gmail dawt com                       |\n| Łukasz Raszka               | lukky513                    | lukky513 à gmail dawt com                         |\n| Martin Castillo             | castilma                    | castilma à uni-bremen dawt de                     |\n| Volodymyr Samokhatko        | ChipmunkV                   | velorums à gmail dawt com                         |\n| Guillaume Desquesnes        | elnabo                      | g dawt desquesnes à gmail dawt com                |\n| Johan Klokkhammer Helsing   | johanhelsing                | johanhelsing à gmail dawt com                     |\n| Jasper v. Blanckenburg      | jazzpi                      | jasper à mezzo dawt de                            |\n| Alexej Disterhoft           | nobbs                       | disterhoft à uni-mainz dawt de                    |\n| Sebastian Brodehl           | sbrodehl                    | sbrodehl à students dawt uni-mainz dawt de        |\n| Gaith Hallak                | ghallak                     | gaithhallak à gmail dawt com                      |\n| Pierre Hallot               | Hallot                      | hallotpierre à gmail dawt com                     |\n| Vicken Simonian             | vsimon                      | vsimon à gmail dawt com                           |\n| Kevin Peters                | kev946                      | klee946 à gmail dawt com                          |\n| Andreas Schulz              | Longhanks                   | andi dawt schulz à me dawt com                    |\n| Shaleen Jain                | Shalzz                      | shaleen dawt jain95 à gmail dawt com              |\n| Johannes Walcher            | tomatower                   | johannes dawt walcher à stusta dawt de            |\n| Akritas Akritidis           | MaanooAk                    | akritasak à gmail dawt com                        |\n| Edgard Mota                 | edgardmota                  | edgardmota à gmail dawt com                       |\n| Boris Dušek                 | dusek                       | me à dusek dawt me                                |\n| Michael Droogleever         | droogmic                    | droogmic à gmail dawt com                         |\n| Christoph Heine             | heinezen                    | heinezen à hotmail dawt de                        |\n| Marco Savelli               | Piruzzolo                   | svlmrc à gmail dawt com                           |\n| Yvan Burrie                 | yvan-burrie                 | yvan dawt burrie à hotmail dawt com               |\n| Martin Bernardi             | martinber                   | martinbernardi à openmailbox dawt org             |\n| Erik Griese                 | citron0xa9                  | erik dawt griese à yahoo dawt de                  |\n| Alex Birch                  | Birch-san                   | vengeance dawt m dawt x+openage à gmail dawt com  |\n| Michal Jarzabek             | stiopa                      | stiopa à gmail dawt com                           |\n| Christopher Wilson          | cdw33                       | cdw33 à zips dawt uakron dawt edu                 |\n| Wojciech Nawrocki           | Vtec234                     | wjnawrocki à protonmail dawt com                  |\n| Folkert van Verseveld       | methos, medicijnman         | folkert dawt van dawt verseveld à gmail dawt com  |\n| Neel Patel                  | ohn0                        | silverskinx à gmail dawt com                      |\n| David Carlier               | devnexen                    | devnexen à gmail dawt com                         |\n| Tushar Maheshwari           | tusharpm                    | tushar27192 à gmail dawt com                      |\n| Piotr Szpetkowski           | piotr-szpetkowski           | piotr dawt szpetkowski à pyquest dawt space       |\n| Julian Guillotel            | arialwhite                  | julian dawt gullotel à gmail dawt com             |\n| Arne Sellmann               | PythonicChemist             | arne dawt sellmann à gmx dawt de                  |\n| Rafael X. Morales Georgi    | chocoladisco                | chocoladisco à gmail dawt com                     |\n| Marcel Schneider            | schnema123                  | marcelschneider5 à outlook dawt de                |\n| Samuel Grigolato            | samuelgrigolato             | samuel dawt grigolato à gmail dawt com            |\n| Andrew Thompson             | mrwerdo                     | mrwerdo331 à me dawt com                          |\n| Benedikt Freisen            | roybaer                     | b dawt freisen à gmx dawt net                     |\n| Finn Günther                | Kawzeg                      | kawzeg à gmail dawt com                           |\n| Akshit Sharma               | akshit-sharma               | akshit9sharma à gmail dawt com                    |\n| Jacek Wielemborek           | d33tah                      | d33tah à gmail dawt com                           |\n| Charles Offenbacher         | coffenbacher                | charles.offenbacher à gmail dawt com              |\n| Ilia Vladimirskiy           | ivvory                      | ivv.evol à gmail dawt com                         |\n| Nicholas Schmidt            | schmidtnicholas             | schmidtnicholas111 à gmail dawt com               |\n| Antti Aalto                 | Anakonda                    | antti dawt aalto dawt 10 à gmail dawt com         |\n| Simon San                   | simonsan                    | simon à systemli dawt org                         |\n| Lorenzo Gaifas              | brisvag                     | brisvag à gmail dawt com                          |\n| Shim Myeongseob             | violet716                   | zzangsim231 à gmail dawt com                      |\n| Serhan Tutar                | randomnoise                 | serhantutar à outlook dawt com                    |\n| Georgy Komarov              | jubnzv                      | jubnzv à gmail dawt com                           |\n| Aristotelis Dossas          | teldosas                    | teldosas à gmail dawt com                         |\n| Martin Sandsmark            | martin                      | martin dawt sandsmark à kde dawt org              |\n| Merlin Stollenwerk          | Mese96                      | merlin-stollenwerk à past-development dawt de     |\n| 段清楠 Duan Qingnan          | duanqn                      | duanqn_own_1 à yeah dawt net                      |\n| Sean Ramey                  | SeanRamey                   | sramey40 à gmail dawt com                         |\n| D R Siddhartha              | drs-11                      | siddharthadr11 à gmail dawt com                   |\n| Martin Matějek              | mmtj                        | martin dawt matejek à gmx dawt com                |\n| Tobias Feldballe            | Namabilis                   | tobias à osandweb dawt dk                         |\n| Ayxan Haqverdili            | Ayxan13                     | aykhanhagverdili à gmail dawt com                 |\n| David Heidelberg            | okias                       | david à ixit dawt cz                              |\n| Giacomo Frascarelli         | 0ro8lu                      | giacomo dawt frascarelli1 à gmail dawt com        |\n| Antonio M. R. Cunha         | Grubben                     | antoniomsprc à gmail dawt com                     |\n| Jens Nyman                  | nymanjens                   | nymanjens dawt nj à gmail dawt com                |\n| Deepak Dinesh               | deepak                      | d.deepakdinesh13 à gmail dawt com                 |\n| Damien Lejay                | dlejay                      | lejay à paracompact dawt space                    |\n| Talha Aamir                 | sarcxd                      | sarcxd à gmail dawt com                           |\n| Matthias Geiger             | CountOmega                  | matthias dawt geiger1024 à outlook dawt com       |\n| Yuvraj Tetarwal             | YuviTz1                     | yuvi56789 à gmail dawt com                        |\n| Tarun Samanta               | TS                          | tarunsamanta77 à gmail dawt com                   |\n| Derek Frogget               | FoggyLight                  | fro22003 à byui dawt edu                          |\n| Martin                      | Starman                     | mstarman à seznam dawt cz                         |\n| Zoltán Ács                  | zoli111                     | acszoltan111 à gmail dawt com                     |\n| Trevor Slocum               | tslocum                     | trevor à rocket9labs dawt com                     |\n| Munawar Hafiz               | munahaf                     | munawar dawt hafiz à gmail dawt com               |\n| Md Ashhar                   | ashhar                      | mdashhar01 à gmail dawt com                       |\n| Fábio Barkoski              | fabiobarkoski               | fabiobarkoskii à gmail dawt com                   |\n| Astitva Kamble              | askastitva                  | astitvakamble5 à gmail dawt com                   |\n| Haoyang Bi                  | AyiStar                     | ayistar à outlook dawt com                        |\n| Michael Seibt               | RoboSchmied                 | github à roboschmie dawt de                       |\n| Nikhil Ghosh                | NikhilGhosh75               | nghosh606 à gmail dawt com                        |\n| Edvin Lindholm              | EdvinLndh                   | edvinlndh à gmail dawt com                        |\n| Jeremiah Morgan             | jere8184                    | jeremiahmorgan dawt bham à outlook dawt com       |\n| Tobias Alam                 | alamt22                     | tobiasal à umich dawt edu                         |\n| Alex Zhuohao He             | ZzzhHe                      | zhuohao dawt he à outlook dawt com                |\n| David Wever                 | dmwever                     | dmwever à crimson dawt ua dawt edu                |\n| Michael Lynch               | mtlynch                     | git à mtlynch dawt io                             |\n| Ngô Xuân Minh               |                             | xminh dawt ngo dawt 00 à gmail dawt com           |\n| Haytham Tang                | haytham918                  | yunxuant à umich dawt edu                         |\n| Ana Trias-Labellarte        | anatriaslabella             | ana dawt triaslabella à ufl dawt edu              |\n| Eelco Empting               | Eeelco                      | me à eelco dawt de                                |\n| Jordan Sutton               | jsutCodes                   | jsutcodes à gmail dawt com                        |\n| Daniel Wieczorek            | Danio                       | danielwieczorek96 à gmail dawt com                |\n|                             | bytegrrrl                   | bytegrrrl à proton dawt me                        |\n\nIf you're a first-time committer, add yourself to the above list. This is not\njust for legal reasons, but also to keep an overview of all those nicknames.\n\nFor some authors, the full names and/or e-mail addresses are unknown. They have\nbeen marked by \"?\". Luckily, those author's contributions are only small typo\nfixes, so no copyright concerns should arise from this.\nIf your info is missing, wrong, or you want it to be removed for whatever\nreason, please contact us.\n\nA full list of all openage authors (\"contributors\") can also be determined\nfrom the VCS, e.g. via `git shortlog -sne`, or conveniently looked up on\n[the GitHub web interface](https://github.com/SFTtech/openage/graphs/contributors).\n\nDetails on individual authorships of files can be obtained via the VCS,\ne.g. via `git blame`, or the GitHub web interface.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License Version 3 for more details.\n\nIf you wish to include a file from openage in your project, make sure to\ninclude all required legal info. The easiest way to do this would probably\nbe to include a copy of this file (`copying.md`), and to leave the file's\ncopyright header untouched.\n\nPer-file license header guidelines:\n\nIn addition to this file, to prevent legal caveats, every source file *must*\ninclude a header.\n\n**openage-native** source files, that is, files that were created by\n_the openage authors_, require the following one-line header, preferably in\nthe first line, as a comment:\n\n    Copyright 20XX-20YY the openage authors. See copying.md for legal info.\n\n`20XX` is the year when the file was created, and `20YY` is the year when the\nfile was last edited. When editing a file, make sure the last-modification year\nis still correct.\n\n**3rd-party** source files, that is, files that were taken from other open-\nsource projects, require the following, longer header:\n\n    This file was ((taken|adapted)|contains (data|code)) from $PROJECT,\n    Copyright 1337-2013 Your Mom.\n    It's licensed under the terms of the 3-clause BSD license.\n    < any amount of lines of further legal information required by $PROJECT,\n      such as a reference to a copy of the $PROJECT's README or AUTHORS file >\n    < if third-party files from more than the one project were used in this\n      file, copy the above any number of times >\n    (Modifications|Other (data|code)|Everything else) Copyright 2014-2014 the openage authors.\n    See copying.md for further legal info.\n\nFor even more details, see the [regular expressions](buildsystem/codecompliance/legal.py).\n\nIn addition to the openage header, the file's original license header should\nbe retained if in doubt.\n\nThe \"license\" line is required only if the file is not licensed as\n\"GPLv3 or higher\".\n\nAuthors of 3rd-party files should generally not be entered in the\n\"openage authors\" list.\n\nAll 3rd-party files **must** be included in the following list:\n\nList of all 3rd-party files in openage:\n\nFrom [cabextract/libmspack](http://www.cabextract.org.uk/) ([LGPL 2.0](/legal/LGPLv2.0))\n\n - `libopenage/util/compress/lzxd.cpp`\n - `doc/code/lzx_compression_info`\n\ncmake modules ([3-clause BSD license](/legal/BSD-3-clause))\n\n - `buildsystem/modules/FindGPerfTools.cmake` (taken from [VAST](https://github.com/mavam/vast))\n - `buildsystem/modules/FindOpusfile.cmake` (taken from [Unvanquished](https://github.com/Unvanquished/Unvanquished))\n\n\n#### Disclaimer\n\nNotes about this file:\n\nI (`mic_e`) am not a lawyer. This is a free software project, we're doing this for\nfun. People convinced me that this legal shit must be done, so I did it, even\nthough I'd rather have spent the time on useful parts of the project.\nIf you see any legal issues, feel free to contact me.\n\nI, personally, despise in-sourcefile legal text blocks. They're a pest,\nand unlike many others, I don't simply accept them because\n\"that is what everybody does\". Thus, I worked out the minimal 1-line text above,\nwhich should be free of legal caveats, and a reasonable compromise.\nI'd be happy to see it used in other projects; you're free to use this file\n(`copying.md`) as a template for your project's legal documentation.\n"
  },
  {
    "path": "dist/CMakeLists.txt",
    "content": "install(\n\tFILES \"openage.desktop\"\n\tDESTINATION \"share/applications\"\n)\n"
  },
  {
    "path": "dist/README.md",
    "content": "# Distribution\n\nContains files useful for package distribution.\n\n## Desktop file\n\nThe [openage.desktop](openage.desktop) file can be found in this directory and will be installed at\n`/usr/share/applications/openage.desktop`.\n"
  },
  {
    "path": "dist/openage.desktop",
    "content": "#!/usr/bin/env xdg-open\n[Desktop Entry]\nName=openage\nComment=Free engine clone of Age of Empires II\nExec=openage %U\nTerminal=true\nType=Application\nIcon=openage\nCategories=Game;StrategyGame;Amusement\n"
  },
  {
    "path": "doc/README.md",
    "content": "Documentation\n=============\n\nMost readers love documentation, its creators hate it.\n\nUnfortunately, nobody can look inside your head and have the same ingenious\nthoughts as you do. To ensure your thoughts also reach the afterworld, please\ndocument your stuff reasonably!\n\nThe documentation is split up in two parts: *static* and *dynamic* docs,\n\n\nStatic Docs\n-----------\n\nDedicated documentation files reside in the `doc/` directory, where this\nreadme file lies in as well. You can find ideas, milestones, planning and\nworkflow descriptions etc in here.\n\nA good entry point may be the file [doc/project_structure](/doc/project_structure.md).\nit roughly explains where to find what code in the project.\n\nIf **you** made any relevant discoveries or gained insights that help others\nunderstand any part of the project:\n\n* Create a file and write down your thoughts\n* Ideally, use markdown syntax, alternatively plain text\n* Submit your contribution (pull request) so we can include it in the official\n  repository\n* This ensures newcomers can start developing easily\n\n\nDynamic Docs\n------------\n\nThis type of documentation is written inside code files:\n\n - C++ methods and classes are documented in their header files\n - Python methods and classes are documented via docstrings\n\nDue to potential documentation laziness,\nthe above statements might be out-right fabrications.\n\n\n### Doc Generation\n\nDynamic documentation can be auto-generated from source documentation using **doxygen**.\n\nafter calling `./configure`, you can invoke\n\n\tmake doc\n\nto create doxygen html and LaTeX files.\n\n\nAfter creation, view them in a browser by\n\n\t$BROWSER bin/doc/html/index.html\n\nor, if you want to create LaTeX documents, run\n\n\tmake -C bin/doc/latex/ pdf\n\t$PDFVIEWER bin/doc/latex/refman.pdf\n"
  },
  {
    "path": "doc/assets.md",
    "content": "openage assets\n==============\n\nThe game engine requires assets to actually run a game.\nAny assets may be used, but the common case will be to convert the original media files.\n\n\nAsset directories\n-----------------\n\nWhen launching the game, [assets are converted automatically](/doc/media_convert.md).\n\nThey will be placed in `assets/`.\n\nMultiple search paths like `/usr/share/openage`\nand `~/.local/share/openage` are currently not implemented.\nThis will hopefully be done soon.\n\n\nMetadata assets\n---------------\n\nMetadata assets are used to describe the assets (duh.).\n\n* Media metadata describe:\n  * Which areas of an texture images show what (texture atlases...)\n  * Which animation and unit is stored in the file\n  * How long does the sound last\n  * etc\n* Game metadata files: Describe existing game elements\n  * What media asset metadata shall be used\n  * Where is the storage location of the techtree\n  * Where are all the units stored\n  * What describes the existing ages and technologies\n  * etc...\n* Interface metadata: Used to construct the interface\n\n\nData assets\n-----------\n\n- Media assets\n   The media asset files feed the engine with images, sounds, animations, whatever.\n   They are accompanied by metadata assets.\n\n- Game content files: **Describe the game logic**\n     - techtree\n     - civilizations\n     - ages\n     - units\n     - etc, you get the point\n"
  },
  {
    "path": "doc/build_instructions/arch_linux.md",
    "content": "# Prerequisite steps for Arch Linux users\n\n> NOTE: `openage` is packaged in the [AUR](https://aur.archlinux.org/packages/openage-git/)!\n\nThis command should provide required packages from the Arch Linux repositories:\n\n`sudo pacman -S --needed eigen python python-mako python-pillow python-numpy python-lz4 python-pygments cython libepoxy libogg libpng ttf-dejavu freetype2 fontconfig harfbuzz cmake opusfile opus python-pylint python-toml qt6-declarative qt6-multimedia`\n\nAdditionally, you have to install [`toml11`](https://aur.archlinux.org/packages/toml11) from the AUR.\nIf you have `yay`, you can run this command:\n\n`yay -S toml11`\n\nIf you don't have a compiler installed, you can select between these commands to install it:\n - `sudo pacman -S --needed gcc`\n - `sudo pacman -S --needed clang`\n\nYou can install both compilers and select the one to be used by `./configure`.\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n"
  },
  {
    "path": "doc/build_instructions/debian.md",
    "content": "# Prerequisite steps for Debian users\n\n - `sudo apt-get update`\n - `sudo apt-get install cmake cython3 libeigen3-dev libepoxy-dev libfontconfig1-dev libfreetype-dev libharfbuzz-dev libogg-dev libopus-dev libopusfile-dev libpng-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 qt6-multimedia-dev  qml6-module-qtquick3d-spatialaudio`\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n"
  },
  {
    "path": "doc/build_instructions/docker.md",
    "content": "### Running with Docker\n\nDocker simplifies the setup process by providing a consistent development environment. It allows you to build and run the project without manually installing dependencies. Currently provided [Dockerfile.ubuntu.2404](../../packaging/docker/devenv/Dockerfile.ubuntu.2404) supports Ubuntu 24.04 as the base operating system.\n\n#### Prerequisites\n\n- **Docker**: Ensure Docker is installed on your machine. Follow the [official documentation](https://docs.docker.com/) for installation instructions.\n- **Display Server**: This guide supports both **X11** and **Wayland** display servers for GUI applications.\n- **Tested Configuration**: These instructions were tested on Ubuntu 24.04 running on WSL2 on Windows 11.\n\n#### Steps to Build and Run\n\n1. **Enable GUI Support**\n\n   Depending on your display server, follow the appropriate steps:\n\n   - **For X11**:\n     Allow the Docker container to access your X server:\n     ```bash\n     xhost +local:root\n     ```\n\n   - **For Wayland**:\n     Allow the Docker container to access your Wayland socket:\n     ```bash\n     sudo chmod a+rw /run/user/$(id -u)/wayland-0\n     ```\n\n2. **Build the Docker Image**\n\n   Build the Docker image using the provided Dockerfile:\n   ```bash\n   sudo docker build -t openage -f packaging/docker/devenv/Dockerfile.ubuntu.2404 .\n   ```\n\n3. **Run the Docker Container**\n\n   Start the Docker container with the appropriate configuration for your display server:\n\n   - **For X11**:\n     ```bash\n     docker run -it \\\n       -e DISPLAY=$DISPLAY \\\n       -v /tmp/.X11-unix:/tmp/.X11-unix \\\n       -v $HOME/.Xauthority:/root/.Xauthority \\\n       --network host openage\n     ```\n\n   - **For Wayland**:\n     ```bash\n     docker run -it \\\n       -e XDG_RUNTIME_DIR=/tmp \\\n       -e QT_QPA_PLATFORM=wayland \\\n       -e WAYLAND_DISPLAY=$WAYLAND_DISPLAY \\\n       -v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/tmp/$WAYLAND_DISPLAY \\\n       --user=$(id -u):$(id -g) \\\n       --network host openage\n     ```\n\n4. **Follow the Regular Setup**\n\nOnce inside the container, follow the regular setup described in the [Development](../building.md#development) chapter. You can skip dependency installation since the Docker image already includes all required dependencies.\n\n#### Notes\n\n- **X11 vs. Wayland**: Ensure you know which display server your system is using. Most modern Linux distributions default to Wayland, but X11 is still widely used.\n- **Permissions**: For Wayland, you may need to adjust permissions for the Wayland socket (`/run/user/$(id -u)/wayland-0`) to allow Docker access.\n- **GUI Applications**: These configurations enable GUI applications to run inside the Docker container.\n\nBy following these steps, you can build and run the `openage` project in a Dockerized environment with support X11 or Wayland display servers.\n"
  },
  {
    "path": "doc/build_instructions/fedora.md",
    "content": "# Prerequisite steps for Fedora users (Fedora >= 34)\n\nRun the following command:\n\n`sudo dnf install clang cmake eigen3-devel fontconfig-devel gcc-c harfbuzz-devel libepoxy-devel libogg-devel libopusenc-devel libpng-devel  opusfile-devel python3-Cython python3-devel python3-mako python3-numpy python3-lz4 python3-pillow python3-pygments python3-toml toml11-devel qt6-qtdeclarative-devel qt6-qtmultimedia-devel`\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n"
  },
  {
    "path": "doc/build_instructions/freebsd.md",
    "content": "# Prerequisite steps for FreeBSD users\n\nThis command should provide required packages for FreeBSD installation:\n\n`sudo pkg install cmake cython eigen3 harfbuzz opus-tools opusfile png py-mako py-numpy py-lz4 py-pillow py-pygments py-toml pylint python qt6 toml11`\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n\n`clang` is the base compiler however, it is possible to use either any version of `gcc>=10` or any other version of `clang` present in `pkg`:\n - `sudo pkg install gcc`\n\nSelect the one to be used by `./configure --help`.\n"
  },
  {
    "path": "doc/build_instructions/gentoo.md",
    "content": "# Installation for Gentoo users\n\n\n## Overlay\n\nUp to date builds are found in our [sft overlay](https://github.com/SFTtech/gentoo-overlay/tree/master/games-strategy/openage).\n\nFor automatic updates, install the overlay with [layman](https://wiki.gentoo.org/wiki/Layman)!\n\n``` shell\n# add the sft-overlay\nlayman -a sft\n```\n\n## Installation\n\n``` shell\n# install openage\nemerge -avt openage\n```\n\nThis will install the latest release.\n\n\n### git version\n\nIf you want to install the latest git version:\n\n```\n# In /etc/portage/package.keywords, add\n=games-strategy/openage-9999::sft **\n```\n\nBeware, the git version does **not** do automatic updates.\nYou have to explicitly \"reinstall\" openage or use `app-portage/smart-live-rebuild`.\n"
  },
  {
    "path": "doc/build_instructions/macos.md",
    "content": "# Instructions for macOS users\n\n## Prerequisite steps\n- XCode >= Xcode 12\n- Install [Homebrew](http://brew.sh). If you use some other package managers, you're on your own :)\n\n```\nbrew update-reset && brew update\nbrew install --cask font-dejavu\nbrew install cmake python3 libepoxy freetype fontconfig harfbuzz opus opusfile qt6 libogg libpng toml11 eigen\nbrew install llvm\npip3 install --upgrade --break-system-packages cython numpy mako lz4 pillow pygments setuptools toml\n```\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies:\n\n```\nbrew install flex make\n```\n\nOptionally, for documentation generation:\n\n```\nbrew install doxygen\n```\n\n## Clone the repository\n\n```\ngit clone https://github.com/SFTtech/openage\ncd openage\n```\n\n## Building\n\nWe advise against using the clang version that comes with macOS (Apple Clang) as it notoriously out of date and often causes compilation errors. Use homebrew's clang if you don't want any trouble. You can pass the path of homebrew clang to the openage `configure` script which will generate the CMake files for building:\n\n```\n# on Intel macOS, llvm is by default in /usr/local/Cellar/llvm/bin/\n# on ARM macOS, llvm is by default in /opt/homebrew/Cellar/llvm/bin/\n./configure --compiler=\"$(brew --prefix llvm)/bin/clang++\" --download-nyan\n```\n\nAfterwards, trigger the build using `make`:\n\n```\nmake -j$(sysctl -n hw.ncpu)\n```\n\n## Testing\n`make test` runs the built-in tests.\n\n\n## Running\n`make run` or `cd bin && ./run` launches the game. Try `./run --help` if you don't know what to do!\n\n\n## To create the documentation\n`make doc`\nFor more options and details, refer to [doc/README.md][/doc/README.md]\n"
  },
  {
    "path": "doc/build_instructions/nix.md",
    "content": "# Building and developing on Nix based systems\n\nThe openage repository is a [nix flake](https://wiki.nixos.org/wiki/Flakes) that\nallow Nix users to easily build, install and develop openage.\n\nTo build openage using nix\n\n1. make sure you have nix with flakes enabled, either by permanent system\n   configuration or by using command line flags, as described on\n   [the wiki](https://wiki.nixos.org/wiki/Flakes);\n2. clone this repository and `cd` into it;\n3. run `nix build .#openage` to start the build process;\n4. the built artifact will be in `./result`.\n\nNix will configure and build the source code automatically via the\n`configurePhase` and `buildPhase` scripts, automatically provided by nix.\n\nIf you want to build the derivation and run it immediately, you can use\n\n```\nnix run .#openage\n```\n\ninstead of\n\n```\nnix build .#openage\n./result/bin/openage\n```\n\n## Development\n\nYou can get a development shell, with the required dependencies, by running\n\n```\nnix shell\n```\n\nThis will download the same dependencies used for build and pop you in a\nshell ready for use. You can call `configurePhase` and `buildPhase` to run\ncmake configuration and building.\n\nPlease note that nyan is downloaded from github as defined in `nix/nyan.nix`,\nso if you want to provide nyan source tree, you'll need to modify that file\nto use a path (such as `../../nyan`, relative to `nyan.nix`) instead of the\n`fetchFromGitHub` function, for example:\n\n```\n# Clone both\ngit clone https://github.com/SFTtech/nyan\ngit clone https://github.com/SFTtech/openage\n```\n\nThen edit `openage/nix/nyan.nix` to use `../../nyan`, the result will be like\n\n```\n$ head -n18 openage/nix/nyan.nix\n\n```\n{ lib\n, stdenv\n, fetchFromGitHub\n, clang\n, cmake\n, flex\n}:\nlet\n  pname = \"nyan\";\n  version = \"0.3\";\nin\nstdenv.mkDerivation\n{\n  inherit pname version;\n\n  src = ../../nyan;\n\n  nativeBuildInputs = [\n```\n"
  },
  {
    "path": "doc/build_instructions/opensuse.md",
    "content": "# Prerequisite steps for openSUSE users\n\n- `zypper install --no-recommends cmake doxygen eigen3-devel fontconfig-devel gcc-c graphviz++ harfbuzz-devel libepoxy-devel libfreetype-dev libogg-devel libopus-devel libpng-devel libtoml11-dev qt6-declarative-dev qt6-quickcontrols2 qt6-multimedia-dev opusfile-devel python3-Cython python3-Mako python3-lz4 python3-Pillow python3-Pygments python3-toml python3-devel`\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n"
  },
  {
    "path": "doc/build_instructions/ubuntu.md",
    "content": "# Prerequisite steps for Ubuntu users (Ubuntu >= 23.04)\n\nRun the following commands:\n\n```bash\n sudo apt-get update\n 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 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 qt6-multimedia-dev  qml6-module-qtquick3d-spatialaudio\n ```\n\nYou will also need [nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md) and its dependencies.\n\n# Additional steps for Ubuntu 22.04 LTS & 24.04 LTS\n\nThe available system version of Cython is too old in Ubuntu 22.04 & 24.04. You have to get the correct version\nfrom pip:\n\n```bash\npip3 install cython --break-system-packages\n```\n\nPlease note that project requires at least **Cython 3.0.10**.\n\n# Linux Mint Issue\nLinux Mint has a [problem with `toml11`](https://github.com/SFTtech/openage/issues/1601), since CMake can't find it. To solve this, download the [toml11.zip](https://github.com/SFTtech/openage/files/13401192/toml11.zip), after, put the files in the `/usr/lib/x86_64-linux-gnu/cmake/toml11` path. (if the `toml11` directory doesn't exist, create it)\n"
  },
  {
    "path": "doc/build_instructions/windows_msvc.md",
    "content": "# Procedure for Microsoft Windows users\n\n<!---\n__NOTE:__ We also have an installer for Win10 (x64), if you just want to play around with *openage* you can find it [here](https://github.com/SFTtech/openage/releases).\n\n Since Windows doesn't offer a native package manager, we use a mixture of manual and automated steps to get the dependencies for openage.\n *Please remember to replace the directories referenced below (written in <...>) with the appropriate values.*\n\n## Using CI to build openage\nIf you use any CI (like Travis-CI or Appveyor) you can make your life easier by using the following yaml-configuration files:\n- Win_x64 - MSVC 15.x - [> Download .yml <](https://gist.githubusercontent.com/simonsan/4c73314e005239938110ec9c91e484c0/raw/)\n- Win_x86 - MSVC 15.x - [> Download .yml <](https://gist.githubusercontent.com/simonsan/390f2e3f60667608f74a2ed687e14dad/raw/)\n\nThey will build you the latest version from our master branch and package them into an installer and a portable 7z-file.\n\n__NOTE:__ You need to manually make sure and doublecheck if the system you are building on has fulfilled all the [dependencies](/doc/building.md).\n-->\n\n## Setting up the build environment\nYou will need to download and install the following manually.\nThose who already have the latest stable versions of these programs can skip this:\n\n- [Visual Studio Buildtools](https://aka.ms/vs/17/release/vs_BuildTools.exe)\n  - With the \"Visual C++ Buildtools\" workload.\n  _NOTE:_ If you are searching for an IDE for development you can get an overview [here](https://en.wikipedia.org/wiki/Comparison_of_integrated_development_environments#C/C++). We've also written some [instructions for developing with different IDEs](/doc/ide/README.md).\n\n- [Python 3](https://www.python.org/downloads/windows/)\n  - With the *pip* option enabled. We use `pip` to install other dependencies.\n  - With the *Precompile standard library* option enabled.\n  - With the *Download debug binaries (...)* option enabled.\n  - If in doubt, run the installer again and choose *Modify*.\n  - You are going to need the 64-bit version of python if you are planning to build the 64-bit version of openage, and vice versa.\n\n - [CMake](https://cmake.org/download/)\n\n### Python Modules\nOpen a command prompt at `<Python 3 installation directory>/Scripts`\n\n```ps\npip install cython numpy lz4 toml pillow pygments pyreadline3 mako\n```\n_Note:_ Make sure the Python 3 instance you're installing these scripts for is the one you call `python` in CMD\n\n_Note:_ Also ensure that `python` and `python3` both point to the correct and the same version of Python 3\n\n### vcpkg packages\nSet up [vcpkg](https://github.com/Microsoft/vcpkg#quick-start). Open a command prompt at `<vcpkg directory>`\n\n```ps\nvcpkg install dirent eigen3 fontconfig freetype harfbuzz libepoxy libogg libpng opus opusfile qtbase qtdeclarative qtmultimedia toml11\n```\n\nIf you also want Vulkan graphics support, you should also run this command:\n\n```ps\nvcpkg install vulkan\n```\n\n_Note:_ The `qt` port in vcpkg has been split into multiple packages, build times are acceptable now. If you want, you can still use [the prebuilt version](https://www.qt.io/download-open-source/) instead. If you do so, include `-DCMAKE_PREFIX_PATH=<QT6 directory>` in the cmake configure command.\n\n_Note:_ If you are planning to build the 64-bit version of openage, you are going to need 64-bit libraries. Add command line option `--triplet x64-windows` to the above command or add the environment variable `VCPKG_DEFAULT_TRIPLET=x64-windows` to build x64 libraries. [See here](https://github.com/Microsoft/vcpkg/issues/1254)\n\n<!---\n__NOTE:__ You can also download the pre-built vcpkg dependencies (without Qt) [from this repository](https://github.com/simonsan/openage-win-dependencies/releases).\n-->\n\n## Building openage\nNote that openage doesn't support completely out-of-source-tree builds yet. We will, however, use a separate `build` directory to build the binaries.\n\n_Note:_ You will also need to set up [the dependencies for nyan](https://github.com/SFTtech/nyan/blob/master/doc/building.md#windows), which is mainly [flex](https://sourceforge.net/projects/winflexbison/).\n\nOpen a command prompt at `<openage directory>`:\n\n```ps\nmkdir build\ncd build\ncmake -DCMAKE_TOOLCHAIN_FILE=<vcpkg directory>\\scripts\\buildsystems\\vcpkg.cmake ..\ncmake --build . --config RelWithDebInfo -- /nologo /m /v:m\n```\n\n_Note:_ If you want to build the x64 version, please add `-G \"Visual Studio 17 2022\" -A x64` (for VS2022) to the first cmake command.\n\n_Note:_ If you want to download and build Nyan automatically add `-DDOWNLOAD_NYAN=YES -DFLEX_EXECUTABLE=<path to win_flex.exe>` to the first cmake command.\n\n## Running openage (in devmode)\nWhile this is straightforward on other platforms, there is still stuff to do to run openage on Windows:\n\n- Install the [DejaVu Book Font](https://dejavu-fonts.github.io/Download.html).\n  - Download and extract the latest `dejavu-fonts-ttf` tarball/zip file.\n  - Select all `ttf\\DejaVuSerif*.ttf` files, right click and click `Install for all users`.\n  _Note:_ This will require administrator rights.\n\n  - Set the `FONTCONFIG_PATH` environment variable to `<vcpkg directory>\\installed\\<relevant config>\\tools\\fontconfig\\fonts\\` or `<vcpkg directory>\\installed\\<relevant config>\\etc\\fonts\\`.\n  - Copy `fontconfig\\57-dejavu-serif.conf` to `%FONTCONFIG_PATH%\\conf.d`.\n- [Optional] Set the `AGE2DIR` environment variable to the AoE 2 installation directory.\n- Set `QML2_IMPORT_PATH` to `<vcpkg directory>\\installed\\<relevant config>\\qml` or for prebuilt Qt `<qt directory>\\<qt-version>\\<compiler-version>\\qml`\n- openage needs these DLL files to run:\n  - `openage.dll` (Usually in `<openage directory>\\build\\libopenage\\<config built>`.)\n  - `nyan.dll` (The location depends on the procedure chosen to get nyan.)\n  - DLLs from vcpkg-installed dependencies. Normally, these DLLs should be copied to `<openage directory>\\build\\libopenage\\<config built>` during the build process. If they are not, you can find them in `<vcpkg directory>\\installed\\<relevant config>\\bin`.\n    - If prebuilt QT6 was installed, the original location of QT6 DLLs is `<QT6 directory>\\bin`.\n\nNow, to run openage:\n\n- Open a CMD window in `<openage directory>\\build\\` and run `python -m openage main`\n- Execute`<openage directory>\\build\\run.exe` every time after that and enjoy!\n\n## Packaging\n\n- Install [NSIS](https://sourceforge.net/projects/nsis/files/latest/download).\n- Depending on the way you installed Qt (vcpkg/pre-built) you need to edit the following line in `<openage-repo-dir>\\buildsystem\\templates\\ForwardVariables.cmake.in`:\n\n```\n# Use windeploy for packaging qt-prebuilt, standard value '1' for windeploy, '0' for vcpkg\nset(use_windeployqt 1)\n```\n\nOpen a command prompt at `<openage directory>\\build` (or use the one from the building step):\n\n```ps\n    cpack -C RelWithDebInfo\n```\n\nThe installer (`openage-<version>-<arch>.exe`) will be generated in the same directory.<br>\n\n_Note:_ Append `-V` to the `cpack` command for verbose output (it takes time to package all dependencies).\n\n_Note:_ <arch> you can set with the environment variable `TARGET_PLATFORM` (e.g. amd64, x86).\n"
  },
  {
    "path": "doc/building.md",
    "content": "# Building the project\n\nThis file should assist you in compiling and running the game.\n\nNote the [troubleshooting](#troubleshooting) and [FAQ](#faq) sections.\n\n\n## Buildsystem Design\n\n*openage* consists of a pure C++ library, `libopenage`, and the `openage` python package.\n\nWe use `CMake` for all our building needs.\nWe wrap `CMake` with an optional `configure` wrapper script.\nYou can build the project like a regular `CMake` project.\n\nFor more build system internals, see [doc/buildsystem.md](/doc/buildsystem.md).\n\n## Dependencies\n\nDependencies are needed for:\n\n* C = compiling\n* R = running\n* A = asset conversion\n* S = sanity checks (make checkall)\n* O = optional, we can continue without it\n\nDependency list:\n\n    C     gcc >=10 or clang >=10\n    CRA   python >=3.9\n    C     cython >=3.0.10 OR (>=0.29.31 AND <=3.0.7)\n    C     cmake >=3.16\n      A   numpy\n      A   lz4\n      A   python imaging library (PIL) -> pillow\n     RA   setuptools (for python>=3.12 and cython<3.1)\n      A   toml\n    CR    opengl >=3.3\n    CR    libepoxy\n    CR    libpng\n       S  clang-tidy\n     R    dejavu font\n    CR    eigen >=3\n    CR    freetype2\n    CR    fontconfig\n    CR    harfbuzz\n    C   O include-what-you-use\n    CR    nyan  (https://github.com/SFTtech/nyan)\n    CR  O ncurses\n    C     mako\n    CR    opusfile\n    CRA   opus\n    CRA   ogg\n       S  pycodestyle\n    C     pygments\n       S  pylint\n    CR    qt6 >=6.2 (Core, Quick, QuickControls, Multimedia modules)\n    CR    toml11\n    CR  O vulkan\n\n      A   An installed version of any of the following (wine is your friend).\n          Other versions _might_ work:\n\n     - Age of Empires I: Rise of Rome Patch 1.0a\n     - Age of Empires I: Definitive Edition\n     - Age of Empires II: The Conquerors Patch 1.0c\n     - Age of Empires II: Forgotten Empires\n     - Age of Empires II: HD (now also called: Age of Empires II (2013))\n     - Age of Empires II: Definitive Edition\n     - Star Wars: Galactic Battlegrounds: Clone Campaigns\n\n\n### Dependency installation\n\nThere are some prerequisite steps that need to be performed so *openage* can be\nbuilt successfully. Those steps vary from platform to platform, and are\ndescribed below for some of the most common ones:\n\n- [Ubuntu](build_instructions/ubuntu.md)\n- [Debian](build_instructions/debian.md)\n- [Fedora](build_instructions/fedora.md)\n- [openSUSE](build_instructions/opensuse.md)\n- [macOS](build_instructions/macos.md)\n- [Arch Linux](build_instructions/arch_linux.md)\n- [FreeBSD](build_instructions/freebsd.md)\n- [Gentoo](build_instructions/gentoo.md)\n- [Nix/NixOS](build_instructions/nix.md)\n- [Microsoft Windows](build_instructions/windows_msvc.md)\n\n### nyan installation\n\n`openage` depends on [`nyan`](https://github.com/SFTtech/nyan), which is the\nengine configuration language.\n\n\n* For development, `nyan` can be built and used **without installation**\n(-> no \"`make install`\", since it can be found using [`cmake` user package registry](https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#user-package-registry)).\nJust clone the repo somewhere and [follow the `nyan` build instructions](https://github.com/SFTtech/nyan/blob/master/doc/building.md)).\n\n* Alternatively, `openage` can download `nyan` automatically. This is\n  activated with `./configure --download-nyan ...` or `cmake\n  -DDOWNLOAD_NYAN=YES ...`.\n\n`cmake` looks for `nyan` in the user package registry (`~/.cmake/packages/nyan/`)\nand the system package registry (`/usr/lib/cmake/nyan/`) for `nyanConfig.cmake`.\n\nIf `nyan` cannot be found but you know where it is, you can hint the location of the `nyanConfig.cmake` with:\n```\n-Dnyan_DIR=/directory/where/nyanConfig/is/in/\n```\n\n\n## Build procedure\n\nMake sure you have all the dependencies installed.\n\n### Development\n\n- (Obviously) clone this repo or acquire the sources some other way\n- Make sure you have everything from the [dependency list](#dependencies)\n- Select the compiler and mode: see `./configure --help`\n  - Linux etc: `./configure`\n  - macOS:  `./configure --compiler=clang`\n    - hassle-free building on macOS (if above is not working)\n      - get the latest llvm build from: `https://github.com/llvm/llvm-project/releases/latest`\n        - (yeah, xcode still needs to be installed)\n      - install python deps: see [pip(3) install](https://github.com/SFTtech/openage/tree/master/doc/build_instructions/macos.md)\n      - install build deps (without llvm): see [brew install](https://github.com/SFTtech/openage/tree/master/doc/build_instructions/macos.md)\n      - configure: `CC=/path/to/downloaded-llvm/bin/clang CXX=/path/to/downloaded-llvm/bin/clang++ LD=/path/to/downloaded-llvm/bin/ld64.lld ./configure --download-nyan`\n      - a small build hint for later: use your freaking cores with the -j option later `make -j$(sysctl -n hw.ncpu)`\n- `make` generates and builds everything\n- `make run` or `bin/run` launches the game. Try `bin/run --help`!\n- `make test` runs the built-in tests.\n\n\n### Release\n\nDisclaimer: Use your distribution package of `openage` instead!\nYour distro package maintainers do all the nasty work for you,\nand will provide you with updates!\nAlso, you don't need to `make install`, you can run `openage` within its git repo.\n\n - Set build mode: `./configure --mode=release --compiler=clang --prefix=/usr/local`\n - `make`\n - `make install` install the game to `/usr/local`\n   - beware, this will add *untracked* files to your drive\n   - please use your distribution package instead!\n - launch `openage`, it's in `/usr/local/bin/openage`\n\n\n### For packagers\n\nThe reference package is [created for Gentoo](https://github.com/SFTtech/gentoo-overlay/blob/master/games-strategy/openage/).\n\n- Don't use `./configure`; instead, handle openage like a regular\n  `cmake` project. In doubt, have a look at `./configure`'s cmake\n  invocation.\n- To specify the to-be-used python version (or rather, executable),\n  pass `-DPython3_EXECUTABLE=...` or `-DPython3_ROOT_DIR=...` to `cmake`\n- Use `make install DESTDIR=/tmp/your_temporary_packaging_dir`,\n  which will then be packed/installed by your package manager.\n\n### Troubleshooting\n\n- I wanna see compiler invocations\n  - `make VERBOSE=1`\n- My `Qt`/`Python`/whatever is installed somewhere, but `cmake` can't find it!\n  - Run `ccmake` or `cmake-gui` in the build directory to see and change config variables.\n  - You can manually tell `cmake` where to look. Try something along the lines of\n    - `./configure -- -DPYTHON_INCLUDE_DIRS=/whereever/python/include/`\n    - `-DPython3_EXECUTABLE=/your/py3/directory/`\n\n- I get compiler errors about missing header files\n  - Make sure to install the developer version (including header files) of the library in question.\n- I get nonsensical cmake errors like `unable to execute /home/user/git/openage/clang++`\n  - This is an issue with `cmake`, or rather, your usage of it. You probably invoked `cmake` directly,\n    and defined a compiler even though the build directory has already been initialized.\n    `cmake` is a bit \"special\" in this regard. Simply omit the compiler definitions,\n    or purge the build directory with `rm -rf build bin .bin`.\n\n\n## FAQ\n\n* Help, it doesn't work!\n\n  * Have a look at [Troubleshooting](#troubleshooting) above.\n    Maybe you've found a bug... [Contact us!](/README.md#contact)\n\n* Why did you make the simple task of invoking a compiler so incredibly\n  complicated? Seriously. I've been trying to get this pile of utter\n  crap you call a 'build system' to simply do its job for half an hour\n  now, but all it does is sputter unreadable error messages. I hate\n  CMake. I'm fed up with you. Why are you doing this to me? I thought we\n  were friends. I'm the most massive collection of wisdom that has ever\n  existed, and now **I HATE YOU**. It can't be for no reason. You\n  **MUST** deserve it.\n\n  - Coincidentally, the exact same question crosses my mind whenever I\n    have to build an `automake` project, so I can sympathize.\n  - Unfortunately, it's not as simple as invoking a compiler. Building\n    `openage` involves code generation and the building of Cython\n    extension modules.\n\n* Why don't you `$proposition`? Your `$component` is crap\n  and would be much better then!\n  - Cool, we totally missed that! [Tell us more](/README.md#contact) and [submit your ideas!](/doc/contributing.md)\n"
  },
  {
    "path": "doc/buildsystem.md",
    "content": "Concept\n=======\n\nOur buildsystem is based on `cmake`.\nWe use a bunch of custom cmake modules and python scripts\nto detect libraries and generate code.\n\nThe entry point of the build system is `/CMakeLists.txt`.\nFrom there, all buildsystem code of `buildsystem/` is then used.\n\nThere is a `/configure` script that wraps the cmake invocation for convenience.\n\nThe `/Makefile` is another wrapper to invoke the build and tests\ncompiler-independently.\n\n\nComponents\n==========\n\nThe buildsystem is pretty sophisticated because *openage* consists of C++\ncode, generated C++ code, generated Cython code and Python code. All C++\nparts are packed in `libopenage`, the Python stuff is in the `openage`\npython package. We generate code with the `openage.codegen` Python\npackage.\n\n\nProcedure\n=========\n\nSteps in building openage:\n\n - generate `openage/config.py` and `libopenage/config.{h, cpp}` from their `*.in` template files, which contain install prefix and version info\n - run `openage.codegen` (Python module) to generate C++ source files (recipe: `codegen`)\n - generate `.pxd` Cython extension declaration files from annotated `.h` files (recipe: `pxdgen`)\n - build and link `libopenage.so` (recipe: `openage`)\n - build Cython extension modules (generate cpp files and compile them, via `buildsystem.cythonize`) (recipe: `cython`); those link against libopenage.\n\nAdditional recipes:\n\n - see `make help`\n - `install`\n - `doc` (generate docs via Doxygen)\n - `test` (runs the various tests)\n - various cleaning recipes: `cleanelf`, `cleancodegen`, `cleancython`, `cleaninsourcebuild`, `cleanpxdgen`, `cleanbuilddirs`, `mrproper`, `mrproperer`\n - various compliance checkers: `checkfast`, `checkmerge`, `checkall`, ...\n\n\nPhases\n======\n\nCMake-time: `./configure`\n------------------------\n\ncmake reads and interprets all cmake modules and `CMakeLists.txt` files, populating the build directory with Makefiles and generating `config.py`, `config.h` and `config.cpp`. In addition, the codegen script is invoked to determine the list of files it will generate, and the list of (python) files it depends on.\n\nThe `./configure` cmake-wrapper script may be used to achieve a pleasant build experience. `./configure` automatically creates the build directory and symlinks it to `bin`, which makes the root Makefile work.\n\nFor each compiler invocation (gcc -Og, clang -O3, etc...), `./configure` creates its own build directory. This allows you to quickly switch compilers and flags (e.g. via `./configure -c clang -O2`) without having to re-build all object files when switching back.\n\n\nBuild time: `make`\n------------------\n\n`cmake` supports many backends: Project files for various IDEs and more.\n\nBy default, `GNU make` is used to interpret the cmake-generated Makefiles at build time. It will detect changes in the relevant `cmake` source files, and re-trigger a cmake execution if necessary (just your standard CMake features; pretty cool stuff).\n\nThe recipes `codegen`, `libopenage`, `pxdgen`, `cythonize`, `compilepy` and `inplacemodules` are then run as needed.\n\nThe `Makefile` in the project root directory may be used to invoke `GNU make` in the build directory (`make -C bin/` would be the manual way of doing this).\n\n\nInstall time `make install`\n--------------------------\n\nAt install time, the openage library, python modules, Cython extension\nmodules and game assets are installed to the prefix that was set at cmake\ntime (default: `/usr/local`), or, if the `DESTDIR` variable is present,\nto `$DESTDIR/$INSTALLPREFIX`. Note: The Python/Cython modules have their\nseparate installation prefix, dictated by the Python environment, and\nmanually settable via the `--py-prefix` option to `./configure`.\n\nSome more `CMake` magic: the `-rpath` is automagically removed from the\ninstalled binaries (if you don't know what that is: Lucky you. CMake\nmakes sure you don't need to).\n\n\nCore buildsystem modules\n========================\n\ncpp\n---\n\nThe [cpp module](/buildsystem/cpp.cmake), apart from verifying the compiler version and setting flags, provides functions for assembling binaries step-by-step.\n\n - `declare_binary` begins the assembly of a new binary.\n - `add_sources` adds translation units (cpp files) to the binary). The `GENERATED` keyword is required for adding `codegen`-generated translation units.\n - `finalize_binary` prints information about the binary, and calls the `add_executable` or `add_library` cmake function, depending on its second argument. No more sources may be added after this point. If any generated sources were added to the executable, the `codegen` recipe is added as a `make` dependency.\n\nAt build time, this will cause all relevant object files and binaries to be built/linked. At install time, it will install the libs and binaries to /bin and /lib, respectively.\n\nPlain invocations of `add_executable` require all translation units to be specified at once; this system is designed to allow each subfolder to have its own `CMakeLists.txt` file, adding all sources specified in that folder to the main binary.\n\npython\n------\n\nThe [python module](/buildsystem/python.cmake) is similar to the cpp module. It checks the Python interpreter and provides functions to declare Cython and Python modules.\n\n - `add_cython_modules` adds Cython modules (i.e. `.pyx` or `.py` files) that will be compiled. See the code for option docs.\n - `pxdgen` adds C++ headers for `.pxd` file generation (see [pyinterface](/doc/code/pyinterface.md)).\n - `add_pxds` adds any additional `.pxd` or `.pxi` files.\n - `add_py_modules` adds pure-Python modules; note that you also must declare `.py` files that were declared in `add_cython_modules`.\n\ncodegen\n-------\n\nProvides the function `codegen_run`, which\n\n - at cmake-time, runs the openage.codegen python module to obtain a list of sources it will generate\n - at build time, actually generates the sources and writes them to the source directory where necessary\n\nIt sets the variable `CODEGEN_TARGET_TUS`, containing a list of created `cpp` files. That variable is designed to be used in a call to `add_sources(openage GENERATED ${CODEGEN_TARGET_TUS})`.\n\nThe python script automatically determines the python dependencies of the codegen script (via `sys.modules`) and adds them as dependencies of the `codegen` recipe. When the list of python dependencies, or output TUS, have changed, it automatically triggers a new `cmake` run.\n\nThe generated `.cpp` files are placed in the `libopenage` folder, and all end in `.gen.cpp` (or `.gen.h`).\n\nother modules\n-------------\n\nread the source, or nag somebody to write more docs on them! wheeee!\n"
  },
  {
    "path": "doc/changelogs/engine/v0.0.0.md",
    "content": "# [0.0.0] - 2013-08-23 (non-public)\nAll notable changes for version [v0.0.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- Add documentation for technical and gameplay features of AoE2\n- Architecture ideas\n- Codestyle examples for C++\n- Add `.gitignore` file\n- Include gameplay ideas\n- Add initial roadmap\n- License project und GPL3+\n- Add README\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/releases/tag/v0.0.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.1.0.md",
    "content": "# [0.1.0] - 2013-08-24 (non-public)\nAll notable changes for version [v0.1.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- Render town center and castle on left/right mouse click\n- Frame counter\n- Logging\n- Add resize listener\n- Add input handler\n- Docstrings for engine interface\n- Engine interface definition\n- Add Gaben graphic\n- Texture drawing\n- Build scripts for `debug` and `release`\n- Doxygen integration\n- CMake configuration\n\n## Changed\n- Adjusted `run` function and project root finding\n- Dynamically insert project name in doxyfile\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.0.0...v0.1.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.2.0.md",
    "content": "# [0.2.0] - 2013-10-01 (non-public)\nAll notable changes for version [v0.2.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- Rendering team colors\n- Save palettes to PNG file\n- Convert SLP frame image data to PNG format\n- Palette version check\n- Scan `interfac.drs` for color tables of SLPs\n- SLP parsing\n- SLP file format documentation\n- DRS file format documentation\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.1.0...v0.2.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.2.1.md",
    "content": "# [0.2.1] - 2014-07-14\nAll notable changes for version [v0.2.1] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- Audio manager that loads OPUS sound files\n- Read CSV data to engine\n- Conversion of gamedata (`empires2_x1_p1.dat` file) to CSV format\n- Add IRC channel for SFTtech to README\n- Instructions for contributing\n- `patternmask.dat` file format documentation\n- Documentation for original AoE2 pathfinder\n- Add list of gameplay feature ideas\n- Quit game by pressing `ESC`\n- Add list of Microsoft language identification strings\n- Implement coordinate system\n- Add ID list for terrain SLPs of AoE2\n- Terrain blending documentation for AoE2\n- `blendomatic.dat` file format documentation\n- Render terrain textures\n- Documentation for AoE2 sound files\n- Save SLP metadata to file\n- Creating and rendering texture atlases\n- Add instructions for meia conversion to README\n- Document example values for unit damage\n- Terrain file documentation\n- More codestyle examples for C++\n- Infrastructure for debug console\n\n## Changed\n- Grahics system now enforces shaders and uses vertex buffers\n- Split conversion of SLP into multiple modules\n\n## Fixed\n- Send logging to console only if engine is running\n- Free FPS counter text\n- Free file content for player color files\n- Free console font\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.2.0...v0.2.1\n"
  },
  {
    "path": "doc/changelogs/engine/v0.2.2.md",
    "content": "# [v0.2.2] - 2014-07-27\nAll notable changes for version [v0.2.2] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- Dynamic struct member function generation\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.2.1...v0.2.2\n"
  },
  {
    "path": "doc/changelogs/engine/v0.2.3.md",
    "content": "# [0.2.3] - 2014-11-18\nAll notable changes for version [v0.2.3] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\nNote that not all changes up until [v0.2.3] were tracked, since the project was not available on Github as an SFTtech/openage repository. The commit hiistory can be accessed via the git log though. Feel free to add missing changes to this file, if you have time to look through a thousand commits.\n\n## Added\n- `gperftools` profiler\n- Reminder in `contributing.md` to add oneself when contributing for the first time\n- Toggle HUD drawing with `F1` hotkey\n- Toggle debug overlay with `F4` hotkey\n- Add `contributing.md` with basic Pull Request how-to\n- Add argument '--version' to executable\n- `.desktop` file and placeholder icon\n- Include GPL3+ legal headers in code files\n- Draft for campaign editor\n- Ideas for AI features\n- Automatic asset updating with `inotify()`\n- Include timestamp and numbering in screenshot filename\n- Version number now displayed in lower left corner\n- Basic unit behavior\n    - Attack\n    - Move\n    - Gather\n- A* pathfinding\n- Build instructions for Ubuntu\n- Build instructions for Arch Linux\n- Build instructions for Fedora\n- Build instructions for OpenSUSE\n- Build instructions for Mac OS X\n- Media conversion of AoE2: HD edition (patch 4.3)\n- Feature demos of engine modules\n- Add support for extracting `.cab` files\n\n## Changed\n- Split up game specification code generation\n- Improve unknown enum value error message\n- Change hotkeys for screenshot and blending toggle from `KEYDOWN` to `KEYUP`\n- Load gamedata and UI separately\n- Rewrite of DRS and SLP documentation files with more info\n- Move terrain logic to own subfolder\n- Use \"British\" as default civ instead of \"Gaia\"\n- Change data directory location to `./assets` subfolder\n- Use $(MAKE) variable in main Makefile instead of explicitely calling `make`\n- Move platform specific includes to separate header file\n- Refactor the opusfile CMake rules into own module\n- Include `termios.h` instead of `termio.h`\n- Move finding Python in separate CMake module\n- Better documentation of project architecture structure\n- Converted documentation to Markdown format\n- Buildsystem overhaul\n\n## Fixed\n- Fix `path.cpp` compilation error in mac clang\n- Fix warnings on 32 bit systems\n- Divide negatives in renderer correctly\n- Use platform-independent `gettempdir` instead of `/tmp/` in converter\n- Play correct building creation and destruction sounds\n- Add `-Wno-mismatched-tags` flag for llvm builds to prevent Travis warnings\n- Cleanup of C++ code with `cppcheck`\n- Fix include of OpenGL headers on OS X systems\n- Fix reserved identifier violations\n- Fix ambiguous call to `abs` for llvm compiler\n- Fix `SDL2_image` include directory hints for OS X systems\n- Fix inclusion of `pty.h` header\n- Fix A* for Xcode 6.0\n- Fix format specifier for `long long` data types\n- Add AoE2: HD Edition to supported asset source versions\n- Fix dead link to `milestones.md` in `development.md`\n- Fix dead link to `milestones.md` in `tasks.md`\n- Use capitals for headlines and first word of bullet points\n- Various horrible typos (#13, #17, #19, #20, #21, #105, #119, #146)\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.2.2...v0.2.3\n"
  },
  {
    "path": "doc/changelogs/engine/v0.3.0-alpha.md",
    "content": "# [0.3.0-alpha] - 2019-06-12\nAll notable changes for version [v0.3.0-alpha] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\n## Added\n- Add OpenSAGE to the list of related projects\n- Check if cython modules were built and imported correctly\n- Add new technical overview to README\n- Add instructions to reproduce 'evolution video' on Youtube channel\n- Add logo version optimized for printing\n- Use GNU gold linker by default\n- Add information in `stemplet-dat.md`\n- Use C++17 as new code standard\n- Out-of-source-tree builds\n- Add proton/steam play as a way to obtain assets\n- Support for building openage under a virtualenv\n- Lazy log message formatting\n- Add support for unpatched Age of Empires 2: The Conquerors\n- Parallel cythonization for build process\n- Initial implementation of new core simulation\n- Add `readwrite` and `append` modes for opening files\n- Add quit button\n- Add donation button to README\n- Option for automatic correction of copyright years\n- Add new SLP draw commands to converter\n- Show known compiler versions in CMake error message\n- Bunch of new gameplay ideas\n- Adapt converter to support all Genie Engine games\n- Add Debian build instructions\n- Link to nyan library\n- Remember previous asset location\n- Resource capacities\n- Buildings limits\n- Windows installer package\n- Introduce interactive conversion shell\n- Add convenience options for using ccache in CMake\n- Support debugging with gdbgui\n- Add Gentoo build instructions\n- Troubleshooting guide\n- Break into debugger on throwing an exception\n- Add Windows documentation and build instructions\n- Windows native support using MSVC - Visual Studio 2017\n- Add FreeBSD build instructions\n- Add FreeBSD support\n- Add python profiling helper class\n- New renderer (initial abstraction implementation)\n- Add researching (aka technologies)\n- Implement mouse controlled game window movement\n- More enhanced filter options for converter: `--no-graphics` and `--no-scripts`\n- Buildsystem benchmarks\n- Allow customization of config and asset dir in buildysystem\n- Prefix-matching for tests\n- Documentation of blending mask lookup table (AoE2)\n- Ignore compiled QML in `.gitignore`\n- Obfuscation for Email addresses in `copying.md`\n- Introduce Mac-specific shims for `utimenstat()` and for `stat.st_mtim`\n- Tau as math constant\n- Unit life cycle management\n- Scores (AoE2)\n- `separate-shared-context` mode in GUI\n- Implement siphash\n- Qualified QML imports for the openage objects\n- Population logic\n- Quaternion\n- Log Qt version\n- Add codecompliance check for Markdown\n- Use multiprocessing with pylint\n- Implement market pricing\n- Documentation of original gameplay features\n    - Accuracy formulas\n    - Market prices\n    - Monks\n    - Rams\n    - Alarm\n    - Town Bell\n    - Waypoints\n    - Pathfinding\n    - Villagers\n    - Formations\n    - AoE1 civilization/unit stats\n    - Multiplayer chat messsaging in AoC\n    - Multiplayer communication protocol\n    - Selection mechanics\n    - Better damage formula\n    - Number of projectiles per garrisoned unit\n- Caching of gamedata files\n- Convert AoE2: African Kingdoms expansion\n- Resource bundles\n- Repair action\n- Option to start game after assets have loaded\n- Instructions for developing with Qt Creator\n- Player teams\n- Inform about the `gl-debug` option when `gl error` is detected\n- Switch to other player via hotkey\n- Build menus\n- Keep building after finishing a building\n- Check for .qml files for legal headers\n- Add documentation about resuming construction of buildings\n- GL debug option for better OpenGL backtrace\n- Render the GUI into an FBO\n- Selection box actions\n- Show the color of the active player in building menu icons\n- multiarch installdir and version for packaging\n- Add documentation for openage nyan usage\n- Optional FPS limiter\n- Unit command queue\n- GUI dynamic middle part of the HUD\n- Adding CVar subsystem\n- Dynamic key bindings\n- Documentation updates to gamedata structs in converter logic\n- Documentation for damage calculations in AoE2\n- `ENSURE` macro for assertions\n- Support for kevin CI\n- GUI ingame background assets adaptation\n- Add guide to download assets with SteamCMD\n- Add asset downloading guide for Steam version to `media_convert.md`\n- Option to convert a single SLP file from a DRS archive\n- Draw all available hotkeys in the right corner\n- Add QtQuick ingame GUI\n- Introduce `GameVersion` enum for better game version checks\n- Added OpenRCT2 as a likeminded project\n- Villagers bring their resources to the correct buildings\n- Support clang on Mac OS X\n- Matrix class with artihmetics\n- Vector class with arithmetics\n- Text renderer with support for colors\n- Ingame console UTF-8 backspace support\n- Engine profiler\n- New official logo\n- Converter parses terrain blobs\n- Source media versioning and simple diffing\n- Media conversion for AoE2: HD Edition version 4.0\n\n## Changed\n- Consistently use `openage::util::hash_combine()`\n- Convert source directory finding improvements\n- Rearrange pyobject `pxd` annotations to enforce consistent style\n- C++14 logic for `constexpr` functions\n- Let converter use memoryviews in visgrep\n- Register files generated by buildsystem as byproducts\n- New simpler `Enum` implementation that also works on Windows\n- Coordinate system rewrite\n- Require C++ compiler to support C++17 features\n- Rename mentions of \"Mac OS X\" to macOS\n- Directly use `CMAKE_DL_LIBS` CMake variable for linked libraries\n- Add the default Steam Linux AoE2 path to the media conversion examples\n- `free_memory()` in `util/system.py` has single exit point now\n- Use binary mode for files in C++\n- Include other Genie Engine games (AoE1, SWGB) in README\n- Test compilation in release mode too\n- Use libpng directly to create screenshots\n- Update C-style cast to `static_cast` in `program.cpp`\n- Update DRS file format conversion\n- Move contact table to the top of README\n- Logo SVG improvements\n- Save alpha channel in screenshots\n- Add hint about restarting Steam for SteamCMD conversion\n- Euclidean distance optimization\n- Use libopus instead of opusenc binary for audio conversion\n- Cleanup color logic in old renderer\n- Update doxyfile with doxywizard\n- Blacklist pythontex in buildsystem\n- Check for A_S_D_F in codecompliance\n- Require cython 0.25\n- Surround `engine::run` with try catch block\n- Doxygen generation improvements\n- Make audio system optional\n- Set verbosity level of spammy runtime messages to `debug` or `spam`\n- Force colored CMake output in kevin builds\n- Textfiles code compliance improvements\n- Attributes system improvements\n- Codestyle simplifications and extensions\n- Handle pep8 module renaming to pycodestyle\n- pylint is now imported, not invoked\n- Rebind `STOP_GAME` to `SHIFT` + `ESC`\n- Cleanup Python `include`s\n- Propose paths for conversion `source_dir`, including Wine prefixes\n- Depend on a newer cython 0.23 version\n- Append selection with SHIFT key instead of CTRL\n- Apply asset path expansion in both `AGE2DIR` var and input path\n- Switch to `#pragma once` header guard instead of constructed unique preprocessor variables\n- Cythonize rgba matrix calculation for converter palette lookups\n- Use typedef `sighandler_t` for WIN32\n- Use C++11 chrono for crossplatform timing\n- Tweaks for configuring via cmake on MSYS2\n- Use `math::PI` instead of `M_PI`\n- Expanded the critical section in logger to include `init()`\n- Texture bin packing improvements in SLP converter\n- Optimize texture frame merging in SLP converter\n- Change copyright dates to 2016\n- Enable backtrace collection by default\n- Expand environment variables and ~ in asset path of converter\n- Let build steps depend on each other\n- Better graphics handling for unit data\n- Improvments to the team colors shader\n- Do not enforce `release` or `debug` build in buildysystem\n- Text rendering using HarfBuzz and vbo's, removing `FTGL`\n- Clean up buildsystem\n- Gameplay ideas restructured and extended with ideas from r/aoe2\n- `Gamemain` split across several classes\n- Seperate existing map rendering code into `GameRenderer`\n- Input handling and UI rendering now done in `GameControl`\n- `GameControl` splits existing input code into `EditorMode` and `ActionMode`\n- `DataManager` renamed to `GameSpec`\n- File loading for `Terrain` now done by `GameSpec` so it only needs loading once\n- Construct `Terrain` objects by just using meta data\n- Update Ubuntu instructions\n- Update openSUSE build instructions\n- Update Fedora build instructions for Fedora 22\n- Update macOS build instructions\n- Update Arch Linux build instructions\n\n## Deprecated\n- Complete engine rewrite in progress\n    - Old lockstep gamestate will be replaced with curve-based event system\n    - Data storage will change from CSV files to storage in `nyan` format\n    - Data management will be replaced with a nyan database\n    - Old OpenGL renderer due to being replaced with more efficient renderer using Vulkan and OpenGL\n\n## Removed\n- Remove `DoublyLinkedList` implementation\n- Remove `--data=assets` argument from `.desktop` file command\n- Remove user input (file path) from regular expression extension check\n- Remove Travis CI support (replaced with kevin)\n- Network and binary compatibility with original game as project goals\n- Remove crossplatform folder and move everything to `util` subfolder\n- Remove unsused lambda captures\n- Remove usage of deprecated `std::hash::result_type`\n- Remove deprecated `std::unary_function` usage\n- Get rid of pylint workarounds\n- No more transitive linking of internally used libraries\n\n## Fixed\n- Fix `path.relative_to` usage for pxdgen\n- Fix mingw Windows build\n- Fix cyclic dependency on unnecessary timefile in buildsystem\n- Fix reference to openage directory to represent canonical form\n- Fix table formatting in `terrain.md`\n- Add missing `<algorithm>` inclusions for `std::min` and `std::max`\n- Fix cythonization for cython 0.29\n- Fix codegen error during cmake/configure phase\n- Support directories that are symlinks for building\n- Fix overly specific headerguard regex in buildsystem\n- Ignore merge commit dates for copyright year\n- Let automoc depend on codegen\n- Fix case-ignoring-dir-test for caseignoring filesystems\n- Fix font glyph memory allocations\n- Fix cython calling static member method in C++\n- Fix units getting stuck when dropsite is not found\n- Fix a cvar python type\n- Fix `ConstMap` type errors\n- Fix python loglevels\n- Fix older compiler invocations\n- Fix build on macOS computers that don't have Sierra && XCode 8\n- Build cython generated files with reduced warnings\n- Fix building with GCC7\n- Fix RGB endianness used in pixel transfer\n- Fix missing braces warnings for subobject initialization\n- Fix codegen path in comments\n- Fix ownerless projectlies causing crash\n- Fix broken audio log messages\n- Fix pylint complaining about kevin\n- Fix build with libbacktrace, clean up log includes\n- Do not show version numbers if the gamespec hash changes\n- Purge engine singleton\n- `noexcept` annotation for all move constructors/assignment operators in GUI\n- Fix wrong priority handling for union in `util` module\n- Add `/usr/bin/env python3` to `FindPython.cmake`\n- Limit building completion to 100%\n- Set `cxx_standard` to fix qt bug\n- Fix encoding error in data formatter of converter\n- Drop invalid interpreters when finding Python\n- Fix Mac OS X clang compile errors\n- Add missing source reference in CMakeLists.txt\n- Fix building placement segfault\n- Added a missing call to call in the C++ examples\n- Fix failure of automatic doc creation, if a parent directory has a white space\n- Correct graphic for idle gathering villager carrying zero amount\n- Fix missing `DEVMODE = True` line in config.py\n- Check if executable of build dependencies was found\n- Reset the amount gathered upon switching resource type\n- Find language file location for AoE2: HD Edition version 3.X\n- Fix runtime dependency check\n- Fix python library dir finding\n- Fix villagers not holding ressources when idle\n- Fix dead units receiving damage\n- Suppress false positives for pylint 1.5.1\n- Check for compiler's `thread_local` support\n- Fix villagers not stopping to gather resources when moved away\n- Add missing dependencies `python-pygments`, `python-pylint` and `cython` under Arch Linux\n- Fix broken link to `building.md` in `copying.md`\n- Include `-L<dir>` flag for linking against python\n- Fix confusion of AoE2: HD Edition with AoC 1.0c\n- Remove deadlock in `JobManager`/`Worker`\n- Fix SLP conversion issues from [v0.3.0] release\n- Stub some unused/unimplemented stuff on WIN32, so it can compile\n- Various horrible typos (#453, #496, #507, #639, #713, #757, #801, #823, #861, #925, #960, #1020, #1106)\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.3.0...v0.3.0-alpha\n"
  },
  {
    "path": "doc/changelogs/engine/v0.3.0.md",
    "content": "# [0.3.0] - 2015-07-28\nAll notable changes for version [v0.3.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\n## Added\n- New gameplay features\n    - Projectile arcs and ranged attack\n    - Dropsites\n    - Garrisoning\n    - Gathering from resource objects\n    - Quicksave/Quickload\n    - Selection boxes\n    - Display HP bar\n    - Switch to correct villager graphics when gathering\n    - Auto-task idle units\n    - Villager commands and building\n    - Show building outlines on selected buildings\n- cython-based C++-/Python interface\n    - Handle openage as shared library\n    - Move C++ source files to `./libopenage` subdirectory\n    - Move Python source files to `./openage` subdirectory\n    - Integrate cython into buildsystem\n- New logging system\n- Add testing for the coordinate system\n- CI caching\n- Customizable key bindings\n- More ideas for AI\n- Add documentation for buildings in AoE2\n- Add a CMake 'doc' target that explains why doc generation will not work\n- Add documentation and instructions for ingame development build hotkeys\n- Star count in README\n- Enable call graph generation in doxygen\n- Test cases for job manager\n- Add tests for pathfinder\n- Use C++14 as new standard\n- `for_each` function for containers\n- Random number generator (XORShift)\n- Introduce namespaced math constants available on all plattforms\n- Add a mode indicator label in map view\n- Color support for CMake\n- Add new discovered variables in `empires.dat` gamedata to converter logic\n- `TerrainObject`s handle building annexes using child/parent spaces\n- `UnitTexture` class handles delta graphics\n\n## Changed\n- Change `./configure` instruction compiler argument to use `gcc` and `clang` instead of `gnu` and `llvm`\n- Use SVG build status image instead of PNG in README\n- Moved documentation files to `./doc` subdirectory\n- Abort build on checkfull fail\n- Replace GLEW with libepoxy\n- Run `make checkfull` before compiling\n- Improve check for C++14 and clang\n- Align dependencies column in `building.md`\n- Improve portability of the CMake detection of opus location\n- use `unsigned int` C++ data type instead of non-standard `uint`\n- Use `_WIN32` macros instead of `WIN32` in CMake\n- Codestyle example updates and simplifications\n- Assure that multiple jobs are executed on the same background thread\n- Jobs can check whether they should be aborted and abort themselves\n- Integrate new job groups into the dynamic audio loading implementation\n- Load missing textures lazily\n- Change uneeded `shared_ptr` references to `unique_ptr`\n- Ensure `glew.h` is loaded before `gl.h`\n- Use pairing heap in pathfinding\n- Look for sound files in both `sounds.drs` and `sounds_x1.drs` folders\n\n## Removed\n- Duplicate code for accessing engine camera\n- SDL memleak example\n- Remove leftover config sections for coverity\n\n## Fixed\n- Gameplay fixes\n    - Ungarrisoning no longer loses units\n    - Prevent ranged units from freezing when garrisoning them\n    - Make decaying units face correct direction and disappear after a short time\n    - Stop segfaulting when gather target is removed\n    - Garrison restricted to allied buildings\n    - Siege now only placable on land\n    - Buildings take time to train a unit\n    - Units can ungarrison while the building is training something\n- Clean annexed buildings properly from memory\n- Visual fixes for dock/ships\n- Fix the CMake `cmp0054` policy for using quoted if evaluation\n- Fix segfault when shutting down\n- Fix `Tile.get_pos_on_chunk()` to return the position of the tile on the chunk\n- Case sensitivity fixes\n- Fix crashes for missing arguments when calling executable\n- Clean up comments in converter code\n- Ignore empty `gamedata.drs` when converting\n- Relocate phys3 hashing to correct file\n- Fix wrong start offset of delta graphics\n- Add forgotten comment line separators in csv output\n- Fix judgement of `child_errno`\n- Do not assign lambda to variable\n- Fix new[]/delete mismatch\n- Fix error messages for legal header copyright years\n- Fix codecompliance script to be compatible with git rebase and cherry-pick\n- Fix conversion of `empires.dat` file strings with garbage data past their \\0 terminators\n- Pass strings as `const` references instead of copying them\n- Better pep8 compliance\n- Various horrible typos (#301, #307)\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.2.3...v0.3.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.4.0.md",
    "content": "# [0.4.0] - 2019-06-27\nAll notable changes for version [v0.4.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\n## Added\n- Add Windows installer reference to README and build instructions\n- Release process documentation\n\n## Changed\n- Use `applocal.ps1` for resolving and deploying Qt dependencies on Windows\n- Clarify logical process of single- and multiplayer architecture\n\n## Fixed\n- Fix Windows 64-bit installer package to include nyan and correct python zip file\n- Start Windows version of openage explicitly from shipped Python version\n- Various horrible typos (#1131)\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.3.0-alpha...v0.4.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.4.1.md",
    "content": "# [0.4.1] - 2020-08-15\nAll notable changes for version [v0.4.1] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [v0.4.0].\n\n## Added\n- Release process documentation\n- Changelogs for engine and modding API\n- Support for AoE1:DE graphic assets formats and documentation\n    - SLP v3.0\n    - SLP v4.0\n    - SLP v4.1\n    - SLP v4.2 (only docs)\n- New option in singlefile converter to convert loose SLP files\n- CI builds for macOS\n- Hints for macOS builds in `README.md`\n- Favicon for webpages\n- Increase verbosity of `openage --version` by adding version numbers of:\n    - Eigen\n    - Harfbuzz\n    - libc\n    - SDL\n    - Qt\n    - nyan\n    - CI config\n    - Python\n    - Python C API\n    - Cython\n    - Jinja2\n    - Numpy\n    - Pillow\n    - Pygments\n- SMP file support and documentation\n- SMX file support and documentation\n- Add several SLP details about SWGB and 32-Bit SLPs from HD Edition\n- More feature ideas in docs\n- Add `make all` target\n- Make `ConcurrentQueue` support both copy-constructible and move-constructible elements\n\n## Changed\n- GUI improvements\n    - Move unit info to QML output\n    - Clean player name and ability info\n    - Set debug overlay to invisible by default\n    - Update FPS counter at a slower rate for better readability\n- Update macOS build instructions\n- Minimize state changes in renderer\n- Use GIthub workflow for macOS builds\n- Use shared pointers for renderer resources\n- Do not define `std::hash<QString>` for qt >= 5.14\n- Buildsystem updated to use `FindPython3.cmake`\n- Change the copyright year of `copying.md` to 2020\n- Update windows MSVC build docs with info from latest attempts\n- Bump CMake version to 3.16\n- Improve buildsystem Python search\n\n# Removed\n- Remove outdated simple build script\n\n## Fixed\n- Fix `math` environment usage in modding API reference guide\n- Fix typos and commands in AoE2 network protocol docs\n- Fix packaging with prebuilt Qt\n- Fix devastating `README.md` typos (#1163)\n- Fix things found by clang-tidy\n- Fix reading previous asst location\n- `enum.h`: Add missing self-reference in assignment operator\n- Request OpenGL explicitely on macOS instead of OpenGLES\n- Fix crash because of missing null-initialization of pointer\n- Correctly display SLP version string\n- Fix incorrect next `cmd_byte` value in SLP lesser draw example\n- Fix Clang-10 warnings in configure script\n- Fix build errors on MingW and gcc10\n- Fix instructions for running the engine after build\n\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.4.0...v0.4.1\n"
  },
  {
    "path": "doc/changelogs/engine/v0.5.0.md",
    "content": "# [0.5.0] - 2023-10-03\nAll notable changes for version [0.5.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\n## Added\n- New converter to create nyan modpacks\n- Support conversion for all game releases:\n  - Age of Empires 1 (1997)\n  - Age of Empires 1: Definitive Edition\n  - Age of Empires 2 (1999)\n  - Age of Empires 2: HD Edition\n  - Age of Empires 2: Definitive Edition\n  - Star Wars Galactic Battlegrounds\n  - *Age of Empires 2 (Trial Version)* for demoing the engine\n- Support for AoE1:DE and AoE2:DE graphic assets formats and documentation\n    - SLP v4.2 (converter)\n    - SMP\n    - SMX\n    - SLD\n- Pattern files for decoding AoE graphic assets with imHex\n  - SLP\n  - SLD\n- openage internal media formats documentation\n  - modpack definition\n  - sprite files (for animations)\n  - texture files (for spritesheet textures)\n  - palette files (for palette tables)\n  - terrain files (for terrain graphics)\n  - blendmask files (for terrain blending)\n  - blendtable files (for terrain blending)\n- Instructions for using vscode IDE\n- New engine subsystem implementations\n  - Presenter\n  - Input System\n  - Game Simulation\n- Demos for showcasing new engine subsystems\n  - 5 new renderer demos\n  - 1 new presenter demo\n  - 1 new input system demo\n  - 2 new game simulation demos\n- CI build status badges in README\n- New dependencies\n  - toml11 (C++)\n  - include-what-you-use (CMake, optional)\n  - lz4 (Python)\n  - mako (Python)\n  - toml (Python)\n- Type hints for Python code\n- NixOS packaging\n- Python log messages support lazy evaluation of strings\n- Add Ubuntu to optional CI build pipeline\n- Progress bar for converter media conversion\n- Integrate new renderer subsystem\n  - Parsers for openage media formats\n  - Game entity rendering\n  - Terrain rendering (in 3D!)\n  - Camera\n  - Simulation clock\n- New event-driven game simulation\n  - Simulation with event system\n  - generic game entities for objects in the game\n  - components for game entity attributes\n    - Idle\n    - Live\n    - Move\n    - Turn\n    - Activity\n    - CommandQueue\n    - Ownership\n    - Position\n  - systems for game logic\n    - idle\n    - move\n  - activity implementation for game entity control flow\n  - simulation event handlers\n    - spawn entity\n    - process command\n    - send command\n    - wait\n- New input management\n  - low level input handling from Qt\n  - high-level controllers for processing inputs\n    - game simulation controller\n    - camera controller\n- Coordinate system improvements\n  - new `scene` coordinate type for usage in renderer\n- nyan API interface\n- Documentation overhaul\n  - Documentation for new subsystems\n    - renderer\n    - GUI\n    - game simulation\n    - input system\n    - time management\n  - more guides\n    - running the engine\n    - optimizing code\n\n## Changed\n\n- Mark old game simulation implementation to legacy\n- Update macOS build instructions\n- Update dependencies\n  - Python >=3.9\n  - Cython >=0.29.32\n  - gcc >= 10\n  - clang >=10\n  - qt6 >= 6.2\n- Use Github Actions v3\n- Exported PNG files are created in-memory\n- Replace `unlikely`/`likely` macros with C++20 attributes `[[unlikely]]`/`[[likely]]`\n- Integrate new renderer subsystem\n  - Use presenter as main interface for graphical display\n  - GUI code using Qt6 and new renderer\n- Coordinate system improvements\n  - deprecate unused coordinate types\n    - CoordManager (replaced by camera class)\n    - camgame_delta (replaced by camera class)\n    - camgame (replaced by camera class)\n  - deprecated forbidden coordinate transformations\n- Update architecture documentation to reflect new engine architecture\n\n## Removed\n\n- Game simulation code generation from game data (replaced by nyan modpacks)\n- Remove unusued dependencies\n  - jinja2\n  - pyreadline\n- Integrate new renderer subsystem\n  - Remove dependencies to SDL2 in renderer\n- Remove Qt->SDL keybinding transformation from input system\n- Obsolete documentation\n  - milestones\n  - pathfinding\n\n## Fixed\n\n- macOS build on ARM architecture\n- Python version detection for Python>=3.10\n- Replace Python `distutils` with something that isn't deprecated\n- Compatibility with Cython 3.0\n- Fix a lot of horrible typos as usual\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.4.1...v0.5.0\n"
  },
  {
    "path": "doc/changelogs/engine/v0.5.1.md",
    "content": "# [0.5.1] - 2023-10-10\nAll notable changes for version [0.5.1] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\n## Removed\n\n- Pre-processor macro error when `jthread` is missing in clang build\n\n\n## Fixed\n\n- Clang build errors\n- Wrong type used for asset path on startup\n- Missing asset directories to CMakeLists\n\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.5.0...v0.5.1\n"
  },
  {
    "path": "doc/changelogs/engine/v0.5.2.md",
    "content": "# [0.5.2] - 2023-10-17\nAll notable changes for version [0.5.2] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\n## Changed\n\n- `make run` now calls `./run main` instead of `./run game`\n\n\n## Fixed\n\n- Out-of-bound access in matrix constructor\n- Version number displayed because non-annotated tags from git were not considered\n- Check if clang jthread is joinable before joining\n- Prevent engine loop from being optimized out in release builds\n- Fix engine modpack export via CLI\n\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.5.1...v0.5.2\n"
  },
  {
    "path": "doc/changelogs/engine/v0.5.3.md",
    "content": "# [0.5.3] - 2023-12-15\nAll notable changes for version [0.5.3] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\n## Added\n\n- Temporary file/directory support for Python files\n- More debug info in converter\n- More fixed-point math functions\n- Dependencies\n  - Qt Multimedia\n  - `setuptools` is now conditional dependency for Python >= 3.12 && Cython < 3.1\n\n## Changed\n\n- Make `main` the default entrypoint command for openage binary\n\n## Removed\n\n- Legacy subsystem code\n  - Asset management (yes, there were 3 deprecated asset managers)\n    - `openage::AssetManager`\n    - `openage::LegacyAssetManager`\n    - `openage::presenter::AssetManager`\n  - Deprecated Coordinate types (`libopenage/coord`)\n    - CoordManager\n    - Deprecated transformations between types\n  - Gamedata dummy classes (`libopenage/gamedata`)\n  - Old gamestate\n    - Game logic (`libopenage/gamestate/old`)\n    - Unit handling  (`libopenage/unit`)\n  - Old input system (`libopenage/input/legacy`)\n  - Old GUI (`libopenage/gui`)\n  - Old renderer\n    - Logic (`libopenage/presenter/legacy`)\n    - Data classes (texure, etc.)\n  - Old Terrain (`libopenage/terrain`)\n- Dependencies\n  - SDL2\n  - SDL2 Image\n\n## Fixed\n\n- Version tag format without `--long` crashes on tagged commits\n- Dangling reference in modpack info file loading\n- No graphics visible on Wayland\n- Wrong anchor positions when sprite is mirrored\n- Several typos in documentation\n\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.5.2...v0.5.3\n"
  },
  {
    "path": "doc/changelogs/engine/v0.6.0.md",
    "content": "# [0.6.0] - 2024-11-26\nAll notable changes for version [0.6.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since release [0.4.0].\n\n## Added\n\n- Flow field pathfinder\n- Activity system for controlling the behavior of game entities\n- Drag selection of game entities in the UI\n- `clang-format` comment formatting\n- Log messages for creation of uniform buffers\n- nix flake\n- GDB pretty printers for various internal data types\n  - Time types (`time::time_t`)\n  - Fixed point values\n  - Coordinate types\n  - openage arrays (`util::Vector`)\n  - Curve keyframes\n  - Flow field types\n- Support for multiple meshes per terrain\n- Frustum culling\n- Example screenshots for the renderer demos\n- Check for outdated modpacks on startup\n- Camera boundaries to prevent camera movement outside of the map terrain\n- Creation of temporary files/directories\n- [Windows] Default paths for DLL searching on startup\n- [Windows] DLL manager class in converter to support loading DLLs in multi-threaded conversion\n\n## Changed\n\n- Use multithreading for media export in the converter\n- Rework curve container `Queue` to be more user-friendly\n  - `front(t)`/`pop_front(t)` now return the most recently added element before time `t`\n  - track lifetime of elements by storing insertion time (`alive` time) and erasure time (`dead` time)\n  - queue elements are now sorted by insertion time\n- Curves now use `std::vector` for their keyframe storage to increase performance\n- Window settings are passed as `struct` instead of arguments\n- Sprite scaling is now handled in shader\n- Replace `constexpr` with `consteval` where appropriate\n- Optimize renderer\n  - Vectorize shader uniform (buffer) input storage\n  - Replace shared pointer usage with references\n- Use raw pointers instead of shared pointers in pairing heap implementation\n\n\n## Deprecated\n\n- Old pathfnder code (`libopenage/pathfinding`)\n\n## Removed\n\n- Exception propagation to Python with `_PyTraceback_Add`\n\n## Fixed\n\n- Bullet point formatting in event system documentation\n- Uniform alignment in uniform buffers\n- Input contexts are now handled in the correct order (top to bottom)\n- Support for GCC 14\n- Support for Clang 19\n- DE2 conversion\n  - *The Mountain Royals* DLC\n  - *Battle for Greece* DLC\n- Thread-safety of render entity\n- Documentation for the single file converter\n- Numerous documentation typos and mistakes\n- [Windows] Order of inclusion for `DbgHelp.h`\n- [Windows] Build instructions\n- [macOS] Build instructions\n\n\n## Full commit log\n\nhttps://github.com/SFTtech/openage/compare/v0.5.3...v0.6.0\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.1.0.md",
    "content": "# [0.1.0] - 2019-05-20\nAll notable changes for version [v0.1.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Added\nInitial draft. Contains 99% of the features from AoE1, AoE2 and SWGB.\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/317947efc7219c5bc049a8b6c20fb79ce75f8323/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.2.0.md",
    "content": "# [0.2.0] - 2019-06-29\nAll notable changes for version [v0.2.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Added\n### Ability module\n- Add `CommandSoundAbility(Ability)` object; plays a sound when the ability is intiated by the player\n- Add `ExecutionSoundAbility(Ability)` object; plays a sound while the ability is executed by the game entity\n- Add `AnimationOverrideAbility(Ability)` object; overrides other abilities' animation with `AnimationOverride` objects\n- Add `source_fee : float` member to `ExchangeResources`; multiplier for `source_resources` member\n- Add `target_fee : float` member to `ExchangeResources`; multiplier for `target_resources` member\n- Add `exchange_mode : ExchangeMode` member to `ExchangeResources`; multiplier for `target_resources` member\n- Add `search_range : float` member to `DropResources`; used for limiting the search range for finding a drop site\n- Add `allowed_types : set(GameEntityType)` member to `DropResources`; used to define the game entity types where the resources can be dropped off\n- Add `blacklisted_game_entities : set(GameEntity)` member to `DropResources`; exceptions for allowed game entity types\n- Add `carry_progress : set(CarryProgress)` member to `Gather`; used for carry amount progression\n- Add `interrupt_cooldown : float` member to `Cloak`\n- Add `allowed_types : set(GameEntityType)` member to `EnterContainer`\n- Add `blacklisted_game_entities : set(GameEntity)` member to `EnterContainer`\n- Add `allowed_types : set(GameEntityType)` member to `SendBackToTask`\n- Add `blacklisted_game_entities : set(GameEntity)` member to `SendBackToTask`\n- Add `spawning_area_offset_x : float` member to `ShootProjectile`; used for location of spawn area\n- Add `spawning_area_offset_y : float` member to `ShootProjectile`; used for location of spawn area\n- Add `spawning_area_offset_z : float` member to `ShootProjectile`; used for location of spawn area\n- Add `starting_progress : int` member to `Constructable`; used for defining the construction progress when game entity is spawned\n- Add `attribute : Attribute` member to `Damageable`; specifies the attribute whos progression is tracked\n- Add `despawn_condition : set(DespawnCondition)` member to `Despawn`; used for triggering despawns\n- Add `despawn_time : float` member to `Despawn`; used for duration of ability\n- Add `state_change : StateChanger` member to `Despawn`; used for state change during despawning\n- Add `death_time : float` member to `Die`; time until completion of ability\n- Add `state_change : StateChanger` member to `Die`; used for state change during death\n\n### Auxilary module\n- Add `AdjacentTilesVariant(Variant)` object; variant based on adjacent game entities\n- Add `LureType(Entity)` object; used by `Lure` effect/resistance for matching\n- Add `ProgressType(Entity)` object; used by `TimeRelativeProgress` effect/resistance for matching\n- Add `Contruct(ProgressType)` object; used for construction progress\n- Add `SendToContainerType(Entity)` object; used by `SendToContainer` effect/resistance for matching\n- Add `InverseLinar(DropoffType)` object; used for calculations influenced by dropoffs\n- Add `Cost(Entity)` object; unifies previous approaches of referencing `ResourceAmount` or `AttributeAmount`\n- Add `ResourceCost(Cost)` object; for payments with resources\n- Add `AttributeCost(Cost)` object; for payments with attribute points\n- Add `PaymentMode(Entity)` object; used by `Cost` object to determine payment due date\n- Add `Advance(PaymentMode)` object\n- Add `Adaptive(PaymentMode)` object\n- Add `Arrear(PaymentMode)` object\n- Add `StateChanger(Entity)` object; can influence the state of a game entity\n- Add `TechResearched(AvailabilityPrerequisite)` object; is true if the referenced tech is researched by the player\n- Add `GameEntityProgress(AvailabilityPrerequisite)` object; is true if the player owns a game entity that has reached this progress\n- Add `ProgressStatus(Entity)` object; used by `GameEntityProgress` to define desired status for fulfillment\n- Add `TerrainOverlay(Progress)` object; overlays `Terrain` stored in `OverlayTerrain` ability after a certain progress is reached\n- Add `StateChangeProgress(Progress)` object; changes state of a game entity after a certain progress is reached\n- Add `Normal(MoveMode)` object\n- Add `ExchangeMode(Entity)` object; used by `ExchangeResources` ability to customize exchanges further\n- Add `Fixed(ExchangeMode)` object; used for default resource exchanges\n- Add `Volatile(ExchangeMode)` object; used for adjusting resource amounts after transactions\n- Add `VolatileFlat(Volatile)` object; mimics AoE2 market behaviour\n- Add `ExchangeScope(Entity)` object; used for global or local resource exchanges involving multiple players\n- Add `AoE1TradeRoute(TradeRoute)` object; used for AoE1 trading machanics\n- Add `AttributeSetting(Entity)` object; used for setting attribute values of game entities\n- Add `DespawnCondition(Entity)` object; used for triggering despawn abilities\n- Add `ResourceSpotsDepleted(DespawnCondition)` object; used for triggering despawn abilities when resource spots are empty\n- Add `Timer(DespawnCondition)` object; used for triggering despawn abilities after a time limit\n- Add `ProjectilePassThrough(DeathCondition)` object; used for triggering death for projectiles\n- Add `ProjectileHitTerrain(DeathCondition)` object; used for triggering death for projectiles\n- Add `priority : int` member to `Variant`; decides which variant is applied first\n- Add `limit : int` member to `TerrainAmbiant`; limits the number of ambient objects created on a chunk of terrain\n- Add `starting_amount : int` member to `ResourceSpot`; sets the starting amount of a resource spot\n- Add `sound : Sound` member to `Cheat`; played on activation\n- Add `ability_preference : orderedset(Ability)` member to `GameEntityStance`; indicates which abilities can be used when a game entity has a certain stance\n- Add `type_preference : orderedset(GameEntityType)` member to `GameEntityStance`; indicates which game entity types are targeted with the abilities\n- Add `range : float` member to `Follow(MoveMode)`; sets the range at which the game entity follows the target\n- Add `creation_sound : Sound` member to `CreatableGameEntity`; plays when game entity is spawned\n- Add `research_sound : Sound` member to `ResearchableTech`; plays when tech finishes researching\n- Add `trade_resource : Resource` member to `TradeRoute`; defines the traded resource\n- Add `start_trade_post : GameEntity` member to `TradeRoute`; defines the type of game entity that has to be the start of the trade route\n- Add `end_trade_post : GameEntity` member to `TradeRoute`; defines the type of game entity that has to be the end of the trade route\n- Add `carry_progress : set(CarryProgress)` member to `Container`; used for carry progression of container\n- Add `state_change : StateChanger` member to `StorageElement`; changes state of a game entity if storage element is in container\n- Add `dispersion_dropoff : DropoffType` member to `Accuracy`; dynamic multiplier for accuracy dispersion\n\n### Effect module\n- Add `TimeRelativeAttributeChange(ContinuousEffect)` object; changes attribute points in a fixed amount of time\n- Add `TimeRelativeAttributeDecrease(TimeRelativeAttributeChange)` object; decreases attribute points in a fixed amount of time\n- Add `TimeRelativeAttributeIncrease(TimeRelativeAttributeChange)` object; increases attribute points in a fixed amount of time\n- Add `TimeRelativeProgress(ContinuousEffect)` object; changes progress in a fixed amount of time\n- Add `TimeRelativeProgressDecrease(TimeRelativeProgress)` object; decreases progress in a fixed amount of time\n- Add `TimeRelativeProgressIncrease(TimeRelativeProgress)` object; decreases progress in a fixed amount of time\n- Add `type : LureType` member to `Lure`; used for matching with the corresponding resistance object\n- Add `min_chance_success : optional(float)` member to `Convert`; defines minimum guaranteed chance for conversion to succeed\n- Add `max_chance_success : optional(float)` member to `Convert`; defines maximum guaranteed chance for conversion to succeed\n- Add `type : SendToContainerType` member to `SendToContainerType`; used for matching with the corresponding resistance object\n\n### Modifier module\n- Add `CreationAttributeCost` object; multiplier for creation attribute costs\n- Add `CreationResourceCost` object; multiplier for creation resource costs\n- Add `ResearchAttributeCost` object; multiplier for research attribute costs\n- Add `ResearchResourceCost` object; multiplier for research resource costs\n- Add `TimeRelativeProgress(EffectMultiplierModifier)` object; multiplier for effects\n- Add `TimeRelativeAttributeChange(EffectMultiplierModifier)` object; multiplier for effects\n- Add `Terrain(FlatAttributeChangeModifier)` object; multiplier for effects\n- Add `ElevationDifferenceHigh(FlatAttributeChangeModifier)` object; multiplier for effects\n- Add `Unconditional(FlatAttributeChangeModifier)` object; multiplier for effects\n- Add `Terrain(FlatAttributeChangeModifier)` object; multiplier for resistances\n- Add `ElevationDifferenceLow(FlatAttributeChangeModifier)` object; multiplier for resistances\n- Add `Unconditional(FlatAttributeChangeModifier)` object; multiplier for resistances\n- Add `DepositResourcesOnProgress(Modifier)` object; used for dropping off resources after building buildings\n- Add `InContainerDiscreteEffect(Modifier)` object; used for applying effects to the game entity when in a container\n- Add `InContainerContinuousEffect(Modifier)` object; used for applying effects to the game entity when in a container\n- Add `RefundOnDeath(Modifier)` object; used for giving back resources on death\n- Add `DiplomaticLineOfSight(Modifier)` object; used for diplomacy viewpoints\n- Add `creatables : set(CreatableGameEntity)` member to `CreationTime`; used for limiting the bonus to specified creatables\n\n### Resistance module\n- Add `TimeRelativeAttributeChange(ContinuousResistance)` object; changes attribute points in a fixed amount of time\n- Add `TimeRelativeAttributeDecrease(TimeRelativeAttributeChange)` object\n- Add `TimeRelativeAttributeIncrease(TimeRelativeAttributeChange)` object\n- Add `TimeRelativeProgress(ContinuousResistance)` object\n- Add `TimeRelativeProgressDecrease(TimeRelativeProgress)` object\n- Add `TimeRelativeProgressIncrease(TimeRelativeProgress)` object\n- Add `type : LureType` member to `Lure`; used for matching with the corresponding effect object\n- Add `type : SendToContainerType` member to `SendToContainer`; used for matching with the corresponding effect object\n- Add `ignore_containers : set(Container)` member to `SendToContainer`; lets resistor ignore defined set of containers\n- Add `resource_spot : resourceSpot` member to `MakeHarvestable`; used for matching with the corresponding effect object\n\n## Changed\n### Ability module\n- Change type of `states` member in `Transform` from `set(TransformState)` to `set(StateChanger)`\n- Change type of `initial_state` member in `Transform` from `TransformState` to `StateChanger`\n- Change type of `target_state` member in `TransformTo` from `TransformState` to `StateChanger`\n- Change type of `manual_cost` member in `Restock` from `set(ResourceAmount)` to `Cost`\n- Change type of `auto_cost` member in `Restock` from `set(ResourceAmount)` to `Cost`\n- Change type of `attributes` member in `Live` from `set(Attribute)` to `set(AttributeSetting)`\n- Move `target_mode` member from `ShootProjectile` to `Projectile`\n- Rename `UnitStance` object to `GameEntityStance`\n- Rename `TradeResources` object to `ExchangeResources`\n- Rename `RegenerateResource` object to `RegenerateResourceSpot`\n- Rename `Stealth` object to `Cloak`\n- Rename `Decay` object to `Despawn`\n- Rename `mode` member of `Move` to `modes`\n- Rename `flag` member of `RallyPoint` to `indicator`\n- Rename `units` member of `Create` to `creatables`\n- Rename `techs` member of `Research` to `researchables`\n- Rename `sell_resources` member of `ExchangeResources` to `source_resources`\n- Rename `receive_resources` member of `ExchangeResources` to `target_resources`\n- Rename `ingame_help` member of `Named` to `long_description`\n- Rename `tile_width` member of `TileRequirement` to `tiles_x`\n- Rename `tile_height` member of `TileRequirement` to `tiles_y`\n- Rename `flatten_terrain` member of `Foundation` to `flatten_ground`\n- Rename `terrain` member of `OverlayTerrain` to `terrain_overlay`\n- Rename `condition` member of `Die` to `death_conditions`\n\n### Auxilary module\n- Change type of `modifiers` member in `Civilization` from `Modifier` to `ScopedModifier`\n- Change type of `progress_sprite` member in `AnimationProgress` from `Animation` to `AnimationOverride`\n- Change type of `cost` member in `CreatableGameEntity` from `set(ResourceAmount)` to `Cost`\n- Change type of `cost` member in `ResearchableTech` from `set(ResourceAmount)` to `Cost`\n- Change type of `conflicts` member in `StorageElement` from `set(GameEntity)` to `set(StorageElement)`\n- Rename `ChanceType` object to `ConvertType`\n- Rename `Default` object to `Fallback`\n- Rename `Prerequiste` object to `AvailabilityPrerequisite`\n- Rename `AnimatedProgress` object to `AnimationProgress`\n- Rename `UnitStance` object to `GameEntityStance`\n- Rename `UnitFormation` object to `GameEntityFormation`\n- Rename `AttributeDeathCondition` object to `AttributeInterval`\n- Rename `Self(ModifierScope)` object to `Standard`\n- Rename `chance` member of `RandomVariant` to `chance_share`\n- Rename `tech_tree_help` member of `Civilization` to `long_description`\n- Rename `ingame_help` member of `Tech` to `long_description`\n- Rename `ambient` member of `Terrain` to `ambience` and change its type from `orderedset(TerrainAmbient)` to `set(TerrainAmbient)`\n- Rename `max_density` member of `TerrainAmbient` to `density`\n- Rename `type` member of `ResourceSpot` to `resource`\n- Rename `impact` member of `Cheat` to `changes`\n- Rename `negation` member of `AvailabilityPrerequisite` to `mode`\n\n### Effect module\n- Change type of `cost` member in `CostEffect` from `set(AttributeAmount)` to `Cost`\n- Rename `ChanceEffect` object to `Convert` and change the type of its `type` member from `ChanceType` to `ConvertType`\n- Rename `SendToStorage` object to `SendToContainer` and change its parent from `ContinuousEffect` to `DiscreteEffect`\n- Rename `min_range_to_destination` member of `Lure` to `min_distance_to_destination`\n- Rename `AoE2Conversion` object to `AoE2Convert`\n\n### Modifier module\n- Change type of `provider_abilities` member in `AoE2ProjectileAmount` from `ApplyDiscreteEffect` to `set(ApplyDiscreteEffect)`\n- Change type of `receiver_abilities` member in `AoE2ProjectileAmount` from `ApplyDiscreteEffect` to `set(ApplyDiscreteEffect)`\n- Rename `PercentageModifier` object to `MultiplierModifier`\n- Rename `AttributeModifier` object to `AttributeSettingsValue`\n- Rename `MoveSpeedModifier` object to `MoveSpeed`\n- Rename `ReloadTimeModifier` object to `ReloadTime`\n- Rename `GatheringEfficiencyModifier` object to `GatheringEfficiency`\n- Rename `GatheringRateModifier` object to `GatheringRate`\n- Rename `StorageElementModifier` object to `StorageElementCapacity`\n- Rename `ContainerCapacityModifier` object to `ContainerCapacity`\n- Rename `CreationTimeModifier` object to `CreationTime`\n- Rename `EffectPercentageModifier` object to `EffectMultiplierModifier`\n- Rename `ResistancePercentageModifier` object to `ResistanceMultiplierModifier`\n- Rename `FlyoverModifier` object to `Flyover`\n- Rename `StrayModifier` object to `Stray`\n- Rename `RevealModifier` object to `Reveal`\n- Rename `AlwaysHerdModifier` object to `AlwaysHerd`\n- Rename `TechUnlockModifier` object to `InstantTechResearch`\n- Rename `AbsoluteProjectileAmountModifier` object to `AbsoluteProjectileAmount`\n- Rename `AoE2ProjectileAmountModifier` object to `AoE2ProjectileAmount`\n- Rename `ContinuousResourceModifier` object to `ContinuousResource` and set `Modifier` as only direct parent\n- Rename `rate` member of `ContinuousResource` to `rates`\n- Rename `provider_ability` member of `AoE2ProjectileAmount` to `provider_abilities`\n- Rename `receiver_ability` member of `AoE2ProjectileAmount` to `receiver_abilities`\n\n### Resistance module\n- Change type of `cost` member in `CostResistance` from `set(AttributeAmount)` to `Cost`\n- Change type of `resist_condition` member in `MakeHarvestable` from `set(AvailabilityRequirement)` to `set(HarvestableRequirement)`\n- Rename `FlatResistance(ContinuousResistance)` object to `FlatAttributeChange`\n- Rename `FlatAttributeDecreaseResistance(FlatAttributeChange)` (continuous branch) object to `FlatAttributeDecrease`\n- Rename `FlatAttributeIncreaseResistance(FlatAttributeChange)` (continuous branch) object to `FlatAttributeIncrease`\n- Rename `FlatAttributeResistance(DiscreteResistance)` object to `FlatAttributeChange`\n- Rename `FlatAttributeDecreaseResistance(FlatAttributeChange)` (discrete branch) object to `FlatAttributeDecrease`\n- Rename `FlatAttributeIncreaseResistance(FlatAttributeChange)` (discrete branch) object to `FlatAttributeIncrease`\n- Rename `LureResistance(ContinuousResistance)` object to `Lure`\n- Rename `SendToStorageResistance(ContinuousResistance)` object to `SendToContainer` and change its parent from `ContinuousResistance` to `DiscreteResistance`\n- Rename `HarvestableResistance(Discreteresistance)` object to `MakeHarvestable`\n- Rename `ChanceResistance(Discreteresistance)` object to `Convert` and change the type of its `type` member from `ChanceType` to `ConvertType`\n- Rename `AoE2ConversionResistance(Convert)` object to `AoE2Convert`\n- Rename `protection_recharge_time` member of `AoE2Convert` to `protection_round_recharge_time`\n\n## Removed\n### Ability module\n- Remove `SoundAbility(Ability)` object; functionality split into `ExecutionSoundAbility` and `CommandSoundAbility`\n- Remove `Construct(Ability)` object; is now an effect\n- Remove `TransferResourceWithModifier(Ability)` object; merged into `ExchangeResources`\n- Remove `AttackGround(ShootProjectile)` object\n- Remove `accept_on_construction` member from `DropSite`; now handled by a modifier\n- Remove `die_on_hit` member from `Projectile`; now handled as death condition\n- Remove `pass_through_range` member from `Projectile`; now handled as death condition\n- Remove `progress` member from `OverlayTerrain`; any progress can now change terrain overlays\n\n### Auxilary module\n- Remove `DiplomaticSetup(Entity)` object; functionality replaced by `DiplomaticPatch`\n- Remove `Exponential(DropoffType)` object; might be reintroduced in a later release\n- Remove `AnimatedTerrain(Terrain)` object; now handled by .terrain definition file and renderer\n- Remove `TechPrerequiste(Prerequisite)` object; functionality replaced by `TechResearched`\n- Remove `BuildingPrerequiste(Prerequisite)` object; functionality replaced by `GameEntityProgress`\n- Remove `TransformState(Entity)` object; functionality replaced by `StateChanger`\n- Remove `Guard(MoveMode)` object; functionality integrated into `Follow`\n- Remove `Constructable(Entity)` object\n- Remove `NearestTradeRoute(Entity)` object\n- Remove `graphic_set` member from `Civilization`; now handled by `civ_setup` member\n- Remove `diplo_setup` member from `Civilization`; now handled by `civ_setup` member\n- Remove `modifiers` member from `Cheat`\n- Remove `enable_abilities` member from `Progress`; now handled by `StateChangeProgress` specialization\n- Remove `disable_abilities` member from `Progress`; now handled by `StateChangeProgress` specialization\n- Remove `enable_modifiers` member from `Progress`; now handled by `StateChangeProgress` specialization\n- Remove `disable_modifiers` member from `Progress`; now handled by `StateChangeProgress` specialization\n- Remove `attribute` member from `DamageProgress`; member was obselete\n- Remove `min_value` member from `Attribute`; now stored in `AttributeSetting`\n- Remove `max_value` member from `Attribute`; now stored in `AttributeSetting`\n- Remove `enable_abilities` member from `StorageElement`; now handled by `state_change` member\n- Remove `disable_abilities` member from `StorageElement`; now handled by `state_change` member\n- Remove `enable_modifiers` member from `StorageElement`; now handled by `state_change` member\n- Remove `disable_modifiers` member from `StorageElement`; now handled by `state_change` member\n\n### Modifier module\n- Remove `DiscreteModifier(Modifier)` object; might be introduced back later\n- Remove `DiscreteResourceModifier(DiscreteModifier)` object; might be introduced back later\n- Remove `CreationCostModifier(MultiplierModifier)` object; split into `CreationAttributeCost` and `CreationResourceCost`\n- Remove `ResearchCostModifier(MultiplierModifier)` object; split into `ResearchAttributeCost` and `ResearchResourceCost`\n- Remove `ConstructionTimeModifier(MultiplierModifier)` object; moved to effect bonus\n- Remove `ElevationModifier(FlatAttributeChangeModifier)` object; replaced by `ElevationDifferenceHigh` modifier\n- Remove `ContinuousModifier(Modifier)` object; might be introduced back later\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/45b8992dc836d239c9f22cae8404a8b2cc16c8ef/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.3.0.md",
    "content": "# [0.3.0] - 2021-03-03\nAll notable changes for version [v0.3.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Renamed\n### Ability module\n- Rename `Damageable` object to `AttributeChangeTracker`\n- Rename `Die` object to `PassiveTransformTo`\n- Rename `TransformTo` object to `ActiveTransformTo`\n- Rename `blacklisted_game_entities` member of `ApplyContinuousEffect` to `blacklisted_entities`\n- Rename `blacklisted_game_entities` member of `ApplyDiscreteEffect` to `blacklisted_entities`\n- Rename `damage_progress` member of `AttributeChangeTracker` to `change_progress`\n- Rename `despawn_conditions` member of `Despawn` to `despawn_condition`\n- Rename `blacklisted_game_entities` member of `DropResources` to `blacklisted_entities`\n- Rename `blacklisted_game_entities` member of `EnterContainer` to `blacklisted_entities`\n- Rename `exchange_mode` member of `ExchangeResources` to `exchange_modes`\n- Rename `blacklisted_game_entities` member of `Herd` to `blacklisted_entities`\n- Rename `death_conditions` member of `PassiveTransformTo` to `condition`\n- Rename `blacklisted_game_entities` member of `SendBackToTask` to `blacklisted_entities`\n- Rename `blacklisted_game_entities` member of `ShootProjectile` to `blacklisted_entities`\n- Rename `empty_threshold` member of `Storage` to `empty_condition`\n- Rename `allowed_trade_routes` member of `TradePost` to `trade_routes`\n\n### Auxiliary module\n- Rename `AvailabilityPrerequisite` object to `Literal`\n- Rename `DamageProgress` object to `AttributeChangeProgress`\n- Rename `Civilization` object to `PlayerSetup`\n- Rename `ExchangePool` object to `PricePool`\n- Rename `StorageElement` object to `StorageElementDefinition`\n- Rename `creation_sound` member of `CreatableGameEntity` to `creation_sounds`\n- Rename `placement_mode` member of `CreatableGameEntity` to `placement_modes`\n- Rename `requirements` member of `CreatableGameEntity` to `condition`\n- Rename `storage_elements` member of `Container` to `storage_element_defs`\n- Rename `blacklisted_game_entities` member of `GameEntityScope(Entity)` to `blacklisted_entities`\n- Rename `civ_setup` member of `PlayerSetup` to `game_setup`\n- Rename `requirements` member of `ResearchableTech` to `condition`\n- Rename `research_sound` member of `ResearchableTech` to `research_sounds`\n- Rename `capacity` member of `ResourceSpot` to `max_amount`\n- Rename all fqons starting with `aux.literal.*` to `aux.logic.literal.*`\n- Rename all fqons starting with `aux.translated.*` to `aux.language.translated.*`\n\n### Modifier module\n- Rename `TimeRelativeProgress` object to `TimeRelativeProgressChange`\n- Rename `RefundOnDeath` object to `RefundOnCondition`\n- Rename `requirements` member of `InstantTechResearch` to `condition`\n- Rename `blacklisted_game_entities` member of `FlyOver` to `blacklisted_entities`\n- Rename `blacklisted_game_entities` member of `DepositResourcesOnProgress` to `blacklisted_entities`\n- Rename `elevation_difference` member of `ElevationDifferenceHigh` to `min_elevation_difference`\n- Rename `elevation_difference` member of `ElevationDifferenceLow` to `min_elevation_difference`\n- Rename all fqons starting with `modifer.multiplier.*` to `modifier.*`\n- Rename all fqons starting with `modifer.relative_projectile_amount.*` to `modifier.*`\n\n### Effect module\n- Rename `AttributeCostResistance` object to `CostResistance`\n- Rename `TimeRelativeProgress` object to `TimeRelativeProgressChange`\n- Rename fqon `engine.effect.continuous.type.Lure` to `engine.effect.continuous.lure.type.Lure`\n- Rename fqon `engine.effect.discrete.type.MakeHarvestable` to `engine.effect.discrete.make_harvestable.type.MakeHarvestable`\n- Rename fqon `engine.effect.discrete.type.SendToContainer` to `engine.effect.discrete.send_to_container.type.SendToContainer`\n\n### Resistance module\n- Rename `AttributeCostResistance` object to `CostResistance`\n- Rename `TimeRelativeProgress` object to `TimeRelativeProgressChange`\n- Rename `harved_conditions` member of `MakeHarvestable` to `resist_condition`\n- Rename fqon `engine.resistance.continuous.type.Lure` to `engine.resistance.continuous.lure.type.Lure`\n- Rename fqon `engine.resistance.discrete.type.MakeHarvestable` to `engine.resistance.discrete.make_harvestable.type.MakeHarvestable`\n- Rename fqon `engine.resistance.discrete.type.SendToContainer` to `engine.resistance.discrete.send_to_container.type.SendToContainer`\n\n## Added\n### Ability module\n- Add `AbilityProperty` object; replaces inherited `Ability` specializations\n- Add `Animated(AbilityProperty)` object\n- Add `AnimationOverride(AbilityProperty)` object\n- Add `CommandSound(AbilityProperty)` object\n- Add `Diplomatic(AbilityProperty)` object\n- Add `ExecutionSound(AbilityProperty)` object\n- Add `Lock(AbilityProperty)` object\n- Add `DetectCloak(Ability)` object; enables game entity to discover cloaked units\n- Add `Lock(Ability)` object; defines lock pools which can be used for preventing abilities from being active at the same time\n- Add `ProductionQueue(Ability)` object; enables game entity to queue game entity or tech production\n- Add `ResourceStorage(Ability)` object; stores resources from gathering\n- Add `properties : dict(AbilityProperty, AbilityProperty)` member to `Ability`\n- Add `batches : set(EffectBatch)` member to `ApplyDiscreteEffect`; stores the effects as transactions\n- Add `activation_condition : set(LogicElement)` member to `Despawn`; activates the ability\n- Add `containers : set(ResourceContainer)` member to `DropResources`\n- Add `accepts_from : set(ResourceContainer)` member to `DropSite`\n- Add `exchange_rate : ExchangeRate` member to `ExchangeResources`\n- Add `resource_a : Resource` member to `ExchangeResources`\n- Add `resource_b : Resource` member to `ExchangeResources`\n- Add `container : ResourceContainer` member to `Gather`\n- Add `gatherer_limit : int` member to `Harvestable`; moved from `ResourceSpot`\n- Add `harvest_progress : set(HarvestProgress)` member to `Harvestable`; moved from `ResourceSpot`\n- Add `harvestable_by_default : bool` member to `Harvestable`; moved from `ResourceSpot`\n- Add `restock_progress : set(RestockProgress)` member to `Harvestable`; moved from `RestockableResourceSpot`\n- Add `strength : int` member to `Herd`; Herders with higher strength will be prefered\n- Add `mode : HerdableMode` member to `Herdable`; determines who gets ownership of the herdable game entity\n- Add `hitbox : Hitbox` member to `Hitbox`; x, y, z dimensions\n- Add `hitbox : Hitbox` member to `Passable`; references hitbox that is passable\n- Add `mode : PassableMode` member to `Passable`; determines special passability\n- Add `transform_progress : set(TransformProgress)` member to `PassiveTransformTo`\n- Add `selection_box : SelectionBox` member to `Selectable`; determines at which relative coordinates the game entity can be selected\n- Add `allowed_types : set(TerrainType)` member to `TerrainRequirement`; specifies which terrain types can be walked on by a game entity\n- Add `blacklisted_terrains : set(Terrain)` member to `TerrainRequirement`; exceptions for allowed terrain types\n\n### Auxiliary module\n- Add `EffectBatch(Entity)` object\n- Add `OrderedBatch(EffectBatch)` object\n- Add `UnorderedBatch(EffectBatch)` object\n- Add `ChainedBatch(EffectBatch)` object\n- Add `BatchProperty(Entity)` object\n- Add `Priority(BatchProperty)` object\n- Add `Chance(BatchProperty)` object\n- Add `OwnsGameEntity(Literal)` object\n- Add `StateChangeActive(Literal)` object\n- Add `Reset(StateChanger)` object\n- Add `Reset(AnimationOverride)` object\n- Add `ProgressProperty(Entity)` object; replaces inherited `Progress` specializations\n- Add `AnimationOverlay(ProgressProperty)` object\n- Add `Animated(ProgressProperty)` object\n- Add `Terrain(ProgressProperty)` object\n- Add `TerrainOverlay(ProgressProperty)` object\n- Add `StateChange(ProgressProperty)` object\n- Add `LockPool(Entity)` object\n- Add `HerdableMode(Entity)` object; determines who gets ownership of the herdable game entity\n- Add `ClosestHerding(HerdableMode)` object\n- Add `LongestTimeInRange(HerdableMode)` object\n- Add `MostHerding(HerdableMode)` object\n- Add `Shadow(PaymentMode)` object; requires resources to be available but does not subtract them from payment resources\n- Add `TerrainType(Entity)` object; can specify type of a terrain\n- Add `CalculationType(Entity)` object; used for `StackedResistance`\n- Add `NoStack(CalculationType)` object\n- Add `Linear(CalculationType)` object\n- Add `Hyperbolic(CalculationType)` object\n- Add `DistributionType(Entity)` object; used for `StackedResistance`\n- Add `Mean(DistributionType)` object\n- Add `OwnStorage(PlacementMode)` object; allows created game entities to be store directly into container of creating game entity\n- Add `ProductionMode(Entity)` object; used by `ProductionQueue` ability\n- Add `Creatables(ProductionMode)` object\n- Add `Researchables(ProductionMode)` object\n- Add `Hitbox(Entity)` object; stores size arguments for `Hitbox(Ability)`\n- Add `PassableMode(Entity)` object; determines special passable modes\n- Add `Normal(PassableMode)` object\n- Add `Gate(PassableMode)` object; hitbox is always passable to everyone with correct stance and passable with any other stance when any other unit is passing through\n- Add `LiteralScope(Entity)` object; specifies where the literal has to be fulfilled\n- Add `Any(LiteralScope)` object\n- Add `Self(LiteralScope)` object\n- Add `SelectionBox(Entity)` object; used by `Selectable` ability\n- Add `MatchToSprite(SelectionBox)` object\n- Add `Rectangle(SelectionBox)` object\n- Add `TransformPool(Entity)` object; defines one explicit state machine that can have one state at a time\n- Add `NyanPatch(Entity)` object; patches must inherit from this object now\n- Add `PatchProperty(Entity)` object\n- Add `Diplomatic(PatchProperty)` object\n- Add `AttributeBelowValue(Literal)` object; replaces `AttributeInterval(Literal)`\n- Add `AttributeAboveValue(Literal)` object; replaces `AttributeInterval(Literal)`\n- Add `AttributeBelowPercentage(Literal)` object\n- Add `AttributeAbovePercentage(Literal)` object\n- Add `Palette(Entity)` object\n- Add `Guard(MoveMode)` object\n- Add `Replace(PlacementMode)` object\n- Add `TechType(Entity)` object; defines types for techs\n- Add `Any(GameEntityType)` object\n- Add `Any(TechType)` object\n- Add `Any(TerrainType)` object\n- Add `Any(DiplomaticStance)` object\n- Add `ResourceStorage(Entity)` object; manages resource capacity and carry progress\n- Add `InternalDropSite(ResourceStorage)` object; resource storage that doesn't need a drop site\n- Add `Sell(ExchangeMode)` object\n- Add `Buy(ExchangeMode)` object\n- Add `ExchangeRate(Entity)` object\n- Add `PriceMode(Entity)` object\n- Add `Fixed(PriceMode)` object\n- Add `Dynamic(PriceMode)` object\n- Add `LogicElement(Entity)` object; logic block for conditions\n- Add `LogicGate(LogicElement)` object; boolean expression, circuit style\n- Add `AND(LogicGate)` object\n- Add `MULTIXOR(LogicGate)` object\n- Add `NOT(LogicGate)` object\n- Add `OR(LogicGate)` object\n- Add `SUBSETMAX(LogicGate)` object\n- Add `SUBSETMIN(LogicGate)` object\n- Add `XOR(LogicGate)` object\n- Add `True(LogicElement)` object\n- Add `False(LogicElement)` object\n- Add `variants : set(Variant)` member to `CreatableGameEntity` object\n- Add `priority : int` member to `Mod` object; determines load order when multiple `Mod` objects are present\n- Add `types: set(TerrainType)` member to `Terrain`; can specify type of a terrain\n- Add `tile_snap_distance : float` member to `Place(PlacementMode)`; lets placed game entities snap to the tile grid\n- Add `clearance_size_x : float` member to `Place(PlacementMode)`\n- Add `clearance_size_y : float` member to `Place(PlacementMode)`\n- Add `allow_rotation : bool` member to `Place(PlacementMode)`\n- Add `max_elevation_difference : int` member to `Place(PlacementMode)`\n- Add `scope : LiteralScope` member to `Literal(Entity)`; specifies where the literal has to be fulfilled\n- Add `patch : NyanPatch` member to `Patch(Entity)`; objects now acts as engine wrapper for patches, previous functionality replaced by `NyanPatch(Entity)`\n- Add `left_boundary: float` member to `Progress(Entity)`; boundary for progress interval\n- Add `right_boundary: float` member to `Progress(Entity)`; boundary for progress interval\n- Add `allowed_types: set(GameEntityType)` member to `Container(Entity)`; now defines which game entites can be stored in the container\n- Add `blacklisted_entities : set(GameEntity)` member to `Container(Entity)`; now restricts which game entites can be stored in the container\n- Add `types : set(TechType)` member to `Tech(Entity)`\n- Add `ordering_priority: int` member to `Subformation(Entity)`; replaces `PreceedingSubformation` logic\n- Add `transform_pool : optional(TransformPool)` member to `StateChanger(Entity)`\n- Add `search_range : float` member to `GameEntityStance(Entity)`\n- Add `fee_multiplier : float` member to `ExchangeMode(Entity)`\n\n### Modifier module\n- Add `ModifierProperty(Entity)` object; replaces inherited `Modifier` specializations\n- Add `Multiplier(ModifierProperty)` object\n- Add `Scoped(ModifierProperty)` object\n- Add `Stacked(ModifierProperty)` object; allows defining how often a modifier is applied\n- Add `properties : dict(ModifierProperty, ModifierProperty)` member to `Modifier`\n- Add `condition : set(LogicElement)` member to `RefundOnCondition(Modifier)`\n\n### Effect module\n- Add `EffectProperty(Entity)` object; replaces inherited `Effect` specializations\n- Add `AreaEffect(EffectProperty)` object\n- Add `Cost(EffectProperty)` object\n- Add `Diplomatic(EffectProperty)` object\n- Add `Priority(EffectProperty)` object\n- Add `properties : dict(EffectProperty, EffectProperty)` member to `Effect`\n\n### Resistance module\n- Add `ResistanceProperty(Entity)` object; replaces inherited `Resistance` specializations\n- Add `Cost(ResistanceProperty)` object\n- Add `Stacked(ResistanceProperty)` object; allows adjustment to effect calculation when multiple effectors are present\n- Add `properties : dict(ResistanceProperty, ResistanceProperty)` member to `Resistance`\n\n## Changed\n### Ability module\n- Change type of `despawn_condition` member in `Despawn` from `set(DespawnCondition)` to `set(LogicElement)`\n- Change type of `state_change` member in `Despawn` from `StateChanger` to `optional(StateChanger)`\n- Change type of `exchange_modes` member in `ExchangeResources` from `ExchangeMode` to `set(ExchangeMode)`\n- Change type of `death_conditions` member in `PassiveTransformTo` from `set(DeathCondition)` to `set(LogicElement)`\n- Change type of `target` member in `Restock` from `RestockableResourceSpot` to `ResourceSpot`\n- Change type of `empty_threshold` member in `Storage` from `AttributeAmount` to `set(LogicElement)`\n\n### Auxiliary module\n- Change type of `condition` member in `ResearchableTech` from `set(AvailabilityRequirement)` to `set(LogicElement)`\n- Change type of `condition` member in `CreatableGameEntity` from `set(AvailabilityRequirement)` to `set(LogicElement)`\n- Change type of `creation_sounds` member in `CreatableGameEntity` from `Sound` to `set(Sound)`\n- Change type of `research_sounds` member in `ResearchableTech` from `Sound` to `set(Sound)`\n- Change type of `overrides` member in `Animated(ProgressProperty)` from `AnimationOverride` to `set(AnimationOverride)`\n- Change type of `placement_modes` member in `CreatableGameEntity` from `PlacementMode` to `set(PlacementMode)`\n- Change type of `object` member in `TerrainAmbient` from `Ambient` to `GameEntity`\n- Change type of `conflicts` member in `StorageElementDefinition` from `set(StorageElement)` to `set(StorageElementDefinition)`\n- Change parent of `Literal` from `Entity` to `LogicElement`\n- Change parent of `ProjectilePassThrough` from `DeathCondition` to `Literal`\n- Change parent of `ProjectileHitTerrain` from `DeathCondition` to `Literal`\n- Change parent of `ResourceDepleted` from `DespawnCondition` to `Literal`\n- Change parent of `Timer` from `DespawnCondition` to `Literal`\n\n### Modifier module\n- Change parent of `CreationTime` from `MultiplierModifier` to `Modifier`\n- Change parent of `AttributeSettingsValue` from `MultiplierModifier` to `Modifier`\n- Change parent of `MoveSpeed` from `MultiplierModifier` to `Modifier`\n- Change parent of `ReloadTime` from `MultiplierModifier` to `Modifier`\n- Change parent of `GatheringEfficiency` from `MultiplierModifier` to `Modifier`\n- Change parent of `GatheringRate` from `MultiplierModifier` to `Modifier`\n- Change parent of `StorageElementCapacity` from `MultiplierModifier` to `Modifier`\n- Change parent of `ContainerCapacity` from `MultiplierModifier` to `Modifier`\n- Change parent of `CreationResourceCost` from `MultiplierModifier` to `Modifier`\n- Change parent of `CreationAttributeCost` from `MultiplierModifier` to `Modifier`\n- Change parent of `ResearchTime` from `MultiplierModifier` to `Modifier`\n- Change parent of `ResearchResourceCost` from `MultiplierModifier` to `Modifier`\n- Change parent of `ResearchAttributeCost` from `MultiplierModifier` to `Modifier`\n- Change parent of `Unconditional` from `FlatAttributeChangeModifier(EffectMultiplierModifier)` to `Modifier`\n- Change parent of `ElevationDifferenceHigh` from `FlatAttributeChangeModifier(EffectMultiplierModifier)` to `Modifier`\n- Change parent of `ElevationDifferenceLow` from `FlatAttributeChangeModifier(EffectMultiplierModifier)` to `Modifier`\n- Change parent of `Flyover` from `FlatAttributeChangeModifier(EffectMultiplierModifier)` to `Modifier`\n- Change parent of `Terrain` from `FlatAttributeChangeModifier(EffectMultiplierModifier)` to `Modifier`\n- Change parent of `TimeRelativeProgressChange` from `EffectMultiplierModifier` to `Modifier`\n- Change parent of `TimeRelativeAttributeChange` from `EffectMultiplierModifier` to `Modifier`\n- Change parent of `Unconditional` from `FlatAttributeChangeModifier(ResistanceMultiplierModifier)` to `Modifier`\n- Change parent of `ElevationDifferenceHigh` from `FlatAttributeChangeModifier(ResistanceMultiplierModifier)` to `Modifier`\n- Change parent of `ElevationDifferenceLow` from `FlatAttributeChangeModifier(ResistanceMultiplierModifier)` to `Modifier`\n- Change parent of `Stray` from `FlatAttributeChangeModifier(ResistanceMultiplierModifier)` to `Modifier`\n- Change parent of `Terrain` from `FlatAttributeChangeModifier(ResistanceMultiplierModifier)` to `Modifier`\n- Change parent of `AoE2ProjectileModifier` from `RelativeProjectileAmountModifier` to `Modifier`\n- Change type of `storage_element` member in `StorageElementCapacity` from `StorageElement` to `StorageElementDefinition`\n- Change type of `elevation_difference` member in `ElevationDifferenceHigh` from `float` to `optional(float)`\n- Change type of `elevation_difference` member in `ElevationDifferenceLow` from `float` to `optional(float)`\n- Change type of `condition` member in `InstantTechResearch` from `set(AvailabilityRequirement)` to `set(LogicElement)`\n\n### Resistance module\n- Change type of `resist_condition` member in `MakeHarvestable` from `set(HavestableRequirement)` to `set(LogicElement)`\n\n## Removed\n### Ability module\n- Remove `AnimatedAbility` object; now handled by `Animated(AbilityProperty)`\n- Remove `AnimationOverrideAbility` object; now handled by `AnimationOverride(AbilityProperty)`\n- Remove `CommandSoundAbility` object; now handled by `CommandSound(AbilityProperty)`\n- Remove `DiplomaticAbility` object; now handled by `Diplomatic(AbilityProperty)`\n- Remove `ExecutionSound` object; now handled by `ExecutionSound(AbilityProperty)`\n- Remove `TileRequirement` object; now handled by `Hitbox`\n- Remove `Gate` object; now handled by `Passable`\n- Remove `Deletable` object; now handled by `ActiveTransformTo`\n- Remove `Transform` object\n- Remove `effects` member from `ApplyDiscreteEffect`; superceded by `batches` member\n- Remove `terrain_requirement` member from `TerrainRequirement`; superceded by `allowed_types` and `blacklisted_terrains` members\n- Remove `radius_x` member from `Hitbox`; moved to referenced object\n- Remove `radius_y` member from `Hitbox`; moved to referenced object\n- Remove `radius_z` member from `Hitbox`; moved to referenced object\n- Remove `indicator` member from `Ambient`; will use animation from `AnimatedAbility` from now on\n- Remove `flatten_ground` member from `Foundation`\n- Remove `carry_capacity` member from `Gather`; moved to `ResourceStorage(Entity)`\n- Remove `carry_progress` member from `Gather`; moved to `ResourceStorage(Entity)`\n- Remove `source_resources` member from `ExchangeResources`\n- Remove `target_resources` member from `ExchangeResources`\n- Remove `source_fee` member from `ExchangeResources`\n- Remove `target_fee` member from `ExchangeResources`\n\n### Auxiliary module\n- Remove `DiplomaticPatch` object; now handled by `Diplomatic(PatchProperty)`\n- Remove `AnimationOverrideProgress` object; now handled by `AnimationOverride(ProgressProperty)`\n- Remove `AnimatedProgress` object; now handled by `Animated(ProgressProperty)`\n- Remove `TerrainProgress` object; now handled by `Terrain(ProgressProperty)`\n- Remove `TerrainOverlayProgress` object; now handled by `TerrainOverlay(ProgressProperty)`\n- Remove `StateChangeProgress` object; now handled by `StateChange(ProgressProperty)`\n- Remove `AvailabilityRequirement` object\n- Remove `RestockableResourceSpot` object; restockable now possible for every resource spot\n- Remove `DeathCondition` object; functionality merged with `LogicElement` and `Literal`\n- Remove `DespawnCondition` object; functionality merged with `LogicElement` and `Literal`\n- Remove `MultiPartBuilding` object; now fully replaced by generic `GameEntity`\n- Remove `BuildingPart` object; now fully replaced by generic `GameEntity`\n- Remove `Ambient` object; now fully replaced by generic `GameEntity`\n- Remove `Building` object; now fully replaced by generic `GameEntity`\n- Remove `Item` object; now fully replaced by generic `GameEntity`\n- Remove `Projectile` object; now fully replaced by generic `GameEntity`\n- Remove `Unit` object; now fully replaced by generic `GameEntity`\n- Remove `AttributeInterval(Literal)` object; fully replaced by lower cealing `AttributeBelowValue` and higher cealing `AttributeAboveValue` literals\n- Remove `ExchangeMode` object\n- Remove `Fixed(ExchangeMode)` object\n- Remove `Volatile(ExchangeMode)` object\n- Remove `VolatileFlat(Volatile)` object\n- Remove `PrecedingSubformation` object; replaced by `ordering_priority` member in `Subformation`\n- Remove `allowed_types` member from `Terrain`; now handled by `TerrainRequirement` ability\n- Remove `blacklisted_game_entities` member from `Terrain`; now handled by `TerrainRequirement` ability\n- Remove `harvest_progress` member from `ResourceSpot`; now handled by `Harvestable` ability\n- Remove `gatherer_limit` member from `ResourceSpot`; now handled by `Harvestable` ability\n- Remove `harvestable_by_default` member from `ResourceSpot`; now handled by `Harvestable` ability\n- Remove `progress: int` member from `Progress(Entity)`; progress is now defined by bounded interval with members `left_boundary : float`, `right_boundary : float`\n- Remove `mode: bool` member from `Literal(LogicElement)`; negation now only supported by `NOT(LogicGate)`\n\n### Modifier module\n- Remove `EffectMultiplierModifier(MultiplierModifier)` object; specialization now implicit\n- Remove `FlatAttributeChangeModifier(EffectMultiplierModifier)` object; specialization now implicit\n- Remove `ResistanceMultiplierModifier(MultiplierModifier)` object; specialization now implicit\n- Remove `FlatAttributeChangeModifier(EffectMultiplierModifier)` object; specialization now implicit\n- Remove `MultiplierModifier` object; now handled by `Multiplier(ModifierProperty)`\n- Remove `ScopedModifier` object; now handled by `Scoped(ModifierProperty)`\n- Remove `StackedModifier` object; now handled by `Stacked(ModifierProperty)`\n- Remove `RelativeProjectileAmountModifier` object\n- Remove `AlwaysHerd` object; functionality now in `Herd` via `strength` member\n\n### Effect module\n- Remove `AreaEffect` object; now handled by `AreaEffect(EffectProperty)`\n- Remove `Cost` object; now handled by `Cost(EffectProperty)`\n- Remove `Diplomatic` object; now handled by `Diplomatic(EffectProperty)`\n\n### Resistance module\n- Remove `CostResistance` object; now handled by `Cost(ResistanceProperty)`\n- Remove `StackedResistance` object; now handled by `Stacked(ResistanceProperty)`\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/b947ad39f10f54ebef8c7e7f732cea9c942f0521/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.4.0.md",
    "content": "# [0.4.0] - 2021-05-05\nAll notable changes for version [v0.4.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Renamed\n### Auxiliary module\n- Rename `aux` module to `util`; makes the resulting folder names Windows-compatible\n\n**The module will be referred to as the *utility module* as of now.**\n\n- Rename `Container` object to `EntityContainer`\n\n### Modifier module\n- Rename `ContainerCapacity` object to `EntityContainerCapacity`\n\n### Root module\n- Rename `Entity` object to `Object`\n\n## Added\n### Ability module\n- Add `container : ResourceContainer` member to `Trade`\n\n### Utility module\n- Add `AttributeChange(ProgressType)` object; replaces `AttributeChangeProgress(Progress)`\n- Add `Carry(ProgressType)` object; replaces `CarryProgress(Progress)`\n- Add `Harvest(ProgressType)` object; replaces `HarvestProgress(Progress)`\n- Add `Restock(ProgressType)` object; replaces `RestockProgress(Progress)`\n- Add `Transform(ProgressType)` object; replaces `TransformProgress(Progress)`\n- Add `type : ProgressType` member to `Progress`\n\n\n## Changed\n### Ability module\n- Change type of `construction_progress` member in `Constructable` from `set(ConstructionProgress)` to `set(Progress)`\n- Change type of `harvest_progress` member in `Harvestable` from `set(HarvestProgress)` to `set(Progress)`\n- Change type of `restock_progress` member in `Harvestable` from `set(RestockProgress)` to `set(Progress)`\n- Change type of `transform_progress` member in `ActiveTransformTo` from `set(TransformProgress)` to `set(Progress)`\n- Change type of `transform_progress` member in `PassiveTransformTo` from `set(TransformProgress)` to `set(Progress)`\n\n### Utility module\n- Change type of `carry_progress` member in `ResourceContainer` from `set(CarryProgress)` to `set(Progress)`\n- Change type of `carry_progress` member in `EntityContainer` from `set(CarryProgress)` to `set(Progress)`\n\n\n## Removed\n### Utility module\n- Remove `AttributeChangeProgress` object; replaced by progress type `AttributeChange`\n- Remove `CarryProgress` object; replaced by progress type `Carry`\n- Remove `ConstructProgress` object; replaced by progress type `Construct`\n- Remove `HarvestProgress` object; replaced by progress type `Harvest`\n- Remove `RestockProgress` object; replaced by progress type `Restock`\n- Remove `TransformProgress` object; replaced by progress type `Transform`\n\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/78051b7f894fdf7f7c6d44c05ac7239fe5a896cb/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.4.1.md",
    "content": "# [0.4.1] - 2023-12-02\nAll notable changes for version [v0.4.1] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Added\n### Ability module\n- Add `Activity(Ability)` object; defines the behaviour of a game entity\n\n### Utility module\n- Add `Activity(Entity)` object; stores behaviour node graph of a game entity\n- Add `Node(Entity)` object; node in behaviour node graph\n- Add `Ability(Node)` object\n- Add `End(Node)` object\n- Add `Start(Node)` object\n- Add `XORGate(Node)` object\n- Add `XOREventGate(Node)` object\n- Add `Condition(Object)` object\n- Add `CommandInQueue(Condition)` object\n- Add `NextCommandIdle(Condition)` object\n- Add `NextCommandMove(Condition)` object\n- Add `Event(Entity)` object; event for behaviour node graph\n- Add `Wait(Event)` object\n- Add `WaitAbility(Event)` object\n- Add `CommandInQueue(Event)` object\n\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/408fc171552bc96a30549d05fceeb9d692fd9d1d/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.5.0.md",
    "content": "# [0.5.0] - 2024-07-29\nAll notable changes for version [v0.5.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Renamed\n### Ability module\n- Rename `Hitbox` to `Collision`\n\n## Added\n### Ability module\n- Add `Pathable(Ability)` object; defines pathing costs for the game entity when the ability is active\n- Add `path_type : PathType` member to `Move`\n\n### Utility module\n- Add `PathType(Object)` object; associates move abilities with pathfinding grids\n- Add `path_costs : dict(children(PathType), int)` member to `Terrain`; defines pathing costs for the terrain\n\n### Removed\n### Ability module\n- Remove `Passable(Ability)` object; functionality superceded by `Pathable`\n\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/f1967c3c002d444510e50f54c9cdbb83419a9ec4/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/changelogs/nyan_api/v0.6.0.md",
    "content": "# [0.6.0] - 2025-05-18\nAll notable changes for version [v0.6.0] are documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## Added\n### Ability module\n- Add `Ranged(Property)` object; defines range of an ability; replaces various range members in `Ability` objects\n\n### Utility module\n- Add `AbilityUsable(Condition)` object; checks if an ability can be used by an entity\n- Add `NextCommand(Condition)` object; checks if a specific command is next in the command queue; replaces individual objects checking for specific commands\n- Add `TargetInRange(Condition)` object; checks if the target of an entity is in range of a specific ability\n- Add `Task(Node)` object; execute internal task when visiting node\n- Add `Task(Object)` object\n- Add `ClearCommandQueue(Task)` object; clears the command queue when visiting task node\n- Add `PopCommandQueue(Task)` object; pops the front command in the command queue when visiting task node\n- Add `MoveToTarget(Task)` object; move to the current target of the entity when visiting task node\n- Add `XORSwitchGate(Node)` object; switch branches based on evaluating single value\n- Add `SwitchCondition(Object)` object\n- Add `NextCommand(SwitchCondition)` object; switch branches based on the next command in the command queue\n- Add `Command(Object)` object; references internal commands of the engine\n- Add `ApplyEffect(Command)` object\n- Add `Idle(Command)` object\n- Add `Move(Command)` object\n\n### Removed\n### Ability module\n- Remove `RangedContinuousEffect(Ability)` object; functionality superceded by `Ranged(Property)` object\n- Remove `RangedDiscreteEffect(Ability)` object; functionality superceded by `Ranged(Property)` object\n- Remove `range` member from `DetectCloak(Ability)`; functionality superceded by `Ranged(Property)` object\n- Remove `range` member from `Herd(Ability)`; functionality superceded by `Ranged(Property)` object\n- Remove `min_range` member from `ShootProjectile(Ability)`; functionality superceded by `Ranged(Property)` object\n- Remove `max_range` member from `ShootProjectile(Ability)`; functionality superceded by `Ranged(Property)` object\n\n### Utility module\n- Remove `NextCommandIdle(Condition)` object; functionality superceded by `NextCommand(Condition)` object\n- Remove `NextCommandMove(Condition)` object; functionality superceded by `NextCommand(Condition)` object\n\n## Reference visualization\n\n* [Gamedata](https://github.com/SFTtech/openage/blob/927f547d4985cba8e172c9492273b34537571c56/doc/nyan/aoe2_nyan_tree.svg)\n"
  },
  {
    "path": "doc/code/README.md",
    "content": "Code documentation\n==================\n\nIn here resides documentation of concepts and APIs implemented in source code\nalready.\n\nThis is to provide information about what the author thought of when the code\nwas written so others can understand the ideas.\n"
  },
  {
    "path": "doc/code/architecture.md",
    "content": "# openage architecture\n\nopenage is separated into many modules (\"subsystems\").\n\n1. [Overview](#overview)\n   1. [Components](#components)\n   2. [Utilities](#utilities)\n2. [Information flow](#information-flow)\n   1. [Current architecture](#current-architecture)\n   2. [Goal architecture](#goal-architecture)\n\n\n## Overview\n\nSome of the components are already implemented, others are not.\nAll of them need to be revisited to implement the goal architecture.\n\n\n### Components\n\n* Audio system\n* Configuration system\n* [Coordinate system](coordinate-systems.md)\n* [Converter](converter/)\n* [Input system](input/)\n* Networking\n* [Game data database](/doc/nyan/)\n* [Rendering](renderer/)\n* [Simulation](game_simulation/)\n* [Time management](time.md)\n* [User interface](gui.md)\n\n\n### Utilities\n\n* Datastructures\n  * [Curves](curves.md)\n* [Error handling](exceptions.md)\n* Filesystem abstraction\n* Job dispatching\n* Live reloading\n* [Logging system](logger.md)\n* [Python interface](pyinterface.md)\n* Random number generator\n* [Test and demo infrastructure](testing.md)\n\n\n## Information flow\n\n### Current architecture\n\nThe current data flow of openage is just to display the raw simulation data.\n\n![Engine architecture workflow](images/engine_architecture.svg)\n\nThe presenter, simulation, and time subsystems each run in a loop in their own threads.\nThey are largely decoupled from each other and only communicate via defined interfaces.\n\nCommunication between all subsystems forms a larger *main* loop that encompasses everything\nhappening in an engine run. It's workflow is roughly like this:\n\n```\nrenderer (window system) -> input -> event system -> simulation -> renderer -> output\n```\n\nopenage does not have simulation steps or *ticks* since everything in the simulation is\nevent-based and scheduled by time. Therefore, updates between threads should generally work\nasynchronously.\n\nDecoupling allows us to treat some subsystems as optional such as the renderer and input\nsystem (basically everything from the presenter).\n\n\n### Goal architecture\n\nThe goal architecture extends the current workflow by subsystems for networking and\nscripting:\n\n![Goal architecture workflow](images/engine_target_architecture.svg)\n\nBoth of these are supposed to be decoupled and optional. The current architecture can be extended\nby adding the missing components in between.\n\nNetworking forwards the relevant events and simulation parameters during multiplayer.\nWe will have a single authoritative server that is also running the simulation asynchronously.\nEach client then receives the data visible for it.\n\nScripting extends the event system with external sources targets for events. Most\nscripting should be integrated using event logic.\n\nThe new workflow would then look something like this:\n\n```\n                                          ---------> scripting\n                                          |             ^\n                                          |             |\n                                          v             v\nrenderer (window system) -> input -> event system -> simulation -> renderer -> output\n                                          ^             ^\n                                          |             |\n                                          |             v\n                                          ---------> network\n```\n"
  },
  {
    "path": "doc/code/converter/README.md",
    "content": "# Converter\n\nThe converter transforms the old and janky Genie Engine formats\ninto new and shiny openage formats.\n\nThe major parts are implemented in Python 3. Where speed is important\nor external libraries are used, we use Cython.\n\n\n## Architecture\n\n[Architecture](architecture_overview.md)\n\nThis document gives you an overview of the paradigms used in the code.\n\n\n## Workflow\n\n[Workflow](workflow.md)\n\nHere you can find out about the workflow of a converter run and what\nhappens in the individual stages of conversion.\n"
  },
  {
    "path": "doc/code/converter/architecture_overview.md",
    "content": "# Converter Architecture Overview\n\nThis document describes the code architecture of the openage converter module.\n\n1. [Design Principles](#design-principles)\n   1. [Value Object](#value-object)\n   2. [Entity Object](#entity-object)\n   3. [Service](#service)\n   4. [Processor](#processor)\n   5. [Tool](#tool)\n\n\n## Design Principles\n\nOur converter uses hierarchical multi-layered object-oriented code principles similar to those found in\n[Domain-Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) and [WAM](http://wam-ansatz.de/).\nThe main focus of using these principles was to provide **readability** and **maintainability** as well\nas enabling us to quickly integrate extensions that support other games and game versions than the original\nAge of Empires 2 release.\n\nA hierarchical structure is achieved by designing objects as part of one of the 5 domains described below.\nEvery domain defines a category of complexity. The idea is that every object can only access functionality\nfrom domains with the same or lower complexity. For example, an object for data storage should not access\nthe methods that drive the main conversion process. The resulting code will thus not evolve into a spaghetti\nmess.\n\n### Value Object\n\nValue objects are used to store primitive data or as definition of how to read primitive data from files.\nIn the converter, the parsers utilize these objects to extract attributes from the original Genie Engine\nfile formats. Extracted attributes are also saved as value objects.\n\nValue objects are treated as *immutable*. Operations on the objects values will therefore always return\na new object instance with the result and leave the original values as-is.\n\n### Entity Object\n\nEntity objects are used for structures that have a defined identity. An example for such a structure would\nbe one `Unit` object from AoE2 or a `GameEntity` instance from the openage API. Entity objects are mutable\nand can have any number of attributes assigned to them. In the converter, they are used to model the\nmore complex structures from Genie games such as units, civilizations and techs. The transition to the nyan\nAPI objects is also done utilizing entity objects.\n\n### Service\n\nServices are interfaces for requesting or passing collections of entity objects and value objects from/to\na processor or tool. It is also used to communicate with external interfaces such as included libraries\nor Python packages. A services main job is to translate the data it receives/forwards into a usable format.\nExamples for services are internal name lookup service for mapping unit IDs to nyan object names as well as\nthe nyan file and graphics exporters.\n\n### Processor\n\nProcessors operate on a given dataset consisting of multiple entity objects to produce a result. For the\nconverter, the input dataset is the data read in from Genie file formats, while the result are modpacks\nfor openage. Processors can be split into several subprocessor modules for which the same hierarchical\nstructure applies. A subprocessor can never access functions from a parent processor, but use any other\n(sub-)processor or entity object/value object.\n\nopenage conversion processor are not instanced and implement most of their functions with the `@staticmethod`\ndecorator. The reason for this is to allow conversion processors for game expansions cross-reference\nprocessor functions of their base game and thus reduce code redundancy.\n\n### Tool\n\nTools are used to encapsulate processor with a user interface (GUI, TUI or CLI), pass user input to processors\nand start the conversion process.\n"
  },
  {
    "path": "doc/code/converter/workflow.md",
    "content": "# Workflow\n\nThe converter has a general workflow that is the same for every game edition.\nThis workflow should stay consistent if changes are made.\n\nOur converter is built to support multiple games. Keep in mind that you\nmust test with all supported version when you add a feature.\n\n![workflow structogram](images/workflow.svg)\n\n1. [Game and Version detection](#game-and-version-detection)\n2. [Mounting](#mounting)\n3. [Reader](#reader)\n   1. [Game Data](#game-data)\n4. [Processor](#processor)\n   1. [Preprocessing](#preprocessing)\n   2. [Grouping/Linking](#groupinglinking)\n   3. [Mapping](#mapping)\n5. [Exporter](#exporter)\n\n\n## Game and Version detection\n\nThe conversion process starts with determining the game version. Users\nare requested to provide a path to a game's root folder. The converter\nthen checks for filenames associated with a specific game version\nand compares hashes of these files to pinpoint the version number of\nthe game.\n\nA *game version* in the openage converter context is always a combination\nof the 3 properties listed below.\n\n- **Game Edition**: Standalone edition of a game. This refers to any release that\nis runnable on its own without dependencies. A game may have been released in multiple\neditions, e.g. AoE2 (1999 release, HD Edition, Definitive Edition).\n- **Expansions**: A list of optional expansions or DLCs that were detected alongside\nthe game edition. The list can be empty if no expansions were found in the given path.\nNote that for simplicity's sake, the *Rise of Rome*, *The Conquerors* and *Clone Campaigns*\nexpansions for the original releases of the games are currently handled as game editions\nas we do not support the expansionless installations of these games.\n- **Patch level**: The version number of the game edition.\n\nTo determine the game version, the converter iterates through all known game\neditions and checks if all associated files can be found in the user-provided\npath. If no matching game edition was found, the conversion process terminates\nhere. Once a matching game edition is detected, the converter iterates\nthrough all possible expansions and again tries to find associated files\nin the given path. In the last step, the patch level of the edition and the\nexpansions is identified. For that purpose, the converter calculates\nthe MD5 hash of expected files and retrieves the version number from a lookup\ndict.\n\n## Mounting\n\nIn the next step, the converter mounts asset subpaths and asset files into\na conversion folder. The paths for these assets, organized by media type,\nare stored with the game edition and expansion definitions and can therefore\nbe determined from the game version.\n\nMount points are derived from the asset media types. Thus files of a certain media\ntype can always be found at the same location, regardless of the game version.\nContainer formats such as DRS and CAB are also mounted as folders, so that their\ncontent can be accessed in the same way as \"loose\" files from the source folder.\n\n## Reader\n\nAfter all relevant source folders are mounted, the converter will begin the main\nconversion process by reading the available data. Every source file format has\nits own reader that is adapted to the constraints and features of the format.\nHowever, reading always follows this general workflow:\n\n* **Registration**: Enumerates all files with the requested format.\n* **Parser**: Declares the structure of the file format and also tells the exporter\nhow to interpret them.\n* **Output**: Use the parser to store a low-level representation of the data in\nmemory.\n\nIt should be noted that graphics/sound files are only registered during the Reader\nstage and not parsed or output. The reason for this is that these files usually\ndo not require further in-memory processing and can be directly exported to\nfile. Therefore, parsing and output of these files does not happen until the\nExport stage. Furthermore, the export of a graphic/sound file is only initiated\non-demand, i.e. if the export is requested during the Processor stage. This\nallows us to skip unused media which results in a faster overall conversion time.\n\n### Game Data\n\nGame data for the Genie Engine is stored in the `.dat` file format. The `.dat` file\nformat contains only attribute values with no additional hints about their data type.\nIn addition to this, the `.dat` format can have a slightly different structure for\ndifferent game versions. As a result, all attribute data types have to be manually\ndeclared and the parser is generated on-the-fly depending on the game version.\n\nAn attribute in the parser is defined by 4 parameters.\n\n* **Human-readable name**: Will be used during the Processor stage to access\nattribute values (e.g. `\"attack\"`).\n* **Read type**: The data type plus additional info such as length (e.g. `char[30]`).\n* **Value type**: Determines how an attribute value is interpreted (e.g. `ID_MEMBER`).\nFor example, a `uint32_t` value could be used for a normal integer value or define\nan ID to a graphics/sound resource. Every unique value type is associated with\na Python object type used for storing the attribute.\n* **Output mode**: Determines whether the attribute is part of the reader output\nor can be skipped (e.g. `SKIP`).\n\nThe Reader parses attributes one by one and stores them in a `ValueMember` subclass\nthat is associated with a value type. Its output is a list of `ValueMember` intances.\n\n## Processor\n\nThe Processor stage is where all conversion magic happens. It can be further divided\ninto 3 substages.\n\n- **Preprocessing stage**: Creates logical entities from the list of `ValueMember`s passed by\nthe Reader. These resemble structures from the *original* game (e.g. a unit in AoC).\n- **Grouping stage**: Organizes logical entities from preprocessor stage into concept\ngroups. The groups represent an API concept from the *openage nyan API*.\n- **Mapping stage**: The concept groups are transformed into actual nyan objects. Values\nand properties from the concept groups are mapped to member values for objects in the\nopenage nyan API.\n\nProcessors may have subprocessors for better code structuring and readability. Furthermore,\nthere exists at least one processor for every game edition, although code sharing for\nconverting similar concepts is used and encouraged. For example, the processor for\nconverting AoE1 reuses methods from the AoE2 processor if the concept of a game mechanic\nhas not changed between the releases, e.g. movement. As a consequence, all functions\nin a processor should be either static or class methods and be able to operate on\nsolely on the input parameters.\n\n### Preprocessing\n\nDuring preprocessing the processor first creates a container object for all objects that will\nbe created during conversion. This container is passed around as point of reference\nfor the converter's dataset. Additionally, logical entity objects are created from\nthe game data reader output. Examples for logical entities are units, techs, civs or\nterrain definitions. Logical entities are inserted into the container object after\ncreation.\n\n### Grouping/Linking\n\nThe grouping stage forms concept groups from single logical entities. A concept group\nis a Python object that represents an openage API concept (and not a concept from the\noriginal game). Concept group implementations bundle all data that are necessary\nto create nyan objects from them during the mapping stage. An example for a concept\ngroup is a *unit upgrade line*, e.g. the militia line from AoE2. The logical entities that\nbelong to this group are the units that are part of this line (militia, swordsman,\nlongswordsman, ...). They are stored in a list sorted by their order of upgrades.\n\nConcept groups additionally provide functions to check if a group instance shows\ncertain properties. In the example of the *unit upgrade line*, such a property\ncould be that the units in the line are able to shoot projectiles or\nthat they are creatable in a building. These properties are then used during\nthe mapping stage to assign abilities to the unit line.\n\nConcept group instances are also inserted into the container object from the\npreprocessing stage after they are created.\n\n### Mapping\n\nDuring the mapping stage, nyan objects are created from data in the concept groups.\nIn general, it involves these 3 steps:\n\n1. Check if a concept group has a certain property\n2. If true, create and assign nyan API objects associated with that property\n3. Map values from concept group data to the objects member values\n\nThis is repeated for every property and for every concept group. Most values\ncan be mapped 1-to-1, although some require additional property checks.\n\nIf a mapped value is associated with a graphics/sound ID, the processor will\ngenerate a *media export request* for that ID. The export request is a Python object\nthat contains the ID and the desired target filename. In the Export stage, the\nsource filename for the given ID is then resolved and the file is parsed, converted\nand saved at the target location.\n\nAt the end of the mapping stage, the resulting nyan objects are put into nyan files\nand -- together will the media export requests -- are organized into modpacks which\nare passed to the exporter.\n\n## Exporter\n\nThe exporter saves files contained in a modpack to the file system. nyan files\nare stored as plaintext files, while media export requests are handled by parsing\nthe specified source file, converting it to a predefined target format and\nwriting the result into a file.\n"
  },
  {
    "path": "doc/code/coordinate-systems.md",
    "content": "Comprehensive list of all coordinate systems\n============================================\n\nPhysical Coordinates\n--------------------\n\nA coordinate in such a system describes positions in the 3D space of the game world and rendering scene.\n\nPositions are stored in NE (northeast), SE (southeast) and UP (elevation).\n\nUnits for distances are terrain units (tu) and terrain elevation units (teu).\n\n\n### `phys2/phys3`\n\nPhysics coordinates in the game world.\n\nUsed in these systems:\n* Core engine\n* Gamestate\n* Game entity (unit/building/projectile/...) positions\n\nOrthonormal 2D/3D (NE, SE)/(NE, SE, UP), in tu/teu\nFixed-point integer with a 16-bit fractional part.\n\nOrigin is in the west corner of tile `(0, 0)` (at sea level),\nand the west corner of chunk `(0, 0)`.\nPositive in NE, SE, UP directions.\n\nA unit at `(0.5, 0.5)` is at the center of tile `(0, 0)`,\nwith on-tile offset `(0.5, 0.5)`.\nA unit at `(8, 8)` is at the center of chunk `(0, 0)`.\n\n * NE: north-east, in tu (terrain length unit)\n * SE: south-east, in tu (terrain length unit)\n * UP: guess what, in teu (terrain elevation unit)\n\n\n### `scene2/scene3`\n\nSame as `phys2/phys3` but for usage in the renderer. All `phys2/phys3`\npassed to the renderer are eventually converted to `scene2/scene3`.\n\nUsed in these systems:\n* World renderer (sprite renderer)\n* Terrain renderer\n* Camera positioning\n\n`scene2/scene3` types can be converted to Eigen vectors that are used for\nrendering operations, e.g. creating uniforms, matrix transformations,\nvertex calculations, etc.\n\nEigen vectors are created from `scene3` coordinates using this transformation:\n\n`Eigen(x, y, z) = (SE, UP / sqrt(8), -NE)`\n\nThis will ensure that objects at coordinates are correctly displayed when using\nthe camera (i.e. the *north east* of a coordinate will also be displayed in the\n*north east* when using the renderer camera).\n\nUP is divided by `sqrt(8)` to mimic AoE2 terrain heights (from aspect ration calculations),\nThis means that a coordinate elevation of `1 teu` is displayed at height `1 / sqrt(8)` in\nthe 3D scene. The actual relation is pretty irrelevant though and could be adjusted\nwith other values.\n\nEigen vectors created from `scene2` coordinates have a fixed `y` value of\n`0.0f` but are otherwise the same:\n\n`Eigen(x, y, z) = (SE, 0.0, -NE)`\n\n\n### `tile/tile3`\n\nTile coordinates.\nOrthonormal 2D/3D (NE, SE)/(NE, SE, UP), integer, in tu/teu\n\nIdentical to phys2/phys3 systems, but uses integers\n\nThe center of a tile has phys coordinates `(0.5, 0.5)`.\n\nUsed in these systems:\n * Terrain\n * Buildings\n\n\n### `chunk`\n\nChunk coordinates.\nOrthonormal 2D (NE, SE), integer, in 16tu/16teu\n\nSimilar to tile system, but describes chunks of `16x16` tiles\n\nUsed in these systems:\n * Chunks\n\n\nPixel Coordinates\n-----------------\n\nCoordinates in these systems designate a position on the screen plane.\n\n### `input`\n\nCoordinates used for input events.\nOrthonormal 2D `(x, y)`, integer, in pixels\n\nOrigin is the top left corner of the viewport, positive in right and down directions\n\nUsed in these systems:\n* Input manager\n\n\n### `viewport`\n\nViewport coordinates. Used for rendering generic objects in the viewport.\nOrthonormal 2D `(x, y)`, integer, in pixels\n\nOrigin is bottom left corner of viewport, positive in right and up directions\n\nUsed in these systems:\n* Debugging\n\n\n### `camhud`\n\nGame camera coordinates.\nOrthonormal 2D `(x, y)`, integer, in pixels\n\nOrigin is bottom left corner of viewport, positive in right and up directions\n\nUsed in these systems:\n* HUD drawing\n\n\n### `term`\n\nTerminal character coordinates.\nOrthonormal 2D `(x, y)`, integer, in characters\n\nOrigin is top left corner of console area, positive in right and down directions\n\nUsed in these systems:\n* Console drawing\n\n\n### `camgame`\n\n**Deprecated:** *replaced by `scene2/scene3` and new camera system*\n\nGame camera coordinates.\nOrthonormal 2D `(x, y)`, integer, in pixels\n\nOrigin is center of viewport, positive in right and up directions\n\n\nConversions\n===========\n\n### For rendering\n\nDepending on whether objects are inside the game world, rendered as part of hud or\nthe GUI, different conversion take place.\n\n* Object that are part of the game world are converted from `phys` to `scene` when passed to the renderer, and finally from `scene` to Eigen vectors when passed to OpenGL/Vulkan shaders\n* HUD objects for ingame objects (e.g. HP bars for units) are transformed from `phys` to `camhud`\n\n### For input processing\n\nAgain depending on whether inputs were aimed at an HUD object or an\nentity rendered by the game camera, they (i.e. mouse clicks) are\ntransformed from input to `phys` or `camhud`.\n\n\n### Possible conversions between coordinate types\n\n```\n           scene2 <----> phys2 <--------> tile <--------> chunk\n              |           |                |\n              |           |                |\n              |           |                |\ninput ***> scene3 <----> phys3 <--------> tile3\n   |          |\n   |          |\n   ------> viewport <--> term\n              |\n              |\n           camhud\n```\n\n- Conversions from `input` to `scene3` are realized with raycasting using the camera direction and position. As the calculation is done with floating point values, the result is subject to float rounding errors and therefore **not precise**.\n\n### Possible conversions to/from renderer types\n\n```\nscene2 ----> world space (Eigen::Vector2f)\n   |\n   |\nscene3 ----> world space (Eigen::Vector3f)\n   |\n   |\nviewport --> normalized device space (Eigen::Vector2f)\n```\n\n### Translation details\n\nThe translations make use of current coordinate state (e.g. scroll position,\nstored in `CoordManager`) and additional infos to solve\ndegrees of freedom (see [below](#parameters)).\n\n\n* `input` -> `viewport`: just flip the y axis\n* `camgame` -> `viewport`: both are 'on-screen' coordinates, the difference is a linear offset\n                         of their `(0, 0)` point and the camera zoom\n* `camhud` -> `viewport`: linear offset of their `(0, 0)`\n* `phys3` -> `camgame`: add the camera scroll position (given in `phys3`), then perform spatial transform\n* The other translations should be intuitive\n\n\n### Parameters\n\nThese parameters are needed to solve remaining degrees of freedom\nwhen translating a coordinate of one system to another.\n\n* `chunk` -> `tile`: needs tile-on-chunk coordinates\n* `tile` -> `phys2`: needs position-on-tile coordinates\n* `phys2` -> `phys3`: needs UP component ()\n* `scene2` -> `scene3`: needs UP component ()\n* `tile` -> `tile3`: needs UP component (elevation)\n* `camgame` -> `phys3`: needs UP component (elevation above terrain.\n  reason: intersection between ray through camera and the terrain,\n  e.g. for 'move here' command on a hill)\n"
  },
  {
    "path": "doc/code/curves.md",
    "content": "# Curves\n\n*Curves* are data structures that manage changes to data over time. More precisely,\nthey do not only store data but time-value pairs (keyframes) that allow the retrieval\nof a curve data value for any point in time.\n\nCurves are an integral part of openage's event-based game simulation.\n\n1. [Motivation](#motivation)\n2. [Architecture](#architecture)\n3. [Curve Types](#curve-types)\n   1. [Primitive](#primitive)\n      1. [Common Operations](#common-operations)\n      2. [Discrete](#discrete)\n      3. [Continuous](#continuous)\n      4. [Segmented](#segmented)\n   2. [Container](#container)\n      1. [Queue](#queue)\n      2. [Unordered Map](#unordered-map)\n      3. [Array](#array)\n\n\n## Motivation\n\nThe openage curve structures are inspired by a [similar implementation](https://www.forrestthewoods.com/blog/tech_of_planetary_annihilation_chrono_cam/#.lmxbu3vld) from the game Planetary Annihilation.\n\nCurves intend to solve synchronicity problems in games. In traditional implementations\nlike lockstep, data is changed incrementally in regular intervals (ticks). Keeping\nthese changes and the overall gamestate consistent/in sync over network or across threads\nis very important, as one missing change can result in a desync of the entire simulation.\nRecovering from a desync can also be very hard.\n\nIn comparison, simulation with curves allows both async operations and an easier recovery\nfrom desync states. Data is not changed incrementally but is instead calculated using\n*keyframe interpolation*. In other words, changes to data are treated as keyframes on\na timeline with in-between values being interpolated from adjacent keyframes.\n\n![Curve keyframes example](images/continuous_curve.png)\n\nAdditionally, curves cannot only access values for the current simulation time but also for\nany past or even future times. Keyframes can also be inserted for any point in time without\ndirectly invalidating the state, making curves more reliable in async scenarios (although\nresolving dependencies for keyframes in the past can still be challenging).\n\nThe usage of curves has a few downsides though. They are less space efficient due to the\nkeyframe storage, interpolation are more costly than incremental changes, and\ntheir integration is more complex than the usage of simpler data structures. However, in\nsituations where operations are predictable, long-lasting, and easy to calculate - which\nis the case for most RTS games - the positives may outweigh the downsides.\n\n## Architecture\n\n![Curve classes hierarchy UML](images/curve_classes_uml.svg)\n\nopenage provides several curve types with different interpolation behaviour that each have\ntheir specific use case. Primitive data types are covered by the `BaseCurve` interface\nand its derivates of which the `Discrete`, `Continuous`, and `Segmented` are the practically\nusable types. For containers, there are classes for `Queue` and `UnorderedMap` available.\nAll curve types are templated, so that they can store any type of value (with some constraints).\nThe usable types are explained in more detail in the [Curve Types](#curve-types) section.\n\nKeyframes are implemented as time-value pairs by the `Keyframe` class. `KeyframeContainer`\nis used by all curves to manage their keyframe storage. It also provides functionality to\nefficiently access and insert keyframes as wel as sorting them by time. For the time\ntype, the [simulation time type](/doc/code/time.md) from the `openage::time` namespace is\nused.\n\nIt should be noted that curves are not useful in every situation as keyframe insertion,\ninterpolation, and searching keyframes based on time create significant overhead. Curves\nshould be used for variables or members where\n\n* data values must be tracked over time (e.g. HP of a game entity), or\n* data values are not modified/read very often (e.g. not every frame), or\n* data values are supposed to be sent over the network.\n\nThis is usually the case for all game data in [game entity components](/doc/code/game_simulation/components.md)\ninside the game simulation.\n\nCurves should generally not be used for variables or members where\n\n* data values are not tracked over time (e.g. for temporary variables), or\n* data values are modified/read very often (e.g. in the rendering loop), or\n* data values don't affect the simulation state (e.g. visual settings).\n\n\n## Curve Types\n\nThis section provides an overview over the available curves types.\n\n### Primitive\n\n![Primitive curve types UML](images/primitive_curves_uml.svg)\n\nPrimitive curves are intended for members with single value types. These include, for example,\nthe primitive C++ types (e.g. `int`, `float`, `std::string`), simple structs and data classes,\nand shared pointers.\n\nOn contruction of a primitive curve object, a keyframe with time `t = time::time_t::min_value()`\nand the value's type is instanciated with its default constructor. This is done to ensure that for any\nrequested time `t`, there is always a valid value to be returned. This mirrors the expected\nbehaviour from declaring primitive values in C++ where members may be auto-initialized without\nexplicit assignment to a default value. The default value for curves can also be explicitely\nassigned in the constructor. Types that don't have a default constructor require that a\ndefault value is passed to the curve constructor.\n\n`BaseCurve` objects can be targeted by or trigger events from the [event system](/doc/code/event_system.md).\nAs a consequence, curves are not copy constructable as they require a unique ID for\nevent management. However, it is possible to copy the keyframes of one curve to\nthe other using the `sync(..)` method. `sync(..)` also works for curves with different\nvalues types if a converter function from one value type to the other is supplied.\n\n#### Common Operations\n\nAll primitive curves support the following operations. They may work slightly different\nfor specific curve types.\n\n**Read**\n\nRead operations retrieve values for a specific point in time.\n\n| Method          | Description                                                 |\n| --------------- | ----------------------------------------------------------- |\n| `get(t)`        | Get (interpolated) value at time `t`                        |\n| `frame(t)`      | Get the previous keyframe (time and value) before or at `t` |\n| `next_frame(t)` | Get the next keyframe (time and value) after `t`            |\n\n**Modify**\n\nModify operations insert values for a specific point in time.\n\n| Method                  | Description                                                                       |\n| ----------------------- | --------------------------------------------------------------------------------- |\n| `set_insert(t, value)`  | Insert a new keyframe value at time `t`                                           |\n| `set_last(t, value)`    | Insert a new keyframe value at time `t`; delete all keyframes after time `t`      |\n| `set_replace(t, value)` | Insert a new keyframe value at time `t`; remove all other keyframes with time `t` |\n\n**Copy**\n\nCopy operations transfer keyframes from one curve to another.\n\n| Method           | Description                                                                                      |\n| ---------------- | ------------------------------------------------------------------------------------------------ |\n| `sync(Curve, t)` | Replace all keyframes from self after time `t` with keyframes from source `Curve` after time `t` |\n\n\n#### Discrete\n\n![Discrete curve function example](images/discrete_curve.png)\n\nDiscrete curves implement **constant interpolation** between keyframes. This means\nthat the value returned by `get(t)` is always equal to the value of the previous\nkeyframe.\n\nDiscrete curves should be used for values that only change at specific points in time,\ne.g. for the health of a game entity.\n\n\n#### Continuous\n\n![Continuous curve function example](images/continuous_curve.png)\n\nContinuous curves implement **linear interpolation** between keyframes. This means\nthat the value returned by `get(t)` is calculated from the value difference\nbetween the keyframes before and after `t`. If there is no keyframe after `t`,\nthe value of the previous keyframe is used (like on discrete curves).\n\nValue types on continuous curves need to implement methods for the `operator*(..)` and\n`operator-(..)` operations to support linear interpolation. In particular, `operator*(..)`\nmust support multiplication with `time::time_t` and `operator-(..)` must support\nsubstraction for values of the same type.\n\nContinuous curves do not allow jumps between keyframe values (hence \"continuous\").\nTherefore, there cannot be two keyframes inserted for the same time `t`.\n\nContinuous curves should be used for values that change gradually over time,\ne.g. a game entity's position or the construction progress of a building.\n\n\n#### Segmented\n\n![Segmented curve function example](images/segmented_curve.png)\n\nSegmented curves implement **linear interpolation** between keyframes and additionally\nallow jumps between keyframe values. As with continuous curves, the value returned by `get(t)`\nis calculated from the value difference between the keyframes before and after `t`.\n\nJumps are inserted using the special methods `set_insert_jump(..)` and `set_last_jump(..)`:\n\n| Method                               | Description                                                                       |\n| ------------------------------------ | --------------------------------------------------------------------------------- |\n| `set_insert_jump(t, value1, value2)` | Insert a two new keyframes at time `t`: `(t, value1)` and `(t, value2)`           |\n| `set_last_jump(t, value1, value2)`   | Insert a two new keyframes at time `t` like above; delete all keyframes after `t` |\n\nSegmented curves should be used for values that change gradually but are not on\nconnected intervals. Typically, this are values that wrap around at some point,\ne.g. angles between 0 and 360 degrees.\n\n\n### Container\n\nContainer curves are intended for storing changes to collections and containers.\nThe currently supported containers are `Queue`, `UnorderedMap` and `Array`.\n\nThe most important distinction between regular C++ containers and curve containers\nis that curve containers track the *lifespan* of each element, i.e. their insertion time,\nmodification time, and erasure time. Erasing elements also does not delete them from memory\nand instead hides them for requests made after the erasure time.\n\n\n#### Queue\n\nQueue curve containers are the equivalent to the `std::queue` C++ containers. As such, they\nshould be used in situations where first-in-first-out (FIFO) access patterns are desired.\n\nElements in the queue are sorted by insertion time. The element with the earliest insertion time\nis the *front* element of the queue.\n\nThe front element at time `t` can be read via the `front(t)` method, which retrieves the first,\nnon-erased element with insertion time before or at `t`. In comparison, `pop_front(t)` returns\nthe same value as `front(t)` and additionally erases the element from the queue at time `t`.\n\nIt should be stressed again that erasing an element does not delete it from memory but\nsimply ends its lifespan inside the curve container. `front(t)` and `pop_front(t)` always\nconsider elements with an active lifespan (i.e. elements that are not erased at time `t`).\nAs a side effect, `pop_front(t1)` and `front(t2)`/`pop_front(t2)` may return the same element\nwhen `t2 < t1` because the element has not ended its lifespan at time `t2` yet. It is in\nthe responsibility of the caller to ensure that this behaviour does not cause any\nside effects.\n\n\n**Read**\n\nRead operations retrieve values for a specific point in time.\n\n| Method     | Description                             |\n| ---------- | --------------------------------------- |\n| `front(t)` | Get front element at time `t`           |\n| `empty(t)` | Check if the queue is empty at time `t` |\n\n**Modify**\n\nModify operations insert values for a specific point in time.\n\n| Method             | Description                                            |\n| ------------------ | ------------------------------------------------------ |\n| `insert(t, value)` | Insert a new element at time `t`                       |\n| `pop_front(t)`     | Get front element at time `t` and erase it at time `t` |\n| `clear(t)`         | Erase all elements inserted before time `t`            |\n\n\n#### Unordered Map\n\nUnordered map curve containers store key-value pairs while additionally keeping\ntrack of element insertion time. Requests for a key `k` at time `t` will return the value\nof `k` at that time. The unordered map can also be iterated over for a specific time `t` which\nallows access to all key-value pairs that were in the map at time `t`.\n\n\n#### Array\n\nArray curve containers store a fixed number of `n` elements where `n` is determined at compile-time.\nThey are the curve equivalent to the `std::array` C++ containers. In comparison to `std::array` each\nelement in the array curve container is tracked individually over time. Hence, each index is associated\nwith its own `KeyframeContainer` whose keyframes can be updated independent from other indices.\nWhen a value is added to the `Array` curve at a given index, a new keyframe is added to the respective\n`KeyframeContainer` stored at that index.\n\n**Read**\n\nRead operations retrieve values for a specific point in time.\n\n| Method             | Description                                                              |\n| ------------------ | ------------------------------------------------------------------------ |\n| `get(t, i)`        | Get value of element at index `i` at time <= `t`                         |\n| `get(t)`           | Get array of values at time <= `t`                                       |\n| `size()`           | Get the number of elements in the array                                  |\n| `frame(t, i)`      | Get the previous keyframe (time and value) at index `i` before or at `t` |\n| `next_frame(t, i)` | Get the next keyframe (time and value) at index `i` after `t`            |\n\n**Modify**\n\nModify operations insert values for a specific point in time.\n\n| Method                     | Description                                                                                |\n| -------------------------- | ------------------------------------------------------------------------------------------ |\n| `set_insert(t, i, value)`  | Insert a new keyframe(`t`, `value`) at index `i`                                           |\n| `set_last(t, i, value)`    | Insert a new keyframe(`t`, `value`) at index `i`; delete all keyframes after time `t`      |\n| `set_replace(t, i, value)` | Insert a new keyframe(`t`, `value`) at index `i`; remove all other keyframes with time `t` |\n\n**Copy**\n\n| Method           | Description                                                                                      |\n| ---------------- | ------------------------------------------------------------------------------------------------ |\n| `sync(Curve, t)` | Replace all keyframes from self after time `t` with keyframes from source `Curve` after time `t` |\n"
  },
  {
    "path": "doc/code/event_system.md",
    "content": "# Event System\n\nGame simulation in openage is event-driven. The internal event system\nmanages the creation, scheduling, and execution of these events.\n\n1. [Architecture](#architecture)\n2. [Event Handler Setup](#event-handler-setup)\n3. [Event Instances](#event-instances)\n4. [Triggering/Targeting Events](#triggeringtargeting-events)\n5. [Canceling Events](#canceling-events)\n\n\n## Architecture\n\n![event system UML](images/event_classes_uml.svg)\n\nThe central component of the event system is the *event loop* (`EventLoop`). It is\nthe main point of interaction between the game simulation logic and the event system.\nNew events (`Event` instances) are added via the `create_event(..)` method and scheduled based on the\nconfiguration of the associated *[event handler](#event-handler-setup)* (`EventHandler`).\nInternally, the event is stored inside an *event queue* (`EventQueue`) instance\nsorts the events by invoke time.\n\nEvents are executed by passing the current [simulation time](time.md) to the\nevent loop's `reach_time(..)` method. This executes all not-yet-executed events\nuntil the given target time stamp\n\n```\nt_old < t_e <= t_cur\n```\n\nwhere `t_cur` is the current simulation time, `t_old` is the last time `reach_time(..)` was\ncalled, and `t_e` is the event's scheduled invoke time. Events are executed in order of\nearliest to latest invoke time. During execution, events may trigger the rescheduling of\nother events if there are dependencies between the events.\n\nBoth the creation and execution of events require passing a `State` object that functions\nas a persistant global storage for values that are not linked to a specific event executions.\nIn practice, `State` is primarily used to pass the state of the [game simulation](/doc/code/game_simulation/)\nto the event system via an object of the derived `GameState` class. This object allows\naccess to the indexing structures of the current game that can be used to retrieve\nthe available game entities, for example.\n\n\n## Event Handler Setup\n\nEvent effects on the game simulation are implemented by *event handlers*. Event handlers\nare attached to the event on creation and are invoked when the event is executed by the event\nloop. They are also used for (re-)scheduling the invocation time of the event.\n\nEvent handlers are implemented as derivatives of the `EventHandler` class. Event handlers\nmust implement 3 methods:\n\n* `invoke(..)`: Called by the event loop when the event is executed.\n* `predict_invoke_time(..)`: Called when the event handler shall determine a new invocation time of an event,\n                             i.e. (re)place it on the event queue\n* `setup_event(..)`: Sets up dependencies during creation of the event.\n\nAdditionally, event handlers require a unique ID (defined as a string) as well as\na *trigger type* which defines under which circumstances an event is executed.\nThese parameters can be defined at runtime when instantiating the event handler.\n\nNew events require that an event handler object is attached during creation. This\ncan be done by passing the event handler object to the `create_event(..)` method.\nIf the event handler should be reusable, it can also be registered on the event loop\nvia the `add_event_handler(..)` method. When calling `create_event(..)` on the event\nloop to create a new event, the event handler can then be referenced by a unique\nID string.\n\n\n## Event Instances\n\nWhen calling `create_event(..)` on the event loop, an `Event` instance is returned which\nallows access to the configuration parameters of the event. Event instances store\nreferences to:\n\n* **Event target**: Entity targeted by the event, i.e. the event entity that the event effects should be applied on. Implemented as a weak reference to let the target expire.\n* **Event handler**: Event handler object used for rescheduling and executing the event.\n* **Invoke time**: Determines when the event is executed.\n* **Time of last change**: Time of the last change to invocation time. Used for rescheduling the event.\n* **Event payload**: Optional map of parameters passed to the event handler on invocation.\n\nThese parameters may be changed after creating the event. Alternatively, it is possible to\n[cancel](#canceling-events) the event and create a new event with the updated parameters.\n\n\n## Triggering/Targeting Events\n\nTo allow an event to modify an object in its invocation, the object's class must be\na derivative of the `EventEntity` class. By inheriting from `EventEntity`, a class can\nbe targeted by events, trigger or change events, or both.\n\nAn event target is passed during the creation of an event (i.e. in `create_event(..)`)\nand later when executing the event via the event handler's `invoke(..)` method. Events\ncan only have one target at a time.\n\n`EventEntity` derivatives may also be used to trigger or reschedule the invocation of events. To do this,\nthe event has to be added as a dependent for the event entity using the `add_dependent(..)`\nmethod of `EventEntity`. Calling the `changes(..)` method of `EventEntity` will then result\nin a rescheduling of the event in the next iteration of the event loop.\n\n\n## Canceling Events\n\nEvents are automatically canceled if their targeted event entity has expired, i.e. it has been deleted\nfrom memory. To accomplish this target references are implemented using `std::weak_ptr`\nwhich are checked for expiration before event execution.\n\nManually canceling events can also be done by calling `cancel(..)` on an `Event` instance.\nIn the current implementation, this change is effective immediately, even if the provided\nreference time is in the future.\n"
  },
  {
    "path": "doc/code/exceptions.md",
    "content": "The openage base exception type is `error::Error`.\n\n - The first argument is a `message` object; the usage is identical to `log::log()` (i.e. preferably via the `MSG` macro).\n - If `true` is given as the second argument (default: `false`), a stack trace is collected, using gcc 4.9's `libbacktrace.a`.\nThe collection itself is quite fast and has a low performance impact, as no symbol look-ups/string translations happen at that time.\n - If `true` is given as the third argument (default: `true`), and currently inside a `catch` block, the current exception is stored as a cause, and can later be re-thrown via `rethrow_cause()`.\n\nExceptions should always be caught by reference, or inheritance won't work.\n\nExample usage:\n\n    #include \"error/error.h\"\n\n    try {\n        int i = 5;\n\n         try {\n            if (true) {\n                throw Error(MSG(err) << \"what an exceptional line of code!\", true);\n            }\n            i = 6;\n        } catch (...) {\n            throw Error(MSG(crit).fmt(\"exception in the 'try' block. i=%d\", i));\n        }\n\n    } catch (Error &e) {\n        std::cout << e << std::endl;\n    }\n\nAlso see the example demo code in `cpp/error/demo.cpp`.\n\nAvoid exceptions that don't inherit from `Error`.\n"
  },
  {
    "path": "doc/code/game_simulation/README.md",
    "content": "# Game Simulation\n\n*Game simulation* is the engine subsystem that implements game mechanics and data structures\nused for gameplay.\n\nThe subsystem only contains components that are strictly necessary to simulate a game. Everything else,\ne.g. the [input system](/doc/code/input/), [rendering](/doc/code/renderer/),\nnetworking or scripting, are handled as separate subsystems.\n\n1. [Architecture](#architecture)\n2. [Workflow](#workflow)\n\n\n## Architecture\n\n![Game simulation overview UML](images/simulation_uml.svg)\n\nThe game simulation is instantiated from the main thread via a `GameSimulation` object.\nThis object controls the game loop, the current game session, and other simulation\nparameters necessary to run the game.\n\nThe `Game` class represents a game session. Its instances store information about the\ngamestate via a `GameState` object which includes references to objects inside the game world\n(e.g. players or game entities). It also contains gameplay settings for the current session.\n\n\"Physical\" objects in the game world are represented by `Terrain` and `GameEntity`. `Terrain`\nis used to model the underlying map terrain, while [`GameEntity`](game_entity.md)\nis used for every type of world object (e.g. units, buildings, trees, resources, ambience).\n\n\n## Workflow\n\nTo initiate a game, a `GameSimulation` object must be created. This is usually done by\nthe `Engine` object in the main thread. On initialization, the game simulation automatically sets up\n several subcomponents required for running the game such as:\n\n* [Event loop](/doc/code/event_system.md) for executing events\n* Entity factory for creating game entities\n* Mod manager for detecting/loading [openage modpacks](/doc/media/openage/modpacks.md)\n\nAfter the `GameSimulation` object is initialized, modpacks that should be loaded in\nthe game session can be passed via the `set_modpacks(..)` method.\n\nThe game simulation loop is started using the `run()` method of the `GameSimulation` object.\n`run()` should be run in its own thread with no other looping subsystems present. Before\nthe simulation loop is entered, the simulation sets up a new game session (as a `Game`\nobject). This loads the modpacks specified in the `set_modpacks(..)` method into the\nsession.\n\nThe logic of the simulation loop is quite simple as most of the execution happens in the\n[event system](/doc/code/event_system.md). In every loop iteration, the current simulation time is fetched from the\n[time subsystem](/doc/code/time.md). This time value is then passed to the simulation's\nevent loop which executes all events queued until this time.\n"
  },
  {
    "path": "doc/code/game_simulation/activity.md",
    "content": "# Activity Control Flow\n\nThe *activity control flow* is openage's method to make complex game entity behaviour\nconfigurable.\n\n1. [Motivation](#motivation)\n2. [Architecture](#architecture)\n3. [Node Types](#node-types)\n\n\n## Motivation\n\nUnit behaviour in RTS games can get very complex. In many cases, units are not\njust controlled by commands but other automated mechanisms, e.g. attacking enemy\nunits in the line of sight. Furthermore, commands do not always translate to a single\nwell-defined action. For example, an attack command usually results in a move action\nand a subsequent attack action. Some commands may even execute different actions depending\non context.\n\nAll this means that we cannot view the control flow of a unit as a simple mapping of\ncommand to action as is done in other games. Instead, we have to treat unit behaviour\nas a complex chain of actions with branching paths, wait states for events, triggers\nand feedback loops. Unless we want every command to be a hardcoded action chain, managing\nthis complexity is key to making unit behaviour configurable.\n\n\n## Architecture\n\nGame entity control flows in openage are modelled as directed node graph, so-called *activities*.\nNodes in the graph correspond to actions that execute for the game entity or conditional queries\nand event triggers that indicate which path to take next. By traversing the node graph along\nits paths, the game entities actions are determined. The currently visited node in the graph\ncorresponds to the current action of a unit.\n\nActivities are reusable, i.e. they are intended to be shared by many game entities Usually,\nall game entities of the same type should share the same behaviour, so they get assigned\nthe same activity node graph.\n\nAn activity can also be represented visually like this:\n\n![graph example](images/activity_graph.svg)\n\nThe design is heavily inpired by the [BPMN](https://en.wikipedia.org/wiki/Business_Process_Model_and_Notation)\nrepresentation. You don't need to know BPMN to understand the activity control flow because\nwe explain everything important about the graphs in our documentation. However,\nyou can use available [BPMN tools](https://bpmn.io/) to draw activity node graphs.\n\n## Node Types\n\n\n| Type             | Inputs | Outputs | Description               |\n| ---------------- | ------ | ------- | ------------------------- |\n| `START`          | 0      | 1       | Start of activity         |\n| `END`            | 1      | 0       | End of activity           |\n| `TASK_SYSTEM`    | 1      | 1       | Run built-in system       |\n| `TASK_CUSTOM`    | 1      | 1       | Run custom function       |\n| `XOR_EVENT_GATE` | 1      | 1+      | Wait for event and branch |\n| `XOR_GATE`       | 1      | 1+      | Branch on condition       |\n"
  },
  {
    "path": "doc/code/game_simulation/components.md",
    "content": "# Built-in Components\n\nOverview of the built-in game entity components in the game simulation.\n\n1. [Internal](#internal)\n   1. [Activity](#activity)\n   2. [CommandQueue](#commandqueue)\n   3. [Ownership](#ownership)\n   4. [Position](#position)\n2. [API](#api)\n   1. [Idle](#idle)\n   2. [Live](#live)\n   3. [Move](#move)\n   4. [Turn](#turn)\n\n\n## Internal\n\nInternal components do not have a corresponding nyan API object and thus only\nstore runtime data.\n\n### Activity\n\n![Activity Component UML](images/component_activity_uml.svg)\n\nThe `Activity` component stores a reference to the top-level activity for the\ngame entity. Essentially, this gives access to the entire activity node graph\nused by the entity.\n\nAdditionally, the current activity state is stored on a discrete curve that\ncontains the last visited node.\n\n`Activity` also stores the handles of events initiated by the activity system\nfor advancing to the next node. Once the next node is visited, these events\nshould be canceled via the `cancel_events(..)` method.\n\n\n### CommandQueue\n\n![CommandQueue Component UML](images/component_activity_uml.svg)\n\nThe `CommandQueue` component stores commands for the game entity in a [queue curve container](/doc/code/curves.md#queue).\n\nCommands in the queue use `Command` class derivatives which specify a command type\nand payload for the command.\n\n\n### Ownership\n\n![Ownership Component UML](images/component_ownership_uml.svg)\n\nThe `Ownership` component stores the ID of the player who owns the game entity.\n\n\n### Position\n\n![Position Component UML](images/component_position_uml.svg)\n\nThe `Position` component stores the location and direction of the game entity\ninside the game world.\n\nThe 3D position of the game entity is stored on a continuous curve with value type\n`phys3`.\n\nDirections are stored as angles relative to the camera vector using clock-wise\nrotation. Here are some example values for reference to see how that works in\npractice:\n\n| Angle (degrees) | Direction             |\n| --------------- | --------------------- |\n| 0               | look towards camera   |\n| 90              | look left             |\n| 180             | look away from camera |\n| 270             | look right            |\n\nAngles are stored on a segmented curve.\n\n## API\n\nAPI components have a corresponding nyan API object of type `engine.ability.Ability` defined\nin the nyan API. This API object can be retrieved using the `get_ability(..)` method of the\ncomponent.\n\n### Idle\n\n![Idle Component UML](images/component_idle_uml.svg)\n\n**nyan API object:** [`engine.ability.type.Idle`](/doc/nyan/api_reference/reference_ability.md#abilitytypeidle)\n\nThe `Idle` component represents the ingame \"idle\" state of the game entity, i.e. when\nit is doing nothing.\n\nThe component stores no runtime data.\n\n\n### Live\n\n![Live Component UML](images/component_live_uml.svg)\n\n**nyan API object:** [`engine.ability.type.Live`](/doc/nyan/api_reference/reference_ability.md#abilitytypelive)\n\nThe `Live` component represents the game entity's ability to have attributes (e.g. health).\n\nAn attribute's maximum limit is stored in the nyan API object, while\nthe game entity's current attribute values are stored in the component\non a discrete curve.\n\n\n### Move\n\n![Move Component UML](images/component_move_uml.svg)\n\n**nyan API object:** [`engine.ability.type.Move`](/doc/nyan/api_reference/reference_ability.md#abilitytypemove)\n\nThe `Move` component represents the game entity's ability to move in the game world.\nThis also allows moving the game entity with move commands.\n\nThe component stores no runtime data.\n\n\n### Turn\n\n![Turn Component UML](images/component_turn_uml.svg)\n\n**nyan API object:** [`engine.ability.type.Turn`](/doc/nyan/api_reference/reference_ability.md#abilitytypeturn)\n\nThe `Turn` component represents the game entity's ability to change directions in the game world.\nTurning is implicitely required for moving but it also works on its own.\n\nThe component stores no runtime data.\n"
  },
  {
    "path": "doc/code/game_simulation/game_entity.md",
    "content": "# Game Entity\n\nGame entities represent objects inside the game world.\n\n1. [Architecture](#architecture)\n2. [Game Entity class](#game-entity-class)\n3. [Component Data Storage](#component-data-storage)\n4. [Control Flow](#control-flow)\n   1. [System](#system)\n   2. [Activities](#activities)\n   3. [Manager](#manager)\n\n\n## Architecture\n\n![Game entity UML](images/game_entity_overview.svg)\n\nGame entity mechanics are structured using the concept of *separation of concerns*.\nThis means that there is not one single `GameEntity` class that implements all data\nand game logic. Instead, data and logic are separated into distinct engine components\nthat each have a dedicated, clearly-defined purpose.\n\nData storage is handled by `Component` objects that manage references to the [nyan](/doc/nyan/nyan.md)\ndatabase and store runtime data on [curves](/doc/code/curves.md). Game logic is implemented\nin *systems*, which are best described as independent functions that perform a specific action\non an entity. Event handling is done by the `GameEntityManager` which initiates and directs\nthe entity's individual control flow.\n\nIt should be noted that while the terminology we use here is very similar to names\nin the *Entity-Component-System* (ECS) architecture, you shouldn't think of the openage\ngame simulation as a traditional ECS-driven architecture. We merely use this terminology\nbecause we can't think of anything better (and because we are too lazy to find better names).\n\n\n## Game Entity class\n\n![Game entity class UML](images/game_entity_uml.svg)\n\nThe `GameEntity` class primarily provides references to the game entity's unique ID, assigned\ncomponents, and event handling manager. If the game entity is supposed to be animated,\na [render entity](/doc/code/renderer/level2.md#updating-render-stages-from-the-gamestate)\ncan also be set. This allows requesting animations for the game entity.\n\n`GameEntity` follows the principle of *composition over inheritance*. Therefore, capabilities\nand attributes of a game entity are defined by the assigned components rather than properties\nof its class. The `GameEntity` class is merely a thin wrapper with a reference ID for\na set of components storing the actual data (via `Component` subclasses). Components\nof the specific entity can be accessed via the `GameEntity` object's `get_component(..)` method.\n\n\n## Component Data Storage\n\nFor a description of the available components, check the [component reference](components.md).\n\n![Component class UML](images/component_uml.svg)\n\nComponents are data storage objects for a game entity that also perform the dual role\nof signifying what a game entity can do or is depending on their component type. Component\ntypes are usually associated with a subclass of `Component`, e.g. `Move`. For example, assigning\na `Move` component to a game entity signifies that the game entity can perform move\nactions during gameplay. Movement data relevant to the game entity is then also stored in this\ncomponent object.\n\nIn general, component classes are designed to be atomic, i.e. they don't depend on other\ncomponents to be assigned to the game entity. While some components may have a close relationship,\ne.g. `Move` and `Position`, it should never be assumed that all components are assigned\nwhen operating on a game entity.\n\nPersistent game data, i.e. data that is the same in every game like unit stats, is not\nmanaged by the component itself but in the game's [nyan](/doc/nyan/nyan.md) database. The component\nonly holds a reference to the nyan object associated with the component. Data from nyan\nmust be fetched via the [openage nyan interface](/doc/nyan/openage-lib.md).\nComponent types usually have a corresponding nyan object type in the nyan API, e.g. the `Move`\ncomponent corresponds to the `engine.ability.type.Move` nyan type.\n\nRuntime data, i.e. data that changes due to gameplay interactions, is stored on [curves](/doc/code/curves.md)\nmanaged by the component object. Curves are keyframe-based time-value storages that track\nchanges to the data over time. Components can provide helper methods to interact with\nthe curve, e.g. `Position` provides `set_position` to insert a new keyframe on its\n`position` data curve.\n\n\n## Control Flow\n\nThe control flow is organized into 3 core mechanisms: Systems, activities, and\nmanagers. This again tries to achieve a *separation of concern* design to\nmake the game logic maintanable and extensible.\n\n### System\n\nFor a description of the available systems, check the [system reference](systems.md).\n\nA *system* in openage is basically a function that operates on game entity\ncomponents. They are explicitely separated from game entity and component objects\nto allow for more flexible implementation. In practice, systems are implemented as static\nfunctions inside the `gamestate::system` namespace.\n\nSystems are *stateless*, so they don't remember anything about the game entity\ncomponents they operate on. If something about the execution state should be remembered,\nit needs to be stored in a component. Additionally, system execution is always\nimmediate. This means that a system never waits for events or for a certain amount\nof time. Waiting is handled by the [game entity manager](#manager) and the\n[activity control flow](#activities) instead.\n\nWhen adding new systems to the engine, interdependencies to other systems should\nbe avoided at all cost, i.e. a system should never call another system directly.\nExceptions should only be made for direct subsystems implementing subroutines\nor to avoid code redundancies. The reasoning behind this is that dependencies\nbetween systems may quickly become unmanageable.\n\n\n### Activities\n\n![Activity Example](images/activity_graph.svg)\n\n*Activities* connect systems together in a node graph to describe the overall control flow\nfor a game entity. In openage, activity node graphs are used to model the complex behaviour\nbehaviour and action chains of RTS game entities, while also allowing the behaviour to\nbe as configurable as possible. One could also think of activities as a behaviour graph\nwhere paths are taken based on the inputs a game entity receives. The architecture\nof the activity control flow is described in more detail in the\n[activity control flow documentation](activity.md).\n\nA game entity's current activity state is stored in its `Activity` component. This component\nholds a reference to the activity node graph used by the entity as well as the\nlast visited node. This node describes which action/behavioural state the\ngame entity currently is in.\n\nAdvancement to the next node can be initiated in several ways, depending on the\n[node type](activity.md#node-types) of the current node.\nIt can happen automatically or be triggered by an event. In the latter case,\nthe event is handled by the `GameEntityManager` which calls an activity *system*\nthat processes the event to choose the next node.\n\n![Activity Workflow](images/activity_workflow.png)\n\n\n### Manager\n\nThe game entity listens for events that target the entity and takes the\nassociated action depending on the event type.\n"
  },
  {
    "path": "doc/code/game_simulation/systems.md",
    "content": "# Built-in Systems\n\nOverview of the built-in systems in the game simulation.\n\n1. [Idle](#idle)\n2. [Move](#move)\n\n\n## Idle\n\n![Idle systems class UML](images/system_idle.svg)\n\nHandles idle actions for game entities.\n\n`idle(..)` updates the animation of the game entity. This requires the game\nentity to have the `Idle` component. The function returns a time of 0 since\nno actionsconsuming simulation time are taken.\n\n\n## Move\n\n![Move systems class UML](images/system_move.svg)\n\nHandles movement actions for game entities.\n\n`move_default(..)` moves a game entity to the new position specified in the function\ncall. This requires the game entity to have the `Move` and `Turn` components.\nWaypoints for the exact path are fetched from the pathfinder.\nFor every straight path between waypoints, the game entity is turned first, then\nmoved (same as in *Age of Empires*). If an animation is available for the `Move`\ncomponent, this animation is forwarded as the game entity's active animation to the\nrenderer. The function returns the cumulative time of all turn and movement actions\ninitiated by this function.\n\n`move_command(..)` processes the payload from a move *command* to call `move_default(..)`\nwith the payload parameters.\n"
  },
  {
    "path": "doc/code/gui.md",
    "content": "# QtQuick-based Graphical User Interface\n\n**Important Notice**: This document is outdated and subject to change.\n\n## Using Qt Creator Integrated Development Environment (IDE)\n\nSee [Qt Creator IDE](/doc/ide/qt_creator.md).\n\n## Writing presentation-level code\n\nIt's being developed in QML language.\n\nSources are in the `assets/qml` directory.\nStart the game and edit the code.\nChanges apply immediately after any QML file is saved.\n\n### Image providers\n\nThe by-filename URLs have the following form: `image://by-filename/<filename>.<subid>`.\nThey are resolved relative to the asset directory.\n\nExample:\n\n\timage://by-filename/gaben.png.2\n\nThe by-id URLs are like: `image://by-graphic-id/<texture-id>.<subid>` or `image://by-terrain-id/<texture-id>.<subid>`.\n\nExample:\n\n\timage://by-graphic-id/7231.5\n\n## Exposing components to the GUI layer\n\nComponents are adapted by writing QObject counterparts for them.\nLook for the examples in the `libopenage/renderer/gui` directory.\n\nThere is a property-based approach and a model-based extension to it.\n\n### Property-based binding\n\nLet's suppose we have a class `ResourceAmount` in `libopenage/economy`, and we want to be able to use it in the GUI.\n\nIn order to do that:\n\n1. A class `ResourceAmountLink` must be created in the `libopenage/renderer/gui`.\nIt must derive from `GuiItemQObject` and `GuiItem<ResourceAmountLink>`.\nIt must be registered in the QML type system using a usual Qt approach:\n```cpp\nqmlRegisterType<ResourceAmountLink>(\"yay.sfttech.openage\", 1, 0, \"ResourceAmount\");\n```\n\n2. Specializations `struct Wrap<ResourceAmount>` and `struct Unwrap<ResourceAmountLink>` must be defined:\n```cpp\nnamespace qtgui {\ntemplate<>\nstruct Wrap<ResourceAmount> {\n\tusing Type = ResourceAmountLink;\n};\n\ntemplate<>\nstruct Unwrap<ResourceAmountLink> {\n\tusing Type = ResourceAmount;\n};\n} // namespace qtgui\n```\n\n3. Also ResourceAmount needs a public member to be added:\n```cpp\npublic:\n\tqtgui::GuiItemLink *gui_link\n```\n\n4. Declare and implement needed properties and signals in the `ResourceAmountLink` using Qt property syntax.\n\n### Model-based binding\n\nThere is a class `GeneratorParameters` in `libopenage/` directory.\nIt has a big list of parameters of different types like `generation_seed`, `player_radius`, `player_names`, etc.\nSo, we're not going to write a Qt property for each one:\n\n1. `GeneratorParameters` must derive from the `qtgui::GuiPropertyMap`.\n\n2. `GeneratorParameters` should set its initial values like so:\n```cpp\nthis->setv(\"generation_seed\", 4321);\nthis->setv(\"player_radius\", 10);\nthis->set_csv(\"player_names\", std::vector<std::string>{\"name1\", \"name2\"});\n```\n\n3. A class `GeneratorParametersLink` must be created in the `libopenage/renderer/gui`.\nIt must derive from `QObject` and `GuiItemListModel<GeneratorParametersLink>`.\nIt must be registered in the QML type system using a usual Qt approach:\n```cpp\nqmlRegisterType<GeneratorParametersLink>(\"yay.sfttech.openage\", 1, 0, \"GeneratorParameters\");\n```\n\n4. Specializations `struct Wrap<GeneratorParameters>` and `struct Unwrap<GeneratorParametersLink>` must be defined:\n```cpp\nnamespace qtgui {\ntemplate<>\nstruct Wrap<GeneratorParameters> {\n\tusing Type = GeneratorParametersLink;\n};\n\ntemplate<>\nstruct Unwrap<GeneratorParametersLink> {\n\tusing Type = GeneratorParameters;\n};\n} // namespace qtgui\n```\n\nThat results into a `ListModel`-like QML type with `display` and `edit` roles.\nBasically, a database table with two columns: `display` and `edit`.\n\nQt properties can be added to the model-based class just like to the property-based.\n\n## Passing data\n\n### Calling member functions of the game class from the its `Link` counterpart\n\nSince the GUI and the game logic may be in different threads, additional care is needed.\n\nIt's done by the `GuiItem::i()` function (`GuiItem` is a base of `Link` classes).\n\nFor example, forwarding of a `clear()` member function call from `GameMainLink` to `GameMain`:\n```cpp\nvoid GameMainLink::clear() {\n\tstatic auto f = [] (GameMain *_this) {\n\t\t_this->clear();\n\t};\n\tthis->i(f, this);\n}\n```\n\nReturning a value synchronously from such call isn't possible.\nSee section about signals for that functionality.\n\n### Declaring properties\n\nThe Link classes act like caches.\nSo, the properties that are needed to be available in QML should be declared as members.\nThe Q_PROPERTY should be used (see Qt docs).\n\n### Using properties\n\nSetters of the properties that may receive constant values must be implemented using the `GuiItem::s()` or `GuiItem::sf()` functions.\n\n### Passing data to the GUI\n\nCreate a class with needed signals, `EditorModeSignals` for example:\n```cpp\nclass EditorModeSignals : public QObject {\n\tQ_OBJECT\n\npublic:\nsignals:\n\tvoid toggle();\n};\n```\n\nCreate a member of this type in the game class `EditorMode`.\nThen connect its signals in the corresponding `EditorModeLink` class by overriding its `on_core_adopted()`:\n```cpp\nvoid EditorModeLink::on_core_adopted() {\n\tQObject::connect(&unwrap(this)->gui_signals, &EditorModeSignals::toggle, this, &EditorModeLink::toggle);\n}\n```\n\n### Including headers\n\nThe files that are outside of the `libopenage/renderer/gui/` are allowed to include only the headers from the `libopenage/renderer/gui/guisys/public` and `libopenage/renderer/gui/integration/public`.\n\n## Directory structure\n\nThe subsystem resides in the `libopenage/renderer/gui`.\n\n* random files in the directory - bindings for the game components\n* `guisys/` - non-openage-specific part\n    * `guisys/public/` - pimpl wrappers\n    * `guisys/private/` - implementation\n    * `guisys/link/` - binding-related classes\n* `integration/` - openage-specific part, notably the image providers\n* `qml/` - QML code\n\n## Development\n\nThe C++ code must be written in a such way that none of the errors in the QML code could provoke crash or trigger a C++ assert (C++ asserts are verifying C++ code, not QML).\nThere are several exceptional cases when the initialization of the QML environment has no choice but to fail (calls `qFatal`).\n\nFiles from `guisys/` are not allowed to include any game headers that are outside of `guisys/`.\n\nHeaders from `guisys/public/` and `integration/public/` are not allowed to include any Qt headers directly or through other headers.\n\n### Commit prefixes explanation\n\n* **gui:** - QML code\n* **guilink:** - binding of the game logic or other subsystems (like image providers) to the GUI\n* **guiinit:** - for the code in the game that creates and uses the GUI renderer\n* **guisys:** - for the code in the `guisys/` directory\n"
  },
  {
    "path": "doc/code/images/component_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\nfontsize=14\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>435</y>\n      <w>420</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>**CommandQueue**\n--\ncommand_queue: curve::Queue\n--\nadd_command(time_t, Command): void\nget_queue(): curve::Queue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>660</x>\n      <y>0</y>\n      <w>540</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>**Components**\nstyle=wordwrap\nfontsize=20</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1260</x>\n      <y>435</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Command**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1125</x>\n      <y>465</y>\n      <w>165</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>75</y>\n      <w>420</w>\n      <h>300</h>\n    </coordinates>\n    <panel_attributes>**Activity**\n--\nstart_activity: Activity\nnode: curve::Discrete\nscheduled_events: vector&lt;Event&gt;\n--\nget_start_activity(): Activity\nget_node(time_t): Node\nset_node(time_t, Node): void\ninit(time_t): void\nadd_event(Event): void\ncancel_events(): void\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1260</x>\n      <y>75</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Activity**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1125</x>\n      <y>105</y>\n      <w>165</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1260</x>\n      <y>210</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Node**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1125</x>\n      <y>240</y>\n      <w>165</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>150</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>720</y>\n      <w>420</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>**Ownership**\n--\nowner: curve::Discrete\n--\nset_owner(time_t, ownership_id_t): void\nget_owners(): curve::Discrete</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>1020</y>\n      <w>420</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>**Position**\n--\nposition: curve::Continuous\nangle: curve::Segmented\n--\nget_positions(): curve::Continuous\nget_angles(): curve::Segmented\nset_position(time_t, coord::phys3): void\nset_angle(time_t, phys_angle_t): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>855</x>\n      <y>1305</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Idle**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>1455</y>\n      <w>465</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>**Live**\n--\nattribute_values: curve::UnorderedMap\n--\nadd_attribute(time_t, fqon_t, curve::Discrete)\nset_attribute(time_t, fqon_t, int64_t): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>855</x>\n      <y>1740</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Move**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>855</x>\n      <y>1890</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Turn**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/curves_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\nfontsize=14\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>510</x>\n      <y>120</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**BaseCurve**\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>390</x>\n      <y>510</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Discrete**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>720</x>\n      <y>315</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**Interpolated**\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>615</x>\n      <y>510</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Continuous**\nbg=green\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>840</x>\n      <y>510</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Segmented**\nbg=green\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>195</y>\n      <w>45</w>\n      <h>345</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;210.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>660</x>\n      <y>195</y>\n      <w>90</w>\n      <h>195</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;110.0;40.0;110.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>750</x>\n      <y>390</y>\n      <w>45</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>855</x>\n      <y>390</y>\n      <w>45</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>930</x>\n      <y>120</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**KeyframeContainer**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>690</x>\n      <y>150</y>\n      <w>270</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1275</x>\n      <y>120</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Keyframe**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1170</x>\n      <y>150</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1050</x>\n      <y>510</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Queue**\nbg=green\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1260</x>\n      <y>510</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**UnorderedMap**\nbg=green\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>300</x>\n      <y>0</y>\n      <w>375</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>**Overview**\nstyle=wordwrap\nfontsize=20</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>315</x>\n      <y>675</y>\n      <w>375</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>**Primitive Curves**\nstyle=wordwrap\nfontsize=20</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>330</x>\n      <y>765</y>\n      <w>360</w>\n      <h>270</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**BaseCurve&lt;T&gt;**\nbg=pink\n--\n/get(time_t): T/\nframe(time_t): pair&lt;time_t, T&gt;\nnext_frame(time_t): pair&lt;time_t, T&gt;\nset_insert(time_t, T): void\nset_last(time_t, T): void\nset_replace(time_t, T): void\nsync(BaseCurve&lt;T&gt;, time_t): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>270</x>\n      <y>1290</y>\n      <w>270</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>**Discrete**\nbg=green\n--\nget(time_t): T\nget_time(time_t): T\nget_previous(time_t): T</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>765</x>\n      <y>1095</y>\n      <w>255</w>\n      <h>120</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**Interpolated**\nbg=pink\n--\nget(time_t): T</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>600</x>\n      <y>1290</y>\n      <w>270</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>**Continuous**\nbg=green\n--\nset_insert(time_t, T): void\nset_last(time_t, T): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>930</x>\n      <y>1290</y>\n      <w>345</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>**Segmented**\nbg=green\n--\nset_insert_jump(time_t, T, T): void\nset_last_jump(time_t, T, T): void\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>420</x>\n      <y>1020</y>\n      <w>45</w>\n      <h>300</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;180.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>615</x>\n      <y>1020</y>\n      <w>180</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;80.0;100.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>780</x>\n      <y>1200</y>\n      <w>45</w>\n      <h>120</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>975</x>\n      <y>1200</y>\n      <w>45</w>\n      <h>120</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>915</x>\n      <y>750</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**KeyframeContainer**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>675</x>\n      <y>780</y>\n      <w>270</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1260</x>\n      <y>750</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Keyframe**\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1155</x>\n      <y>780</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/engine_architecture.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\nfontsize=14\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>690</x>\n      <y>345</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Input**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1020</x>\n      <y>180</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Renderer**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>690</x>\n      <y>90</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GUI**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1035</x>\n      <y>600</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventLoop**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1275</x>\n      <y>405</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Simulation**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>525</x>\n      <y>15</y>\n      <w>195</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>**Presenter**\nfontsize=20\n\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>0</y>\n      <w>780</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;500.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>0</y>\n      <w>45</w>\n      <h>600</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;380.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>555</y>\n      <w>450</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>280.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>0</y>\n      <w>45</w>\n      <h>360</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;220.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>315</y>\n      <w>375</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;170.0;230.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1620</x>\n      <y>90</y>\n      <w>105</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>**Time**\nfontsize=20\n\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1500</x>\n      <y>180</y>\n      <w>135</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Clock**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>75</y>\n      <w>495</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;310.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1695</x>\n      <y>75</y>\n      <w>45</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;170.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1440</x>\n      <y>315</y>\n      <w>300</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>180.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1695</x>\n      <y>315</y>\n      <w>45</w>\n      <h>510</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;320.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>315</y>\n      <w>495</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;310.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>780</y>\n      <w>825</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>530.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>555</y>\n      <w>45</w>\n      <h>270</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;160.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>840</x>\n      <y>225</y>\n      <w>210</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;80.0;10.0;10.0;120.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>765</x>\n      <y>165</y>\n      <w>45</w>\n      <h>210</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>870</x>\n      <y>375</y>\n      <w>255</w>\n      <h>255</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>150.0;150.0;150.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1185</x>\n      <y>465</y>\n      <w>120</w>\n      <h>165</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0;10.0;90.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1155</x>\n      <y>255</y>\n      <w>150</w>\n      <h>195</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;110.0;80.0;110.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>255</y>\n      <w>150</w>\n      <h>225</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;130.0;80.0;130.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1200</x>\n      <y>210</y>\n      <w>330</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;200.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>840</x>\n      <y>210</y>\n      <w>225</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Key/Mouse events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>630</x>\n      <y>225</y>\n      <w>150</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Mouse events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>915</x>\n      <y>390</y>\n      <w>150</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Input events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1200</x>\n      <y>510</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Entity updates\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1155</x>\n      <y>450</y>\n      <w>150</w>\n      <h>180</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;100.0;10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1500</x>\n      <y>450</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1110</x>\n      <y>435</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Scheduled events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1215</x>\n      <y>435</y>\n      <w>390</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;140.0;240.0;140.0;240.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1440</x>\n      <y>645</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>990</x>\n      <y>285</y>\n      <w>195</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Animation requests\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1380</x>\n      <y>195</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>690</x>\n      <y>1215</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Input**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1020</x>\n      <y>1050</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Renderer**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>690</x>\n      <y>960</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GUI**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1035</x>\n      <y>1470</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventLoop**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1275</x>\n      <y>1275</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Simulation**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>525</x>\n      <y>885</y>\n      <w>195</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>**Presenter**\nfontsize=20\n\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>870</y>\n      <w>780</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;500.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>870</y>\n      <w>45</w>\n      <h>600</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;380.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>510</x>\n      <y>1425</y>\n      <w>450</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>280.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>870</y>\n      <w>45</w>\n      <h>360</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;220.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>1185</y>\n      <w>375</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;170.0;230.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1620</x>\n      <y>960</y>\n      <w>105</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>**Time**\nfontsize=20\n\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1500</x>\n      <y>1050</y>\n      <w>135</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Clock**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>945</y>\n      <w>495</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;310.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1695</x>\n      <y>945</y>\n      <w>45</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;170.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1440</x>\n      <y>1185</y>\n      <w>300</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>180.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1695</x>\n      <y>1185</y>\n      <w>45</w>\n      <h>510</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;320.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>1185</y>\n      <w>495</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;310.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>1650</y>\n      <w>825</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>530.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>1425</y>\n      <w>45</w>\n      <h>270</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;160.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>840</x>\n      <y>1095</y>\n      <w>210</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;80.0;10.0;10.0;120.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>765</x>\n      <y>1035</y>\n      <w>45</w>\n      <h>210</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>870</x>\n      <y>1245</y>\n      <w>255</w>\n      <h>255</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>150.0;150.0;150.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1185</x>\n      <y>1335</y>\n      <w>120</w>\n      <h>165</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0;10.0;90.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1155</x>\n      <y>1125</y>\n      <w>150</w>\n      <h>195</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;110.0;80.0;110.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>1125</y>\n      <w>150</w>\n      <h>225</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;130.0;80.0;130.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1200</x>\n      <y>1080</y>\n      <w>330</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;200.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>840</x>\n      <y>1080</y>\n      <w>225</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Key/Mouse events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>630</x>\n      <y>1095</y>\n      <w>150</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Mouse events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>915</x>\n      <y>1260</y>\n      <w>150</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Input events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1200</x>\n      <y>1380</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Entity updates\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1155</x>\n      <y>1320</y>\n      <w>150</w>\n      <h>180</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;100.0;10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1500</x>\n      <y>1320</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1110</x>\n      <y>1305</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Scheduled events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1215</x>\n      <y>1305</y>\n      <w>390</w>\n      <h>240</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;140.0;240.0;140.0;240.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1440</x>\n      <y>1515</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>990</x>\n      <y>1155</y>\n      <w>195</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Animation requests\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1380</x>\n      <y>1065</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Current time\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1035</x>\n      <y>1770</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Networking**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>660</x>\n      <y>1470</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Scripting**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>840</x>\n      <y>1485</y>\n      <w>225</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>130.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>840</x>\n      <y>1515</y>\n      <w>225</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;130.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1065</x>\n      <y>1545</y>\n      <w>45</w>\n      <h>255</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;150.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1110</x>\n      <y>1545</y>\n      <w>45</w>\n      <h>255</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;150.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1215</x>\n      <y>1350</y>\n      <w>225</w>\n      <h>510</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>130.0;10.0;130.0;320.0;10.0;320.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1215</x>\n      <y>1350</y>\n      <w>195</w>\n      <h>480</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;-</panel_attributes>\n    <additional_attributes>10.0;300.0;110.0;300.0;110.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1335</x>\n      <y>1830</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Server updates\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1275</x>\n      <y>1755</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Client data\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>1125</x>\n      <y>1665</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Server events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>960</x>\n      <y>1665</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Client events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>870</x>\n      <y>1530</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Event notifiers\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>870</x>\n      <y>1470</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>Script events\nfontsize=12\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/event_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\nfontsize=14\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>750</x>\n      <y>450</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventLoop**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1095</x>\n      <y>450</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventQueue**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1005</x>\n      <y>645</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Event**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>735</x>\n      <y>780</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventHandler**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>585</x>\n      <y>645</y>\n      <w>120</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**State**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>915</x>\n      <y>480</y>\n      <w>210</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>870</x>\n      <y>525</y>\n      <w>165</w>\n      <h>195</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;110.0;10.0;110.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1110</x>\n      <y>525</y>\n      <w>45</w>\n      <h>150</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;80.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>795</x>\n      <y>525</y>\n      <w>45</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;170.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>900</x>\n      <y>720</y>\n      <w>165</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;90.0;70.0;90.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>630</x>\n      <y>480</y>\n      <w>150</w>\n      <h>195</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;110.0;10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1185</x>\n      <y>780</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventEntity**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1095</x>\n      <y>720</y>\n      <w>120</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;70.0;10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/game_entity_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>0</x>\n      <y>390</y>\n      <w>390</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>**GameEntity**\n--\nid: entity_id_t\ncomponents: unordered_map\nrender_entity: WorldRenderEntity\nmanager: GameEntityManager\n--\ncopy(): GameEntity\nget_component(): Component\nadd_component(Component): void\nhas_component(component_t): bool\nrender_update(time_t, string): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>510</x>\n      <y>405</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Component**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>510</x>\n      <y>555</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GameEntityManager**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>75</x>\n      <y>765</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**WorldRenderEntity**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>180</x>\n      <y>660</y>\n      <w>45</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>375</x>\n      <y>585</y>\n      <w>165</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.&gt;</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>375</x>\n      <y>435</y>\n      <w>165</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>90</x>\n      <y>0</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GameEntity**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>375</x>\n      <y>0</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Component**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>90</x>\n      <y>165</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GameEntityManager**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>195</x>\n      <y>75</y>\n      <w>45</w>\n      <h>120</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.&gt;</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>285</x>\n      <y>30</y>\n      <w>120</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>330</x>\n      <y>195</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>465</x>\n      <y>195</y>\n      <w>150</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>/systems.../</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1290</x>\n      <y>375</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**Component**\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1095</x>\n      <y>525</y>\n      <w>270</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**InternalComponent**\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1425</x>\n      <y>525</y>\n      <w>255</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>&lt;&lt;interface&gt;&gt;\n**APIComponent**\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1320</x>\n      <y>450</y>\n      <w>45</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1440</x>\n      <y>450</y>\n      <w>45</w>\n      <h>105</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1065</x>\n      <y>675</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Activity**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1065</x>\n      <y>780</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**CommandQueue**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1065</x>\n      <y>885</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Ownership**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1065</x>\n      <y>990</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Position**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1515</x>\n      <y>675</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Idle**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1515</x>\n      <y>780</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Live**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1515</x>\n      <y>885</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Move**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1515</x>\n      <y>990</y>\n      <w>210</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Turn**\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1305</x>\n      <y>600</y>\n      <w>45</w>\n      <h>465</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>600</y>\n      <w>45</w>\n      <h>465</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1260</x>\n      <y>705</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1260</x>\n      <y>810</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1260</x>\n      <y>915</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1260</x>\n      <y>1020</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>1020</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>915</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>810</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1455</x>\n      <y>705</y>\n      <w>90</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/pathfinder_architecture.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <zoom_level>10</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>710</x>\n      <y>510</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Grid*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>710</x>\n      <y>620</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Sector*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>630</x>\n      <y>730</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*CostField*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>790</x>\n      <y>730</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Portal*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>710</x>\n      <y>400</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Pathfinder*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>980</x>\n      <y>400</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Integrator*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1040</x>\n      <y>490</y>\n      <w>160</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*IntegrationField*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1040</x>\n      <y>560</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*FlowField*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>520</x>\n      <y>400</y>\n      <w>120</w>\n      <h>60</h>\n    </coordinates>\n    <panel_attributes>\n*Path*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>760</x>\n      <y>450</y>\n      <w>30</w>\n      <h>80</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>760</x>\n      <y>560</y>\n      <w>30</w>\n      <h>80</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>720</x>\n      <y>670</y>\n      <w>30</w>\n      <h>80</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>800</x>\n      <y>670</y>\n      <w>30</w>\n      <h>80</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>820</x>\n      <y>420</y>\n      <w>180</w>\n      <h>30</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>990</x>\n      <y>450</y>\n      <w>30</w>\n      <h>160</h>\n    </coordinates>\n    <panel_attributes>lt=.</panel_attributes>\n    <additional_attributes>10.0;140.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>990</x>\n      <y>510</y>\n      <w>70</w>\n      <h>30</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>990</x>\n      <y>580</y>\n      <w>70</w>\n      <h>30</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>630</x>\n      <y>420</y>\n      <w>100</w>\n      <h>30</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>760</x>\n      <y>200</y>\n      <w>30</w>\n      <h>220</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;200.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>460</x>\n      <y>330</y>\n      <w>240</w>\n      <h>70</h>\n    </coordinates>\n    <panel_attributes>*Pathfinder*\nfontsize=22\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>300</y>\n      <w>770</w>\n      <h>30</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;750.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>730</x>\n      <y>160</y>\n      <w>130</w>\n      <h>70</h>\n    </coordinates>\n    <panel_attributes>Movement, collision, etc.\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>460</x>\n      <y>160</y>\n      <w>240</w>\n      <h>70</h>\n    </coordinates>\n    <panel_attributes>*Gamestate*\nfontsize=22\nstyle=wordwrap</panel_attributes>\n    <additional_attributes/>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/simulation.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1170</x>\n      <y>525</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Game**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>705</x>\n      <y>495</y>\n      <w>375</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>**GameSimulation**\n--\nrunning: bool\ngame: Game\n--\nset_modpacks(vector&lt;string&gt;): void\ninit_event_handlers(): void\nrun(): void\nstart(): void\nstop(): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1170</x>\n      <y>690</y>\n      <w>180</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GameState**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1440</x>\n      <y>645</y>\n      <w>195</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**GameEntity**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1440</x>\n      <y>750</y>\n      <w>165</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**Terrain**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1335</x>\n      <y>690</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1335</x>\n      <y>750</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1245</x>\n      <y>600</y>\n      <w>45</w>\n      <h>120</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1065</x>\n      <y>555</y>\n      <w>135</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>705</x>\n      <y>870</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**EventLoop**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>900</x>\n      <y>870</y>\n      <w>150</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>\n**TimeLoop**</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>960</x>\n      <y>765</y>\n      <w>45</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>765</x>\n      <y>765</y>\n      <w>45</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/system_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>795</x>\n      <y>405</y>\n      <w>510</w>\n      <h>165</h>\n    </coordinates>\n    <panel_attributes>**Move**\n--\nmove_default(GameEntity, phys3, time_t): time_t\nmove_command(GameEntity, time_t): time_t</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>810</x>\n      <y>630</y>\n      <w>375</w>\n      <h>165</h>\n    </coordinates>\n    <panel_attributes>**Idle**\n--\nidle(GameEntity, time_t): time_t</panel_attributes>\n    <additional_attributes/>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/images/time_classes.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\nfontsize=14\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>15</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>675</x>\n      <y>300</y>\n      <w>315</w>\n      <h>180</h>\n    </coordinates>\n    <panel_attributes>**TimeLoop**\n--\nget_clock(): Clock\nrun(): void\nstart(): void\nstop(): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1095</x>\n      <y>300</y>\n      <w>315</w>\n      <h>285</h>\n    </coordinates>\n    <panel_attributes>**Clock**\n--\nupdate_time(): void\nget_time(): time_t\nget_real_time(): time_t\nget_speed(): speed_t\nset_speed(speed_t): void\nstart(): void\nstop(): void\npause(): void\nresume(): void</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>975</x>\n      <y>375</y>\n      <w>150</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/code/input/README.md",
    "content": "# Input Management\n\nInput managment concerns the handling external input sources that are used to interact with the engine.\nThe most common example for such input sources are probably **keyboard and mouse**, but other things like the **GUI**,\n**networking** and **scripting** also fall under this umbrella.\n\n1. [Motivation](#motivation)\n2. [Basic Architecture](#basic-architecture)\n3. [Window System](#window-system)\n   1. [Workflow](#workflow)\n   2. [Class Relationships](#class-relationships)\n   3. [Low-level Interface](#low-level-interface)\n   4. [High-Level Interface (Controller)](#high-level-interface-controller)\n4. [Scripting](#scripting)\n5. [Network](#network)\n6. [AI](#ai)\n\n\n## Motivation\n\nInput handling in real-time strategy games is inherently more complex than in other games for a\nmultitude of reasons. Players do not have control over just one but many entities, which can also have\na variety abilities and possible levels of control. Depending on the selected entity there may be\ndifferent actions available to the player. Keybindings may also switch depending on the current usage\ncontext, so a key press can have different consequences depending on when and where it is pressed.\nBesides that, not all inputs have a direct effect on the game world such as camera movement or\ninformation displayed in the UI.\n\nAll this means that inputs cannot be translated one-to-one to gameplay events like in other games.\nInstead, we require a system that provides context management, multiple input sources\nand targeted control, while still being as flexible and configurable as possible when exposing\nthe built-in features of the engine.\n\n\n## Basic Architecture\n\nThe basic architecture workflow is roughly the same for every type of external input source.\nThere is a low-level and a high-level interface.\n\nLow-level interfaces do some basic pre-processing, sorting, context management and delegation\nfor raw input events received from an input source (e.g. window system, network socket, scripting,\netc.). Relevant information from the raw input is stripped and converted into a more generalized\ninput format for internal usage in the engine. The interface also may discard unnecessary inputs,\ne.g. duplicate network packets or unbound key presses, at this point. Accepted inputs are then\nsent to an associated high-level component for further processing.\n\nHigh-level interfaces interact with specific engine components by executing the actions associated\nwith an input event. In other words: This is where the magic happens and the inputs have an actual\neffect on the engine (or more specifically, the game simulation). Actions are looked up via\nmappings of input event to action which are usually configurable, e.g. as hotkey bindings for inputs\nfrom the window system.\n\n\n## Window System\n\nInput management for the window system handles all input events received via Qt, i.e. everything\nfrom input devices such as\n\n- keyboard\n- mouse\n- GUI\n- ... (anything else that Qt can detect as an input device)\n\n\n### Workflow\n\n![Workflow](images/workflow_input_controller.svg)\n\n\n### Class Relationships\n\n![Class Relationships](images/class_relationships_input_controller.svg)\n\n\n### Low-level Interface\n\nThe central component of the low-level interface is the `InputManager` which is managed by\nthe engine's presenter. It processes all raw input events (from `QEvent` objects) received from Qt.\nKeyboard and mouse events are captured every frame and are forwarded directly to the input\nmanager via callbacks setup by the presenter.\n\nGUI events are a special case as the GUI exists separately from the application window and\ndoes not automatically receive the same keyboard/mouse inputs. Therefore, the input manager\nmay forward keyboard/mouse inputs to the GUI, check if they are accepted, and then process\nthe resulting GUI event.\n\nIn any case, the resulting `QEvent` representing the raw input is converted to a generalized\n`input::Event` object containing the following information:\n- **event class**: Basic event categorization, usually type of input device (e.g. keyboard, mouse, GUI)\n- **code**: Unique identifier of the specific key/button that was pressed\n- **modifiers**: Keyboard modifiers pressed alongside the key/button (e.g. CTRL, SHIFT, ALT)\n- **state**: State of the button/key (e.g. pressed, released, double click)\n- **raw event**: Reference to the original `QEvent`\n\nThe unique combination of *class*, *code*, *modifiers*, and *state* represents a specific key press\nand provides the **signature** of an input event. (Key) bindings are created by mapping signatures\nto actions, which are executed when the signature is encountered in a received input event. Other than\nby signature, actions can also be mapped by event class to allow for catch-all scenarios where all\nkeys of a class are handled in the same way.\n\nBindings for the input manager are stored in `InputContext` objects. An input context represent a scope of\nbindings which are accepted when the context is active. Contexts can become active through actions from\na binding or other events. An example for such a context is the villager build menu in AoE2 which activates\nwhen a villager is selected. Different input contexts can bind the same event signature or class which\nmakes multiple key assignments possible. Additionally, the input manager always has a global input\ncontext used as fallback if no other contexts are active.\n\nThe input manager allows multiple input contexts to be active at the same time. Contexts are managed\non a stack with the most recently added input context on top. When the input manager processes a new\ninput event, it checks all active contexts on the stack for bindings in the following order:\n\n1. check top context\n2. check other contexts on stack by traversing downwards\n3. check global context\n\nIf no binding is found, nothing is done and the input manager waits for the next raw input input.\n\nIf a binding exists for the input event, the input manager retrieves the mapped `input_action` struct\nsignifying the action executed for the event. It should be stressed that, as a low-level interface,\nthe actions taken by the input manager mainly consists of forwarding event data to the correct\nhigh-level interfaces. Therefore, these actions should not have any effect on the game simulation.\n\n`input_action` contains the following information:\n- **action type**: One of the pre-defined types (see below). Used for determining a default action.\n- **custom action function**: Executed instead of the default action if set (optional).\n- **execution flags**: Key-value pairs for configuration settings (optional).\n\nMost types have a default action that is executed unless a custom function is defined in `input_action`.\nThese default actions are:\n- **push context**: push a context on top of the stack\n- **pop context**: remove the current top context\n- **remove context**: remove a context from the stack\n- **Controller**: forward event arguments in direction of gamestate (i.e. to high-level interface)\n- **GUI**: forward event arguments to the GUI\n\nIn most cases, it should be sufficient to bind one of these options to an input event. Custom\nfunctions should only be used for edge cases which cannot be handled otherwise.\n\nFor forwarding actions, additional arguments may be passed to the high-level interface. In the\ncase of the controller, the input manager passes the `event_arguments` struct which contains:\n- **input event** (i.e. the generalized `input::Event`)\n- **mouse position**\n- **mouse motion** (e.g. for calculating mouse movement direction)\n- **flags**: Key-value pairs for configuration settings (optional)\n\n\n### High-Level Interface (Controller)\n\nThe high-level interface is implemented by the `Controller` class. Controllers provide a\nconnection between input management and gamestate. One controller represents the control scope\nof a player outside (or faction inside) the game. Thus, they also have significantly more awareness\nof what's going on inside the gamestate than the low-level input manager.\n\nA controller receives input event data (as an `event_arguments` struct) as well as a `BindingContext`\nfrom the input manager. It uses the provided information to create a game event that can then be attached\nto the gamestate event loop. To do this, the controller looks up a `binding_action` struct from\nin binding context using the input event signature or class (similiar to how its works in the low level\ninterface).\n\n`binding_action` contains the following information:\n- **transform function**: Transformation from event arguments to game event.\n- **queue type**: Determines whether the created event is queued or passed to the gamestate immediately .\n- **execution flags**: Key-value pairs for configuration settings (optional).\n\nGame events can be queued before they are forwarded to the gamestate to allow chained commands,\ne.g. setting a bunch of waypoints before giving the final move command.\n\n\n## Scripting\n\nTODO\n\n\n## Network\n\nTODO\n\n\n## AI\n\nTODO\n"
  },
  {
    "path": "doc/code/logger.md",
    "content": "Synopsis:\n\n    #include \"log/log.h\"\n\n    log::log(MSG(info) << \"test\" << 1337);\n\n    auto msg = MSG(warn);\n    msg.fmt(\"%d %8.3f\", 10, 2.0) << \"lol test\";\n    msg << \"rofl\";\n    log::log(msg);\n\n    villager.log(MSG(dbg) <<\n            \"This is how you're supposed to break log lines that are too\"\n            \"long. \" << 1337 <<\n            \"foo\");\n\n    throw Error(MSG(err) << \"Exceptions use the MSG system as well!\");\n\nThe logging system consists of the following main components:\n\n - `log::LogSource`: Objects that have a `.log()` member function and accept messages.\n - `log::log()`: Accepts messages using a \"general\" LogSource.\n - `MSG`: Macro that creates objects for the `log()` methods.\n - `log::LogSink`: prints/renders/writes log messages.\n\n#### MSG\n\nThe `MSG` macro collects all sorts of information, including `__FILE__` and `__LINE__`.\n`MSG` evaluates to a `log::MessageBuilder` constructor; the message builder accepts input in two ways:\n\n - \"c++-style\" with `operator <<` (\"iostreams\")\n - \"c-style\" with `.fmt()` (\"printf\")\n\nAll input is appended to the internal `log::message` object. The `MessageBuilder` is auto-converted to `log::message` if needed.\n\n#### message\n\nDumb struct that holds the text and metadata.\n\n#### LogSource\n\nAny class that wishes to provide `.log()` may inherit from `log::LogSource`.\nIt just needs to implement `virtual std::string logsource_name()`.\n\n#### LogSink\n\nA list of all log sinks is maintained (by the `LogSink` constructor/destructor) in `LogSink::log_sink_list`.\n\nEach time a message is passed to `LogSource::log()`, it is forwarded to each `LogSink`, which may proceed as it wishes.\n\nPopular `LogSink` classes include¹:\n\n| Sink         | What does it do to my messages?                             |\n|--------------|-------------------------------------------------------------|\n| StdOutSink   | Prints them to stdout.                                      |\n| FileSink     | Writes them and all their metadata to a file.               |\n| InGameSink   | Displays them next to the in-game objects they refer to.    |\n| ConsoleSink  | Prints them to a in-game terminal buffer.                   |\n\n¹) Disclaimer: May not actually be popular and/or available.\n"
  },
  {
    "path": "doc/code/optimization.md",
    "content": "# Code Optimization\n\nopenage should be efficient in its resource usage and offer high performance.\nAs we are building a game engine, it is kind of expected that everything runs\nsmoothly out-of-the-box.\n\n1. [Basic Rules](#basic-rules)\n2. [Checking Performance](#checking-performance)\n\n\n## Basic Rules\n\nBefore anyone dives into the codebase and starts changing things around for better performance,\nthey should consider these simple rules:\n\n- **readability comes first**: openage depends on outsiders being able to understand the engine code. If the code is super fast but totally incomprehensible, then nobody is going to maintain it.\n- **avoid premature optimization**: optimization without reason wastes time, reduces readability, and can sometimes lead to even worse performance. It's often better to implement a workable solution first and then use profiling to identify bottlenecks. Modern compilers are often smart enough to find small improvements during compilation anyway.\n- **prefer architecture improvements over speeding up specific use cases**: openage is a game *engine*, so we don't exactly know what people will use it for. We want the general case to be fast, not the edge cases.\n\n\n## Checking Performance\n\nPerformance bottlenecks are best identified with a *profiler* of your choice.\n\nFor **Python**, the built-in modules [cProfile](https://docs.python.org/3/library/profile.html)\n(speed) and [tracemalloc](https://docs.python.org/3/library/tracemalloc.html) (memory) are recommended.\nopenage also provides convenience functions for them in its `openage.util.profiler` module.\n\nFor **C++**, [valgrind](https://valgrind.org/info/tools.html) with `callgrind` (speed) and\n`memcheck` (memory) can be used. If you use valgrind, you should supply it the suppression\nfile in our repo: [`etc/valgrind-python.supp`](/etc/valgrind-python.supp). This lets\nvalgrind handle the Python parts of the engine a bit better.\n\nAny other profiler will also do the trick as long as you know how to use it (e.g. `perf`,\n`VTune`, `uProf`, etc.).\n\nProfiling a full engine run can be very resource intensive to profile. Therefore, it\ncan be beneficial to profile engine [demos](/doc/code/testing.md#demos) for a specific\nsubsystem, e.g. the renderer or the event system. This is also useful to\ncheck the performance of subsystems in isolation. However, consider that a full engine\nrun may have different bottlenecks that are not obvious in the demos.\n\nStresstests are demos that are specifically designed to test performance, particularly\nin the renderer subsystem. They can also include benchmarks with additional\ninformation, e.g. an FPS counter. When profiling performance critical code, implementing\na stresstest should be considered to complement profiling results.\n"
  },
  {
    "path": "doc/code/pathfinding/README.md",
    "content": "# Pathfinding\n\nopenage's pathfinding subsystem implements structures used for navigating game entities in the\ngame world. These structures define movement costs for a map and allow search for a path\nfrom one coordinate in the game world to another.\n\nPathfinding is a subsystem of the [game simulation](/doc/code/game_simulation/README.md) where\nit is primarily used for movement and placement of game entities.\n\n1. [Architecture](#architecture)\n2. [Workflow](#workflow)\n\n\n## Architecture\n\nThe architecture of the pathfinder is heavily based on the article *Crowd Pathfinding and Steering*\n*Using Flow Field Tiles* by Elijah Emerson (available [here](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf)). A core design\ndecision taken from this article was the usage of flow fields as the basis for the pathing algorithm.\n\nFlow fields offer a few advantages for large group movements, i.e. the movement you typically see in\nRTS games like Age of Empires. For example, they allow reusing already computed path results for\nsubsequent pathing requests and can also be used for smart collision avoidance. One downside\nof using flow fields can be the heavy upfront cost of pathing calculations. However, the downsides\ncan be mitigated to some degree with caching and high-level optimizations.\n\nThe openage pathfinder is tile-based, like most flow field pathfinding implementations. Every tile\nhas a movement cost associated with it that represents the cost of a game entity moving on that tile.\nWhen computing a path from A to B, the pathfinder tries to find the sequence of tiles with the cheapest\naccumulated movement cost.\n\nIn openage, the pathfinding subsystem is independent from the terrain implementation in the game\nsimulation. Terrain data may be used to influence movement cost during gameplay, but pathfinding\ncan be initialized without any requirements for terrain code. By only loosely connecting these two system,\nwe have a much more fine-grained control that allows for better optimization of the pathfinder.\nThe most relevant similarity between terrain and pathfinding code is that they use the same\n[coordinate systems](/doc/code/coordinate-systems.md#tiletile3). To make the distinction between\npathfinding and terrain more clear, we use the term *cells* for tiles in the pathfinder.\n\n![UML pathfinding classes](/doc/code/images/pathfinder_architecture.svg)\n\nThe relationship between classes in the pathfinder can be seen above. `Grid` is the top-level structure\nfor storage of movement cost. There may be multiple grids defined, one for each movement type\nused by game entities in the game simulation.\n\nEvery grid is subdivided into sectors, represented by the `Sector` class. Each sectors holds a\npointer to a `CostField` which stores the movement cost of individual cells. All sectors on the\nsame grid have a fixed square size that is determined by the grid. Thus, the sector size on\na grid is always consistent but different grid may utilize different sector sizes.\n\nSectors on a grid are connect to each other with so-called portals, see the `Portal`\nclass. Portals represent a passable gateway between two sectors where game entities\ncan pass through. As such, portals store the coordinates of the cells in each sector\nwhere game entities can pass. `Portal` objects are created for every continuous sequence of cells\non the edges between two sectors where the cells are passable on both sides of the two sectors.\nTherefore, there may be multiple portals defined for the edge between two sectors.\n\nAdditionally, each portal stores which other portals it can reach in a specific sector. The\nresult is a portal \"graph\" that can be searched separately to determine which portals\nand sectors are visited for a specific pathing request. This is used in the high-level\npathfinder to preselect the sectors that flow fields need to be generated for (see the\n[Workflow section](#workflow)).\n\nThe individual movement cost of each cell in a sector are recorded in a `CostField` object.\nThe cost of a cell can range from 1 (minimum cost) to 254 (maximum cost), while a cost of 255\nmakes a cell impassible. For Age of Empires, usually only the minimum and impassable cost\nvalues are relevant. The cost field is built when the grid is first initialized and\nindividual cost of cells can be altered during gameplay events.\n\nTo get a path between two coordinates, the game simulation mainly interfaces with a `Pathfinder`\nobject. The `Pathfinder` class calls the actual pathfinding algorithms used for searching\nfor a path and stores references to all available grids in the pathfinding subsystems. It can receive\n`PathRequest` objects that contain information about the grid that should be searched as well as\nthe start and target coordinates of the desired path. Found paths are returned as `Path` objects\nwhich store the waypoint coordinates for the computed path.\n\nFlow field calculations are controlled by an `Integrator` object, which process a cost field\nfrom a sector as well as target coordinates to compute an `IntegrationField` and a `FlowField` object.\n`IntegrationField`s are intermediary objects that store the accumulated cost of reaching\nthe target cell for each other cell in the field, using the cell values in the cost field as basis.\nFrom the integration field, a `FlowField` object is created. Cells in the flow field store\nmovement vectors that point to their next cheapest neighbor cell in the integration field. `Path`s\nmay be computed from these flow field by simply following the movement vectors until the\ntarget coordinate is reached.\n\n\n## Workflow\n\nTo initiate a new search, the pathfinder receives a `PathRequest` object, e.g. from the game simulation.\nEvery path request contains the following information:\n\n- ID of the grid to search\n- start cell coordinates\n- target cell coordinates\n\nThe actual pathfinding algorithm is split into two stages.\n\n1. High-level portal-based search to identify the visited sectors on the grid\n2. Low-level flow field-based search in the identified sectors to find the waypoints for the path\n\nThe high-level search is accomplished by utilizing the portal graph, i.e. the\nconnections between portals in each sector. From the portal graph, a node mesh is created\nthat can be searched with a graph-traversal algorithm. For this purpose, the A\\* algorithm\nis used. The result of the high-level search is a list of sectors and portals that are\ntraversed by the path.\n\nThe high-level search is mainly motivated by the need to avoid costly flow field\ncalculations on the whole grid. As the portal graph should already be precomputed when\na path request is made, the main influence on performance is the A\\* algorithm. Given\na limited number of portals, the A\\* search should overall be very cheap.\n\nThe resulting list of sectors and portals is subsequently used in the low-level flow\nfield calculations. More details can be found in the [field types](field_types.md) document.\nAs a first step, the pathfinder uses its integrator to generate\na flow field for each identified sector. Generation starts with the target sector\nand ends with the start sector. Flow field results are passed through at the cells\nof the identified portals to make the flow between sectors seamless.\n\nIn a second step, the pathfinder follows the movement vectors in the flow fields from\nthe start cell to the target cell. Waypoints are created for every direction change, so\nthat game entities can travel in straight lines between them. The list of waypoints\nfrom start to target is then returned by the pathfinder via a `Path` object.\n"
  },
  {
    "path": "doc/code/pathfinding/field_types.md",
    "content": "# Field Types\n\nThis document describes the field types used in the flow field pathfinding system.\n\nMost of the descriptions are based on the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article by Elijah Emerson.\n\n## Cost Field\n\nA cost field is a square grid of cells that record the cost of movement on the location\nof each cell. Higher cost values indicate that it is less desirable to move through that cell.\nThe field is usually initialized at the start of the game and persists for the lifetime of\nthe entire pathfinding grid. During gameplay, individual cell costs may be altered to reflect\nchanges in the environment.\n\nCost values are represented as `uint8_t` (0-255) values. The range of usable cost values\nis `1` to `254`. `255` is a special value that represents an impassable cell. `0` is reserved\nfor initialization and should not be used for flow field calculations.\n\n![Cost Field](images/cost_field.png)\n\n- **green**: minimum cost\n- **red**: maximum cost\n- **black**: impassable cell\n\n## Integration Field\n\nThe integration field is created from a cost field when a path is requested. For a specific\ntarget cell, the integration field stores the accumulated cost of reaching that cell from\nevery other cell in the field.\n\nIntegration values are calculated using a wavefront algorithm. The algorithm starts at the\ntarget cell(s) and propagates outward, updating the integration value of each cell it visits.\nThe integration value is calculated by adding the cost value of the current cell to the lowest\nintegration value of the 4 cardinal neighbors. The integration value of the target cell(s) is `0`.\n\nIntegration values are represented as `uint16_t` (0-65535) values. The range of usable integration\nvalues is `1` to `65534`. `65535` is a special value that represents an unreachable cell. During\ninitialization, all cells are set to `65535`.\n\nAn additional refinement step in the form of line-of-sight testing may be performed before the\nintegration values are calculated. This step flags every cell that is in line of sight of the\ntarget cell. This allows for smoother pathing, as game entities can move in a straight line to\nthe target cell. The algorithm for this step is described in more detail in section 23.6.2\nof the [*Crowd Pathfinding and Steering Using Flow Field Tiles*](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf) article.\n\nIn addition to the integration values, the integration field also stores flags for each cell:\n\n- `FOUND`: cell has been visited\n- `TARGET`: cell is a target cell\n- `LOS`: cell is in line of sight of target cell\n- `WAVEFRONT_BLOCKED`: cell is blocking line of sight to target cell\n\n![Integration Field](images/integration_field.png)\n\n- **green**: lower integration values\n- **purple**: higher integration values\n- **black**: unreachable cell\n\n## Flow Field\n\nCreating the flow field is the final step in the flow field calculation. The field\nis created from the integration field. Cells in the flow field store the direction to\nthe neighbor cell with the lowest *integrated* cost. Thus, directions create a \"flow\"\ntowards the target cell. Following the directions from anywhere on the field will lead\nto the shortest path to the target cell.\n\nFlow field values are represented as `uint8_t` values. The 4 least significant bits are used\nto store the direction to the neighbor cell with the lowest integrated cost. Therefore, 8\ndirections can be represented. The 4 most significant bits are used for flags:\n- `PATHABLE`: cell is passable\n- `LOS`: cell is in line of sight of target cell\n- `TARGET`: cell is a target cell\n\n![Flow Field](images/flow_field.png)\n\n- **white**: line of sight\n- **bright/dark grey**: passable cells (not in line of sight)\n- **black**: impassable cell\n"
  },
  {
    "path": "doc/code/pyinterface.md",
    "content": "openage C++ <-> Python interface\n================================\n\nopenage consists of python modules, which contain the program entry point,\nand the library `libopenage.so`, which contains all C++ code.\n\nCython is used for glue code.\n\n\nCython crash course\n-------------------\n\n\n### Python with types\n\nCython modules are written in `.pyx` files, and roughly equivalent to .py files.\nIn addition to regular Python syntax, `.pyx` files allow you to define typed\nfunctions and objects:\n\n``` cython\ncdef int square(int x):\n    return x * x\n\ncdef cppclass Rectangle:\n    int h, w\n\n    int size():\n        return this.h * this.w\n\ndef foo():\n    cdef Rectangle r\n    r.h = 5\n    r.w = 6\n    return square(r.size())\n```\n\nMore language information [is in the official documentation](https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html).\n\n`.pyx` files are translated to `.cpp` by Cython (\"cythonized\") as part of the\nopenage build process; syntax errors are shown in this step.\n\nEach `.cpp` file is then compiled to a Python extension module, which may be\nused from everywhere.\n\n- `def` functions and `cdef class` classes can be used from Python\n- `cdef` functions can't be used directly\n  - suitable for storage in a `C` function pointer\n  - callable from `def` functions in the `.pyx` file\n\n```\n$ cython --cplus -3 test.pyx\n$ g++ -shared -fPIC $(python3-config --includes) test.cpp -o test.so\n$ python3\n>>> import test\n>>> test.foo()\n900\n```\n\n### Interface definitions\n\nCython can use any regular C++ function or type; for that purpose, they\nneed to be declared in `.pxd` files (which are analogous to C++ `.h` files):\n\n``` cython\ncdef extern from \"<stdlib.h>\":\n    int atoi(const char *s)\n```\n\nTo use this function declared in `foo.pxd`,\n\n``` cython\nfrom foo cimport atoi as c_atoi\n\ndef atoi(s):\n    # invokes the c implementation according to the\n    # interface defined in a .pxd file\n    return c_atoi(s)\n```\n\n### pxd generation\n\n`openage` has a helper, `pxdgen`, which auto-generates `.pxd` files for `.h`\nfiles in the `libopenage/` subdirectory, from `pxd:` annotations in these files, as part\nof the build system.\n\nThe pxd annotations are really simple; just have a look at some of the headers.\nYou'll find the generated `.pxd` files next to the C++ header files.\n\nTo `cimport` a class Foo that was pxd-annotated in `util/foo.h`, type\n\n``` cython\nfrom libopenage.util.foo cimport Foo\n```\n\nCython [ships lots of `.pxd` files](https://github.com/cython/cython/tree/master/Cython/Includes) for most C, C++ and CPython functions:\n\n``` cython\nfrom libc.math cimport sin\nfrom libcpp.vector cimport vector\n\ncdef vector[float] vector_sin(vector[float]& args):\n    return [sin(arg) for arg in args]\n\nprint(vector_sin(range(10)))\n```\n\nFrom time to time, it may be useful to have a look at the generated `.cpp` file,\nespecially if you require more exotic functionality or something doesn't work\nout as you expected. For that purpose, `.html` files are generated next to the `.cpp` files.\n\n\nCalling C++ functions from Python\n---------------------------------\n\nTo make a C++ function available for calling from Cython code, annotate the header\nfile with `pxd` comments:\n\n`libopenage/example.cpp`\n\n``` cpp\nnamespace openage {\n\nint foo(int arg0, std::string arg1) {\n        return 5;\n}\n\n}\n```\n\n`libopenage/example.h`\n\n``` cpp\n// pxd: from libcpp.string cimport string\n#include <string>\n#include \"util/compiler.h\"\n\nnamespace openage {\n\n/**\n * The famous foo function. Warning: might bar occasionally.\n *\n * pxd:\n *\n * int foo(int arg0, string arg1) except +\n */\nOAAPI int foo(int arg0, std::string arg1);\n\n}\n```\n\n_Always_ pxd-declare your functions as `except +` (\"may potentially rise a C++ exception\"), unless the C++ function is marked `noexcept` itself.\nIf a function that is not declared `except +` throws anyways, the entire CPython interpreter is likely to be shredded.\n(Sidenote: the `except +` annotation enables openage-specific exception translation. To see how this is used, look in the `exctranslate_tests.pyx/cpp`)\n\nTo enable `pxdgen`, `cmake` must be informed about the `pxd`-annotated header file:\n\n`libopenage/CMakeLists.txt`\n\n``` cmake\npxdgen(example.h)\n```\n\nThe function is now available from Cython. To make it available for pure-python modules, write a wrapper:\n\n`openage/foo.pyx`\n\n``` cython\nfrom libopenage.foo cimport foo as c_foo\n\ndef foo(int arg0, str arg1):\n    with nogil:\n        return c_foo(arg0, arg1)\n```\n\n`openage/bar.py`\n\n``` python\nfrom openage.foo import foo\n\nprint(foo(10, \"test\"))\n```\n\n\nCalling Python functions from C++\n---------------------------------\n\nThe interface works one-way: Cython can access `libopenage`, but not the other way round.\nThus, `libopenage` must find its way back to Python:\n\n* By `pyinterface::PyIfFunc` function pointers that are filled by Cython during initialization.\n* By `py::PyObj` objects that hold a Python object. It may be callable.\n\n\n### Calling directly to C++\n\nAny Python object can be stored in a `PyObj` (`from libopenage.pyinterface.pyobject`),\nwhich C++ can use to do calls to Python callables.\n\nThis works in the way that you create C++ objects in Cython and then\n[call a C++ function](#calling-c-functions-from-python) where you pass it.\n\n\n``` c++\n// pxd: from libcpp.string cimport string\n#include <string>\n\n// pxd: from libopenage.pyinterface.pyobject cimport PyObj\n#include \"libopenage/pyinterface/pyobject.h\"\n\n\n/**\n * pxd:\n *\n * cppclass demo_struct:\n *     PyObj obj\n *     string text\n */\nstruct OAAPI demo_struct {\n    py::PyObj obj;\n    std::string text;\n};\n\n/**\n * pxd: int cpp_function(lol_struct arg, int another_arg) except +\n */\nvoid cpp_function(lol_struct &arg, int another_arg) {\n    // native data can directly be used\n    std::cout << \"native_arg: \" << arg.text << std::endl;\n\n    // call the python\n    // with automatic argument conversion!\n    std::cout << \"python call: \"\n        << arg.obj->getattr(\"py_func\").call(\"some binary\", another_arg).str()\n        << std::endl;\n}\n```\n\n``` cython\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom libopenage.main cimport demo_struct, cpp_function\n\nclass TestClass:\n    def __init__(self):\n        self.some_member = \"rofl\"\n\n    def py_func(self, arg0, arg1):\n        return \"test: 0={} 1={} 2={}\".format(self.some_member, arg0, arg1)\n\ndef entry():\n    # create the object and deliver it to c++\n    test_obj = TestClass()\n\n    # python object wrapping for c++\n    cdef PyObj pyobj_wrapped = PyObj(<PyObject*> some_object)\n\n    # create a c++ object\n    cdef demo_struct cppobj\n    cppobj.obj = pyobj_wrapped\n    cppobj.text = \"behold the automatic type conversion!\"\n\n    # call to c++\n    cpp_function(cpp_obj, 1337)\n```\n\nThis means that Python calls to C++ with `cpp_function`,\nthen C++ calls back to Python to the `py_function`.\n\nC++ can access a `PyObj` in many more ways, perform casts, etc.\n\n\n### Registering Python functions in C++\n\nTo access call Python functions some time later, they can be remembered in C++:\n\nAny `cdef` functions can be stored in C++ function pointers.\nThe `openage::pyinterface::PyIfFunc` type has been created for this purpose;\nit allows *binding arguments* and makes sure that the pointer is properly initialized.\n\nWe will call this pure-Python function from C++:\n\n`openage/bar.py`\n\n``` python\ndef bar(arg0, arg1):\n    \"\"\"\n    This function involves rainbows and unicorns.\n    arg0 shall be an integer, and arg1 a string.\n    \"\"\"\n    return 6.283185307179586\n```\n\nDeclare, define and pxd-export the function pointer:\n\n`libopenage/foo.cpp`\n\n``` cpp\n#include \"foo.h\"\n\nPyIfFunc<float, int, std::string> bar;\n```\n\n`libopenage/foo.h`\n\n``` cpp\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"pyinterface/functional.h\"\n\n// pxd: PyIfFunc2[float, int, string] bar\nextern OAAPI PyIfFunc<float, int, std::string> bar;\n```\n\nWrap the python function in a `cdef` function, and define a method `setup()`,\nwhich binds the `cdef` function to the PyIfFunc object.\n\n`openage/foo.pyx`\n\n``` cython\nfrom libopenage.foo cimport bar as c_bar\n\nfrom .bar import bar as py_bar\n\ncdef float bar(int arg0, string arg1) except * with gil:\n    return py_bar(arg0, <str> arg1)\n\ndef setup():\n    c_bar.bind0(bar)\n```\n\n`PyIfFunc2` means that the function takes 2 arguments, and `bind0` means that 0\narguments are bound (this is needed because Cython currently doesn't support variadic template arguments).\n\nAdd a call to `openage.foo.setup()` to `openage.cppinterface.setup.setup`.\n\n`openage/pyinterface/setup.pyx`\n\n``` python\ndef setup():\n    # (...)\n\n    from openage.foo import setup\n    setup()\n\n    # (...)\n```\n\nIf you forget to do that, `openage.pyinterface.setup.setup` will raise a fatal exception.\n\n`libopenage/bar.cpp`\n\n``` cpp\n#include \"foo.h\"\n\nstd::cout << openage::bar.call(5, \"test\") << std::endl;\n```\n\nThe __only__ way of accessing Python code from C++ should be via the `Func` or `PyIfFunc` function wrappers, as those\nguarantee that exceptions are properly translated, among other things.\n\n\nReal-life examples\n------------------\n\n* For code that wraps a C++ class for Python, see:\n  * `openage/cabextract/lzxd.pyx`\n  * `libopenage/util/lzxd.h`\n* For code that wraps a Python class for C++, see:\n  * `libopenage/util/fslike/path.h`\n  * `openage/util/fslike/cpp.pyx`\n* Search for `.pyx` files in the repo\n\n\nNotes on the GIL\n----------------\n\nThe GIL must be acquired for any Python functionality (even as simple as `PyErr_Occurred`).\n\nHowever, GIL-safety is guaranteed by the combination of Cython, the pyinterface code,\nand the fact that `libopenage` doesn't link against Python itself / include any Python headers.\n\nAny code in `libopenage` can safely be run without the GIL.\n\nOnly functions that are marked `with gil` can be bound to `PyIfFunc` or `Func` objects;\nthis ensures that the GIL is always re-acquired when jumping into Cython code.\n\n__Never__ use raw function pointers in the interface;\nalways use the `PyIfFunc` or `Func` objects; otherwise, you'll lose all safety guarantees.\n\nThe `OAAPI` macro\n-----------------\n\n`OAAPI` marks the DLL entry-points which is necessary for the Windows build.\nAll pxd interface functions, classes, and `extern` objects must be declared with `OAAPI` in the header file.\nSee the samples above to understand the exact position of insertion for each of those.\nThis can be ignored for functions defined inline in the header.\n"
  },
  {
    "path": "doc/code/renderer/README.md",
    "content": "# Openage graphics\nThe graphics subsystem is implemented in two levels. The first level is an abstraction over graphics APIs (OpenGL, Vulkan) and provides generic shader execution methods. The second level uses the first to draw openage-specific graphics, i.e. the actual world, units, etc.\n\n![Renderer Hierarchy](images/renderer_hierarchy.svg)\n\n## Level 1 Renderer\n\n[More about the level 1 renderer](level1.md)\n\nFirst things first, we might want to support multiple APIs. For now just OpenGL, but maybe Vulkan or some others.  We want to abstract over these, but this can't unfortunately be done at the level of graphics primitives like textures, buffers, etc. Well, it can, but it introduces unnecessary complexity and possible overhead. That is because the next-gen (Vulkan, Metal, DX12) APIs are vastly different from the old ones - most importantly, they're bindless, so something like a Vulkan context (GL notion) doesn't even make sense. We therefore choose to abstract on the higher level of things-to-draw.\n\nIt works similarly to the Unity engine. The user can submit resources to be uploaded to the GPU and receives a handle that identifies the uploaded resource. Resources can be added, updated and removed. Currently supported resource types: shader, texture.\n\n## Level 2 Renderer\n\n[More about the level 2 renderer](level2.md)\n\nOn top of the level 1 renderer, we build a level 2 graphics subsystem. It has an API that is specific to openage, and is threadsafe. The level 2 renderer calls the level 1 renderer and updates it to match the gamestate. It is part of the `Presenter` component of the engine.\n\nLevel 2 rendering involves several stages that each handle a slightly different use case. For example, the `TerrainRenderer` stage is used for drawing the terrain, the `WorldRenderer` is used for drawing sprites of units/buildings, etc. Each stage usually utilizes specific shaders and draws into its own framebuffer. The basic workflow and interface is roughly the same for all stages, with only slight differences in how they manage their internal render state.\n\n\n### Namespaces:\n#### Level 1 renderer\n- `openage::renderer::opengl` - the OpenGL backend\n- `openage::renderer::vulkan` - the Vulkan backend\n- `openage::renderer::resources` - management of graphics assets\n- `openage::renderer::resources::parser` - parsers for openage media [metadata files](/doc/media/openage)\n\n##### Level 2 renderer\n- `openage::renderer::skybox` - the background rendering stage\n- `openage::renderer::terrain` - the terrain rendering stage\n- `openage::renderer::world` - the unit/building rendering stage\n- `openage::renderer::gui` - the GUI rendering stage\n- `openage::renderer::screen` - final compositing of all level 2 rendering results for display on screen\n\nEvery namespace is an actual directory and all its classes are contained there.\n"
  },
  {
    "path": "doc/code/renderer/demos.md",
    "content": "# Interactive Demos & Stresstests\n\nopenage builds contain several *interactive* renderer tech demo entrypoints that show of specific features.\nThese demos are also useful for learning the renderer API and for testing new functionality. In addition to\nthe demos, there are also stresstests that are used to test the performance of the renderer.\nThe source code for renderer demos and stresstests is located in [`libopenage/renderer/demo`](/libopenage/renderer/demo/).\n\nThis documents describes the purpose of each demo and contains instructions on how to interact with them.\n\n1. [Demos](#demos)\n   1. [Demo 0](#demo-0)\n\n## Demos\n\n### Demo 0\n\nThis demo shows the creation of a minmal renderer setup and the rendering of a simple mesh.\n\nThe demo initializes a GUI application, a window, a renderer object, and a render pass.\nIt then loads a shader program and creates a single mesh object which is then rendered to the screen\nusing the shader program.\n\nThe demo mostly follows the steps described in the [Level 1 Renderer - Basic Usage](level1.md#basic-usage)\ndocumentation.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 0\n```\n\n**Result:**\n\n![Demo 0](/doc/code/renderer/images/demo_0.png)\n\n\n### Demo 1\n\nThis demo shows how simple *textured* meshes can be created and rendered. It also demonstrates\nhow to interact with the window and the renderer using window callbacks.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 1\n```\n\n**Controls:**\n\n- <kbd>LMB</kbd>: Click on the textured meshes to get a debug message with the object ID.\n\n**Result:**\n\n![Demo 1](/doc/code/renderer/images/demo_1.png)\n\n\n### Demo 2\n\nIn this demo, we show how animation and texture metadata files are parsed and used to\nload and render the correct textures and animations for a mesh.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 2\n```\n\n**Controls:**\n\n- <kbd>LMB</kbd>: Click on the sprite to get a debug message with the object ID.\n- <kbd>←</kbd>: Go back one frame in the animation.\n- <kbd>→</kbd>: Advance the animation by one frame.\n\n**Result:**\n\n![Demo 2](/doc/code/renderer/images/demo_2.png)\n\n\n### Demo 3\n\nThis demo shows a minimal setup for the [Level 2 Renderer](level2.md) and how to render objects\nwith it. The demo also introduces the camera system and how to interact with it.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 3\n```\n\n**Controls:**\n\n- <kbd>W</kbd>, <kbd>A</kbd>, <kbd>S</kbd>, <kbd>D</kbd>: Move the camera in the scene.\n- `Mouse Wheel`: Zoom in and out.\n\n**Result:**\n\n![Demo 3](/doc/code/renderer/images/demo_3.png)\n\n\n### Demo 4\n\nThis demos shows how animation frame timing works and how to control the animation speed\nwith the engine's internal clock.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 4\n```\n\n**Controls:**\n\n- <kbd>Space</kbd>: Pause/resume the clock.\n- <kbd>+</kbd>, <kbd>-</kbd>: Increase/decrease the simulation speed.\n- <kbd>Return</kbd>: Toggle between real time and simulation time.\n\n**Result:**\n\n![Demo 4](/doc/code/renderer/images/demo_4.png)\n\n\n### Demo 5\n\nThis demo shows how to create [uniform buffers](level1.md#uniform-buffers) and how to use them to pass data to shaders.\nAdditionally, uniform buffer usage for the camera system is demonstrated.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 5\n```\n\n**Controls:**\n\n- <kbd>W</kbd>, <kbd>A</kbd>, <kbd>S</kbd>, <kbd>D</kbd>: Move the camera in the scene.\n\n**Result:**\n\n![Demo 5](/doc/code/renderer/images/demo_5.png)\n\n\n### Demo 6\n\nThis demo shows how to use [frustum culling](level2.md#frustum-culling) in the renderer.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 6\n```\n\n**Controls:**\n\n- <kbd>W</kbd>, <kbd>A</kbd>, <kbd>S</kbd>, <kbd>D</kbd>: Move the camera in the scene.\n\n**Result:**\n\n![Demo 6](/doc/code/renderer/images/demo_6.png)\n\n\n### Demo 7\n\nThis demo shows how to use [shader templating](level1.md#shader-templates) in the renderer.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_demo 7\n```\n\n**Result:**\n\n![Demo 7](/doc/code/renderer/images/demo_7.mp4)\n\n\n## Stresstests\n\n### Stresstest 0\n\nThis stresstest tests the performance when rendering an increasingly larger number of objects.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_stresstest 0\n```\n\n**Result:**\n\n![Stresstest 0](/doc/code/renderer/images/stresstest_0.png)\n\n### Stresstest 1\n\nThis stresstest tests the performance when [frustum culling](level2.md#frustum-culling) is enabled and an increasingly larger\nnumber of objects is rendered on the screen.\n\n```bash\ncd bin && ./run test --demo renderer.tests.renderer_stresstest 1\n```\n\n**Result:**\n\n![Stresstest 1](/doc/code/renderer/images/stresstest_1.png)\n"
  },
  {
    "path": "doc/code/renderer/level1.md",
    "content": "#  Level 1 Renderer\n\nLow-level renderer for communicating with the OpenGL and Vulkan APIs.\n\n## Overview\n\n1. [Overview](#overview)\n2. [Architecture](#architecture)\n3. [Basic Usage](#basic-usage)\n   1. [Window/Renderer Creation](#windowrenderer-creation)\n   2. [Adding a Shader Program](#adding-a-shader-program)\n   3. [Creating a Renderable](#creating-a-renderable)\n   4. [Rendering and Displaying the Result](#rendering-and-displaying-the-result)\n4. [Advanced Usage](#advanced-usage)\n   1. [Addressing Uniforms via numeric IDs](#addressing-uniforms-via-numeric-ids)\n   2. [Framebuffers / Multiple Render Passes](#framebuffers--multiple-render-passes)\n   3. [Defining Layers in a Render Pass](#defining-layers-in-a-render-pass)\n   4. [Complex Geometry](#complex-geometry)\n   5. [Uniform Buffers](#uniform-buffers)\n   6. [Shader Templates](#shader-templates)\n5. [Thread-safety](#thread-safety)\n\n\n## Architecture\n\nThe abstract interface can be vaguely categorized into 4 important components:\n\n1. Abstract interface\n1. OpenGL backend\n1. Vulkan backend\n1. Resources\n\nThe abstract interface of the level 1 renderer provides a unified interface to the low-level\nrendering capabilities of the GPU. It is independent of the specific backends, i.e. usage is the\nsame no matter which backend is selected at runtime. The level 2 renderer should always use\nthe abstract interface and should rarely, if ever, have to interact with the OpenGL/Vulkan backends\nthemselves. At runtime, the interface instantiates either an OpenGL or a Vulkan backend depending\non what graphics API is available.\n\nBoth the OpenGL and Vulkan backends are, for the most part, direct implementations of the abstract interface\nin their domain-specific APIs. We won't go into much detail about their features in this document,\nespecially since there are much better explanations of the concepts behind both [OpenGL](https://learnopengl.com/Introduction)\nand [Vulkan](https://vulkan-tutorial.com/Introduction) available.\n\nThe `resources` namespace provides classes for initializing and loading meshes, textures, shaders, etc.\nThese classes are independent from the specific OpenGL/Vulkan backends and can thus be passed to the\nabstract interface of the renderer renderer to make them usable with graphics hardware.\n\n## Basic Usage\n\nCode examples can be found in the [renderer demos](/libopenage/renderer/demo/).\nSee the [testing docs](/doc/code/testing.md#python-demos) on how to try them out.\n\n### Window/Renderer Creation\n\nTo create the renderer, we first have to create a window that the renderer can draw into. This should be done\nwith the static method `renderer::Window::create(..)` which also automatically determines whether the\nOpenGL or Vulkan backend should be used.\n\n```c++\nstd::shared_ptr<Window> window = Window::create(\"title\", 1024, 768);\n```\n\nBy default, OpenGL is chosen, so the created window object for the above example should have type\n`renderer::opengl::GlWindow`. The specific type should never be relevant for usage, however, since\nall interaction can be done via the abstract interface.\n\nThe created window can be used to add and initialize a renderer, returning a pointer to the\nabstract `renderer::Renderer` interface.\n\n```c++\nstd::shared_ptr<Renderer> renderer = window->make_renderer();\n```\n\nBehind the scene, this again is initialized as a specific type determined by the type of the\nwindow implementation behind the scenes. I.e., a `GlWindow` will make a `GlRenderer` when\ncalling this method.\n\n\n### Adding a Shader Program\n\nShaders can be added in two steps. First, we have to load the shader source code from a\nstring or a file. To do this, we can use the `renderer::resources::ShaderSource` class as\nseen below.\n\n```c++\nresources::ShaderSource vshader_src = resources::ShaderSource(\n    resources::shader_lang_t::glsl,\n    resources::shader_stage_t::vertex,\n    \"#version 330\\nvoid main() {}\"\n);\n\nutil::Path shader_path = root_dir / \"assets\" / \"shaders\";\nresources::ShaderSource fshader_src = resources::ShaderSource(\n    resources::shader_lang_t::glsl,\n    resources::shader_stage_t::fragment,\n    shader_path / \"source.frag\"\n);\n```\n\nAfterwards, we can create the shader program from the shader sources by passing them to\nthe `add_shader()` method of our renderer instantiation.\n\n```c++\nstd::shared_ptr<ShaderProgram> shader_prog = renderer->add_shader( { vshader_src, fshader_src } );\n```\n\nopenage's `ShaderProgram` encapsulates all shader units that should be run in one iteration\nof the OpenGL/Vulkan graphics pipeline. Therefore, we have to supply at least a vertex shader\nand a fragment shader source, since these stages are mandatory in both the OpenGL and the\nVulkan graphics pipeline.\n\n\n### Creating a Renderable\n\nTo use our shader program, we have to define something that it can operate on. These \"somethings\"\nare called *renderables* in openage and basically represent an object that we want to draw on screen.\nCreating a `renderer::Renderable` object only requires the definition of two parameters:\n\n1. Vertex inputs for the vertex shader stage\n1. Uniform inputs for any defined uniforms in the shader stages of the shader program\n\nThe renderer provides a method to create a simple 4-vertex quad that spans the entire\nviewport. Doing this creates a `renderer::Geometry` that also manages the underlying buffers\non the GPU and the associated vertex data:\n\n```c++\nstd::shared_ptr<Geometry> geom = renderer->add_bufferless_quad();\n```\n\nBufferless quads are usually enough to draw anything rectangular, e.g. sprites. However,\nthe renderer is also able to handle more complex geometry with changing vertex data\nwhich is described in [this section](#complex-geometry).\n\nUniform inputs are created from the shader program that they are defined in by calling\n`renderer::ShaderProgram::new_uniform_input(..)` on it. This creates a new `UniformInput` object\nthat will store the unique uniform input values for the renderable that we want to display.\nUsually, a new `UniformInput` object should be created for each renderable.\n\n```c++\nstd::shared_ptr<UniformInput> input = shader_prog->new_uniform_input(\n  \"color\", Eigen::Vector3f{ 0.0f, 1.0f, 0.0f },\n  \"time\", 0.0f,\n  \"num\", 1337\n);\n```\n\nNote that the definition order doesn't matter and the method doesn't differentiate\nbetween different shader stages, so uniform inputs for vertex and fragment shaders\ncan be freely mixed.\n\nInput values are passed to the method in pairs consisting of the uniform ID and the\ninput value. Uniform IDs can either be the uniform name from the shader source (as\nshown above) or a numeric ID that is determined at load time by the shader program.\nNumeric ID usage is explained in [this section](#addressing-uniforms-via-numeric-ids).\n\nUniform input values are automatically converted to the correct types expected by the uniform\ndefinition, e.g. a `uint8_t` for a uniform with type `uint` will be transformed to the\ncorrect type.\n\nAfter creating a `renderer::UniformInput`, it can be updated at any time, e.g. when preparing\nthe next frame:\n\n```c++\ninput->update(\n  \"condition\", false\n);\n```\n\nFrom the geometry and the uniform input objects, we can finally create the `renderer::Renderable`\nobject that we want to display.\n\n```c++\nRenderable obj {\n  input,\n  geom\n};\n```\n\n### Rendering and Displaying the Result\n\nGraphics operations using a shader program are executed by organizing renderables in a *render pass*.\nRender passes render multiple objects into a single display target, e.g. the application window.\n\nCreating a render pass from the renderer only requires passing a list of renderables and the\ndisplay target that should be used. The window the renderer was created from is the default\ndisplay target and can be acquired by calling `renderer::Renderer::get_display_target()`.\n\n```c++\nstd::shared_ptr<RenderPass> pass =  renderer->add_render_pass({ obj }, renderer->get_display_target())\n```\n\nRender passes can also be updated with new renderables:\n\n```c++\npass->add_renderables({ obj });\n```\n\nFinally, we can execute the rendering pipeline for all objects in the render pass:\n\n```c++\nrenderer->render(pass);\n```\n\n<!-- TODO: We have deactivated this behaviour for now\nBefore rendering, the render pass optimizes the order in which renderables are rendered to\nminimize state changes on the GPU and save computation time. For example, the render pass\nsorts the renderables by shader program, since changing them is an expensive operation.\n-->\n\nAfter rendering is finished, the window has to be updated to display the rendered result.\n\n```c++\nwindow->update();\n```\n\n## Advanced Usage\n\nThese are some of the more advanced features of the renderer.\n\n### Addressing Uniforms via numeric IDs\n\nNumeric uniform IDs are unique identifiers for a uniform in a shader program. They are\nassigned at load time and can be used to address uniforms instead of their string names.\nThe type used for numeric IDs is `renderer::uniform_id_t`. The numeric ID of a uniform\ncan be fetched from the shader program using the uniform name by calling the\n`renderer::ShaderProgram::get_uniform_id(..)` method.\n\n```c++\nuniform_id_t color_id = shader_prog->get_uniform_id(\"color\");\nuniform_id_t time_id = shader_prog->get_uniform_id(\"time\");\nuniform_id_t num_id = shader_prog->get_uniform_id(\"num\");\nstd::shared_ptr<UniformInput> input = shader_prog->new_uniform_input(\n  color_id, Eigen::Vector3f{ 0.0f, 1.0f, 0.0f },\n  time_id, 0.0f,\n  num_id, 1337\n);\n```\n\nSetting uniform values via numeric IDs can be much faster than using strings as\nstring lookups are avoided. This is especially useful for uniforms which are updated\nvery frequently, e.g. every frame. However, this requires that the IDs are fetched\nat runtime and have to be stored somewhere.\n\n\n### Framebuffers / Multiple Render Passes\n\nSometimes it is useful to render the scene in multiple passes, e.g. for post-processing\nof rendered objects or simply for organizing different rendering stages. To do this,\na render pass can be instructed to render into an intermediary texture attached to a\nframebuffer.\n\n```c++\nstd::shared_ptr<Window> window = Window::create(\"title\", 1024, 768);\nstd::shared_ptr<Renderer> renderer = window->make_renderer();\n\n... // shader program initialization\n\nstd::shared_ptr<Geometry> geom = renderer->add_bufferless_quad();\nstd::shared_ptr<UniformInput> input1 = shader_prog->new_uniform_input();\nRenderable obj1{input1, geom};\n\nstd::shared_ptr<Texture2d> color_texture = renderer->add_texture(\n  resources::Texture2dInfo(\n    1024,\n    768,\n    resources::pixel_format::rgba8\n  )\n);\nstd::shared_ptr<RenderTarget> target = renderer->create_texture_target({ color_texture });\nstd::shared_ptr<RenderPass> pass1 = renderer->add_render_pass({ obj1 }, target);\n```\n\nThe color texture assigned as the display target for the render pass can be assigned\nas a uniform input value in subsequent render passes.\n\n```c++\nstd::shared_ptr<UniformInput> input2 = shader_prog->new_uniform_input(\n  \"tex\", texture\n);\nRenderable obj2{input2, geom};\nstd::shared_ptr<RenderPass> pass2 = renderer->add_render_pass({ obj2 }, renderer->get_display_target());\n```\n\nA color texture is not the only type of texture that can be assigned to a texture\ntarget. We can also add depth textures or additional (color) textures that the shader\ncan write arbritrary values into.\n\n```c++\nstd::shared_ptr<Texture2d> depth_texture = renderer->add_texture(\n  resources::Texture2dInfo(\n    1024,\n    768,\n    resources::pixel_format::depth24 // 24 Bit depth values\n  )\n);\nstd::shared_ptr<Texture2d> id_texture = renderer->add_texture(\n  resources::Texture2dInfo(\n    1024,\n    768,\n    resources::pixel_format::r32ui // unsigned integer\n  )\n);\nstd::shared_ptr<RenderTarget> target = renderer->create_texture_target({color_texture, depth_texture, id_texture});\nstd::shared_ptr<RenderPass> pass = renderer->add_render_pass({ obj }, target);\n```\n\nAttaching a depth texture is required for enabling optional depth testing for a renderable.\nDepth testing can be activated per renderable:\n\n```c++\nRenderable obj {\n  input,\n  geom\n};\nobj.depth_test = true;\n```\n\n\n### Defining Layers in a Render Pass\n\nLayers give more fine-grained control over the draw order of renderables in a render pass. Every\nlayer has a priority that determines when associated renderables are drawn. Lower priority\nrenderables are drawn earlier, higher priority renderables are drawn later.\n\nIn comparison to using multiple render passes, layers do not require the (expensive) switching\nof framebuffers between passes. The tradeoff is a slight overhead when inserting new renderables\ninto the render pass.\n\nTo assign renderables to a layer, we have to specify the priority in the `RenderPass::add_renderables(..)`\nfunction call.\n\n```c++\nRenderable obj {\n  input,\n  geom\n};\npass->add_renderables({ obj }, 42);\n```\n\nFor existing layers, new renderables are always appended to the end of the layer. Renderables\nare sorted into the correct position automatically when they are added:\n\n```c++\npass->add_renderables({ obj1, obj2, obj3 }, 42);\npass->add_renderables({ obj4 }, 0);\npass->add_renderables({ obj5, obj6 }, 1337);\npass->add_renderables({ obj7 }, 0);\n// draw order: obj4, obj7, obj1, obj2, obj3, obj5, obj6\n// layers:     prio 0    | prio 42         | prio 1337\n```\n\nWhen no priority is specified when calling `RenderPass::add_renderables(..)`, the highest\npriority is assumed (which is `std::numeric_limits<int64_t>::max()`). Therefore,\nobjects added like this are always drawn last. It also means that these two calls are equal:\n\n```c++\npass->add_renderables({ obj });\npass->add_renderables({ obj }, std::numeric_limits<int64_t>::max());\n```\n\n\nLayers are created lazily during insertion if no layer with the specified priority exists yet.\nWe can also create layers explicitly for a specific priority:\n\n```c++\npass->add_layer(42);\n```\n\nWhen executing the rendering pipeline for a specific pass, renderables are drawn layer by layer.\nBy default, the renderer clears the depth buffer when switching to a new layer. This is done\nunder the assumption that layers with higher priority should always draw over layers with lower\npriority, even when depth tests are active. This behavior can be deactivated when explicitly\ncreating a layer:\n\n```c++\n// keep depth testing\npass->add_layer(42, false);\n```\n\n\n### Complex Geometry\n\nFor displaying complex geometry like 3D objects or non-rectangular surfaces, the renderer\nallows the definition meshes that can be configured down to the individual vertex information.\n\nConsider the vertices used for the creation of a bufferless quad which essentially is a textured\nrectangle. Every vertex has to store its position as a 2D coordinate as well as its associated\ntexture coordinates (also 2D). Therefore, we need 4 coordinates for each vertex, so 4 vertices\nresult in 16 coordinates in total.\n\n```c++\nstd::array<float, 16> verts = {\n\t{\n\t\t-1.0f, 1.0f, 0.0f, 1.0f,  // top left\n\t\t-1.0f, -1.0f, 0.0f, 0.0f, // bottom left\n\t\t1.0f, 1.0f, 1.0f, 1.0f,   // top right\n\t\t1.0f, -1.0f, 1.0f, 0.0f   // bottom right\n\t}\n};\n```\n\nWe also need to define the layout in a `resources::VertexInputInfo` struct so that the\nrenderer can properly initialize the underlying vertex buffer.\n\n```c++\nresources::VertexInputInfo info{\n  { resources::vertex_input_t::V2F32, resources::vertex_input_t::V2F32 },\n  resources::vertex_layout_t::AOS,\n  resources::vertex_primitive_t::TRIANGLE_STRIP\n};\n```\n\nAs seen above, we have to define 3 parameters.\n\n1. **Vertex Input Layout**: Defines how the vertex data of each vertex is split up in the vertex shader. In this case, a vertex consists of a `vec2` for the position and a `vec2` for the texture coordinates.\n1. **Vertex Buffer Layout**: Defines how the vertex data of all vertices is layed out in the whole buffer. `AOS` is *array of structs* which means that vertex data is interleaved.\n1. **Vertex Primitive**: Type of primitive used for drawing the vertices.\n\nAfterwards, vertices can be copied into a byte array which is then passed\nalongside the vertex info to the `resources::MeshData` constructor.\n\n```c++\nauto const vert_data_size = verts.size() * sizeof(float);\nstd::vector<uint8_t> vert_data(vert_data_size);\nstd::memcpy(vert_data.data(), reinterpret_cast<const uint8_t *>(verts.data()), vert_data_size);\n\nresources::MeshData mesh{ std::move(vert_data), info };\n```\n\nThe resulting mesh can then be used to create a `renderer::Geometry` object from the\nrenderer. This will also create the vertex buffer on the GPU.\n\n```c++\nstd::shared_ptr<Geometry> geom = renderer->add_mesh_geometry(mesh);\n```\n\nIn addition to regular vertex meshes, the renderer also supports indexed rendering with an\nindex buffer. This is useful in scenarios where vertices get revisited very often, e.g. in\ndense 3D meshes.\n\n```c++\nstd::array<uint16_t, 16> idxs { 0, 1, 2, 1, 3, 4, 1 };\n```\n\nWhen indexed rendering should be used, we have to pass one additional parameter to the\n`resources::VertexInputInfo` struct that specifies the layout of the index buffer:\n\n```c++\nresources::VertexInputInfo info{\n  { resources::vertex_input_t::V2F32, resources::vertex_input_t::V2F32 },\n  resources::vertex_layout_t::AOS,\n  resources::vertex_primitive_t::TRIANGLES,\n  resources::index_t::U16                   // index size -> 16 Bit unsigned integer\n};\n```\n\nFurthermore, we have to copy the indices into a byte array and pass it to the mesh\nconstructor.\n\n```c++\nauto const idx_data_size = idxs.size() * sizeof(uint16_t);\nstd::vector<uint8_t> idx_data(idx_data_size);\nstd::memcpy(idx_data.data(), reinterpret_cast<const uint8_t *>(idxs.data()), idx_data_size);\n\nresources::MeshData mesh{ std::move(vert_data), std::move(idx_data), info };\n```\n\n### Uniform Buffers\n\nUniform buffers provide a storage- and performance-efficient way to pass uniform\ninput values to multiple shader programs. Uniform inputs inside a uniform buffer are de-facto global\nvariables that can be accessed by any shader. They are best used for uniform values that\nchange infrequently or are the same across many shader iterations. A good use case example\nare camera matrices which are at most updated once per frame and may be used in different\nshader programs.\n\nThere are two ways the openage renderer can create uniform buffers. Option 1 is to\ncreate the uniform buffer from a named uniform block in an already loaded shader program, which is\ndiscussed below. Option 2 creates the uniform buffer from a `renderer::resources::UniformBufferInfo`\nobject which manually specifies the uniforms and layout of the buffer.\n\n```c++\nstd::shared_ptr<ShaderProgram> shader_prog = renderer->add_shader( { vshader_src, fshader_src } );\nstd::shared_ptr<UniformBuffer> buffer = renderer->add_uniform_buffer(shader_prog, \"unif_block\");\n```\n\nTo make the shader actually use the buffer, the shader's uniform block has to be bound to the\nbuffer first. This tells the GPU to fetch data for the uniform block from the uniform buffer\nduring a shader iteration.\n\n```c++\nshader_prog->bind_uniform_buffer(\"unif_block\", buffer);\n```\n\nSetting uniform input values in the buffer works very similar to regular uniform inputs for shaders.\nInstead of fetching a new uniform input object from a shader, we fetch it from the buffer object\nwe created.\n\n```c++\nstd::shared_ptr<UniformBufferInput> buff_input = buffer->new_uniform_input(\n  \"color\", Eigen::Vector3f{ 0.0f, 1.0f, 0.0f },\n  \"time\", 0.0f,\n  \"num\", 1337\n);\n```\n\nThis creates a `renderer::UniformBufferInput` that can be updated like their counterparts for regular\nuniform inputs.\n\n```c++\nbuff_input->update(\n  \"condition\", false\n);\n```\n\nAn additional step is required to upload the input values to the GPU. For regular uniform inputs, this\nis done automatically for each renderable in a render pass. Since uniform updates are usually updated\nmuch less frequently or irregularly, the buffer has to be manually requested to transfer the input\nvalues to the buffer on the GPU:\n\n```c++\nbuffer->update_uniforms(buff_input);\n```\n\nInstead of creating the uniform buffer from an existing shader program, in some cases it can be beneficial\nto manually define the buffer. This is particularly useful in scenarios where a shader program is not loaded\nyet or if the uniform buffer is bound to multiple different shader programs.\n\nManually defining the buffer requires you to specify the uniform block layout as well as the name and input\ntype of each uniform in the buffer. From these definitions, a `renderer::resources::UniformBufferInfo`\nobject can be initialized, which can then be passed to the renderer to create the buffer.\n\n```c++\nresources::UBOInput view_input{ \"view\", resources::ubo_input_t::M4F32 };\nresources::UBOInput proj_input{ \"proj\", resources::ubo_input_t::M4F32 };\n\nresources::UniformBufferInfo ubo_info{\n  resources::ubo_layout_t::STD140,\n  { view_input, proj_input }\n};\n\nstd::shared_ptr<UniformBuffer> buffer = renderer->add_uniform_buffer(ubo_info);\n```\n\n### Shader Templates\n\nShader templates allow the definition of a shader source code with placeholders that can be replaced at\nload- or run-time. Templates are help if only specific parts of the shader code are supposed to\nbe configurable. This may be used, for example, to alter certain code path of the built-in shaders\nwithout recompiling the engine.\n\nAn example shader template looks like this:\n\n```glsl\n#version 330\n\nin vec2 tex_coord;\nout vec4 frag_color;\n\n// PLACEHOLDER: uniform_color\n\nvoid main() {\n\t// PLACEHOLDER: alpha\n\tfrag_color = vec4(col.xyz, alpha);\n}\n```\n\nPlaceholders are inserted as comments into the GLSL source. Every placeholder has an ID for\nreferencing. For these IDs, *snippets* can be defined to insert code in place of the placeholder\ncomment. Placeholders can be placed at any position in the template, so they can be used to insert\nother statements than the control code, including unforms, input/output variables, functions and more.\n\nThe snippets for the above example may look like this:\n\n`uniform_color.snippet`\n\n```glsl\nuniform vec4 col;\n```\n\n`alpha.snippet`\n\n```glsl\nfloat alpha = 0.5;\n```\n\nIn the renderer, shader templates can be created using the `renderer::resources::ShaderTemplate` class.\n\n```cpp\nutil::Path template_path = shaderdir / \"example_template.frag.glsl\";\nresources::ShaderTemplate template(template_path);\n```\n\nAfter loading the template, snippets for the placeholders can be added. These are either loaded\nas single files or from a directory.\n\n```cpp\nutil::Path snippets_path = shaderdir / \"example_snippets\";\ntemplate.load_snippets(snippets_path);\n```\n\nor\n\n```cpp\nutil::Path snippets_path = shaderdir / \"example_snippets\";\ntemplate.add_snippet(snippets_path / \"uniform_color.snippet\");\ntemplate.add_snippet(snippets_path / \"alpha.snippet\");\n```\n\nIf snippets have been loaded for all placeholder IDs, the shader template can generate the final\nshader source code as `renderer::resources::ShaderSource`. This can then be used to create the actual\n`renderer::ShaderProgram` uploaded to the GPU.\n\n```cpp\nresources::ShaderSource source = template.generate_source();\nShaderProgram prog = = renderer->add_shader({source});\n```\n\n\n## Thread-safety\nThis level might or might not be threadsafe depending on the concrete backend. The OpenGL version is, in typical GL fashion, so not-threadsafe it's almost anti-threadsafe. All code must be executed sequentially on a dedicated window thread, the same one on which the window and renderer were initially created. The plan for the Vulkan version is to make it at least independent of thread-local storage and hopefully completely threadsafe.\n"
  },
  {
    "path": "doc/code/renderer/level2.md",
    "content": "# Level 2\n\nHigh-level renderer for transforming data from the gamestate to render objects for the level 1 renderer.\n\n## Overview\n\n1. [Overview](#overview)\n2. [Stages](#stages)\n   1. [Updating Render Stages from the Gamestate](#updating-render-stages-from-the-gamestate)\n3. [Camera](#camera)\n   1. [Frustum Culling](#frustum-culling)\n\n## Stages\n\nEvery stage has its own subrenderer that manages a `RenderPass` from the level 1 renderer and updates it with `Renderable`s created using update information from the gamestate. Stages also store the vertex and fragment shaders used for drawing the renderable objects.\n\nThere are currently 6 stages in the level 2 rendering pipeline:\n\n1. `SkyboxRenderer`: Draws the background behind the terrain (as a single color).\n1. `TerrainRenderer`: Draws the terrain. Terrains are handled as textured 3D meshes.\n1. `WorldRenderer`: Draws animations and sprites for units/buildings and other 2D ingame objects.\n1. `HudRenderer`: Draws \"Head-Up Display\" elements like health bars, selection boxes, and others.\n1. `GuiRenderer`: Draws the GUI overlay. The drawing part in this stage is actually done by Qt, while the level 1 renderer only provides the framebuffer.\n1. `ScreenRenderer`: Alpha composites the framebuffer data of previous stages and draws them onto the screen (i.e. it overlays the outputs from the other stages).\n\nWith the exception of the `ScreenRenderer`, all stages are independent from each other, i.e. the order in which they are rendered each frame does not matter. To render the current frame for a stage, call its `update()` method. The `ScreenRenderer` must be rendered last as it combines the output of all other stages into the result shown on screen.\n\n### Updating Render Stages from the Gamestate\n\nGamestate (game simulation) and level 2 rendering are largely decoupled, i.e. communication between these components only happens via a few dedicated connection interfaces in the codebase. Furthermore, the information flow between gamestate and renderer is strictly one-way (always gamestate -> renderer). In practice, this means that the gamestate constantly updates the level 2 renderer on what it should display. The renderer only acts on what it receives from the gamestate and does not actively request or send information to the gamestate. We mainly do this for thread-safety reasons as gamestate and renderer live in two different threads at runtime.\n\nThere are two render stages that can receive updates from the gamestate: `WorldRenderer` and `TerrainRenderer`. You can see how updates are propagated from the gamestate to these stages here:\n\n| **WorldRenderer**                           | **TerrainRenderer**                             |\n| ------------------------------------------- | ----------------------------------------------- |\n| ![world drawing](images/world_renderer.svg) | ![terrain drawing](images/terrain_renderer.svg) |\n\nAs you can see, the workflow for both render stages is roughly the same and involves these components:\n  - **game entity**: Object in the game world managed by the gamestate, e.g. a unit or a building.\n  - **render entity**: Connector object between game entity and the render stage. The game entity sends updates to the render entity to communicate what should be displyed. Furthermore, the render entity transforms said updates into something usable by the renderer, e.g. loading the texture for a texture ID. Render entities also contain the critical path between the gamestate and renderer thread boundaries.\n  - **render object(s)**: Store the current render state for a `Renderable` created for a render entity, e.g. uniforms, meshes or IDs. There can be a variable number of these objects depending on the complexity of the render state. For example, the `TerrainRenderer` stage has separate objects for the mesh (`TerrainMesh`) and model (`TerrainModel`) for its terrain. Render objects poll the render entity for updates every frame and may change the render state when updated.\n  - **(sub-)renderer**: Manages all render objects for the render stage, i.e. by polling them for updates and creating level 1 `Renderable`s from their render state which are added to the stage's render pass. New render entities are also registered at the subrenderer. Upon registration, the subrenderer creates the necessary render objects and attaches them to the render entity.\n  - **render factory**: Factory in the gamestate for creating and registering new render entities. Game entities can request render entities from the factory, which then also registers the render entity at the corresponding subrenderer. The render factory lives in the gamestate thread, but crosses thread boundaries when registering new render entities. Therefore, it is also part of the critical path.\n\n## Camera\n\nWhat parts of the scene is shown on screen is controlled by the `Camera` class. The camera is handled like an object in the rendered 3D scene that determines what is displayed depending on its position, zoom level and angle. Position and zoom level of the camera can be changed at runtime, while the angle is fixed to the dimetric/isometric view used in Age of Empires games. More precisely, the camera has a yaw of `-135` degrees and a pitch of `-30` degrees (pointed in the `(-x, -y, -z)` direction in the OpenGL coordinate system). The projection method used by the camera is orthographic projection.\n\nThe `Camera` class provides the following methods for positioning the camera in the scene:\n\n- `move_to(...)`: Move to a specific scene position.\n- `move_rel(...)`: Move relative to the position using a direction vector. This is usually used to move the camera with key presses or mouse movements.\n- `look_at_scene(...)`: Point the camera on a position in the scene. This will move the camera in such a way that the target position is in the center of the viewport.\n- `look_at_coord(...)`: Same as `look_at_scene(...)` but using openage's `coord::scene3` coordinates as input.\n\nZoom levels can also be adjusted with these methods:\n\n- `set_zoom(...)`: Set the zoom level to an absolute value. Values <1.0f zoom the camera in, while values >1.0f zoom the camera out.\n- `zoom_in(...)`: Let the camera incrementally zoom in.\n- `zoom_out(...)`: Let the camera incrementally zoom out.\n\nFor displaying 3D objects, the `Camera` can also calculate a view matrix (`get_view_matrix()`) and projection matrix (`get_projection_matrix()`) that take current position and zoom level into account.\n\nCamera parameters may be used for raycasting operations, e.g. mouse picking/selection. Since the camera utilizes orthographic projection and a fied angle, the ray direction is exactly the same as the camera direction vector (accessible as `cam_direction`). To find the origin point of a ray for a pixel coordinate in the viewport, the `get_input_pos(..)` method can be used. This method calculates the position of the pixel coordinate on the othographic camera plane that represents the viewport. The result is the absolute position of the pixel coordinate inside the 3D scene. Ray origin point and direction can then be used to perform calculations for line-plane or line sphere intersections.\n\n### Frustum Culling\n\nFrustum culling is a technique used to discard objects that are outside the view frustum of the camera.\nThis can save a lot of computation time that would be spent on updating shaders and rendering objects\nthat are not visible in the camera's view.\n\nThe openage renderer provides two frustum types: 2D and 3D frustums. 2D frustums are used\nfor sprite animations and other 2D objects, while 3D frustums are used for 3D objects. Both frustums\ncan be created from a camera object.\n\n```c++\nstd::shared_ptr<Camera> camera = std::make_shared<Camera>(renderer, {800, 600});\n\nFrustum2d frustum_2d = camera->get_frustum_2d();\nFrustum3d frustum_3d = camera->get_frustum_3d();\n```\n\n`Frustum2d` and `Frustum3d` provide a method `is_visible(..)` that can be used to check if an object is\nlocated inside the frustum. The required inputs differ depending on the frustum type. For 3D frustums,\nonly the 3D scene position is required:\n\n```c++\nbool is_visible = frustum_3d.is_visible({0.f, 0.f, 0.f});\n```\n\nFor 2D frustums, in addition to the 3D scene position, a model matrix, the animation's scalefactor\nas well as the bounding box of the animation must be provided.\n\n```c++\nbool is_visible = frustum_2d.is_visible(\n  {0.f, 0.f, 0.f},\n  model_matrix, // the model matrix of the animation\n  scalefactor, // how much the animation is scaled\n  {10, 20, 50, 10} // max distance from the center to the edges of the bounding box\n);\n```\n"
  },
  {
    "path": "doc/code/testing.md",
    "content": "# Test system\n\n## openage computer-enriched testing automation\n\n### Tests\n\nThere are various supported kinds of tests:\n\n - cpp tests\n - py tests\n - py doctests\n\nTests run without user interaction to check for errors automatically.\n\nAll tests are run automatically by [Kevin](https://github.com/SFTtech/kevin/) for pullrequests.\n\n\nYou can invoke them with `bin/run test -a` or `make test`\n\nHave a look at `bin/run test --help` for further options.\n\n\nYou are encouraged to write tests for all your contributions, as well as other components that currently lack testing.\n\n\n### Demos\n\nIn addition to testing, openage supports _demos_:\n\n - cpp demos\n - py demos\n\nAs opposed to tests, demos are run manually and individually by the user.\nThey usually produce lots of output on stdout or may even be interactive. Python demos even accept an `argv` parameter.\n\nAll tests must be registered in `openage/testing/testlist.py` (else the game won't know about them).\n\nAlso see `bin/run test --help`.\n\n## Adding new tests\n\n### C++ tests\n\nC++ tests are simple `void()` functions somewhere in the `openage` namespace.\n\nThey shall return on success, and raise `openage::testing::TestError` on failure.\n\nThey shall not be declared in a header file; instead, add them to `openage/testing/testlist.py`.\n\nThe header `libopenage/testing/testing.h` provides `TestError` and some convenience macros:\n\n - `TESTFAIL`\n    throws `TestError`\n - `TESTFAILMSG(\"a\" << \"b\")`\n    throws `TestError(\"ab\")`\n - `TESTEQUALS(left, right)`\n    evaluates `left` and `right`\n    throws `TestError(left)` if `left != right`\n    throws `TestError(exc)` if a non-`TestError` exception `exc` is raised.\n - `TESTTHROWS(expr)`\n    evaluates `expr`, catching any exception, including `TestError`.\n    raises `TestError` if no exception was caught.\n\nExample test function:\n\n``` cpp\nvoid test_prime() {\n    is_prime(23) or TESTFAIL;\n    is_prime(42) and TESTFAIL;\n}\n```\n\n\n### Python tests\n\nPython tests are simple argument-less functions somewhere in the `openage` package.\n\nThey shall return `None` on success, and raise `openage.testing.testing.TestError` on failure.\n\nAdd their names to `openage/testing/testlist.py`.\n\nThe module `openage.testing.testing` provides `TestError` and some convenience functions:\n\n - `assert_value(<expr>, expected)`\n    checks whether expr == expected, and raises `TestError` if not.\n - `assert_raises(expected_exception_type)`\n    a context guard that verifies that the named exception occurs inside;\n    consult the example in `openage/testing/testing.py`.\n\nYou may define tests in `.pyx` files.\n\nExample test function:\n\n``` python\ndef test_prime():\n    assert_value(is_prime(23), True)\n    assert_value(is_prime(42), False)\n\n    with assert_raises(ValueError):\n        result(is_prime(-1337))\n```\n\n\n### Python doctests\n\n[Doctests](https://docs.python.org/3/library/doctest.html) are an integrated feature of Python.\n\nThey defined in function and module docstrings, are extremely lightweight and also serve as documentation.\n\nSimply add the name of a Python module to `openage/testing/testlist.py`, and all doctests in that module will run.\n\nExample doctest for a function:\n\n``` python\ndef is_prime(p):\n    \"\"\"\n    High-performance, state-of-the-art primality tester.\n\n    >>> is_prime(23)\n    True\n    >>> is_prime(42)\n    False\n    \"\"\"\n    return not any(p % x == 0 for x in range(2, p))\n```\n\n### C++ demos\n\nTechnically, those are very much like `C++` tests. In fact, the only difference to tests is the section in `openage/testing/testlist.py` where they are declared.\n\nC++ demos don't support `argv`; if you want that, make it a Python demo in a `.pyx` file and do the argparsing in Python; the Python demo function can then easily call any C++ function using the Python interface.\n\n\n### Python demos\n\nSimilar to Python tests, but have one argument, `argv`. Pass arguments in the invocation:\n\n    bin/run test -d prime_demo 100\n\nExample demo:\n\n``` python\ndef prime_demo(argv):\n    import argparse\n    cli = argparse.ArgumentParser()\n    cli.add_argument('max_number', type=int)\n    args = cli.parse_args(argv)\n\n    for p in range(2, args.max_number):\n        if is_prime(p):\n            print(p)\n```\n\n\n## Why?\n\n### Demos\n\nDemos should be used to implement and develop new features.\nYou can directly call your code without having to launch up the whole engine.\n\nUse demos while developing new things or improvements.\n\n\n### Tests\n\nAll tests are run for each pull requests, so we can detect your change broke something.\nPlease try to write tests for everything you do.\n\nThis is our only way of automated regression detection.\n"
  },
  {
    "path": "doc/code/time.md",
    "content": "# Simulation Time\n\n*Simulation time* is the driving force behind pretty much all internal routines of the engine.\nIts used to schedule and execute events in the [event system](event_system.md),\nmanaging data of game entities at runtime via [curves](curves.md), and time\nanimations in the [renderer](/doc/code/renderer/level2.md).\n\nTo keep track of and synchronize time between different engine components, openage implements\na low-level time control interface in the `openage::time` namespace. This system is very simple:\nBasically, it consists of a loop in a dedicated thread that constantly updates an internal clock.\n\n![time loop UML](images/time_classes_uml.svg)\n\n`TimeLoop` provides a basic interface to create an internal clock and run the update loop. Once\ninitialized, the `Clock` instance can be accessed with the `get_clock()` method. For most calculations,\nthe clock object is sufficient. Access to the time loop is only necessary if you want to\npause/stop the entire simulation.\n\nopenage represents time with the internal type `time::time_t` (which must not be confused with the C standard\ntype `time_t`). `time::time_t` is a 64 bit signed fixed-point value (scaling factor 16)\nwhich represents the time passed in **seconds**, i.e. a value of `1.25` denotes 1.25 seconds or\n1250 milliseconds, respectively.\n\n`Clock` manages the current simulation time using the system clock. It also allows the configuration\nof a simulation speed (which can be negative). Time is advanced in each\nloop iteration of `TimeLoop` by calling the clock object's `update_time()` method. This method\nfirst calculates the real time difference between the previous and current update call. The result\nis then multiplied by the simulation speed and added to the total simulation time. Additionally, `Clock`\nkeeps track of how much real time has passed, regardless of simulation speed. Real time is used\nin the renderer to time animation frames which should not affected by simulation speed.\n\nThe time resolution used for the system clock is **milliseconds**. When requesting the current time\nfrom a clock via `get_time()` or `get_real_time()`, this gets converted into the openage `time::time_t`\nformat.\n\nTo prevent the clock updates from advancing too far if the program is stopped by the operating system\nor a debugger, `update_time()` sets a maximum limit on how much real time can pass between two update\ncalls. This limit is currently hardcoded to 50ms. Similarly, there is a minimum limit of 1ms to\nprevent the time loop thread from stalling.\n"
  },
  {
    "path": "doc/contributing.md",
    "content": "Contributing to openage\n=======================\n\n**\"Can I even help?\"**\n\nYou can't do anything wrong. Except not doing anything.\n\nThere is no wrongdoing, just wrong restraint.\n\n\nWhat can I do?\n--------------\n\n**\"Help - I want to contribute something, but I don't know what?\"**\n\nYou're in luck. There's various sources for tasks:\n\n - Have a look at the [issue tracker](https://github.com/sfttech/openage/issues), especially issues with labels:\n   - [good first issue](https://github.com/SFTtech/openage/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)!\n   - [just do it](https://github.com/SFTtech/openage/issues?q=is%3Aissue+is%3Aopen+label%3A%22just+do+it%22)!\n - Use [your favorite code searching tool](https://github.com/ggreer/the_silver_searcher) to find `TODO` messages!\n - The [projects](https://github.com/SFTtech/openage/projects) page for issues listed by engine core component!\n - [Ask us](/README.md#contact)!\n\n\nWhat's the concept?\n-------------------\n\nWe basically do whatever we think is good to do.\n\nThe [openage architecture](/doc/code/architecture.md) is the foundation.\nFor our development concept, see the [development guide](/doc/development.md).\n\nIf you need some inspiration [what to work on, see above](#what-can-i-do).\n\n\n\n**Please at least skim over this whole file if you want to contribute some code to *openage**!\n\n\n\ntl;dr\n-----\n\n- Have a [GitHub](https://github.com) account\n- Click the [fork button](https://github.com/SFTtech/openage)\n- `git clone git@github.com:YourAccount/openage.git`\n- `cd openage`\n- `git remote add upstream https://github.com/SFTtech/openage.git`\n- `git checkout -b tentacle-monster-fix`\n- Edit file, adhere to [coding style](/doc/code_style)!\n- Add yourself to `copying.md`\n- `git add libopenage/unit/tentacle_monster.cpp`\n- `git commit -m \"engine: fixed vomiting animation of tentacle monster\"`\n- `make checkmerge`\n- `make test`\n- `git push origin tentacle-monster-fix`\n- Create a pull request and look at the CI output\n\n- Watch [cat pictures](https://www.flickr.com/search/?text=cat) while waiting for feedback\n\n\nRead on below if you need more detailed instructions and quality hints.\n\n\nWorkflow\n--------\n\nWe use the git fork/commit/pull request model.\n\nNote: The following is for *larger features*.\nFor tiny stuff like typo fixes, just create your PR and be done with it.\n\n\n- [Fork the repo and add the needed remotes](https://help.github.com/articles/fork-a-repo/).\n  - the `upstream` remote is **SFTtech/openage**\n  - the `origin` remote is **YourAccount/openage**\n- Create a branch for your feature (\"*feature branch*\": `git checkout -b feature-name`).\n  - This should only contain commits for `feature-name`,\n    e.g. all changes relevant for branch `really-secure-drm`.\n  - You can always update to upstream state by [rebasing](#rebasing).\n- Discuss your ideas and your work:\n  - On the [group chats](/README.md#contact)\n  - That way, \"bad\" ideas can be resolved beforehand and \"better\" ideas are found\n\n- \"Release early and often!\" also applies to pull requests!\n  - Once your branch has some visible work, create a draft/`[WIP]` pull request\n  - Give the pull request a description of what you did or want to do, so we can discuss it\n  - Make sure you are in the `copying.md` file\n  - People will be able to look at your code and give feedback\n  - You'll get free checks from the build bot\n- Once your work is done, mark it as ready for review/remove the `[WIP]` so it can be merged\n- Do the changes that are requested by the reviewers.\n- Aaaaaand you're done.\n\n\nWhat makes a good Pull Request good?\n------------------------------------\n\nBefore making a pull request, it's good to review these things:\n- Run `make test` to check whether any functionality has been broken\n- [Check your whitespaces](https://github.com/SFTtech/openage/blob/master/doc/code_style/tabs_n_spaces.md)\n- [Read all the codestyle docs]( https://github.com/SFTtech/openage/tree/master/doc/code_style)\n- Before pushing, run `make checkmerge`. If that fails, the automatic buildbot will reject your code.\n- If this is your first contribution, add yourself to the authors list in [copying.md](/copying.md).\n- Commit messages should be meaningful, they should say in a sentence (or very little text) what\n  changes it has without requiring to read the entire diff. [tpope knows this very well!](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)\n- You should [rebase](#rebasing) your work to avoid \"clutter\" in your commits and use the latest upstream code.\n\nWe have a buildbot (currently [`kevin-ci`](https://github.com/SFTtech/kevin)) that runs all sorts of checks.\nIt can be very strict at times, so don't be shocked if it rejects your code, and go fix it instead.\n\n\nThe pull request will present your code to the community, which may point out\nsome things that you might haven't noticed. You should fix stuff until everybody\nis happy.\n\n\nRebasing\n--------\n\n**What the hell is it, and (why) do I need it?**\n\n**Rebasing** is 'moving' your commits to a different parent commit.\n\nIn other words: *Cut off* your branch from its tree, and *attach it* somewhere else.\n\nThere's two main applications:\n\n- If you based your work on a older master (so old that stuff can't be automatically merged),\n  you can rebase to move your commits to the current [upstream](https://help.github.com/articles/fork-a-repo/) master:\n\n```bash\n# update the upstream remote to receive new commits\ngit fetch upstream\n\n# be on your feature branch (you probably are)\ngit checkout my-awesome-feature\n\n# make backup (you never know, you know?)\ngit branch my-awesome-feature-backup\n\n# rebase: put your commits on top of upstream's master\ngit rebase -m upstream/master\n```\n\n- If you want to fix an older commit of yours, or merge several commits into a single one (**squash** them), rebase interactively.\n  We ***don't*** want to have a commit history like this:\n\n  - `add stuff`\n  - `fix typo in stuff`\n  - `fix compilation`\n  - `change stuff a bit`\n  - and so on...\n\n\n### `rebase` in practice\n\n`git log --graph --oneline` shows your commit history as graph.\nTo make some changes in that graph, you do an **interactive rebase**:\n\n```\ngit rebase -i -m upstream/master\n```\n\nWith this command, your new \"base\" is `upstream/master` and you can\nthen change any of your branch's commits.\n\n`-i` will open an interactive editor where you can choose actions for each individual commit:\n\n- re-order commits\n- drop commits by deleting their line\n- squash/fixup (\"meld\") your commits\n- reword a commit message\n- stop rebasing at a commit to edit (`--amend`) it manually\n\nJust follow the messages on screen.\n\n\n### Changing commits with `amend` and `fixup`\n\nThere's also `git commit --amend` which is a \"mini-rebase\" that modifies just the last commit with your current changes by `git add`.\nIt just skips the creation of a new commit and instead melds the changes into the last one you made.\n\nIf you want to update a single commit in the range `[upstream/master, current HEAD]` which is not the last commit:\n\n- `edit stuff you wanna change in some previous commit`\n- `git add changed_stuff`\n- `git commit --fixup $hash_of_commit_to_be_fixed`\n- `git rebase --autosquash -i -m upstream/master`\n\n\n### Pushing changes\n\nAfter you have rebased stuff ([\"rewritten history\"](https://www.youtube.com/watch?v=9lXuZHkOoH8)) that had already been pushed,\ngit will not accept your pushes because they're not simple fast-forwards:\n\n- The commit contents and the parent commit have changed as you updated the commit, therefore the commit hash changed, too.\n  - If somebody used those commits, they will keep a copy\n    and have a hard time updating to your updated version (because they \"use\" the old hashes).\n  - Update your pull request branch with your re-written history!\n\n- **force push** is the standard way of overwriting your development work with the fixed and mergeable version of your contribution!\n  - Why? You changed the commits, so you want the old ones to be deleted!\n\n  You can use any of:\n  - `git push origin +my-awesome-feature`\n  - `git push origin -f my-awesome-feature`\n  - `git push origin --force my-awesome-feature`\n\nSome extra tutorials on `git rebase`:\n * [Atlassian's Git Tutorial](https://www.atlassian.com/git/tutorials/rewriting-history/)\n * [Pro Git book](http://git-scm.com/book)\n * `man git-rebase`\n"
  },
  {
    "path": "doc/convert/README.md",
    "content": "# Convert Script\n\nThe convert script transforms media files to openage-compatible\nformats, and generates code for parsing the generated files. It is written\nin **Python**, **Cython** and contains **C++ extensions**.\n\nAs we are using the media assets of the original game, the input format is\npre-existing and we have to deal with it.\n\nUnfortunately the original asset binary formats are a bit \"special\",\nso we need to convert the files to a format that is less special, and more\nusable. This is what the convert script does.\n\n\nThe convert script is divided into two usage modes: *singlefile media conversion* and\n*modpack conversion*.\n\n## Singlefile Media Conversion\n\nThis converts graphics and sounds to open/compatible formats. Graphics are\nexported to PNG files. If the graphics file is an animation with multiple\nsprites, the converter will automatically pack all of them into a spritesheet.\nSounds are exported to OPUS files.\n\n[See here](convert_single_file.md) for usage examples of the singlefile conversion.\n\n\n## Modpack Conversion\n\nModpack conversion takes an original game installation and converts its data\nand media files into an [openage modpack](/doc/media/openage/modpacks.md).\nThe converted game can then be run inside openage.\n"
  },
  {
    "path": "doc/convert/convert_single_file.md",
    "content": "# Singlefile conversion\n\nYou can use openage to convert a single graphics file to PNG or sound file to OPUS. The\nasset can also be unpacked from a DRS archive.\n\nThe invocation could be:\n\nSLPs in DRS archives (for older versions of Age of Empires 1, Age of Empires 2 and SWGB):\n```\npython3 -m openage convert-file --palettes-path ~/games/aoe2/Data/ --drs ~/games/aoe2/Data/graphics.drs 326.slp /tmp/rofl.png\n```\n\nStandalone SLPs (Age of Empires 1: DE and Age of Empires 2: HD):\n```\npython3 -m openage convert-file --palettes-path ~/games/aoede/Assets/Palettes 326.slp /tmp/rofl.png\n```\n\nStandalone SLDs (Age of Empires 2: DE):\n```\npython3 -m openage convert-file --palettes-path ~/games/aoe2de/Data/ u_elite_eagle.sld /tmp/rofl.png\n```\n\nStandalone SMXs (Age of Empires 2: DE):\n```\npython3 -m openage convert-file --palettes-path ~/games/aoe2de/Data/ u_elite_eagle.smx /tmp/rofl.png\n```\n\nStandalone SMPs (Age of Empires 2: DE):\n```\npython3 -m openage convert-file --palettes-path ~/games/aoe2de/resources/_common/palettes u_elite_eagle.smp /tmp/rofl.png\n```\n\nWAVs in DRS archives (for older versions of Age of Empires 1, Age of Empires 2 and SWGB):\n```\npython3 -m openage convert-file --drs ~/games/aoe2/Data/sounds.drs 123.wav /tmp/rofl.opus\n```\n\nStandalone WAVs (Age of Empires 2: HD Edition):\n```\npython3 -m openage convert-file ~/games/aoe2/resources/123.wav /tmp/rofl.opus\n```\n\nHave a look at `openage/convert/singlefile.py`, this is also a simple API demo\nfor how to interact with the aoe files.\n\n\n### Interactive Shell\n\nYou can also [browse the archives and data interactively](interactive.md).\n"
  },
  {
    "path": "doc/convert/debugger.md",
    "content": "# Converter Semantic Debugger\n\nThe converter can generate debug info for a run. This provides\ninformation about the contents of converter parameters\nduring conversion. It can help to find *semantic errors* in\nthe code that would be hard to discover otherwise. Syntax and\nruntime errors are handled by the general logger.\n\n## Loglevels\n\nThere are 4 loglevel to choose from. They can be manually specified using\nthe `--debug-info [0, 1, 2, 3]` CLI parameter when starting a run. Otherwise\nthe converter will use its default loglevels. These are 0 when started in `no-devmode`\nand 3 when started in `devmode`.\n\n* Level 0\n    * No output\n    * Default for `--no-devmode`\n\n* Level 1\n    * Converter CLI args\n    * Generated info file\n    * Generated manifest file\n    * Logfile\n\n* Level 2\n    * Info from Level 1\n    * Summaries of all stages (init, read, processor, export)\n        * Game version (init)\n        * Mount points (init)\n        * Used `.dat` file data format (read)\n        * Found languages (read)\n        * Found graphic files (read)\n        * Number of `ConverterObject`s and `ConverterObjectGroup`s by type (processor)\n        * Number of generated data, media and metadata files by type (export)\n\n* Level 3\n    * Info from Level 2\n    * Summaries for every `ConverterObjectGroup`\n        * Output of helper functions (`is_XYZ()` or `get_XYZ()`)\n        * Content of parameters\n        * Nyan object name (if available)\n    * Default for `--devmode`\n"
  },
  {
    "path": "doc/convert/interactive.md",
    "content": "# Interactive conversion\n\n***Important notice***: This functionality is currently broken. Interactive browsing\nwill be reintegrated in a later update.\n\nWe support browsing the original media files with an interactive shell.\n\nIt can be invoked the following way:\n\n``` shell\npython3 -m openage convert --interactive --source-dir ~/games/age2\n```\n\nIt will then display some beautiful welcome message and a short howto.\nHave fun exploring the hidden depths of the original data archives with Python.\n\n\n### Single-File Conversion\n\nYou can also [convert single files non-interactively](convert_single_file.md).\n"
  },
  {
    "path": "doc/convert/nyan.md",
    "content": "# nyan data conversion\n\nWe depend on the original game.\nWe have a converter which spits out a files in our formats\nby parsing the original data.\n\n## Gamedata conversion\n\n`openage/convert/value_object/read/media/datfile/` specifies the format of the `.dat` from various games.\n"
  },
  {
    "path": "doc/debug.md",
    "content": "# How to debug openage?\n\n## Your favorite IDE\n\nCheck the docs in [the `/doc/ide` subfolder](/doc/ide/).\n\n## GDB\nGDB can be used to debug C++ code in a terminal.\n\nTo being able to debug with GDB use `./configure` script such as:\n```\n./configure --mode=debug --compiler=gcc\n```\nbuild the game\n```\nmake\n```\nThen\n```\ngdb -ex 'set breakpoint pending on' -ex 'b openage::run_game' -ex run --args run game\n```\nThe game will be paused at the start of the function run_game() located in `libopenage/main.cpp`\n\n**Note:** The `run` executable is a compiled version of `run.py` that also embeds the interpreter.\nThe game is intended to be run by `run.py` but it is much easier to debug the `./run` file\n\n### Pretty Printers\n\nEnabling pretty printing will make GDB's output much more readable, so we always recommend\nto configure it in your setup. Your [favourite IDE](/doc/ide/) probably an option to enable pretty printers\nfor the standard library types. If not, you can get them from the [gcc repository](https://github.com/gcc-mirror/gcc/tree/master/libstdc%2B%2B-v3/python/libstdcxx) and register them in your local `.gdbinit` file.\n\nAdditionally, we have created several custom GDB pretty printers for types used in `libopenage`,\nthe C++ library that contains the openage engine core. To enable them, you have to load the project's\nown init file [openage.gdbinit](/etc/openage.gdbinit) when running GDB:\n\n```gdb\n(gdb) source <path-of-openage-dir>/etc/openage.gdbinit\n```\n\nYour IDE may be able to do this automatically for a debug run. Alternatively, you can configure\nan [auto-loader](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-Auto_002dloading.html#Python-Auto_002dloading)\nthat loads the scripts for you.\n\n\n### GDBGUI\n\n[gdbgui](https://github.com/cs01/gdbgui) is a browser-based frontend for GDB.\n\nTo install gdbgui in Ubuntu:\n\n```\nsudo pip3 install gdbgui --upgrade\n```\nThen\n```\ngdbgui ./run\n```\nThe gdbgui web page will be at `http://127.0.0.1:5000`\nUse the command `run` in the GDB prompt to start debugging\n"
  },
  {
    "path": "doc/development.md",
    "content": "About development\n=================\n\nDev team\n--------\n\nThere is a \"core dev team\" who work on the project in their free time,\nbut since we all have other life-originating interrupts and fluctuating motivation,\nour activity will alternate equally over time (just take a look at the commit statistics).\nWe \"core devs\" have known each other personally for a long time;\nthus, communication happens mostly over private channels (Matrix chats, Mumble, maybe even real life).\n\n\nDevelopment workflow\n--------------------\n\nMany contributions originate from discussions on the Matrix/IRC channel,\nor an issue on the [issue tracker](https://github.com/sfttech/openage/issues).\n\nThe pull requests are then open for review by everybody.\nIf the pull request author thinks that their goals aren't met yet,\nthey create it as a draft or tag it with `[WIP] $title`.\nThe author removes it once the goals are met.\n\nIn the big picture, we're working on completing those WIP pull requests,\nuser-submitted issues or the next milestone.\nIn reality however, we're often working on unrelated stuff that is not covered\nby any milestone (buildsystem, infrastructure, ...),\njust convenient to implement, or needed right now.\n\nWe're releasing early and often: whenever we add/improve a feature or\nfix a bug and the code still compiles, we push the code to the github master.\nOnce significant changes have accumulated,\nwe tag that commit with a new version.\n\nWe don't have dedicated \"subsystem maintainers\" yet.\nYou can track down the person who did things with `git log` or `git blame`.\n\n\nContributing\n------------\n\nAll sorts of contributions are welcome, including\n\n - feature requests\n - bug reports\n - code/doc typo fixes\n - code style fixes when our code doesn't conform to our own style guide (yes, that happens.)\n - bug fixes\n - new features\n - game assets under a free license\n - raytraced intro cutscenes\n - whatever else you can think of\n\nBut read [contributing.md](/doc/contributing.md) first to learn about the contribution workflow/requirements.\n\nAs always, join the [chat](/README.md#contact) and discuss your ideas.\n\n\nCode philosophy\n---------------\n\n - If upstream code (other parts of openage, other libraries,\n   or even the compilers) doesn't work or is missing some feature,\n   _report / fix it_! We contributed to compilers, cmake, Python, Cython and more because of openage.\n - Do things properly, not quickly. It takes research, time and maybe even upstream fixes, but it's worth it.\n - If you absolutely _must_ use a dirty hack (e.g. because you're waiting for upstream to fix it), write a TODO message with an explanation.\n - Regularly review old code to find rusty parts. Remove them, rewrite them, refactor them or at least update the comments.\n - The project is in development. Don't be shy about adding, removing and changing interfaces, but tell other people if you break their WIP stuff.\n - Others can't look into your brain. Document your stuff:\n   - Explanations about inner workings algorithms and data structures should be comments in the source code.\n   - Design decisions, API specification, rants about mathematical concepts and the like are explained in `doc/`.\n"
  },
  {
    "path": "doc/ide/README.md",
    "content": "# Developing openage in Integrated Development Environments (IDEs)\n\nWe have created a collection instructions for setting up IDEs to use them for openage.\n\n* [Qt Creator](qt_creator.md)\n* [Visual Studio Code](vscode.md)\n"
  },
  {
    "path": "doc/ide/configs/vscode/tasks.json",
    "content": "// Copyright 2021-2021 the openage authors. See copying.md for legal info.\n{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Configure & Build\",\n            \"type\": \"shell\",\n            \"group\": {\n                \"kind\": \"build\",\n                \"isDefault\": true\n            },\n            \"dependsOrder\": \"sequence\",\n            \"dependsOn\": [\n                \"Configure\",\n                \"Build\"\n            ],\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        },\n        {\n            \"label\": \"Configure\",\n            \"type\": \"shell\",\n            \"command\": \"${workspaceFolder}/configure\",\n            \"args\": [\n                // \"--optimize\", \"0\", // Comment this out to disable compiler optimization for debug runs\n                // \"--ccache\",        // Comment this out if you want to use ccache\n                // \"--download-nyan\"  // Comment this out if you do not want to clone and build nyan yourself\n            ],\n            \"group\": \"build\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        },\n        {\n            \"label\": \"Build\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"group\": \"build\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        },\n        {\n            \"label\": \"Sanity Check\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"args\": [\n                \"checkmerge\"\n            ],\n            \"group\": \"build\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        },\n        {\n            \"label\": \"Run Tests\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"args\": [\n                \"test\"\n            ],\n            \"group\": \"test\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        },\n        {\n            \"label\": \"Clean\",\n            \"type\": \"shell\",\n            \"command\": \"make\",\n            \"args\": [\n                \"cleanbuilddirs\"\n            ],\n            \"group\": \"build\",\n            \"presentation\": {\n                \"reveal\": \"always\",\n                \"panel\": \"new\"\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "doc/ide/emacs.md",
    "content": "# Emacs\n\n[Emacs](https://www.gnu.org/software/emacs/) can be set up in infinitely many ways, depending on your needs and skills.\n\n[Emacs](https://www.gnu.org/software/emacs/tour/) consists of a bare-metal core and many many addons written in [Emacs Lisp](https://en.wikipedia.org/wiki/Emacs_Lisp).\nSome of the packages are built-in, others are in the [official repo](https://elpa.gnu.org/), and even more are fetched from [external repos](https://melpa.org).\nPackages provide features and can modify each other.\n\nBecause plugging them together takes a lot of effort and basically behaves like your Linux distro just within Emacs, there's Emacs distributions.\nTo name a few:\n\n* [Spacemacs](https://www.spacemacs.org/)\n* [Doom](https://github.com/hlissner/doom-emacs)\n* [Prelude](https://prelude.emacsredux.com)\n\n\n## Spacemacs\n\n> The best editor is neither Emacs nor Vim, it's Emacs and Vim!\n\nSpacemacs is cloned to `~/.emacs.d` and provides the building blocks for a very well integrated Emacs IDE.\n\nIt supports three editing styles (and all available at once): Vim, Emacs and Spacemacs.\n\nProbably best to start with [the quickstart guide](https://www.spacemacs.org/doc/QUICK_START.html).\n\n[Below](#spacemacs-basics) is my rough introduction on how you can do things.\n\n\n## JJ's setup\n\nI'll try to present my setup in order to inspire your Emacs experience.\n\nI use Spacemacs with [my custom ~/.spacemacs configuration](https://github.com/TheJJ/conffiles/blob/master/.spacemacs).\nIt's an all-in-one config file which configures and customizes Spacemacs.\n\nThe config enables and sets up several features, among them are:\n\n* C++ and Python semantic autocompletion (with [lsp-mode](https://emacs-lsp.github.io/), [clangd](https://clangd.llvm.org/))\n  * Symbol (function, class, variable, ...) reference and definition tracking\n* Git project navigation and tracking ([projectile](https://projectile.mx/), [magit](https://magit.vc/))\n  * Project compilation\n  * Error highlighting\n* Code snippet template expansion ([yasnippet](https://joaotavora.github.io/yasnippet/))\n* Perfect codestyle indentation and formatting\n  * Depending on the language: [Indent with tabs, align with spaces](/doc/code_style/tabs_n_spaces.md)\n* More custom keybindings\n* ...\n\n\nIn order for C++ semantic indexing to work with `openage`, symlink the `bin/compile_commands.json` to the project git root!\n`clangd` needs to find this file in a parent dir of the source files so it knows the compiler invocations.\n\n### Spacemacs Installation\n\n* Distribution packages:\n  * Install `emacs`, which is the GNU Emacs system package\n  * Install `clangd` for semantic c++ parsing, usually part of `clang` distro package\n  * Install `python-lsp-server` for semantic Python parsing\n* Home directory configuration setup:\n  * Clone Spacemacs: `git clone https://github.com/syl20bnr/spacemacs ~/.emacs.d`\n  * Clone JJ's configs: `git clone https://github.com/TheJJ/conffiles ~/.jjconfigs`\n  * Symlink the Spacemacs config: `ln -s .jjconfigs/.spacemacs ~/.spacemacs`\n* Launch `emacs`\n  * Wait until initial package setup is complete\n  * Press <kbd>Space</kbd> to begin\n\n\n### Spacemacs Updates\n\nYou need to **update** 4 things regularly:\n* Emacs (your distro updates do this)\n* Spacemacs (go to `.emacs.d` and do `git pull`)\n* `.spacemacs`-config (go to `.jjconfigs` and do `git pull`, or whatever you use to track the `.spacemacs`)\n* Emacs packages (click on `Update Packages` or press <kbd>SPC f e U</kbd> or run <kbd>M-x configuration-layer/update-packages</kbd>)\n\n\n### Spacemacs Basics\n\nYou have three input methods active at once: **Vim**, **Emacs** *and* **Spacemacs**.\n\n* If you're used to `vim`, most commands you're used to will probably just work.\n* Most Emacs keybindings work too after you pressed `i` for the insert mode.\n* Spacemacs bindings start by, who'd have thought it, pressing <kbd>Space</kbd>\n* Press <kbd>ESC</kbd> to leave the current mode.\n* Press <kbd>C-g</kbd> (control-g) **to abort whatever** input/action you're currently in.\n\nBut how can you do anything?\n\n* I recommend you treat Emacs like Firefox - leave it open all the time\n  * you can have many open tabs (\"buffers\") ([centaur for real tabs](https://develop.spacemacs.org/layers/+emacs/tabs/README.html))\n  * you can have many open windows (\"frames\")\n  * you can split the views (\"windows\")\n  * open a new file in running Emacs from the commandline with: `emacsclient -n`\n* Key combination syntax:\n  * <kbd>C</kbd> = Control key\n  * <kbd>M</kbd> = Meta = Alt key\n  * <kbd>S</kbd> = Super = The key between <kbd>C</kbd> and <kbd>M</kbd>\n  * <kbd>M-g f</kbd> = First press <kbd>Meta</kbd> and then <kbd>g</kbd>, let go of both, then press <kbd>f</kbd>\n* Every key (and combination) is bound to a function invocation\n  * You can invoke functions directly with <kbd>M-x</kbd>\n    * What does a keypress do? <kbd>M-x describe-key</kbd>\n    * Find definitions and documentation of a function: <kbd>M-x describe-function</kbd>\n* Quit and Exit:\n  * Spacemacs-style: <kbd>SPC q q</kbd>\n  * Emacs-style: <kbd>C-x C-c</kbd>\n  * Vim-style: <kbd>ESC :q</kbd>\n* Buffer = opened real file (or for any other IO, like logs, ...)\n  * Switch to buffer: <kbd>SPC b b</kbd> (<kbd>C-x b</kbd>)\n  * Close buffer: <kbd>SPC b d</kbd> (<kbd>C-x k</kbd>)\n* File Operations:\n  * Open file browser: <kbd>SPC f f</kbd> (<kbd>C-x C-f</kbd>)\n    * Open project: <kbd>SPC p p</kbd>\n    * Open/switch to project file: <kbd>SPC p h</kbd>\n  * Save: <kbd>SPC f s</kbd> (<kbd>C-x C-s</kbd>)\n  * Close: <kbd>SPC b d</kbd> (<kbd>C-x k</kbd>)\n  * Browse projects: <kbd>SPC p p</kbd>\n* Window and views:\n  * Split the view horizontally: <kbd>SPC w /</kbd> (<kbd>C-x 3</kbd>)\n  * Close current view <kbd>SPC w d</kbd> (<kbd>C-x 0</kbd>)\n  * Open a new Emacs window: <kbd>SPC F n</kbd> (<kbd>C-x 5 2</kbd>)\n  * Close current Emacs window <kbd>SPC F d</kbd> (<kbd>C-x 5 0</kbd>)\n  * Open the file browser tree: <kbd>SPC f t</kbd>\n* Code naviation:\n  * Go to line: <kbd>M-g g</kbd>\n  * Find definition: <kbd>, g d</kbd> (<kbd>M-g d</kbd>)\n  * Find references: <kbd>, g r</kbd> (<kbd>M-g f</kbd>)\n  * Refactor variable name: <kbd>, r r</kbd>\n  * Search the whole project code for something <kbd>M-x projectile-ag</kbd>\n* Git status, commits, ...:\n  * Launch Magit: <kbd>SPC g s</kbd>\n  * View git-blame: <kbd>M-x vc-annotate</kbd>\n\n\nAn open buffer has several active modes:\n* It's major-mode (e.g. `c++-mode`) and\n* Minor modes (e.g. `line-number-mode`, `auto-completion`, ...; see them with <kbd>M-x describe-mode</kbd>).\n* Each mode contributes to some behavior of the buffer (basically by providing/registering functions and setting variables).\n\nThis design allows you to fully customize and extend Emacs with whatever new feature and behavior you desire.\n\n\n### Some features to start with\n\nYou can start these things with <kbd>M-x</kbd>, or hack them into `.spacemacs` once you get going with elisp.\n\n* Classical menu bars: `menu-bar-mode`, `tool-bar-mode`\n* Markdown live editing preview: `vmd-mode`\n* Git: `magit-status`\n* Remote over-ssh editing: `tramp` (just open file with name `/ssh:host:path/to/file`)\n* Text Search: `ag`\n* Debugging: `realgud`\n* File tree: `treemacs`, `speedbar`\n* Programming Symbol tree: `lsp-treemacs-symbols`\n* Syntax check: `lsp-treemacs-error-list`, `flycheck-mode`\n* Easy config customization: `customize`\n* ...\n\nRemember <kbd>M-x describe-$thing</kbd> to start digging into `$thing`!\n\n### Your journey\n\nOnce you start missing features and wanting to customize the configuration further, please:\n* either branch off my config repo to have your own `.spacemacs` and you can still merge my changes\n* or start your own `.spacemacs` config!\n\nPlease [contact us](/#contact) if you need help or are overwhelmed by the awesomeness!\n"
  },
  {
    "path": "doc/ide/qt_creator.md",
    "content": "# Qt Creator IDE\n\nQt Creator can be used to launch build, edit, run and debug C++ and QML code.\n\nInstall Qt Creator from the repository of your distribution or with an installer from the Qt website.\n\nTo launch QtCreator in English on Linux:\n\n```\n$ LANGUAGE=C qtcreator\n```\n\n## Opening and configuring the project\n\nOpen the root `CMakeLists.txt` with QtCreator and select the Qt installation that you want to target and click **Configure Project**.\n\nWatch the **General Messages** log window for CMake errors.\n\n\n## Enabling parallel build\n\nGo to **Projects** -> **openage** -> **Build & Run** -> **Build** -> **Build Steps** -> **Build** -> **Details**, and add `-j4` to the **Tool arguments**.\n\n\n## Run & Debug\n\n1. To set the correct executable, first go to **Projects** -> **openage** -> **Build & Run** -> **Run** page.\n1. In the **Run configuration** combo-box select **Custom Executable** and set **Executable** to `<path-to-openage-src>/run`.\n1. Set **Working directory** to `<path-to-openage-src>`.\n\n`<path-to-openage-src>` is the directory where the sources of the openage are (the directory of the root `CMakeLists.txt` file).\n\nThe `<path-to-openage-src>/run` binary has no executable bit set when first built, set it manually with `chmod`.\n"
  },
  {
    "path": "doc/ide/vscode.md",
    "content": "# Visual Studio Code\n\nvscode can be used to launch build, edit, run and debug C++ code.\n\nThere are two different versions of vscode that you can use:\n\n* The official *vscode* release from Microsoft for which you can get an installer on the the Visual Studio Code website.\n* The inofficial fork *vscodium* that removes all proprietary components and disables telemetry. You can find it [here](https://vscodium.com/).\n\n\n## Setting up vscode for openage\n\nIt is a good idea to install extensions that enable language support for the programming languages used in the project. The languages we use can be found in the general [README](/README.md#technical-foundation) file.\n\nAdd extensions to vscode by going to **File** -> **Preferences** -> **Extensions**.\n\nIf you use *vscodium*, some extensions might not be available from the start and have to be added manaually. Read more on this in the project's [documentation](https://github.com/VSCodium/vscodium/blob/master/DOCS.md#extensions-marketplace).\n\nIf you use the official *vsode* release, now would be a good time to disable telemetry. To do this, go to **File** -> **Preferences** -> **Online Services Settings** and disable the telemetry options.\n\n\n## Opening and configuring the openage workspace\n\nCreating a workspace for openage is very straightforward:\n\n1. Clone the *openage* repository: https://github.com/SFTtech/openage/\n1. Install all openage dependencies for your platform: https://github.com/SFTtech/openage/blob/master/doc/building.md#dependency-installation\n1. Clone the *nyan* repository: https://github.com/SFTtech/nyan\n1. Install all nyan dependencies for your platform: https://github.com/SFTtech/nyan/blob/master/doc/building.md#dependencies\n1. Go to **File** -> **Open Folder...** and select the root folder of the openage repository\n\nAfterwards, vscode will try to initialize your workspace with your installed extensions. Once that is finished, you should change a few settings of the default settings in your workspace.\n\n**Files: Watcher Exclude**\n\nYou should add these entries to the list of directories that are not watched:\n\n* `**/.bin/**`\n* `**/bin/**`\n* `**/assets/converted/**`\n\nThese directories contain generated build files, binaries created from builds and media assets converted from the original games. Hence, there is no need to watch these files for changes during development. If you do not add these entries, vscode will likely complain that there are too many files for it to index.\n\n**C_Cpp > Default: Include Path**\n\nIf you use a C/C++ language support extension, you need to add the path to the *nyan* directory to your workspace's include path. Otherwise, vscode will not be able to find the nyan header files.\n\n1. Select **Edit in settings.json**\n1. Scroll down and add a new parameter entry to the end of the JSON object. This will enable custom include paths.\n\n```\n\"C_Cpp.default.includePath\": [\n    \"${default}\"\n]\n```\n\n1. Open **c_cpp_properties.json** in the same folder and add a new entry to `includePath` containing the path to the nyan repository on your file system. If your local nyan repository is in the same folder as the openage repository, your entry should look like this:\n\n```\n\"includePath\": [\n    \"${workspaceFolder}/**\",\n    \"${workspaceFolder}/../nyan/**\"\n]\n```\n\n**Python > Formatting: Autopep8 Args**\n\nIf you use a Python language support extension with `autopep8` formatting, you should add these settings to get the openage Python code style. Add these entries to the list of passed arguments:\n\n* `--ignore`\n* `E221,E241,E251,E501`\n\nIgnoring the `E221,E241,E251` errors allows placing multiple spaces around commas and before comments for alignment, while ignoring `E501` disables auto-formatting of overlong lines.\n\nIf you don't want to ignore `E501`, you should add this argument which increases the maximum line length to 100:\n\n* `--max-line-length`\n* `100`\n\n\n## Add openage configure and build tasks\n\nvscode will likely automatically generate a build configuration for CMake when you create the workspace, but it's not recommended to use that. Instead, you should always use our official build configuration using the `configure` script that comes with the project. To do this, you can use our template task file that you can find at `/doc/ide/configs/vscode/tasks.json` in the openage repository files. Copy this file to the `/.vscode/` directory to make the tasks available in your workspace.\n\nTask                | Description\n--------------------|-------------\n`Configure & Build` | Shortcut to run the `Configure` and `Build` tasks consecutively.\n`Configure`         | Starts the CMake configuration, i.e. runs the `configure` script.\n`Build`             | Starts the CMake build, i.e. runs `make`.\n`Sanity Check`      | Checks for code compliance errors. You need to fix the complaints before your PR can be merged.\n`Run Tests`         | Run all test for the engine modules.\n`Clean`             | Clean all build directories by removing all build files.\n"
  },
  {
    "path": "doc/ideas/ai.md",
    "content": "# Artificial Intelligence\n\n## Interface\n\nThis is a very early draft with some ideas taken\nfrom http://bwmirror.jurenka.sk/javadoc/bwapi/package-summary.html\n\nThis file shall provide information about the AI interface design.\n\n\n### General ideas\n\n * An AI registers hooks for desired events\n * The C++ engine triggers python AI functions when desired events occur\n * The AI then can trigger actions on the controllable units\n * Periodically (1 Hz?), the AI gets ticked to allow event independent decisions\n * Get the AI its own dedicated thread\n\n\n### Event types\n\nPython interface basic events:\n\n * on_start(GameInfo info)\n * on_frame(Game game)\n * on_end(Player winner)\n\nSome AIs may prefer an event driven approach (rule based AI):\n\n * on_unit_discover(Unit unit)\n * on_unit_complete(Unit unit)\n * on_unit_lost(Unit unit)\n * many more...\n\n\nSome AIs will need to simulate ticks into the future to help\nmake decisions for the current tick. (min-max, MCTS, etc.)\n\n\n### Relevant structures for the AI\n\nRemember: These are just some ideas for possible interfaces,\nthey are not existent in the game yet.\n\n```cpp\nstruct game_info {\n\tint num_players;\n\tint map_size;\n\tint pop_limit;\n\tresources_type;\n\tmap_view;\n\tstarting_age;\n\tvictory_mode;\n}\n```\n\n```cpp\nclass Game {\n\tPlayer[] allies();      // all the ally players that have not left or been defeated.\n\tbool     can_build_at(Unit builder, TilePosition position, UnitType type);\n\tbool     can_create(Unit builder, UnitType type);\n\tbool     can_research(Unit unit, TechType type);\n\tbool     can_upgrade(Unit unit, UpgradeType type);\n\tPlayer[] enemies();     // all the enemy players that have not left or been defeated.\n\tUnit[]   get_all_units(); // returns all the visible units.\n\n\t// many more examples at: http://bwmirror.jurenka.sk/javadoc/bwapi/Game.html\n}\n```\n\n```cpp\nclass Player {\n\tstring  get_name();   // returns the name of the player.\n\tFaction get_faction();   // returns the faction of the player.\n\tUnit[]  get_units();  // returns the set of units the player own.\n\tbool    has_researched(TechType tech);\n\tint     get_resource()    // Returns the amount of resources the player owns.\n\t// many more...\n}\n```\n\n```cpp\nclass Unit {\n\t// used to get information about individual units as well as issue orders to units\n}\n```\n\n```cpp\nclass Calculation {\n\t// used to get computation heavy calculations from the C++ engine\n}\n```\n\n\n## Gameplay\n\nImprovements for the AI's gameplay\n\n### Villagers\n\n - Don't collect resources in hazardous areas / send the military to clear them out\n - Send military task forces to guard strategic resources / tower them\n - Don't build in hazardous areas\n - Build more gates in walls to prevent lock-ins\n - Prefer to build castles and towers near the water/thin places\n - Create more trade cogs and trade ships\n\n### Military\n\n - Avoid fighting in hazardous areas / prefer fighting in range of own castles\n - Coordinates reaction to attacks on units (during march, ...) (including attacks by towers/castles)\n - Disable scouting if the \"all visible\" option was selected.\n - Lame you (steal boar in the beginning on purpose)\n - Understand, how to do economic damage\n   - e.g. if losing of all AI-units is inevitable, run and attack villagers to hurt economy\n - don't wait for hours to mass up army and then try to overrun you at minute 40\n   - send smaller amount of troops earlier in the game to annoy you\n   - while building up a big army in the back\n\n### Ship\n\n  - Transport ship can deliver villagers to another islands\n"
  },
  {
    "path": "doc/ideas/editor/README.md",
    "content": "# openage --editor\n\n\n## Editor doc structure\n\n* **Terrain editor** -  for all your terrain editing needs. Your favourite TV show ten years ago was Bob the Builder? Then go ahead and look what tools he has in [terrain.md](terrain.md)!\n* **Economy editor** - discover the communist in yourself! In contrast to real life everything works as expected - so plan the economy in [economy.md](economy.md)!\n* **Script editor** - you don't need to believe in god. You ARE god. Set your evil mind free in [scripting.md](scripting.md)\n* **Campaign editor** - You're fed up with low-quality campaigns that you've known for years? You think the original tutorial campaign doesn't quite cut it? Your search is over - read [campaigns.md](campaigns.md) now!\n* **Unit editor** - Everyone's favourite tool to create cats in nyan. Bonus points for correct unit music. Listen to it at [units.md](units.md).\n* **Tech editor** - because having nuclear weapons in the middle age is not ignorant and totally not the point of this game. It's freakin *gnarly*. See how you can make your own weapons of math instruction at [tech.md](tech.md)\n* **Multiplayer Mode** - are you tired of working on maps all by yourself? do you want others to do your editing work for you? Fear no more and embrace *massive multiplayer online editor mode* (MMOEM) by looking inside [multiplayer.md](multiplayer.md).\n\n## General\n* It doesn't matter if the engine doesn't implement feature X. Don't you **dare** implementing something in the engine that isn't in the editor already!\n* If the editor produces invalid files, fix the engine. No exceptions.\n* Remember general attitude: bugs are features, and bugreports are for losers ONLY.\n* In case it isn't completely evident: if you've missed the irony, you should go and repair your laugh box.\n"
  },
  {
    "path": "doc/ideas/editor/campaigns.md",
    "content": ""
  },
  {
    "path": "doc/ideas/editor/economy.md",
    "content": ""
  },
  {
    "path": "doc/ideas/editor/multiplayer.md",
    "content": "# Multiplayer\n\n## Idea\n\nPeople like collaborating and sharing scenarios, but strangely enough the number of games who allow simultaneous editing over the network is close to zero. The openage editor should different and designed to be used by multiple people. A lot of inspiration can be taken from Minecraft where \"editing\" a map is the whole point of the multiplayer mode.\n\n## Features\n\n* Editor runs on the client, server distributes changes to everyone connected\n* Map status is saved on the server for download\n* Map is saved on the client for offline editing (changes could be pushed to the server, similar to `git`)\n* Simple rights management (view, place, delete)\n* Advanced rights management\n    * Users can be allowed or denied to place/delete certain objects\n    * Users can be allowed or denied to use certain editor functions (scripting, terrain editing)\n    * Put users in groups\n    * Designate zones on the map which can have their own rights management\n* Spectator mode\n* Cloning the map\n* Save multiple release versions on the server\n* Password protected access\n* Let users attach notes with explanations to objects\n* share editor config/quickbar/whatever with other users\n\n## Optional\n\n* Web Interface to edit maps when you're bored at work\n* Version control with git\n* Automatic upload to aok.heavengames.com, openage map repository\n"
  },
  {
    "path": "doc/ideas/editor/scripting.md",
    "content": ""
  },
  {
    "path": "doc/ideas/editor/tech.md",
    "content": ""
  },
  {
    "path": "doc/ideas/editor/terrain-ui.txt",
    "content": "+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-+-+-+\n| openage terrain editor                                                                                                                                                     |.|O|X|\n+------+------+------+-----+-------+----------------------------------------------------------------------------------------------------------------------------------------++-+-+-+\n| File | Edit | Here | Are | Menus |                                                                                                                                        | Help |\n+----+-+--+---++----++-----+-------+----------------------------------------------------------------------------------------------------------------------------------------+------+\n| Q1 | Q2 | Q3 | Q4 |                                                                                                                                                              |\n+----+----+----+------------------------------------------------------------------------------------------------------------------------------------------------+------------------+\n| Object Tree       |:~?O888NNNNDD8DNMMNMNOZ$7IIIII7$$77IZ777IIII??7$$ZZ7OOOZZ77I+=~~~=?77II77$77$777II?++++=+=++==~==+++7OODDDNNNMNMNMNNNDO7~:::::,,,,,,,,,,,,,| Brush groups    V|\n+-------------------++I888ODNNNNNNDDMMMMND88DO7II?I7777I$+++++??+++I77777$$OOOZI+=~:::~=+I$ZZOOOZII7I?~~::=?++?II??+===++?I$Z8DNNMMNNDMNNDNDO=:::::,,,,,,,,,,,,,+------------------+\n|                   |=ZOOZ8NNMMNNNNDNND8DDOZ$7I$OD888OOI+??7Z8D8ODZO8OO7III$O$Z7?=~:,,:~~+777$ZI$7777I=~~:~~?$????II?===+++I$O8NNMNMNNNMNN8N8I::,,,,,,,,,,,,,,,,| X--X X--X X--X   |\n|                   |+I$?O8NNMMNMNNNDZ77$8OZ7I??I?I8ZO$$7ODDO+=$7$$I:II7Z7???7?$I=::,,:~~?II8????I7II?++==~=++?$+??II+=====+I$ODNNMMNNNMNDDDDD7::,,,,,,,,,,,,,,,| X--X X--X X--X   |\n|                   |:+7Z8ZNNMMNNMMNDZ7I$8O$77???+==+7O$ZZOO?=$7MD87=,?OI==+?O?I$O~:~:Z==+~Z+++?$O77$$D8OI+~~~==O+?I?++=====+?$ODDMMNMNMMMNNNND7::,,,,,,,,,,,,,,| X--X X--X X--X   |\n|                   |::$O$DNMMNNNNMMMMO7ZOZ$$7???+=~=+$IIIII???$Z$7I+7?=:,:::?I+I+:,,,:~ZOD~=+?=?$=,~I$ZOIZDZ+~==7ZZ$+=======I7$O8MMMNMMMMMDNN8D=:,,,,,,,,,,,,,,|                  |\n|                   |:~D$ZDMMMNNNNMMNND$$OZ$$7I?+=~:~=I=+?IIII7??7I?=~~~::::,7=I?=:,,,::??8:~:+$7=::I7OOZ$:788I+++??7ZDZI?I7Z88DDNMMMNMMMMMMDD887:,,,,,,,,,,,,,,| X--X X--X X--X   |\n|                   |:IO=ODNMMMMNNMMMO$Z$ZZ$77?+==~::~=7++??++++=~~:::::::::IZ=??~::,,,:++O~~~~~=??+$IND7Z:=$O8I+?DDZN+====+++I$ZNMMMMMNMMMMNDNO8=,,,::,::::::::| X--X X--X X--X   |\n|                   |:?Z+ODMNNMMMNNDDI?7ZOZ$7I?++=~~~~=+?++++===~~~::::::::7I?=I+~:,,,:~==Z?~~~~===+++=IIIII?++I+7?+=~~~~==++II$ODDDNNMMMMMMMDNDZ~:::,,:::::::::| X--X X--X X--X   |\n|                   |::$7ZDNNMMMMN8ZZ??IZ8$77II?+=~~~~=++7======~~~~:::::~$:~=?+=::,,,:~=??7:~~::~===++??++?+=~~=+~::::~==++?II$Z8D8DNMMMMMMMMDNO$~:::::::::::::|                  |\n|                   |,:::IDNNMMMMN8$7??7OO$7II??+===~====~=?~~~~::::::~~I~~~~=?=~:,,,,:~====I~:::::~==++++++==~~?~:::::~==++?II$ZOO8ODDMNMMMMMMDD8Z=::::,,,,,,,,| X--X X--X X--X   |\n|                   |:::::ODNNMMMNDZI?I7OZ77II???+======~=~~~+=~~~~~~?I~~~~==++=~:,,,:~~==~++7~:::~~~~======~~~I~~:,:::~==++??7$$$Z8Z88DNMMMMMMD8OO7=:::::::::::| X--X X--X X--X   |\n|                   |,,,,:=DDNNDMNNO?+?7Z$7IIII?++=====~~~~~~~~~:::::::~~==+++=~:,,,,::~==~:+++7:~:,:~~~=~~==Z~~:,:::~:~==+???77IIZ8ODO8DNMMMMMND88I~~::,,,,,,,,| X--X X--X X--X   |\n|                   |:::::,?DNNDMDOZ=~+7Z77IIII??++===~~::::::::,,,,,,::=++I7I=::,,,,:~~===~~?+~:==:,,:~~?$~~:::,::::~~==+++?III?I8OOO$$8DMMMMMND8OO?,,,::::::::|                  |\n|                   |:::::,:IODD888Z?=?7$77IIIII?++===~~:::::,,,,,,,,,:~=??I7?=::,..,::~=??+++===~~~~:~~~~~::::,,:::~~~~=+++??IIZOOZZZZ7ZNMMMMMNNDD8Z+::::::::::| X--X X--X X--X   |\n|                   |::::::::+$OO88OOII7$777III???+++==~::::::,,,,,,::~=?+~=?=~:,,..,::~=??++~::::::~~~~~:::::,,::::~~~=++++++?D8OOZZOZ7$NNMMNMMNNN8O$I=:,::::::| X--X X--X X--X   |\n|                   |:::?::,,:~?+O8D$OZ$$7IIIII????+++=~~::::,,,,::::~=I?=~=I=:,,,..,::~=~~~+~:,,,,,,,,,,,:,,,,,::::~~~=++++++?OOZZ$Z$ZZO8NNNNNNMDNDZZI=+:::::::| X--X X--X X--X   |\n|                   |:::$I~,:,:~IZOD8IZ$$77IIII????+++=~~:::,:::::~~~=?7+=~+?=:,,,..,:~~=:::~~:,,,,,,,,,,,,,,,:::::~~===++++++?ZZ$$777$O8D8MNND8NNDDO$ZZ+:::::::|                  |\n|                   |:::~7II?7?I$88DN77$77IIIIII???++==~~~~::::::~===+?Z$7777+~:,,,,,:~=+~~~~=~:,,,,,,,,,,,,:::::~~~~===++++++?I$7I??I88DDDZNDNDII$Z8OZI7I::::::| X--X X--X X--X   |\n|                   |,,:::I,:=7ZZOODM$?7777IIII????+++=~~::~~~:~~~==~~?$OOO88$+~~~~~~=?77II?+?~~:,,,,,,,,,,::::::~~====+++?+++????~~~?8Z8ND88DN8D$?:~+I7I~~:,:::| X--X X--X X--X   |\n|                   |::::,::OOZZ$ZOOMII7$77IIII???+++====~:~~:::~=~::~+7ZO88O8Z7?+++I$ONMNDZ$?~::~,,,,,,,,,::::~~~~====++++++++?=~::~$Z8DN8D88D888O7:,,::::,::::| X--X X--X X--X   |\n|                   |::::::$ZZZ$OOODNZI$777IIII????+++===~~~::~~~:,,::=?7$ZOOOOOZ$$ZODD88OZZ$:,:::::::::::::~:~~~~=====++++++++I=~:=?ODDDNDD8O88OZZ8I:::::::::::|                  |\n+-------------------+::::,=$77$$$ZOODNOZ77I7III????++++==~~~~~~::,,:::=+?I$$ZOO88888O$III??+~,,,,:~:::::,:~:::~~~=====+++++++++?===7$88N88DZD8Z8O$7I7Z?~::::::::| X--X X--X X--X   |\n|                   |,,,,::7$OOZI?I$8MM87777III????++===~~~~::::,,:::~~=+?I77ZZOOOOZI+=~~~:::,,,,:::::::::::~~~~~~~~~=++++++=+?I?7+=$888DZO$OOOOZOZZI=~:,:::,,,,+------------------+\n|                   |:::::,78OO$7$Z88DMD7777III?????+=~~~~~~::::::,::~=~~++??I7$$77I+~:::::::,,,,,,:::::::~::~~~~~~~==+++++==++?~~~ZNDOO88Z7I?I$$7I=::::::::::::|Properties        |\n|                   |:::::,?ZOZ$7II$ONMD$777IIII??+++=~~~~:::::,,:::~==~~~~~==+??++=~::::::~::::::::::::::~~~~~~~====+++++++++++::ZNNNDD8O88$77II+~:::,::::,::::+------------------+\n|                   |,,,,:,7O8Z$$IIZ8NMN$$$777III?+++=~~~~~:::::::::~~~~~~~~~:::::::,,,,,,:::::::::,,,::::~~~~~~~~~~~=+??+++++??::8NNND88O7$ZZZ$?~:::::,,,:,,,,,|                  |\n|                   |::::::=Z7OZ7$ZZ8NMM$$$$777II??+===~~~~::::::::~===+???III?=~::::::,,,,,:::::,:,,,,:::::~~~~~=~~=++++++++??+~=NNN8D8ZZZ?=~~:,:,,,:::::::::::| Brush Size:      |\n|                   |::::,:,:=ZI8ZZ$NMMM$$$$$$7III?+===~~~~:::::::~=?7Z8DD88888D8OOZ$7I?+~::,:::::,,,,,,:::::~~~~=====++?=+++?I77DMNNDO8OOZZ$I~:::::::::::::::::| +-------------+  |\n|                   |,:,,,,,::7$?8DONNMMZ$$$$$77II?++==~~~~:::::~?7ODZ?==~:::::::::::~IZNO$?+=::,::,,:,::::::~~~==~==+++?++++?IMMMMMNNDDDD8Z$77$I::::,,,,,:,::,:|                  |\n|                   |::::,,,,,=77O88NNMMZ$ZZ$$$77II?++=~~~::::~~=++=+++==~::::,,,,,,,,,:~+?$OO7?=::::,::::::~~~===~==+?+?++?+I$MMMMMMNDD8OOOO$I?~:,:::::::::::::|                  |\n|                   |:::::::,,:=~I$8NMMMD$ZZZ$$77II??+==~~::::::~~==+?II?+++++++====~~~~~~~====IOZI=~::::::::~~==~===++++????IOMMMMMMNN8DOOOZO7+,:::::::::::::::| Brush Shape:     |\n|                   |:::::::::,,=~=ZDNNMN$ZZZZ$$$7II??+==~::::::::~~~==+?II7$ZOO88888Z$7I?++==~~~~:~~~::::::~~=======+???????IDD8MNNNDNDD8$??+~:,:::::::::::::::| +-------------+  |\n|                   |,,,,,,,,::,:,:,ZODMMZZ$ZZZZ$7III??+=~~~:::::::::~~~~~===++=++++?+=~=~~~~:::::::::::::~:~~======+?I7II???IN88O8O888D88Z++:::,,,,,,,,,,,,,,,,| | Circle     V|  |\n|                   |::::::::::::::,:I8NMOZ$ZOZZ$$$77I??==~~:::::::::::::::::::~~::~:~~~~::::::::::::::::~~~~======+?II7I???I7D8O7O7O888D8O~::::,:::::::::::::::| +-------------+  |\n|                   |:::::::::::,:::::~?N8ZZZZZZZZ$77II??+=~~~~~~:~~::,::,:::~:~::::::::,::::::::::::::::~~~=====++?I7$7???I7Z8OOO8ZO88DDO7~:::::,,:::::::::::::|                  |\n|                   |,,,,,,,,,,,,,,:::,:=$ZZZ$ZZOZZ$777I?+==~~~~~::::::,:::::::::::::::,,,:::::::::::::~~~~===+==+?I$$$I???I$O888DN88D88OZ+:,,,,,,,,,,,,,,,,,,,,|                  |\n|                   |::::::::::::::::,,,::OZZZ$ZOZZ$$$777I?+=~~~:::::::::::::,,::,:::::::,,,,:::~~~::::~~===+++++?I7$7II?II77DDNNND8DND87+~:,,::::::::::::::::::|                  |\n|                   |::::::::::::,,:,,,,::$ZZZZZZOZZZ$$$77I?+==~~~~~:::::::::,,,,,,,,:::,:,,,::::::~~~~~===??+??I7$Z7I????I77NNMMNNNMDDZ?:,:,::::,,:::::::::::::|                  |\n|                   |,,,,,,,,,,,,,,:,~78NNDZZZZ$ZOOZZZZZ$$7II++===~~~~~::::,,,,,,,,:,,,,,:::::::~::~~~=++++????IIZZ7I?+??I7$OMMMMMNNNNN8Z7:,,:::,,,,,,,,,,,,,,,,|                  |\n+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------+------------------+\n| Ready...                                                                                                                                                                  100%   |\n+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+\n"
  },
  {
    "path": "doc/ideas/editor/terrain.md",
    "content": "# Terrain editor\n\n## General\nThe terrain editor is in fact a few toolboxes and an engine graphics context in the middle. The toolboxes contain terrain tiles and objects to be placed on the map. An object is hereby placed by a terrain brush, which size is adjustable. Imagine Bob Ross painting trees. Exactly like that.\n\n## Toolboxes\n*Note: As of writing, the engine has no concept of height. Therefore, it will be ignored here as well.*\nThe terrain tool boxes may be divided in subgroups with descriptive names. Also, there should be distinguished between game objects (such as trees or buildings) and the flat terrain tiles (like grass, water or dirt).\n\nNote also that not every object is valid on every terrain tile. For example, a footbridge needs water underneath and a non-water, accessible tile on the connecting side.\n\nThe toolboxes should be filled with preview images of the property they represent. A informative description may be present, but must be short. I believe that it is easier to find the correct visual by simply clicking on what i want to paint rather than having to figure out what $creator meant with $description.\n\n## Painting\nThe painting area is where terrain manipulation happens. It is essentially an graphics area from the engine, but has a grid painted over it. Once something from the toolboxes is selected, moving the mouse over the painting area outlines the size (`n` tiles diameter, `n*m` tiles wide) and shape of the current brush (square, circle, ...). Clicking (and holding while moving) paints all tiles within the current bounds with the selected terrain tool.\n\n## UI Layout\nSee [terrain-ui.txt](terrain-ui.txt) for a layout draft.\n"
  },
  {
    "path": "doc/ideas/editor/units.md",
    "content": ""
  },
  {
    "path": "doc/ideas/fr_technical_overview.md",
    "content": "Technical overview for requested features\n--------\n\nThese are some thoughts for a feature rich mode. Don't worry, there will be a vanilla mode without all that fancy stuff just feeling as good as this famous 20-years-old game. So every fancy feature changing the gameplay drastically could be also disabled by users.\n\n\n### Explanations:\n\n1. ITEM: A description of the item\n2. COMPLEXITY: subjective evaluation for the complexity of the implementation\n3. AFFECTED ENGINE PARTS: core engine modules that needs to be worked on, e.g. renderer, API, converter\n4. IMPLEMENTATION STATUS: links to pull requests or dependencies\n5. MILESTONE: Which milestone this feature will appear in\n\n\n|   Type           |  Task No.  |   Item        | Comp            | Aff. Engine   | Imp. Status   | Mile           |\n| ---------------- | ---------- | ------------- | --------------- | ------------- | ------------- | -------------- |\nGAIA               | G1         | Dynamic change of weather, seasons, day and night. Influence on terrain & line of sight (LOS). could be influenced by players with monk/myth unit.\nGAIA               | G2         | Wooden buildings and other flammable material can be lit, fire spreads around your city. fire departments (new building) to extinguish fires or villagers need to put out a fire of with water from a well (new building)\nGAIA               | G3         | Wild animals can steal food from your farms and food resources\nGAIA               | G4         | Wolves form pack and act together, wander the map, circle around towns, get hungry and seek food (deer, villagers, wild boar, bear, etc.).\nGAIA               | G5         | Renewable resources such as trees and animals that repopulate. Forest regeneration or seeding.\nGAIA               | G6         | Buildings in later ages cost stone with possibility of deep mining stone. villagers can dig/mine natural resources.\nGAIA               | G7         | Better fauna (Tapir, Tiger, ...). Improved animal colours\nGAIA AI            | GI1        | Fish migrate and replenish.\nGAIA AI            | GI2        | Mercenaries could be hired on behalf of the player\nGameplay           | GM1        | Roads on commonly traveled areas allow faster travel. Pathfinding would let the units stay on the road. Appearance and quality depends on traffic (matted grass, dirt path, etc.). Neutral to all players. Turns soft/regular terrains into roads, requires stone, does not work on irregular terrains; grants movement speed to all allied units + passive vision (like a foundation spots units walking over them).\nGameplay           | GM2        | Bridges for connections between islands. Blocks ships, allows land units to walk on. Terraforming. Wooden and stone bridges with different hp.\nGameplay           | GM3        | Canals: creates empty, shallow and navigatable water.\nGameplay           | GM4        | Single units can hide/bunk in the forest, only visible to a certain type of unit. Garrison function with no symbol, you can lose your units if you don't remember them.\nGameplay           | GM5        | New Unit: spies (can burn down your city), lame your production\nGameplay           | GM6        | New Unit: monk ship on water to convert ships\nGameplay           | GM7        | New Unit: transport waggon on land to slowly transport ships over land\nGameplay           | GM8        | Villagers need sleep at night, die at work (New Building: Caffeine industry)\nGameplay           | GM9        | GAIA and terrain interaction, e.g. flock of bird rises up from tree when tree is felled or many people approach, hearable sound indicator, players hear and see birds when someone comes to their woodline at home. Triggered rocks falling of cliffs.\nGameplay           | GM10       | Relics have special abilities (AOM), bonuses for attack and range. Generated randomly. Maybe also rolling relics with ranged attributes to bring into battle.\nGameplay           | GM11       | Bring Kings/Queens into battle for special abilities within some tiles radius.\nGameplay           | GM12       | Building produces in loop (AOM) as long as resources and pop space are available. Useful for unit prudction and farms in mill. Indicator over buildings or somewhere in the GUI for loop production. Toggle reseeding on/off.\nGameplay           | GM13       | Mills can produce sheep, cows, turkeys\nGameplay           | GM14       | \"Conversion is enabled by researching Faith. Before, monks can only carry relics and heal units.\nGameplay           | GM15       | Buildings can be deconstructed to recover materials, slower than destroying. Leave a salvage pile behind. Salvage diminishes over time. Salvage can be collected by villagers at a higher rate than other resources.\nGameplay           | GM16       | Buildings can be captured when damaged to 20% or less. Player who repairs over 20% owns the building. New units can be produced out of this captured building from 50% on.\nGameplay           | GM17       | Attack/defense bonuses for some unit formations. Dependent on minimum amount of units in this formation. New marching formation with speed bonus but defense malus. Cavalry flanking.\nGameplay           | GM18       | Researches only affect newly produced units. Old ones need to be manually upgraded/retrained at production building.\nGameplay           | GM19       | Scale wonder cost to max pop, number of players and available resources.\nGameplay           | GM20       | A new 5th \"free trade\" age where allies share resources and give each other massive battles.\nGameplay           | GM21       | New unit: Lance Cavalry, run over units in defending formations\nGameplay           | GM22       | Researches can be globally and locally applied. e.g. upgrade just units garrisoned in the barracks for less research cost; e.g. you need 3 pikemen for the battle now, but you don't want to produce them in the future in masses (see GM18)\nGameplay           | GM23       | Units can level up by gaining experience doing what they normally do, fighting in battle against their counter unit will give more XP against that unit style. Villagers chopping a Forest Nothing map might be a real pro at chopping trees at the end of the game e.g. one experienced villager could be as productive as three unexperienced villagers in the end of the game.\nGameplay           | GM24       | Queue for technology researches.\nGameplay           | GM25       | Farms cannot be placed on desert ground.\nGameplay           | GM26       | Improved water battles.\nGameplay           | GM27       | Percentage based armor system similar to AoEO and AoM. Will need to rebalance whole gameplay.\nGameplay           | GM28       | Entirely rebalance milita line. Main gold unit similar to knights/archers. Instead of a counter unit currently.\nGameplay           | GM29       | Nerf the range on towers early and make them scale much better into the late game.\nGameplay           | GM30       | Unit queuing doesn't block resources. Resources are taken in account, when the unit is actually starting to be produced.\nGameplay           | GM31       | Make denying upgrades more easy, like in SC2. Often players will destroy buildings to cancel an upgrade in progress and gain the upgrade advantage. Maybe in a special mod with less hp for buildings.\nGameplay           | GM32       | Possibility to put archers/ranged units on a wall/castle to get elevation bonus.\nGameplay           | GM33       | Quickwalling: first XX% (variable percentage) of a foundation is walkable, setting for e.g. tournaments\nGameplay           | GM34       | Mesoamerican civilizations (already the most novel civs in the game) may have a unique tech tree. Possibility for a unique tech tree in API.\nGameplay           | GM35       | Controlled forest fires: Allows the player (technology researched at the university in the castle age) to destroy forests to create more space; this goes much faster/efficient than chopping, but obviously at the expense of the lumber and generates regular terrain.\nGameplay           | GM36       | Give siege rams an upgrade that prevents unit production while it's attacking a building. (5-second debuff that refreshes each attack).\nGameplay           | GM37       | Bigger maps, map sizes between giant and ludicrous\nGameplay           | GM38       | Foundation: turns regular terrain into solid terrain, requires stone, turns irregular terrain into regular terrain.\nGameplay           | GM39       | Different terrain types, take a look into gameplay.md for more explanations\nGameplay           | GM40       | Be able to temporarily toggle the map to remove terrain and show building footprints for walling without gaps.\nGameplay           | GM41       | Make rams push units away\nGameplay           | GM42       | Raise the amount of ressources that can be sold with hotkeys at the market (forest nothing)\nGameplay           | GM43       | A special flag that raises on a building that is researching a technology, much like the garrison flag. (see GR10)\nUnit AI            | UAI1       | Improved grouping. Double click on a grouped unit in group formation selects same group not all units of same kind. Allow more than 10 groups. More intelligent assigning of units to existing groups. Subgroup selections. New unit to group takes same stance as the whole group. Multi-group functionality.\nUnit AI            | UAI2       | Designated unit pings its position periodically.\nUnit AI            | UAI3       | Deselect certain units quickly from a selection by right clicking their portrait.\nUnit AI            | UAI4       | Better waypoints (AOM-style) - able to queue tasks for units, especially villagers.\nUnit AI            | UAI5       | More intelligent units.\nUnit AI            | UAI6       | Emergency evacuation points.\nUnit AI            | UAI7       | Dynamic hardlocking on target units when attacking.\nUnit AI            | UAI8       | Aggressive stance for monks (auto-convert). Auto-flee stance. Non-coward stance for villagers. Allow selecting the default stance in production buildings.\nUnit AI            | UAI9       | Set rally point on moving units.\nUnit AI            | UAI10      | Better attack move.\nUnit AI            | UAI11      | Option to allow a unit to auto-explore the map. Can be deactivated in the lobby options.\nPathing            | PA1        | Groups can move as group or on command each with its own movement speed.\nPathing            | PA2        | Auto-resolution of blocking situations\nPathing            | PA3        | Individual path searching cost for tiles. Allow users to manually mark parts of the map as more expensive/No-go areas. e.g. firing range of castles.\nPathing            | PA4        | Allow setting any number of waypoints (basically an extension of the 'patrol' mechanic)\nPathing            | PA5        | Formations and movement when attacking.\nMultiplayer        | MP1        | Finegraded options in Lobby-Settings (e.g. formations, allow/disallow functions). Vanilla mode to have them good ol' feelings. Disallow also units and buildings, e.g. bombardtower, walls, etc.\nMultiplayer        | MP2        | Boosts/handicaps such as resource multipliers for players\nMultiplayer        | MP3        | Forced no-rush (time limit or score limit until attacks are possible)\nMultiplayer        | MP4        | Official accounts on servers for everyone. PGP-Support for network of trust. Built-in multiplayer mode that is so good that it unifies the community.\nMultiplayer        | MP5        | Good matchmaking and skill-/experience points-algorithm. Ranked mode for multiplayer.\nMultiplayer        | MP6        | Better team interaction. Work/resource sharing. Temporary unit control of groups for allies. Resource and unit sharing when defeated or drop. Alternatively if a player drops all of their units move to an ally and that ally's population cap changes. Cannot work when diplomacy is enabled.\nMultiplayer        | MP7        | Tournament drafts, files, settings, replays are stored in a container. Host sets up the game, shares important (players) part of the container, game lobby allows no changes.\nMultiplayer        | MP8        | Simple and fast qualifying for tournaments.\nMultiplayer        | MP9        | Connection through in-game server browser/direct IP and connection through a browser-based protocol. Combination of dedicated servers, P2P and LAN multiplayer.\nMultiplayer        | MP10       | In-game, lag-free, high-quality voice-communication between teams, same for casters.\nMultiplayer        | MP11       | Figure out how to measure map control of someone and give a value for it. Show it in casting and spectating multiplayer games.\nMultiplayer        | MP12       | More then eight player slots for multiplayer games (planned up to 40)\nMultiplayer        | MP13       | More balance changes regularly. Mechanism to make changes globally easier. Update mechanisms. Balance crew. Meta adjustments.\nMultiplayer        | MP14       | Opt-in spectating: You can join a multiplayer even if it already started. Doesn't take a player slot.\nMultiplayer        | MP15       | Multiplayer mods looking for smurfs, trolls, racists to ban them.\nMultiplayer        | MP16       | Game modes, See gameplay.md\nUI                 | UI1        | Organizers can manage the whole tournament in the openage interface within their accounts. Drafting, (Time-)Planning, Invitations, Promo, Hosting.\nUI                 | UI2        | Multiplayer submenu should give you relevant information about most viewed live games and caster channels, upcoming tournaments, upcoming qualifier rounds.\nUI                 | UI3        | Spectator layer for public watching. Lock to casters viewpoints or freely move camera. Chat with spectators and mark points of interests (POI) on the map. No sync needed. Spectator dashboard.\nUI                 | UI4        | Casters interface: Picture-in-picture mode and replay function (command for making a clip and queue it). Pinging on minimap. Marking POI. Casters views need to be synced. Show casters current camera positions on minimap. See which units are visible to players without FOG. Second window for casting preferences and overviews. Caster dashboard.\nUI                 | UI5        | APM should be derivable for interfaces and also be show in the GUI.\nUI                 | UI6        | Customizable hotkeys for everything. But good hotkeys from the beginning.\nUI                 | UI7        | Help spotting units which are target  of a conversion. Highlight monk and unit.\nUI                 | UI8        | Better after-game analysis. Normal mode with the basic information. Advanced mode with a massive amount of statistical infos and easy to read diagrams for smallest things. Population graphs AOE3-style at the end of a match. Show number of villager kills and lost resources (villagers carrying resources), maybe even in-game (caster- and spectator mode).\nUI                 | UI9        | Copy and paste for building structures, e.g. select mill and the farms around it, copy and paste the foundations of them somewhere else. Same for production lines. (Factorio-style)\nUI                 | UI10       | Show research and production status in the top right corner to easier follow your running researches.\nUI                 | UI11       | Show how many villagers are working on each ressource (like in AOM, 100 Gold / 4 \\[Villager-Symbol\\] --> 100 Gold in the bank and 4 vills working to mine gold).\nUI                 | UI12       | Add resource gain per minute data next to the original resources. It would be something like food: 537 (+68). make it switchable with amount of vills working at all the resource spots. (see UI11)\nUI                 | UI13       | Hotkeys for technology researches and upgrades for units\nUI                 | UI14       | All these things in here should be switchable-on/off in the options.\nUI                 | UI15       | MP-GUI: game-browser in list-style but with HDs lobby filtering, amount of players? search bar for game names. better filters for games.\nUI                 | UI16       | Save Camera positions: Ctrl+F1 to create a camera location hotkey similar to how you create control groups. Whenever you press the key it moves your screen to the exact location you created it. Also for spectating and casters.\nUI                 | UI17       | Looking for a SC2-Interface in AOE-style? UI-Modding should be made easy.\nUI                 | UI18       | Tooltips should be improved to properly showcase all the hidden information and bonuses for players who don't want to deeply research every facet of the game by themselves.\nUI                 | UI19       | Host of lobby to ban civilizations, give handicaps\nUI                 | UI20       | Idle villager button change colour when a villager goes idle\nAI                 | AI1        | Handle units more intelligent.\nAI                 | AI2        | Don't build in hazardous areas. Send military to clear out.\nAI                 | AI3        | Better and more intelligent building placements, e.g. build more gates to prevent lock-ins. Better castle placement. Better market placements for trade in the late game.\nAI                 | AI4        | Military units shouldn't fight in castle arrow fire.\nAI                 | AI5        | Units marching react and don't just go away.\nAI                 | AI6        | AI lames you (as the Barbarian AI). Understands how to do economic damage to your base. Adopts more to the current playing meta and also randomly choses to use older metas.\nAI                 | AI7        | AI can deliver villagers with transportships to other islands.\nGraphics           | GR1        | Zooming in and out with different settings: middle of screen, where mouse points, to selected units.\nGraphics           | GR2        | Units throw torches to put buildings on fire instead of fighting against them with their swords. Villagers have different attack animations depending on where they were working before attacking units/buildings, e.g. farms -> hay fork, lumberjacks -> axe, stone or gold -> pickaxe.\nGraphics           | GR3        | Animations for construction and destruction of buildings.\nGraphics           | GR4        | Unique military and economic unit skins for each different region. Knights from Europe shouldn't look like the ones from Japan, but should be similar enough to tell they are both knights. At least 4 regions for unit graphics.\nGraphics           | GR5        | Beautiful weather effects (see G1)\nGraphics           | GR6        | Campaign cutscenes\nGraphics           | GR7        | Some graphical optimisations for competitive play by design, e.g. small trees, alignment grid, fish outlines, short walls, etc., other option: to redesign everything that the problems why those mods exists are fixed. e.g. make the look behind walls better by design -> no short walls needed.\nGraphics           | GR8        | Resource spots change size based on the amount of resource they have left.\nGraphics           | GR9        | Sync ram attack animation with the actual attack.\nGraphics           | GR10       | A special flag that raises on a building that is researching a technology, much like the garrison flag. (see GM44)\nSound              | SO1        | Options to change the volume of special game sounds (e.g. attack bell, farm depleted, units produced). In late game this sounds can get really annoying.\nCampaign           | CP1        | Co-Op Campaigns\nLocalisation       | LC1        | Placeholder\nModding            | MD1        | Placeholder\n"
  },
  {
    "path": "doc/ideas/gameplay.md",
    "content": "Gameplay improvement ideas\n==========================\n\nThis file contains ideas to \"enhance\" the original Age of Kings gameplay.\nDon't worry, vanilla mode will always be available.\n\n\nEnvironment\n-----------\n\n### Weather\n\n - Change view distance (Line of sight / Fog of War)\n - Fog, rain, winter, summer\n - Unit slowdowns (depending on terrain)\n - Can be predicted by tech\n - Can be modified globally or locally (by voodoo priests)\n\n### Fire\n\n - Buildings made out of wood and other flammable material can be lit.\n - Fire departments in the city can extinguish those fires (Stronghold anybody?).\n - Sneaky spies could burn your city down\n\n### Day/night cycle\n\n - At night: View distance reduced, everything darker.\n - Villagers need sleep, otherwise they die at work.\n - Chemistry industry: Caffeine, Penicillin, ...?\n\n\n### Forests\n\n - A single villager can hide (per some tile area) in the forest for sneak attacks. Maybe can only visible to a certain type of unit. But invisible to other villagers (until they pop out and start building stuff)\n - Gaia flock of birds rises up from a tree when startled, when tree is felled or when many people approach and fly off-map, sound indicator hearable a little bit more far (players know when someone goes near a woodline at your starting base -> maybe towering?)\n\n### Gaia Units\n\n - Bear\n   - could have forage sights, farms, and shore fish as locations that it will try to \"eat\" from\n\n - Mercenaries could be hired in the wilderness at a camp to act on behalf of a player\n   - not just buying a unit for gold, resources have to be determined\n   - not really another unit to control for a player\n   - could be kind of an extra scout, if you lose yours\n   - or three units relatively weak but in replacement of the militia\n     - you can send them to raid to your oppponent\n\n  - Wolves\n    - will form a pack and will act together\n    - Wolves could wander the map, making efforts to avoid (circle around) \"towns\"\n    - they should get hungry and seek food (deer, villagers, wild boar, bear, etc.)\n    - while hungry, they should attack in a pack, kill something, and \"eat\".\n    - when not hungry, there should be a chance of attack but it should be more likely that the wolves will avoid contact and move on\n    - Wolves will howl from time to time. Players will be able to judge the severity of their \"wolf problem\" based on the frequency of these howls\n\n### Terrain Types:\n\nSoft terrains (= marshland/desert): farms, mills, l/m camps, palisades can be built without problems, stone walls and medium-sized buildings (3x3) take +50% damage on this terrain, and military units move 20% slower on this terrain.\n\nRegular terrains (= grass/dirt): farms, mills, camps, palisades, stone walls, medium (3x3) and large (4x4) buildings can be built without problems, heavy structures like towers, gates and castles take +50% damage on this terrain.\n\nSolid terrains (= rock/foundation): farms, camps & palisades cannot be built on this terrain, all other buildings without problems.\n\nIrregular terrains (= beaches/mountains/swamps): nothing can be built on these terrains, military units move 20% slower on these terrains.\n\nRegular terrain makes up most of the player starting areas, with soft and rock terrains becoming more common as you go further from the player starting areas and irregular terrain makes up larger spaces of the map in-between the player starting areas.\n\n\nResources\n---------\n\n### Infinite regeneration\n\n - Forest regeneration or seeding\n - Animals reproduce as long as there are two of the same kind left on the whole map\n\n### Stone balancing\n\n - Stone cost for all buildings, rebalance stone amount per pile\n - Maybe new deep stone mines for later ages\n\n### Salvage\n\n  - salvage pile containing xx% (configurable percentage) of the resources originally used to construct a building appears any time a building is destroyed or deconstructed\n  - resources in this salvage pile will gradually diminish at the rate of 1 unit of resource per 10 seconds of game time\n  - resource piles can be harvested by villagers or pillage capable units\n\n### Deconstructing buildings\n\n  - Buildings can be deconstructed to recover raw materials\n  - and also deleted: doesn't recover material, but is faster\n\n\nMap extensions\n--------------\n\n### 3D terrain\n\n - Villagers can dig down, can discover natural resources.\n - Dirt, stone, etc are new resources, can be placed elsewhere\n - (add any minecraft/terraria-like stuff here)\n\n### Infinite maps\n\n - Maps with infinite size! (Your PC is the limit!)\n - Spherical/Toroidal/Cylindrical surfaces would be possible, too.\n\n\nResearch\n--------\n\n - Only newly-built units receive the bonus when units are upgraded\n - Already-built units need to be \"overhauled\" for a cost back at the barracks.\n   (reminder: just an idea, there will be vanilla mode)\n - Research should be placed in creating order, like in WarCraft. It's too annoying\n   to click on building again after each research\n\n\nCapture buildings\n--------\n  - when a building is damaged to 20% or less, buildings will eject any units garrisoned, cancels all research or training and cease any inherent behaviors (i.e. towers will no longer attack)\n  - a building which has been damaged to 20% HP or less will become the property of the next player who repairs it to a level above 20%\n\n\nGame modes\n----------\n\n### Zombie Survival Mode\n\nThe map has a bunch of zombie spawners; after an initial build-up phase, they start producing waves zombie hordes, fast zombies, tanks, petard zombies; the zombie AI tries to get to your villagers (magic pathfinding); killed units turn into zombies as well. spawners can be destroyed, but the closer or the more powerful you get, the more zombies will spawn. To win, survive for a specified period of time, build a wonder, or destroy the spawners (or something...).\n\n### Conquer the castle\n\nSimilar to \"Regicide\", but with a building. Players have some time to fortify their castle. A player loses if the castle falls.\n\n### Barbarian Invasion\n\nAn asymmetric game mode where 1 or 2 technologically advanced players compete against 3 - 6 \"barbarians\". The barbarians start in Dark Age with a few extra villagers, but have to build up their economy from the ground up. The \"civilized\" players start in Castle Age with a large amount of villagers, a decent economy and some defensive buildings. For barbarian players, the goal is to destroy a wonder in the civilized players' cities before a time limit is reached. The civilized players have to build defenses and try to protect against the hordes that are pressing at their gates.\n\n### Phantom Mode\n\nA mode similar to *Trouble in Terrorist Town* and *Secret Hitler*. The game starts with all 8 players being neutral or allied. Their goal is to find 2 \"phantoms\" and eliminate them. The 2 phantoms are allied from the start and know about each other, while every other player is clueless whether other players are friendly or phantoms. Phantom players have to manipulate the others into distrusting their friends. Gameplay could be spiced up with feature like purchasing units which don't have player colours from a town in the middle of the map, sabotage units and limiting the allied line of sight gained from researching cartography.\n\n### Pure Battle Mode\n\nNo buildings, just units. The game generates a map and players can choose a starting position. Then they have a few minutes and a set amount of resources to select an army composition and some techs. After the first phase is over they place their units on the battlefield and have to use what they assembled to destroy their opponent. Utilizing height advantages, microing and tactical positioning contrast the strategic decisions of creating the army. The player who destroys their opponent, inflicts the most resource damage to others or holds strategic positions wins the battle.\n\n### Micro-nerd Mode (or Mod)\n\nMixture of RTS (Age of Empires) and Tactical Stealth (Shadow Tactics) will require the players to use features of both genres to win a game. Gameplay could look like the following: In the beginning you start as usual the game and build up an economy (macro). As soon as you need to scout the opponents base the parts of tactical stealth come into play. Units can be used with special, advanced micro commands (formations, special abilities for single unit types). You can switch into this mode explicitly, after switching everything which is not important to this mode turns grey/dark to filter out your attention. In the ultra micro mode you could see hidden paths in the woods on which you could sneak to the opponents base. There you could carry on sabotage the economy and military production of the enemy. For example you could distract villagers from wood cutting or gold mining (slow down the working speed or make them carry less resources) or spy on production processes of military units. With this you have another level for micro-nerds and another way to fight against people who just wall themselves in and boom behind.\n\n### Random Ideas\n\nExtra options like regenerating gold, wood, and stone could be interesting for casual multiplayer games. Goofy game modes like randomized techs and bonuses would be fun. Or what about a mode where you gain random civ bonuses and techs each time you advance an age?\n\n## Tactical Unit Comparator\n\nA game mode where two players assemble an army of AI controlled units that are pinned\nagainst each other in a fight to the death. The player with the surviving army wins.\n\n\nUnit handling\n-------------\n\n### Improved grouping\n\n - Manually coloring units (e.g. for teammates)\n - \"Mobile ping\": designated unit pings its position periodically\n - Implicit group definitions/reselections (double-click?)\n\n### Better movement\n\n - Move groups as group, or each unit with its own movement speed\n - Auto-resolution of blocking situations\n - Individual path searching cost for tiles\n  - Allow user to mark parts of the map as more expensive\n  - Auto-set a higher path cost for tiles in the firing range of an enemy castle\n - Allow setting any number of waypoints (basically an extension of the 'patrol' mechanic)\n - Formations:\n  - Attack/defense bonuses for some unit formations.\n  - Dependent on minimum amount of units in this formation.\n  - New marching formation with speed bonus but defense malus.\n  - Cavalry flanking\n\n### More intelligent units\n\n - Restrictions for action areas (don't do anything here)\n   - Wood chopping\n   - Castle rampages\n   - Avoid hazardous areas during pathfinding (via a cost modifier)\n     - Automatically for stationary hazards (castles, towers, ...)\n     - Manually \"painted\" areas\n - Emergency evacuation points\n - Formations and movement when attacking\n - Dynamic hardlocking on target units when attacking\n   - Don't lock on unreachable current target\n   - Attack the blocking units first\n\n### Better attack stances\n\n - Aggressive stance for monks (auto-convert)\n - Auto-flee stance (especially for monks): Auto-task \"flee\" action as soon as an enemy unit comes near\n - Non-coward stance for villagers\n - Allow selecting the default stance\n\n### Viewcone-feature for additional complexity\n\n - a viewcone shows in which direction the unit is looking at and how much it can see from that\n   - more complex line of sight (LOS) than 360°-LOS\n   - e.g. a unit cannot see behind buildings -> you can hide units behind buildings from auto-attack, as long as the player is not  manually giving the attack command\n     - you could even render them invisible as long as the player didn't see them and they could vanish after a time when they hide behind a building\n   - scouting would be more focused -> only scouting in the direction your scout looks/moves, rest stays under FOG\n - possibility to even have completely other gameplay styles/mods (like Stealth-RTS, see Micro-nerd Mode)\n - Kosmonautblog wrote an article about the viewcone in Shadow Tactics and how the developers were implementing it, you can find it here: https://web.archive.org/web/20190428103242/https://kosmonautblog.wordpress.com/2017/01/09/shadow-tactics-rendering-breakdown/\n\n\nMultiplayer\n-----------\n\n### Lobby settings\n\n - Most of the above should be available as lobby settings\n - Boosts/handicaps such as resource multipliers for players\n - Forced no-rush (timelimit or score limit until attacks are possible)\n\n### Matchmaking and competitive mode\n\n - Some kind of \"official\" account on servers for everybody\n - Automatic skill groups to create balanced matches\n - Scale wonder cost to max pop and number of players (make wonders great again)\n - We can show the world that competitive games can be free open source!\n\n### Tournament mode\n\n - tournament settings (SQ/MQ, etc.) are stored in a container (importable file/or directly in the server environment depending on the architecture of the multiplayer)\n\n   - the container stores direct link to download maps from an openage community servers for this tournament (no p2p-sharing of maps with different versions anymore, still possible in private games)\n\n   - container will contain also the drafting, which will become static during ongoing tournament\n     - before the tournament the civ, team and map drafting is handled directly by the community server and stores information inside the container\n     - admin changes still possible for special use cases\n   - when starting the tournament every player will download the important information from their pre-chosen and fixed parts and everything will be pre-configured and grey (if no spontaneous changes are allowed)\n\n   - this brings the possibility to bring all the systems taking part in the tournament to one comparable and static setup (no admin RE because of forgotten MQ anymore)\n\n     - Regarding tournament settings, I would personally prefer to let the tournament host setup the game and the settings while players would just be \"pulled\" into the game lobby without having the ability to change the game's settings. Counter-Strike has a similar method of organizing tournament games. (_ColonelPanic_ @reddit)\n\n   - everything regarding the tournament (replays, civdrafts, etc.) should be stored in this container after the event (so you could easily import it into your OpenAge and replay games -> also see 2. Spectating/Casting-Mode)\n\n - as an organiser of a tournament you should have the possibility to manage the whole tournament inside the openage platform (not: making promo there, drafting here, hosting matches there, and so on)\n\n   - also the possibility to directly invite a player from your openage account based on characteristics (like direct invites for 1v1 Top15 ladder tournament or teamgames)\n     - teams can connect their accounts to real clans reaching together a team ELO depending on the combination of actual players (TatoH + DauT + TheViper > TatoH + DauT + Slam)\n\n - make it easier to qualify for your tournament (e.g. as a potential player -> Open \"Tournaments\" in multiplayer menu -> open \"Qualifiers\" -> Play games with fixed tournament settings -> maybe qualify for Tournament)\n\n - After clicking on the Tournament-Menu-Item you could have an overview of upcoming and open tournaments, with a calendar view of the next tournaments, a timeplan, the brackets and maybe even the possibility to one-click-spectate a Live-Tournament if you want (see 2. Spectating/Casting-Mode)\n\n - depending on the architecture of the multiplayer it could be even possible to spare out all that client modeling and just integrate an basic in-game browser which connects just to an openage community-server and from there you could manage everything. To play a game the user could just click on a link from the game browser which gives data to an internal protocol like openage://<game-id>/<foo>/<bar>/<link-to-settings-file>/<...>\n\n     - this would result in a thinner client\n     - but: taking too much multiplayer features out of the client leaves it helpless for private matches -> thin grade\n     - we could do both actually: connection through in-game server browser/direct IP and connection through a browser-based protocol\n\n\n### Spectating/Casting\n\n - the casting mode could be an advanced spectating mode, with more features than just replaying/live-spectating the game\n   - Spectator layer: For the general public watching the game. This could be a mirror of the casting layer that the casters see. You can either choose to lock the viewpoint to one of the casters' cameras or move the camera freely around the map. Also you could have the ability to chat with other spectators and draw notes on the map, mark your own points of interest. Sync is not as important here.\n\n\n - ZOOM! (also in-game not just for casters)\n - maybe even turning the game in 90° angles\n   - the old models could stay 2D and just be fit into the turned environment(?)\n\n - the feature set of CaptureAge could be understood as a spectator mode\n   - if they add Picture-in-Picture and replay-functions goes more into direction what's understood in this context as casting mode\n\n - casting mode should help you to either work on your own or collaboratively work together with one or more Co-Caster(s) on the presentation of the actions in a game\n\n   - Casting layer: For casters and co casters. Could support features like pinging something on the map for the co caster, marking points of interest/units/buildings, show picture-in-picture views of the other casters and so on. An important requirement would be that the casters' views are synced.\n   - casters should have the possibility to join a live-game together as a caster-team\n   - e.g. two separate openage accounts could be temporarily bridged (master-slave-principle)\n     - they both work together at the master-device (streaming device from main caster), each caster stil has it's own independant view of the game\n\n   - there should be an option to cut scenes together in a live game, so that if one caster sees an action somewhere on the map, they give a command to the game that itself clips (saves camera position and game status) it and puts it into a queue on the master-device to replay it\n\n   - outside of the main spectating window could be a second window (used for two monitor solutions) with the waiting list for the replays and all the other options and features special to the casting mode\n\n - in-game lag-free voice communication between casters\n\n - actions per minute (APM) should be derivable from game -> possible important influence on design decisions!\n\n - show current camera locations of casters (maybe on minimap)\n\n - small indicators for units that show if they are visible to another player, even if the fog of war (FOW) is turned off\n   - to get rid of the switching between the FOW of players just to know, if they can see each other\n   - see also: viewcone feature (unit handling)\n\n\n### Team interaction\n\n - Better team interaction\n - Work/resource sharing\n - Unit sharing (transport boats)\n   - give temporary unit control of a selected group to an ally (e.g. in a fight) with an easy command\n - Give resources of resigning players to allies\n   - maybe even the research of team ressources in late-imp\n   - or could be even a 5th \"basic globalisation/Hanse/free trade\" age where allys share ressources\n   - when you resign, give your buildings to your allies (original AoE devs idea)\n - Color markings\n - Create signs\n - Paint on map\n - Create arrows\n - in-game lag-free voice communication between teamplayers\n\n\n### MMO games\n\nPlay on one single map with 9001 players. Dynamically extend the map.\n\nMassively slow down the game speed, and you get a \"browser game\"-like experience.\n\n\nNew Buildings\n-------------\n\n### Bridges\n\n - Ability to build bridges in water\n - Blocks ships, allows land units to walk\n - Forgotten Empires might already have wooden bridge textures\n - Destructible, repairable, ...\n - construction per tile\n - \"terraforming\"-style\n   - build a construction into the sea and build docks & harbours at it\n - Long build time per tile\n - wooden bridge with less HP, stone bridge with more HP and faster walking speed?\n\n### Streets\n\n - Ability to build streets on land\n   - textures already exist\n - increases the speed of trade carts, vills, siege or in general land units\n - destructible just by mangonel-line or trebuchet (ground attack units) and maybe decay over time or usage, repairable (or not?)\n - uses stone, maybe 1/3 the cost of a stonewall\n\n - Ideas of the original developers:\n   - Roads improve trade by allowing trade units to move more quickly over the terrain\n   - Neutral to all players\n   - Build automatically\n   - Progressive\n   - Decay when not used\n   - Appearance and quality depends on traffic (matted grass, dirt path, etc.)\n\nNew Units\n-------------\n\n### Water\n\n - monk ship to convert ships on water\n\n### Land\n\n - transport waggon to slowly transport ships over land from one sea to another\n\n\nRelics & Kings\n---------\n\n - Relics could have special abilities like in AOM\n   - e.g. they can have attack bonuses for special units or economic/military bonuses\n   - special abilities for every relic could either be generated when the map is generated or when the relic is discovered based on the actual needs of the player\n   - so the later you go out to get the relic the more it could get useful for you, because it could be better shaped on your personal military/economy but the risk is higher, that another player was going out before you\n   - if a player scouts the relic first and the ability gets generated in this moment, it will be for the player who scouted it first, so they know, that this relic could help their own economy/military a lot so the player will try to fight about this relic against the enemy heavily -> new gameplay aspect\n\n  - Relics could have ranged attributes\n    - idea of the devs of AoE II\n    - bring the relic into the battle\n      - attributes with up to 12 range could have an big impact of the fight\n\n  - Kings could be special units that abstractly represent the player in the game\n    - could be better than average infantry units, could provide a +1 bonus to attack and defense for all of their units (not allied units) within a 5 tile radius\n  - Queen units with an somehow identical functionality, may also be added to the game\n\n\nProduction\n---------\n\n - let the last produced unit in a building be produced for an infinite time (as long as resources and pop space are available) --> AOM-style\n - same for farms (either reseed a certain number with a waiting line or an option \"reseed forever\" as long as ressources are available)\n - Mills can produce sheep, cows, turkeys, etc.\n\nFor the lulz\n------------\n\n - Big fat damage numbers!!!1\n - Friendly-fire toggle!!111\n\n\nImplement features the AoE-devs didn't do\n------------\nSee here:\nhttps://www.reddit.com/r/aoe2/comments/bg4p3m/spirit_of_the_law_stealing_villagers_and_other/\n\n\n\n - They considered having gaia mercenaries who could be hired to fight on the player's behalf (apparently using their own AI rather than direct control)\n - Destroyed buildings were going to leave behind \"salvage\" that villagers or pillagers could harvest\n - Faith allowed you to START converting enemy units, rather than providing resistance (gameplay change, first a monk is used to collect relics, after researching it can convert units)\n"
  },
  {
    "path": "doc/ideas/interface.md",
    "content": "As opposed to the gameplay improvements, these interface enhancements are pretty straightforward, so they will probably be hard-coded in.\n\n - Allow selecting more than 30 units (duh.)\n - Allow more than 10 groups (prefixes?)\n - Set default behaviour for units\n - Better re-grouping\n  - Intelligent assigning of new units to existing groups\n  - Sub-group selections\n - Customizable hotkeys for *everything*\n - Possibility to set rally points on moving units\n - Show a symbol on a unit, which is the target of a conversion (e.g. empire earth)\n - Better minimap\n    - Big version of the minimap for a better overview\n    - Use symbols for points of interest (castles, town centers, resources) instead of colors\n - Zoom\n   - Possible options \"Zoom in center of screen\", \"Zoom towards mouse cursor\" or \"zoom towards selected units\" (to figure out what makes sense in which situations)\n\n\n## After game stats\n\n - by looking at stats it should be clear how many kills were just friendly fire kills (by siege units, deleting idle vills, etc...) and how many were actual opponent kills\n - what was the lumberjack/farmer/miner/trade cart composition during game\n   - maybe a slider in the timeline trend with MIN/MAX values shown\n   - would be beneficial both to casters and to gamers who wish to improve on their skill\n   - having a concise aftergame analysis tool would be quite welcome\n\n\n## Picture-In-Picture Windows\n\nLet developers create more (non-)interactive windows in the interface that show other\nparts of the map or that can be locked on specific buildings or units.\n\n\n## Warplanning Interface\n\nPlayers can plan strategic and tactical moves on a bigger version of the minimap.\nThe players are able to draw on the map, e.g. by drawing arrows or symbols. They\ncan also highlight specific points of interest.\n"
  },
  {
    "path": "doc/ideas/technical.md",
    "content": "# Technical Ideas\n\n## Monitor Game Asset Folder (e.g. with inotify)\n\nWhen the user modifies an asset, such as a texture, the ingame models are updated instantly.\n\n-> awesome live-testing for modders\n\nFar more challenging: make this work for unit stats.\n\n\n## Built-in Snapshot Creation for Mods\n\nLet modders make a snapshot of their current build via a provided engine tool. The mod\ncan be rolled back to a previous state with this feature. Snapshots could be incremental\n(only changes to previous builds are saved) or fully-featured (the snapshot itself works\nlike a standalone build).\n\nIncremental snapshots could make use of a version control system like git or SVN, although\nthis would be limited to non-binary files only. It would require a git wrapper in the editor\nand the modding tools.\n\nSnapshots could contain information that are unnecessary for normal builds such as debug\ninformation or generated data from test cases.\n"
  },
  {
    "path": "doc/media/aoc-slp-list.md",
    "content": "graphics.drs\n============\n\n```\nGRA00364 4 Bigger Flames\nGRA00363 4 Medium Flames\nGRA00362 4 Small Flames\nGRA00736 Animation for Arabic Mill\nGRA00735 Animation for Asian Mill\nGRA00734 Animation for North European Mill\nGRA01683 Animation for Siege/Capped Ram's Front\nGRA00737 Animation for West European Mill\nGRA00744 Arabic animation for mill sails shadows? Castle Age & Imperial Age\nGRA00035 Arabic Archery Range Castle Age & Imperial Age\nGRA00023 Arabic Archery Range Feudal Age\nGRA04234 Arabic back of small sail sequence - combine with GRA04230?\nGRA00144 Arabic Barracks Castle Age & Imperial Age\nGRA00132 Arabic Barracks Feudal Age\nGRA00104 Arabic Blacksmith Castle Age & Imperial Age\nGRA00092 Arabic Blacksmith Feudal Age\nGRA02548 Arabic bombard tower\nGRA00304 Arabic Castle\nGRA02246 Arabic Castle Age & Imperial Age houses\nGRA01020 Arabic Castle Age & Imperial Age Stable\nGRA00411 Arabic Castle Age dock? with blankets similar to GRA00399\nGRA03611 Arabic Castle Age town center left side roof\nGRA03619 Arabic Castle Age town center left side roof support\nGRA03615 Arabic Castle Age town center left side roof supports\nGRA04627 Arabic Castle Age Town Center, right side roof\nGRA04635 Arabic Castle Age Town Center, right side roof support\nGRA04631 Arabic Castle Age Town Center, right side roof supports\nGRA04171 Arabic Damaged Stone Wall\nGRA00399 Arabic Dock Castle Age & Imperial Age\nGRA00387 Arabic Dock Feudal Age\nGRA02234 Arabic Feudal Age houses\nGRA01008 Arabic Feudal Age Stable\nGRA03599 Arabic Feudal Age town center left side roof\nGRA03607 Arabic Feudal Age town center left side roof support\nGRA03603 Arabic Feudal Age town center left side roof supports\nGRA04615 Arabic Feudal Age Town Center right side roof\nGRA04619 Arabic Feudal Age Town Center right side roof supports\nGRA04175 Arabic Fortified Damaged Wall\nGRA04077 Arabic fortified gate building sequence -- position\nGRA03716 Arabic Fortified gate building sequence / position\nGRA03732 Arabic Fortified gate building sequence \\ position\nGRA04165 Arabic fortified gate building sequence | position\nGRA01940 Arabic Fortified Gate closed / position\nGRA02441 Arabic Fortified Gate closed \\ position\nGRA04037 Arabic Fortified Gate open -- position\nGRA04125 Arabic Fortified Gate open | position\nGRA02369 Arabic Fortified Gate raised / position\nGRA02477 Arabic Fortified Gate raised \\ position\nGRA04013 Arabic Fortified gate shut -- position\nGRA04101 Arabic Fortified Gate shut | position\nGRA02405 Arabic Fortified gate tower\nGRA04191 Arabic Fortified severely Damaged Wall\nGRA02112 Arabic Fortified Wall\nGRA03752 Arabic Fortified wall building sequence\nGRA04663 Arabic fortified wall shadows?\nGRA04623 Arabic Fuedal Age Town Center far right roof support\nGRA04069 Arabic gate building sequence -- position\nGRA03708 Arabic gate building sequence / position\nGRA03724 Arabic gate building sequence \\ position\nGRA04157 Arabic gate building sequence | position\nGRA01928 Arabic Gate closed / position\nGRA02429 Arabic Gate closed \\ position\nGRA04025 Arabic Gate open -- position\nGRA04113 Arabic Gate open | position\nGRA02357 Arabic Gate raised / position\nGRA02465 Arabic Gate raised \\ position\nGRA04001 Arabic Gate shut -- position\nGRA04089 Arabic Gate shut | position\nGRA02666 Arabic Guard Tower\nGRA04183 Arabic heavily damaged Fortified Wall\nGRA04179 Arabic heavily Damaged Stone Wall\nGRA03796 Arabic Imperial Age Market\nGRA04639 Arabic Imperial Age Town Center, right side roof\nGRA04647 Arabic Imperial Age Town Center, right side roof support\nGRA04643 Arabic Imperial Age Town Center, right side roof supports\nGRA02540 Arabic Keep\nGRA03506 Arabic lumber camp\nGRA00819 Arabic Market Castle Age\nGRA02277 Arabic Market Feudal Age\nGRA00748 Arabic mill animation Feudal Age\nGRA00740 Arabic Mill Feudal Age without Animation\nGRA00752 Arabic Mill without Animation Castle Age & Imperial Age\nGRA03494 Arabic Mining Camp\nGRA00280 Arabic Monastery\nGRA04247 Arabic sail sequences\nGRA04187 Arabic severely Damaged Stone Wall\nGRA04303 Arabic ships main sail sequence\nGRA04600 Arabic ships main sail sequence\nGRA04226 Arabic Ships sail sequence\nGRA04297 Arabic ships sail sequence\nGRA04311 Arabic ships sail.\nGRA04319 Arabic ships sail.\nGRA04307 Arabic ships small triangular sail.\nGRA00957 Arabic Siege Workshop\nGRA04238 Arabic small sail sequence\nGRA04230 Arabic Small ships sail sequence\nGRA04242 Arabic small triangular sail sequence\nGRA00100 Arabic Smoke from Blacksmith\nGRA02393 Arabic Stone gate tower\nGRA02100 Arabic Stone Wall\nGRA00914 Arabic top piece of Castle Age Town Center\nGRA00902 Arabic top piece of Feudal Age Town Center\nGRA00926 Arabic top piece of Imperial Age Town Center\nGRA00922 Arabic town center flooring Imperial age\nGRA03471 Arabic town center left side roof supports Imperial Age\nGRA03475 Arabic town center left side roof supports Imperial Age\nGRA00906 Arabic town center support shadows Castle Age\nGRA00894 Arabic town center support shadows Feudal Age\nGRA00918 Arabic town center support shadows Imperial age\nGRA03467 Arabic Towncenter roof left side Imperial Age\nGRA03834 Arabic University Castle Age\nGRA03838 Arabic University Imperial Age\nGRA03744 Arabic wall building sequence\nGRA04659 Arabic wall shadows?\nGRA02654 Arabic Watch Tower\nGRA02698 Arbalest attacking\nGRA02705 Arbalest decaying\nGRA02701 Arbalest dying\nGRA02708 Arbalest moving\nGRA02704 Arbalest standing\nGRA00009 Archer Decaying\nGRA00005 Archer Dying\nGRA00002 Archer firing\nGRA00012 Archer Moving\nGRA00008 Archer standing\nGRA03799 Arrow\nGRA03812 Arrow or rocket?\nGRA03547 Arrow or spear?\nGRA00034 Asian Archery Range Castle Age & Imperial Age\nGRA00022 Asian Archery Range Feudal Age\nGRA04233 Asian back of small sail sequence - combine with GRA04229?\nGRA00143 Asian Barracks Castle Age & Imperial Age\nGRA00131 Asian Barracks Feudal Age\nGRA00103 Asian Blacksmith Castle Age & Imperial Age\nGRA00091 Asian Blacksmith Feudal Age\nGRA02547 Asian bombard tower\nGRA00303 Asian Castle\nGRA02245 Asian Castle Age & Imperial Age houses\nGRA01019 Asian Castle Age & Imperial Age Stable\nGRA00410 Asian Castle Age dock? Similar to GRA00398\nGRA03610 Asian Castle Age town center left side roof\nGRA03618 Asian Castle Age town center left side roof support\nGRA03614 Asian Castle Age town center left side roof supports\nGRA04626 Asian Castle Age Town Center, right side roof\nGRA04634 Asian Castle Age Town Center, right side roof support\nGRA04630 Asian Castle Age Town Center, right side roof supports\nGRA04170 Asian Damaged Stone Wall\nGRA00398 Asian Dock Castle Age & Imperial Age\nGRA00386 Asian Dock Feudal Age\nGRA02233 Asian Feudal Age houses\nGRA01007 Asian Feudal Age Stable\nGRA03598 Asian Feudal Age town center left side roof\nGRA03606 Asian Feudal Age town center left side roof support\nGRA03602 Asian Feudal Age town center left side roof supports\nGRA04614 Asian Feudal Age Town Center right side roof\nGRA04618 Asian Feudal Age Town Center right side roof supports\nGRA04174 Asian Fortified Damaged Wall\nGRA04076 Asian fortified gate building sequence -- position\nGRA03715 Asian Fortified gate building sequence / position\nGRA03731 Asian Fortified gate building sequence \\ position\nGRA04164 Asian fortified gate building sequence | position\nGRA01939 Asian Fortified Gate closed / position\nGRA02440 Asian Fortified Gate closed \\ position\nGRA04036 Asian Fortified Gate open -- position\nGRA04124 Asian Fortified Gate open | position\nGRA02368 Asian Fortified Gate raised / position\nGRA02476 Asian Fortified Gate raised \\ position\nGRA04012 Asian Fortified gate shut -- position\nGRA04100 Asian Fortified Gate shut | position\nGRA02404 Asian Fortified gate tower\nGRA04190 Asian Fortified severely Damaged Wall\nGRA02111 Asian Fortified Wall\nGRA03751 Asian Fortified wall building sequence\nGRA04662 Asian fortified wall shadows?\nGRA04622 Asian Fuedal Age Town Center far right roof support\nGRA04068 Asian gate building sequence -- position\nGRA03707 Asian gate building sequence / position\nGRA03723 Asian gate building sequence \\ position\nGRA04156 Asian gate building sequence | position\nGRA01927 Asian Gate closed / position\nGRA02428 Asian Gate closed \\ position\nGRA04024 Asian Gate open  -- position\nGRA04112 Asian Gate open | position\nGRA02356 Asian Gate raised / position\nGRA02464 Asian Gate raised \\ position\nGRA04000 Asian Gate shut -- position\nGRA04088 Asian Gate shut | position\nGRA02665 Asian Guard Tower\nGRA04182 Asian heavily damaged Fortified Wall\nGRA04178 Asian heavily Damaged Stone Wall\nGRA03795 Asian Imperial Age Market\nGRA04638 Asian Imperial Age Town Center, right side roof\nGRA04646 Asian Imperial Age Town Center, right side roof support\nGRA04642 Asian Imperial Age Town Center, right side roof supports\nGRA02539 Asian Keep\nGRA03505 Asian lumber camp\nGRA00818 Asian Market Castle Age\nGRA02276 Asian Market Feudal Age\nGRA00747 Asian mill animation Feudal Age\nGRA00739 Asian Mill Feudal Age without Animation\nGRA00751 Asian Mill without Animation Castle Age & Imperial Age\nGRA03493 Asian Mining Camp\nGRA00279 Asian Monastery.\nGRA04246 Asian sail sequences\nGRA04186 Asian severely Damaged Stone Wall\nGRA04302 Asian ships main sail sail sequence\nGRA04599 Asian ships main sail sequence\nGRA04310 Asian ships sail\nGRA04225 Asian Ships sail sequence\nGRA04296 Asian ships sail sequence\nGRA04318 Asian ships sail.\nGRA04306 Asian ships small triangular sail\nGRA00956 Asian Siege Workshop\nGRA04237 Asian small sail sequence\nGRA04229 Asian Small ships sail sequence\nGRA04241 Asian small triangular sail sequence\nGRA00099 Asian Smoke from Blacksmith\nGRA02392 Asian Stone gate tower\nGRA02099 Asian Stone Wall\nGRA00913 Asian top piece of Castle Age Town Center\nGRA00901 Asian top piece of Feudal Age Town Center\nGRA00925 Asian top piece of Imperial Age Town Center\nGRA00921 Asian town center flooring Imperial age\nGRA03470 Asian town center left side roof supports Imperial Age\nGRA03474 Asian town center left side roof supports Imperial Age\nGRA00905 Asian town center support shadows Castle Age\nGRA00893 Asian town center support shadows Feudal Age\nGRA00917 Asian town center support shadows Imperial age\nGRA03466 Asian Towncenter roof left side Imperial Age\nGRA01137 Asian Trade Workshop\nGRA03833 Asian University Castle Age\nGRA03837 Asian University Imperial Age\nGRA03743 Asian wall building sequence\nGRA04658 Asian wall shadows?\nGRA02653 Asian Watch Tower\nGRA04327 Back of Arabic ships sail\nGRA04315 Back of Arabic ships sail - combine with GRA04311?\nGRA04323 Back of Arabic ships sail - combine with GRA04319\nGRA04326 Back of Asian ships sail\nGRA04314 Back of Asian ships sail - combine with GRA04310?\nGRA04322 Back of Asian ships sail - combine with GRA04318?\nGRA04325 Back of North European ships sail\nGRA04313 Back of North European ships sail - combine with GRA04309?\nGRA04321 Back of North European ships sail - combine with GRA04317?\nGRA04328 Back of West European ships sail\nGRA04316 Back of West European ships sail - combine with GRA04312\nGRA04324 Back of West European ships sail - combine with GRA04320\nGRA02295 Bamboo chopped down stumps\nGRA04584 Bamboo stumps\nGRA02293 Bamboo Trees\nGRA03589 Banner between two poles\nGRA03592 Banner between two poles – similar to GRA03589\nGRA00171 Battering Ram\nGRA00179 Battering Ram – possibly the same as GRA00171\nGRA00181 Battering Ram – Possibly the same as GRA00171 and GRA00179\nGRA00180 Battering Ram Decaying\nGRA02560 Berry bush\nGRA04373 Berserker attacking\nGRA04386 Berserker attacking - appears identical to GRA04373\nGRA04393 Berserker decaying\nGRA04376 Berserker dying\nGRA04389 Berserker dying - appears identical to GRA04376\nGRA04383 Berserker moving\nGRA04396 Berserker moving - appears identical to GRA04383\nGRA04379 Berserker standing\nGRA04392 Berserker standing - appears identical to GRA04379\nGRA04499 Beta Ice\nGRA00409 Beta release dock\nGRA02793 Beta version Pikeman?\nGRA01355 Beta version walls\nGRA00075 Bigger Explosion Smoke\nGRA00429 Bigger Fire\nGRA00430 Bigger Fire than GRA00428\nGRA00431 Bigger Fire than GRA00430\nGRA04518 Birds from the dock\nGRA01871 Bitmap with Church 4 on it\nGRA02555 Boar attacking\nGRA02558 Boar decaying\nGRA02556 Boar dying\nGRA02559 Boar moving\nGRA02557 Boar standing\nGRA00068 Bombard Decaying\nGRA00064 Bombard Dying\nGRA00061 Bombard firing\nGRA00071 Bombard Moving\nGRA00067 Bombard standing still\nGRA04574 Bottom Part of Forest Trees\nGRA04577 Bottom Part of Palm Trees\nGRA04580 Bottom Part of Pine Trees\nGRA00212 Boulder possibly for Trebuchet\nGRA03692 Bridge left end / slope\nGRA03694 Bridge left end \\ slope\nGRA03690 Bridge middle / slope\nGRA03696 Bridge middle \\ slope\nGRA03688 Bridge right end / slope\nGRA03698 Bridge right end \\ slope\nGRA03534 Britons Wonder\nGRA04588 Broken Bridge / position left hand end\nGRA04587 Broken Bridge / position middle section\nGRA04589 Broken Bridge / position right hand end\nGRA04591 Broken Bridge \\ position left hand end\nGRA04590 Broken Bridge \\ position middle section\nGRA04592 Broken Bridge \\ position right hand end\nGRA00414 Burning ball?\nGRA00210 Burning Boulder possibly Trebuchet shot\nGRA03814 Burning catapult boulder\nGRA03819 Burning spear\nGRA03545 Byzantines Wonder\nGRA04474 Cactus\nGRA00683 Camel decaying\nGRA00679 Camel dying\nGRA00676 Camel fighting\nGRA00686 Camel moving\nGRA00682 Camel standing\nGRA04244 Cannon galleon hull\nGRA01681 Capped ram\nGRA01689 Capped ram\nGRA01691 Capped ram\nGRA01690 Capped ram decaying\nGRA01686 Capped ram dying\nGRA01693 Capped ram wheel animation\nGRA04481 Carpets.\nGRA00206 Cataphract Decaying\nGRA00202 Cataphract Dying\nGRA00199 Cataphract Fighting\nGRA00209 Cataphract Moving\nGRA00205 Cataphract Standing still\nGRA03803 Catapult Boulder\nGRA03635 Cathedral\nGRA00856 Cavalier decaying\nGRA00852 Cavalier dying\nGRA00849 Cavalier fighting\nGRA00859 Cavalier moving\nGRA00855 Cavalier standing\nGRA00327 Cavalry Archer decaying\nGRA00323 Cavalry Archer dying\nGRA00320 Cavalry Archer firing bow\nGRA00330 Cavalry Archer moving\nGRA00326 Cavalry Archer standing still\nGRA03538 Celts Wonder\nGRA03085 Champion attacking\nGRA03092 Champion decaying\nGRA03088 Champion dying\nGRA03095 Champion moving\nGRA03091 Champion standing\nGRA03546 Chinese Wonder\nGRA00215 Chu-Ko-Nu Attacking\nGRA00222 Chu-Ko-Nu decaying\nGRA00218 Chu-Ko-Nu dying\nGRA00225 Chu-Ko-Nu moving\nGRA00221 Chu-Ko-Nu standing still\nGRA00229 Cliff section\nGRA00230 Cliff section\nGRA00231 Cliff section\nGRA00232 Cliff section\nGRA00233 Cliff section\nGRA00234 Cliff section\nGRA00235 Cliff section – lighter colour?\nGRA00226 Cliffs\nGRA04685 Cobra Car\nGRA04585 Construction sequence for Fish Trap\nGRA04478 Crater\nGRA00193 Crossbowman Decaying\nGRA00189 Crossbowman Dying\nGRA00186 Crossbowman Firing\nGRA00196 Crossbowman Moving\nGRA00192 Crossbowman Standing Still\nGRA00898 Cup? and something?\nGRA02683 Dark Age Barracks\nGRA00376 Dark Age Dock\nGRA02223 Dark Age houses\nGRA03482 Dark Age Mill donkey Animation\nGRA03483 Dark Age Mill without Donkey Animation\nGRA03595 Dark Age town center roof support, left side front\nGRA03596 Dark Age town center roof supports, left side\nGRA03594 Dark Age town center roof, left side\nGRA04610 Dark Age Town Center roof, right side\nGRA00889 Dark Age town center support shadows\nGRA04612 Dark Age Town Center, right side roof support\nGRA04611 Dark Age Town Center, right side roof supports\nGRA00733 Darker version of GRA00730\nGRA01262 Dead Tree\nGRA01268 Dead Tree\nGRA03979 Decaying foot figure two tone trousers bare top?\nGRA03978 Decaying foot figure?\nGRA00344 Deer decaying\nGRA00339 Deer dying\nGRA00336 Deer Eating\nGRA00342 Deer Eating again?\nGRA00343 Deer moving\nGRA00348 Deer moving again?\nGRA04347 Demolition Ship exploding.\nGRA04299 Demolition ship's hull\nGRA04397 Dock building sequence\nGRA04267 Dome of the Rock\nGRA00176 Dying Battering Ram\nGRA00614 Elite Skirmisher decaying\nGRA00610 Elite Skirmisher dying\nGRA00617 Elite Skirmisher moving\nGRA00613 Elite Skirmisher standing\nGRA00607 Elite Skirmisher throwing spear\nGRA01124 Empty trade cart decaying\nGRA01119 Empty trade cart dying\nGRA04486 Empty trade cart moving\nGRA01122 Empty trade cart standing\nGRA04586 Explosion\nGRA04605 Explosion\nGRA00073 Explosion Smoke\nGRA04370 Explosion?\nGRA04268 Fast fire ship hull\nGRA01878 Female hunter possibly cutting something\nGRA02568 Female village carrying basket dying\nGRA01876 Female villager berry gatherer kneeling\nGRA01875 Female villager berry gatherer moving\nGRA01874 Female villager builder building\nGRA01402 Female villager builder decaying\nGRA01398 Female villager builder dying\nGRA01405 Female villager builder moving\nGRA01401 Female villager builder standing\nGRA01389 Female villager decaying\nGRA01467 Female villager decaying not sure if miner or builder or what?\nGRA01385 Female villager dying\nGRA03840 Female Villager farmer sowing seed\nGRA01382 Female villager fighting?\nGRA04602 Female Villager forager moving\nGRA01877 Female villager hunter moving with meat\nGRA01421 Female villager hunting with bow\nGRA02573 Female villager kneeling picking berries\nGRA02118 Female villager miner carrying gold\nGRA01879 Female villager miner carrying stone\nGRA01454 Female villager miner decaying\nGRA01450 Female villager miner dying\nGRA01880 Female villager miner mining\nGRA01457 Female villager miner moving\nGRA01453 Female villager miner standing\nGRA01392 Female villager moving\nGRA02576 Female villager moving with empty basket\nGRA04656 Female Villager shepherd decaying\nGRA03678 Female Villager shepherd dying\nGRA03841 Female Villager shepherd herding sheep\nGRA03677 Female Villager shepherd killing sheep\nGRA03681 Female Villager shepherd moving\nGRA03679 Female Villager shepherd standing\nGRA01388 Female villager standing\nGRA01414 Female villager standing carrying a basket\nGRA01415 Female villager with basket decaying\nGRA02572 Female villager with basket decaying\nGRA01411 Female villager with basket dying\nGRA01418 Female villager with basket moving\nGRA02571 Female villager with basket standing\nGRA01428 Female villager with bow decaying\nGRA01424 Female villager with bow dying\nGRA01431 Female villager with bow moving\nGRA01427 Female villager with bow standing\nGRA01883 Female villager woodcutter carrying wood\nGRA01434 Female villager woodcutter chopping\nGRA01884 Female villager woodcutter chopping\nGRA01441 Female villager woodcutter decaying\nGRA01437 Female villager woodcutter dying\nGRA01444 Female villager woodcutter moving\nGRA01440 Female villager woodcutter standing\nGRA04194 Fire Ball sequence\nGRA04255 Fire ships hull\nGRA03983 Fireball\nGRA03984 Fireball\nGRA00420 Fish (Perch)\nGRA01910 Fish Dorado\nGRA01905 Fish Marlin\nGRA01906 Fish Marlin\nGRA01912 Fish Perch\nGRA01911 Fish Salmon\nGRA01913 Fish snapper\nGRA03593 Fish Trap\nGRA03549 Fish Tuna\nGRA00444 Fishing ship\nGRA00449 Fishing ship – same as GRA00444?\nGRA00446 Fishing ship nets\nGRA02660 Flag\nGRA04356 Flag\nGRA04521 Flag\nGRA02662 Flag - appears identical to GRA02660\nGRA02663 Flag - appears identical to GRA02660 & GRA02662\nGRA03586 Flag – fleur-de-lise ?\nGRA04534 Flag - partial animation sequence?\nGRA03583 Flag as above, pointing diagonal down right\nGRA01361 Flag Banner\nGRA01362 Flag Banner\nGRA01363 Flag Banner\nGRA01372 Flag Banner\nGRA01373 Flag Banner\nGRA01374 Flag Banner\nGRA01360 Flag Banner\nGRA01375 Flag Banner – beta? Hanging in doorway?\nGRA02401 Flag for Arabic Fortified gate tower - GRA02403\nGRA02389 Flag for Arabic stone gate tower - GRA02393?\nGRA02400 Flag for Asian fortified gate tower - GRA02402\nGRA02388 Flag for Asian stone gate tower - GRA02392?\nGRA02399 Flag for North European fortified gate tower - GRA02401\nGRA02387 Flag for North European stone gate tower - GRA02391?\nGRA02402 Flag for West European Fortified gate tower - GRA02404\nGRA02390 Flag for West European stone gate tower - GRA02394?\nGRA01886 Flag pole from beta version of West European Imperial market\nGRA04358 Flag same as GRA04356 etc\nGRA04359 Flag same as GRA04356 etc\nGRA04360 Flag same as GRA04356 etc\nGRA04362 Flag same as GRA04356 etc\nGRA04363 Flag same as GRA04356 etc\nGRA03580 Flag sideways on\nGRA04597 Floating rubble from a docks destruction?\nGRA01251 Forest Tree\nGRA04652 Forest Trees\nGRA00435 Forest/Oak Trees\nGRA03541 Franks Wonder\nGRA01127 Full trade cart moving\nGRA01665 Gaia Crack\nGRA04487 Gaia Destroyed Ship / position\nGRA04488 Gaia Destroyed Ship \\ position\nGRA02519 Gaia Flowers Num. 1\nGRA02520 Gaia Flowers Num. 2\nGRA02521 Gaia Flowers Num. 3\nGRA02522 Gaia Flowers Num. 4\nGRA03737 Gaia Rocks\nGRA02530 Gaia Ruined Building\nGRA02682 Gaia Torch\nGRA04199 Galleon ships hull\nGRA00696 Galley ROR\nGRA04300 Galley's hull\nGRA04479 Gold Mine\nGRA02561 Gold objects\nGRA03535 Goths Wonder\nGRA02261 Green Snow Topped Mountain Number 1\nGRA02262 Green Snow Topped Mountain Number 2\nGRA00588 Hand cannoneer decaying\nGRA00584 Hand cannoneer dying\nGRA00581 Hand cannoneer firing\nGRA00587 Hand cannoneer standing\nGRA00591 Hand cannonner moving\nGRA00578 Hawk flying\nGRA00576 Hawk gliding\nGRA00577 Hawk shadow flying\nGRA00575 Hawk shadow gliding\nGRA02762 Heavy Camel attacking\nGRA02769 Heavy camel decaying\nGRA02765 Heavy camel dying\nGRA02768 Heavy camel standing\nGRA02772 Heavy camel standing\nGRA03764 Heavy Cavalry Archer decaying\nGRA03760 Heavy Cavalry Archer dying\nGRA03757 Heavy Cavalry Archer firing\nGRA03767 Heavy Cavalry Archer moving\nGRA03763 Heavy Cavalry Archer standing\nGRA04338 Heavy Demolition Ship exploding.\nGRA04253 Heavy demolition ship hull\nGRA02813 Heavy scorpion attacking\nGRA02820 Heavy scorpion decaying\nGRA02816 Heavy scorpion dying\nGRA02821 Heavy scorpion moving\nGRA02819 Heavy scorpion standing\nGRA02823 Heavy scorpion wheels\nGRA03770 Hero Frankish Paladin type attacking\nGRA03777 Hero Frankish Paladin type decaying\nGRA03773 Hero Frankish Paladin type dying\nGRA03780 Hero Frankish Paladin type moving\nGRA03776 Hero Frankish Paladin type standing\nGRA03790 Hero Joan of Arc Cavalry decaying\nGRA03786 Hero Joan of Arc Cavalry dying\nGRA03783 Hero Joan of Arc Cavalry fighting\nGRA03793 Hero Joan of Arc Cavalry moving\nGRA03789 Hero Joan of Arc Cavalry standing\nGRA01742 Hero Joan the maid decaying\nGRA01738 Hero Joan the maid dying\nGRA01735 Hero Joan the maid fighting\nGRA01745 Hero Joan the maid moving\nGRA01741 Hero Joan the maid standing\nGRA00239 Huge size building sequence\nGRA04537 Huscarl attacking.\nGRA04540 Huscarl decaying\nGRA04538 Huscarl dying.\nGRA04541 Huscarl moving\nGRA04539 Huscarl standing\nGRA00641 Janissary decaying\nGRA00637 Janissary dying\nGRA00634 Janissary firing\nGRA00644 Janissary moving\nGRA00640 Janissary standing\nGRA03537 Japanese Wonder\nGRA01761 King\nGRA01768 King decaying\nGRA01764 King dying\nGRA01771 King moving\nGRA01767 King standing\nGRA00670 Knight decaying\nGRA00666 Knight dying\nGRA00663 Knight Fighting\nGRA00673 Knight moving\nGRA00669 Knight standing\nGRA03811 Large burning arrow\nGRA03734 Large Pavilion\nGRA04369 Large Pyramid\nGRA00238 Large size building sequence\nGRA04496 Large Yurt, canvas roof\nGRA04489 Large Yurt, thatched roof\nGRA03987 Larger smoke puff\nGRA02998 Light cavalry attacking\nGRA03005 Light cavalry decaying\nGRA03001 Light cavalry dying\nGRA03008 Light cavalry moving\nGRA03004 Light cavalry standing\nGRA04609 Loaded Trade Wagon decaying\nGRA04607 Loaded Trade Wagon dying\nGRA04608 Loaded Trade Wagon standing\nGRA00710 Longbowman decaying\nGRA00705 Longbowman dying\nGRA00702 Longbowman firing\nGRA00713 Longbowman moving\nGRA00708 Longbowman standing\nGRA01182 Longswordsman decaying\nGRA01178 Longswordsman dying\nGRA01175 Longswordsman fighting\nGRA01185 Longswordsman moving\nGRA01181 Longswordsman standing\nGRA01495 Male villager builder decaying\nGRA01490 Male villager builder dying\nGRA01496 Male villager builder kneeling\nGRA01499 Male villager builder moving\nGRA01493 Male villager builder standing\nGRA02583 Male villager carrying basket dying\nGRA01481 Male villager decaying\nGRA01476 Male villager dying\nGRA01511 Male villager farmer decaying\nGRA01506 Male villager farmer dying\nGRA01512 Male villager farmer farming\nGRA01515 Male villager farmer moving\nGRA03842 Male Villager farmer sowing seed\nGRA01509 Male villager farmer standing\nGRA01473 Male villager fighting\nGRA03980 Male Villager fisher moving with fish\nGRA03479 Male Villager forager attacking\nGRA01519 Male villager hunter carrying meat\nGRA01527 Male villager hunter decaying\nGRA01522 Male villager hunter dying\nGRA01528 Male villager hunter kneeling slaughtering meat\nGRA01531 Male villager hunter moving\nGRA01525 Male villager hunter standing\nGRA01518 Male villager hunter using bow\nGRA02589 Male villager kneeling picking berries\nGRA02117 Male villager miner carrying gold\nGRA01552 Male villager miner carrying stone\nGRA01559 Male villager miner decaying\nGRA01555 Male villager miner dying\nGRA01560 Male villager miner mining\nGRA01563 Male villager miner moving\nGRA01558 Male villager miner standing\nGRA01484 Male villager moving\nGRA02592 Male villager moving with empty basket\nGRA04649 Male Villager shepherd decaying\nGRA03683 Male Villager shepherd dying\nGRA03843 Male Villager shepherd herding sheep\nGRA03682 Male Villager shepherd killing sheep\nGRA03686 Male Villager shepherd moving\nGRA03684 Male Villager shepherd standing\nGRA01479 Male villager standing\nGRA02588 Male villager with basket decaying\nGRA02586 Male villager with basket standing\nGRA01536 Male villager woodcutter carrying wood\nGRA01535 Male villager woodcutter chopping\nGRA01545 Male villager woodcutter chopping - appears identical to GRA01535\nGRA01544 Male villager woodcutter decaying\nGRA01539 Male villager woodcutter dying\nGRA01548 Male villager woodcutter moving\nGRA01542 Male villager woodcutter standing\nGRA00351 Mameluke attacking\nGRA00358 Mameluke decaying\nGRA00354 Mameluke dying\nGRA00361 Mameluke moving\nGRA00357 Mameluke standing still\nGRA01045 Man-at-arms decaying\nGRA01041 Man-at-arms dying\nGRA01038 Man-at-arms fighting\nGRA01048 Man-at-arms moving\nGRA01044 Man-at-arms standing\nGRA00723 Mangonel decaying\nGRA00719 Mangonel dying\nGRA00716 Mangonel firing\nGRA00724 Mangonel moving\nGRA00722 Mangonel standing\nGRA00726 Mangonel wheels turning\nGRA00789 Mangudai decaying\nGRA00785 Mangudai dying\nGRA00782 Mangudai fighting\nGRA00792 Mangudai moving\nGRA00788 Mangudai standing\nGRA00427 Medium Fire\nGRA00428 Medium Fire\nGRA00237 Medium size building sequence\nGRA00994 Militia decaying\nGRA00990 Militia dying\nGRA00987 Militia fighting\nGRA00997 Militia moving\nGRA00993 Militia standing\nGRA03539 Mongols Wonder\nGRA03827 Monk carrying relic\nGRA03824 Monk carrying relic disappearing?\nGRA03831 Monk carrying relic moving\nGRA00775 Monk decaying\nGRA00771 Monk dying\nGRA00768 Monk fighting\nGRA00776 Monk fighting appears identical to GRA00768\nGRA00779 Monk moving\nGRA00774 Monk standing\nGRA03982 Mosque\nGRA04594 Mountain 3\nGRA04595 Mountain 4\nGRA02296 Multiple shadows from deciduous forest\nGRA02308 Multiple shadows from forest\nGRA02300 Multiple shadows from palm forest\nGRA02304 Multiple shadows from pine forest\nGRA02533 Nice Green Tree\nGRA04480 Nine Bands trophy/flag of Gengis Khan.\nGRA00742 North European animation for mill sails shadows? Castle Age & Imperial Age\nGRA00033 North European Archery Range Castle Age & Imperial Age\nGRA00021 North European Archery Range Feudal Age\nGRA04232 North European back of small sail sequence - combine with GRA04228?\nGRA00142 North European Barracks Castle Age & Imperial Age\nGRA00130 North European Barracks Feudal Age\nGRA00102 North European Blacksmith Castle Age & Imperial Age\nGRA00090 North European Blacksmith Feudal Age\nGRA02546 North European bombard tower\nGRA00302 North European Castle\nGRA02244 North European Castle Age & Imperial Age houses\nGRA01018 North European Castle Age & Imperial Age Stable\nGRA03609 North European Castle Age town center left side roof\nGRA03617 North European Castle Age town center left side roof support\nGRA03613 North European Castle Age town center left side roof supports\nGRA04625 North European Castle Age Town Center, right side roof\nGRA04633 North European Castle Age Town Center, right side roof support\nGRA04629 North European Castle Age Town Center, right side roof supports\nGRA04169 North European Damaged Stone Wall\nGRA00397 North European Dock Castle Age & Imperial Age\nGRA00385 North European Dock Feudal Age\nGRA02232 North European Feudal Age houses\nGRA01006 North European Feudal Age Stable\nGRA03597 North European Feudal Age town center left side roof\nGRA03605 North European Feudal Age town center left side roof support\nGRA03601 North European Feudal Age town center left side roof supports\nGRA04617 North European Feudal Age Town Center right side roof supports\nGRA04613 North European Feudal Age Town Center, right side roof\nGRA04173 North European Fortified Damaged Wall\nGRA04075 North European fortified gate building sequence -- position\nGRA03714 North European Fortified gate building sequence / position\nGRA03730 North European Fortified gate building sequence \\ position\nGRA04163 North European fortified gate building sequence | position\nGRA01938 North European Fortified Gate closed / position\nGRA02439 North European Fortified Gate closed \\ position\nGRA04035 North European Fortified Gate open -- position\nGRA04123 North European Fortified Gate open | position\nGRA02367 North European Fortified Gate raised / position\nGRA02475 North European Fortified Gate raised \\ position\nGRA04011 North European Fortified Gate shut -- position\nGRA04099 North European Fortified Gate shut | position\nGRA02403 North European Fortified gate tower\nGRA04189 North European Fortified severely Damaged Wall\nGRA02110 North European Fortified Wall\nGRA03750 North European Fortified wall building sequence\nGRA04661 North European fortified wall shadows?\nGRA04621 North European Fuedal Age Town Center far right roof support\nGRA04067 North European gate building sequence -- position\nGRA03706 North European gate building sequence / position\nGRA03722 North European gate building sequence \\ position\nGRA04155 North European gate building sequence | position\nGRA01926 North European Gate closed / position\nGRA02427 North European Gate closed \\ position\nGRA04023 North European Gate open -- position\nGRA04111 North European Gate open | position\nGRA02355 North European Gate raised / position\nGRA02463 North European Gate raised \\ position\nGRA03999 North European Gate shut -- position\nGRA04087 North European Gate shut | position\nGRA02664 North European Guard Tower\nGRA04181 North European heavily damaged Fortified Wall\nGRA04177 North European heavily Damaged Stone Wall\nGRA03794 North European Imperial Age Market\nGRA04637 North European Imperial Age Town Center, right side roof\nGRA04645 North European Imperial Age Town Center, right side roof support\nGRA04641 North European Imperial Age Town Center, right side roof supports\nGRA02538 North European Keep\nGRA03504 North European lumber camp\nGRA00817 North European Market Castle Age\nGRA02275 North European Market Feudal Age\nGRA00746 North European mill animation Feudal Age\nGRA00738 North European Mill Feudal Age without animation\nGRA00750 North European mill without Animation Castle Age & Imperial Age\nGRA03492 North European Mining Camp\nGRA00278 North European Monastery\nGRA04245 North European sail sequences\nGRA04185 North European severely Damaged Stone Wall\nGRA04301 North European ships main sail sequence\nGRA04598 North European ships main sail sequence\nGRA04309 North European ships sail\nGRA04317 North European ships sail.\nGRA04224 North European Ships sails sequence\nGRA04305 North European ships small triangular sail.\nGRA00955 North European Siege Workshop\nGRA04236 North European small sail sequence\nGRA04228 North European Small ships sail sequence\nGRA04240 North European small triangular sail sequence\nGRA00098 North European Smoke from Blacksmith\nGRA02391 North European Stone gate tower\nGRA02098 North European Stone Wall\nGRA00912 North European top piece of Castle Age Town Center\nGRA00900 North European top piece of Feudal Age Town Center\nGRA00924 North European top piece of Imperial Age Town Center\nGRA00920 North European town center flooring Imperial age\nGRA03469 North European Town center left side roof supports Imperial Age\nGRA03473 North European Town center left side roof supports Imperial Age\nGRA00904 North European town center support shadows Castle Age\nGRA00892 North European town center support shadows Feudal Age\nGRA00916 North European town center support shadows Imperial age\nGRA03465 North European Towncenter roof left side Imperial Age\nGRA03832 North European University Castle Age\nGRA03836 North European University Imperial Age\nGRA03742 North European wall building sequence\nGRA04657 North European wall shadows?\nGRA02652 North European Watch Tower\nGRA03017 Onager attacking\nGRA04168 Onager decaying\nGRA03020 Onager dying\nGRA03024 Onager moving\nGRA03023 Onager standing\nGRA03026 Onager wheels\nGRA04330 Outpost\nGRA04573 Packed Trebuchet decaying\nGRA04572 Packed trebuchet dying\nGRA02279 Packed Trebuchet moving\nGRA03072 Paladin attacking\nGRA03079 Paladin decaying\nGRA03075 Paladin dying\nGRA03082 Paladin moving\nGRA03078 Paladin standing\nGRA01828 Palisade Wall\nGRA04653 Palm Trees\nGRA02524 Path Num. 1\nGRA02525 Path Num. 2\nGRA02526 Path Num. 3\nGRA02523 Path Num. 4\nGRA03540 Persians Wonder\nGRA00053 Piece of the True Cross?\nGRA02826 Pikeman attacking\nGRA02833 Pikeman decaying\nGRA02829 Pikeman dying\nGRA02836 Pikeman moving\nGRA02832 Pikeman standing\nGRA04654 Pine Trees\nGRA00272 Possible Cannonball in a cloud of bits?\nGRA00118 Possible Rocket or Burning Large Arrow\nGRA00120 Possible Scorpion Arrow\nGRA04606 Possible spear or arrow?\nGRA04682 Possibly shadow sequence from pallisades\nGRA03405 Red four arrows command here mark\nGRA00057 Relic\nGRA04279 Relic or piece of true cross - appears identical to GRA04278\nGRA04278 Relic or piece of true cross - not sure which (or if used)\nGRA00228 Reverse Cliff tops\nGRA00227 Reverse Cliff tops\nGRA02711 ROR Archer attacking\nGRA02718 ROR Archer decaying\nGRA02714 ROR Archer dying\nGRA02721 ROR Archer moving\nGRA02717 ROR Archer standing\nGRA02787 ROR Phalanx attacking\nGRA02794 ROR Phalanx decaying\nGRA02790 ROR Phalanx dying\nGRA02797 ROR Phalanx moving\nGRA00601 ROR Swordsman decaying\nGRA00597 ROR Swordsman dying\nGRA00594 ROR Swordsman fighting\nGRA00604 ROR Swordsman moving\nGRA00600 ROR Swordsman standing\nGRA00928 Rubble 2 x 2\nGRA00932 Rubble 2 x 2 more burnt\nGRA00929 Rubble 3 x 3\nGRA00930 Rubble 4 x 4\nGRA00931 Rubble 5 x 5\nGRA04498 Saboteur moving\nGRA04497 Saboteur standing\nGRA04522 Same flag as GRA04358?\nGRA04523 Same flag as GRA04358?\nGRA00432 Same size fire as GRA00428?\nGRA00981 Samurai decaying\nGRA00977 Samurai dying\nGRA00974 Samurai fighting\nGRA00984 Samurai moving\nGRA00980 Samurai standing\nGRA03542 Saracens Wonder\nGRA00943 Scorpion decaying\nGRA00939 Scorpion dying\nGRA00936 Scorpion firing\nGRA00944 Scorpion moving\nGRA00942 Scorpion standing\nGRA00946 Scorpion wheels\nGRA02086 Scout cavalry decaying\nGRA02082 Scout cavalry dying\nGRA02079 Scout cavalry fighting\nGRA02089 Scout cavalry moving\nGRA02085 Scout cavalry standing\nGRA01659 Sea Rock\nGRA01663 Sea Rock\nGRA03807 Shadow - appears identical to GRA03803\nGRA00119 Shadow / Background for GRA00118 arrow\nGRA04015 Shadow for 4023\nGRA04016 Shadow for 4024\nGRA04017 Shadow for 4025\nGRA04018 Shadow for 4026\nGRA02544 Shadow for Arabic bombard tower - GRA02548\nGRA00296 Shadow for Arabic castle\nGRA04073 Shadow for Arabic fortified gate building sequence -- position - GRA04077\nGRA03712 Shadow for Arabic fortified gate building sequence / position - GRA03716\nGRA03728 Shadow for Arabic fortified gate building sequence \\ position - GRA03732\nGRA04161 Shadow for Arabic fortified gate building sequence | position - GRA04165\nGRA01932 Shadow for Arabic fortified gate closed / position - GRA01940\nGRA02433 Shadow for Arabic fortified gate closed \\ position - GRA02441\nGRA04029 Shadow for Arabic Fortified Gate open -- position - GRA04037\nGRA04117 Shadow for Arabic Fortified Gate open | position - GRA04125\nGRA02361 Shadow for Arabic fortified gate raised / position - GRA02369\nGRA02469 Shadow for Arabic fortified gate raised \\ position - GRA02477\nGRA04005 Shadow for Arabic Fortified gate shut -- position - GRA04013\nGRA04093 Shadow for Arabic Fortified Gate shut | position - GRA04101\nGRA02397 Shadow for Arabic fortified gate tower - GRA02403\nGRA04065 Shadow for Arabic gate building sequence -- position - GRA04069\nGRA03704 Shadow for Arabic gate building sequence / position - GRA03708\nGRA03720 Shadow for Arabic gate building sequence \\ position - GRA03724\nGRA04153 Shadow for Arabic gate building sequence | position - GRA04157\nGRA01920 Shadow for Arabic gate closed / position - GRA01928\nGRA02421 Shadow for Arabic gate closed \\ position - GRA02429\nGRA04105 Shadow for Arabic Gate open | position - GRA04113\nGRA02349 Shadow for Arabic gate raised / position - GRA02357\nGRA02457 Shadow for Arabic gate raised \\ position - GRA02465\nGRA03993 Shadow for Arabic Gate shut -- position - GRA04001\nGRA04081 Shadow for Arabic Gate shut | position - GRA04089\nGRA02658 Shadow for Arabic guard tower - GRA02666\nGRA02536 Shadow for Arabic keep - GRA02540\nGRA02385 Shadow for Arabic stone gate tower - GRA02393\nGRA04350 Shadow for Arabic watch tower\nGRA03800 Shadow for arrow?\nGRA02543 Shadow for Asian bombard tower - GRA02547\nGRA00295 Shadow for Asian castle\nGRA04072 Shadow for Asian fortified gate building sequence -- position - GRA04076\nGRA03711 Shadow for Asian fortified gate building sequence / position - GRA03715\nGRA03727 Shadow for Asian fortified gate building sequence \\ position - GRA03731\nGRA04160 Shadow for Asian fortified gate building sequence | position - GRA04164\nGRA01931 Shadow for Asian fortified gate closed / position - GRA01939\nGRA02432 Shadow for Asian fortified gate closed \\ position - GRA02440\nGRA04028 Shadow for Asian Fortified Gate open -- position - GRA04036\nGRA04116 Shadow for Asian Fortified Gate open | position - GRA04124\nGRA02360 Shadow for Asian fortified gate raised / position - GRA02368\nGRA02468 Shadow for Asian fortified gate raised \\ position - GRA02476\nGRA04004 Shadow for Asian Fortified gate shut -- position - GRA04012\nGRA04092 Shadow for Asian Fortified Gate shut | position - GRA04100\nGRA02396 Shadow for Asian fortified gate tower - GRA02402\nGRA04064 Shadow for Asian gate building sequence -- position - GRA04068\nGRA03703 Shadow for Asian gate building sequence / position - GRA03707\nGRA03719 Shadow for Asian gate building sequence \\ position - GRA03723\nGRA04152 Shadow for Asian gate building sequence | position - GRA04156\nGRA01919 Shadow for Asian gate closed / position - GRA01927\nGRA02420 Shadow for Asian gate closed \\ position - GRA02428\nGRA04104 Shadow for Asian Gate open | position - GRA04112\nGRA02348 Shadow for Asian gate raised / position - GRA02356\nGRA02456 Shadow for Asian gate raised \\ position - GRA02464\nGRA03992 Shadow for Asian Gate shut -- position - GRA04000\nGRA04080 Shadow for Asian Gate shut | position - GRA04088\nGRA02657 Shadow for Asian guard tower - GRA02665\nGRA02535 Shadow for Asian keep - GRA02539\nGRA02384 Shadow for Asian stone gate tower - GRA02392\nGRA04349 Shadow for Asian watch tower\nGRA03519 Shadow for Byzantines wonder - GRA03545\nGRA03512 Shadow for Celts wonder - GRA03538\nGRA03515 Shadow for Frank wonder - GRA03541\nGRA03509 Shadow for Goths wonder - GRA03535\nGRA00377 Shadow for GRA00385\nGRA00378 Shadow for GRA00386\nGRA00379 Shadow for GRA00387\nGRA00380 Shadow for GRA00388\nGRA00389 Shadow for GRA00397\nGRA00390 Shadow for GRA00398\nGRA00391 Shadow for GRA00399\nGRA00392 Shadow for GRA00400\nGRA03804 Shadow for GRA03803\nGRA03813 Shadow for GRA03811 & GRA03812\nGRA03816 Shadow for GRA03814 & GRA03815\nGRA03511 Shadow for Japanese wonder - GRA03537\nGRA03513 Shadow for Mongolian wonder - GRA03539\nGRA02542 Shadow for North European bombard tower - GRA02546\nGRA00294 Shadow for North European castle\nGRA04071 Shadow for North European fortified gate building sequence -- position - GRA04075\nGRA03710 Shadow for North European fortified gate building sequence / position - GRA03714\nGRA03726 Shadow for North European fortified gate building sequence \\ position - GRA03730\nGRA04159 Shadow for North European fortified gate building sequence | position - GRA04163\nGRA01930 Shadow for North European fortified gate closed / position - GRA01938\nGRA02431 Shadow for North European fortified gate closed \\ position - GRA02439\nGRA04027 Shadow for North European Fortified Gate open -- position - GRA04035\nGRA04115 Shadow for North European Fortified Gate open | position - GRA04123\nGRA02359 Shadow for North European fortified gate raised / position - GRA02367\nGRA02467 Shadow for North European fortified gate raised \\ position - GRA02475\nGRA04003 Shadow for North European Fortified Gate shut -- position - GRA04011\nGRA04091 Shadow for North European Fortified Gate shut | position - GRA04099\nGRA02395 Shadow for North European fortified gate tower - GRA02401\nGRA04063 Shadow for North European gate building sequence -- position - GRA04067\nGRA03702 Shadow for North European gate building sequence / position - GRA03706\nGRA03718 Shadow for North European gate building sequence \\ position - GRA03722\nGRA04151 Shadow for North European gate building sequence | position - GRA04155\nGRA01918 Shadow for North European gate closed / position - GRA01926\nGRA02419 Shadow for North European gate closed \\ position - GRA02427\nGRA04103 Shadow for North European Gate open | position - GRA04111\nGRA02347 Shadow for North European gate raised / position - GRA02355\nGRA02455 Shadow for North European gate raised \\ position - GRA02463\nGRA03991 Shadow for North European Gate shut -- position - GRA03999\nGRA04079 Shadow for North European Gate shut | position - GRA04087\nGRA02656 Shadow for North European guard tower - GRA02664\nGRA02534 Shadow for North European keep - GRA02538\nGRA02383 Shadow for North European stone gate tower - GRA02391\nGRA04348 Shadow for North European watch tower\nGRA03514 Shadow for Persian wonder - GRA03540\nGRA03516 Shadow for Saracens wonder - GRA03542\nGRA03518 Shadow for Teutons wonder - GRA03544\nGRA04364 Shadow for the outpost\nGRA03802 Shadow for throwing axe?\nGRA04683 Shadow for Tower of the Flies GRA04684\nGRA03517 Shadow for Turkish wonder - GRA03543\nGRA03510 Shadow for Vikings wonder - GRA03536\nGRA02545 Shadow for West European bombard tower - GRA02549\nGRA00297 Shadow for West European castle\nGRA04074 Shadow for West European fortified gate building sequence -- position - GRA04078\nGRA03713 Shadow for West European fortified gate building sequence / position - GRA03717\nGRA03729 Shadow for West European fortified gate building sequence \\ position - GRA03733\nGRA04162 Shadow for West European fortified gate building sequence | position - GRA04166\nGRA01933 Shadow for West European fortified gate closed / position - GRA01941\nGRA02434 Shadow for West European fortified gate closed \\ position - GRA02442\nGRA04030 Shadow for West European Fortified Gate open -- position - GRA04038\nGRA04118 Shadow for West European Fortified Gate open | position - GRA04126\nGRA02362 Shadow for West European fortified gate raised / position - GRA02370\nGRA02470 Shadow for West European fortified gate raised \\ position - GRA02478\nGRA04006 Shadow for West European Fortified gate shut -- position - GRA04013\nGRA04094 Shadow for West European Fortified Gate shut | position - GRA04102\nGRA02398 Shadow for West European fortified gate tower - GRA02404\nGRA04066 Shadow for West European gate building sequence -- position - GRA04070\nGRA03705 Shadow for West European gate building sequence / position - GRA03709\nGRA03721 Shadow for West European gate building sequence \\ position - GRA03725\nGRA04154 Shadow for West European gate building sequence | position - GRA04158\nGRA01921 Shadow for West European gate closed / position - GRA01929\nGRA02422 Shadow for West European gate closed \\ posion - GRA02430\nGRA04106 Shadow for West European Gate open | position - GRA04114\nGRA02350 Shadow for West European gate raised / position - GRA02358\nGRA02458 Shadow for West European gate raised \\ position - GRA02466\nGRA03994 Shadow for West European Gate shut -- position - GRA04002\nGRA04082 Shadow for West European Gate shut | position - GRA04090\nGRA02659 Shadow for West European guard tower - GRA02667\nGRA02537 Shadow for West European keep - GRA02541\nGRA00273 Shadow for West European monastery\nGRA02386 Shadow for West European stone gate tower - GRA02394\nGRA04351 Shadow for West European watch tower\nGRA02517 Shadow from a beta mill - appears identical to GRA02515\nGRA02518 Shadow from a beta mill - appears identical to GRA02515 & GRA02517\nGRA02515 Shadow from a beta mill include rope stays?\nGRA00374 Shadow from Dark Age Dock\nGRA00730 Shadow of a Mills sails\nGRA04336 Shadow of a ship\nGRA04501 Shadow of a ships hull\nGRA04502 Shadow of a ships hull\nGRA04503 Shadow of a ships hull\nGRA04508 Shadow of a ships hull\nGRA04509 Shadow of a ships hull\nGRA04510 Shadow of a ships hull\nGRA04512 Shadow of a ships hull\nGRA04513 Shadow of a ships hull\nGRA04514 Shadow of a ships hull\nGRA04515 Shadow of a ships hull\nGRA04516 Shadow of a ships hull\nGRA04517 Shadow of a ships hull\nGRA02104 Shadows for Arabic fortified wall - GRA02112\nGRA03748 Shadows for Arabic Fortified wall building sequence - GRA03752\nGRA02092 Shadows for Arabic stone wall - GRA02100\nGRA03740 Shadows for Arabic wall building sequence - GRA03744\nGRA02103 Shadows for Asian fortified wall - GRA02111\nGRA03747 Shadows for Asian Fortified wall building sequence - GRA03751\nGRA02091 Shadows for Asian stone wall - GRA02099\nGRA03739 Shadows for Asian wall building sequence - GRA03743\nGRA01353 Shadows for beta walls in GRA01353\nGRA02102 Shadows for North European fortified wall - GRA02110\nGRA03746 Shadows for North European Fortified wall building sequence - GRA03750\nGRA02090 Shadows for North European stone wall - GRA02098\nGRA03738 Shadows for North European wall building sequence - GRA03742\nGRA02105 Shadows for West European fortified wall - GRA02113\nGRA03749 Shadows for West European Fortified wall building sequence - GRA03753\nGRA02093 Shadows for West European stone wall - GRA02101\nGRA03741 Shadows for West European wall building sequence - GRA03745\nGRA03631 Sheep decaying\nGRA03626 Sheep dying\nGRA03634 Sheep moving\nGRA03629 Sheep standing\nGRA00692 Ship destroyed - appears identical to GRA00495\nGRA00499 Ship from ROR\nGRA01838 Ship from ROR - appears identical to GRA00499\nGRA01808 Ship possibly ROR unit - appears identical to GRA00653 & GRA00657\nGRA00653 Ship ROR?\nGRA00657 Ship ROR? - appears identical to GRA00653\nGRA00495 Ship sinking – maybe a galleon?\nGRA02753 Ship sinking wreckage\nGRA02778 Ship sinking wreckage\nGRA02116 Ship wreckage sinking - appears identical to GRA00495, 693 & 1834\nGRA01812 Ship’s hull - unidentified\nGRA01784 Ship’s hull unknown - appears identical to GRA01780\nGRA01780 Ship’s hull unknown - beta\nGRA01834 Ship’s wreckage sinking - appears identical to GRA00495 & GRA00692\nGRA04195 Ship's Sail sequence with a lion head?\nGRA04196 Ship's Sail sequence with a lion head?\nGRA04197 Ship's Sail sequence with a lion head?\nGRA04198 Ship's Sail sequence with a lion head?\nGRA03560 Siege Onager decaying\nGRA03556 Siege Onager dying\nGRA03553 Siege Onager firing\nGRA03561 Siege Onager moving\nGRA03559 Siege Onager standing\nGRA03563 Siege Onager Wheels\nGRA03027 Siege ram attacking\nGRA03036 Siege ram decaying\nGRA03032 Siege ram dying\nGRA03037 Siege ram moving\nGRA03029 Siege Ram ram section attacking\nGRA03035 Siege ram standing\nGRA03039 Siege ram wheels\nGRA04193 Single Fire Ball\nGRA00167 Single Handed Axeman Decaying\nGRA00163 Single Handed Axeman Dying\nGRA00160 Single Handed Axeman Fighting\nGRA04476 Skeleton\nGRA01651 Skirmisher decaying\nGRA01647 Skirmisher dying\nGRA01644 Skirmisher fighting\nGRA01654 Skirmisher moving\nGRA01650 Skirmisher standing\nGRA00441 Small boat sinking maybe the fishing boat?\nGRA00424 Small Fire\nGRA00425 Small Fire\nGRA00426 Small Fire\nGRA03736 Small Pavilion door to left\nGRA03735 Small Pavilion door to right, armour outside\nGRA04280 Small Pyramid\nGRA00236 Small size building sequence\nGRA00971 Small Smoke puff or explosion?\nGRA00732 Smaller part shadow of Mill\nGRA03815 Smoke Ball\nGRA04520 Smoke from ????\nGRA00416 Smoke from explosion\nGRA03977 Smoke or splash?\nGRA03406 Smoke or Water Splash\nGRA03986 Smoke puff or splash?\nGRA03974 Smoke puff?\nGRA00050 Some sort of Arrow\nGRA03820 Spear\nGRA03821 Spear\nGRA00874 Spearman decaying\nGRA00870 Spearman dying\nGRA00867 Spearman fighting\nGRA00877 Spearman moving\nGRA00873 Spearman standing\nGRA04655 Splash of Water\nGRA03975 Splash?\nGRA01869 Stone Icon?\nGRA01034 Stone mine\nGRA04482 Stone Mine sequence\nGRA00173 Swinging Part of a Battering Ram\nGRA02264 Swordsman fighting?\nGRA01195 Teutonic knight decaying\nGRA01191 Teutonic knight dying\nGRA01188 Teutonic knight fighting\nGRA01198 Teutonic knight moving\nGRA01194 Teutonic knight standing\nGRA03544 Teutons Wonder\nGRA04684 The Tower of Flies\nGRA03801 Throwing Axe\nGRA01058 Throwing axeman decaying\nGRA01054 Throwing axeman dying\nGRA01051 Throwing axeman fighting\nGRA01061 Throwing axeman moving\nGRA01057 Throwing axeman standing\nGRA04583 Thrown scimitar sequence\nGRA04575 Top Part of Forest Trees\nGRA04578 Top Part of Palm Trees\nGRA04581 Top Part of Pine Trees\nGRA00891 Top piece of Dark Age Town Center main building\nGRA01900 Trade cart loaded - grey version - not used\nGRA04254 Trade cog hull\nGRA01138 Trade Workshop same as GRA01136\nGRA01139 Trade Workshop same as GRA01136\nGRA04331 Transportation ship's hull\nGRA01246 Trebuchet decaying\nGRA01241 Trebuchet dying\nGRA01237 Trebuchet firing\nGRA01244 Trebuchet standing\nGRA01254 Tree\nGRA01256 Tree\nGRA01258 Tree\nGRA01260 Tree\nGRA01264 Tree\nGRA01266 Tree\nGRA01270 Tree\nGRA01272 Tree\nGRA01252 Tree chopped down\nGRA01273 Tree shadow\nGRA01250 Tree Shadow for GRA01251\nGRA01253 Tree shadow for GRA01254\nGRA01255 Tree shadow for GRA01256\nGRA01257 Tree shadow for GRA01258\nGRA01259 Tree shadow for GRA01260\nGRA01261 Tree shadow for GRA01262\nGRA01263 Tree shadow for GRA01264\nGRA01265 Tree shadow for GRA01266\nGRA01267 Tree shadow for GRA01268\nGRA01269 Tree shadow for GRA01270\nGRA01271 Tree shadow for GRA01272\nGRA01035 Tree Stumps\nGRA03543 Turks Wonder\nGRA00183 Turning Wooden Wheels for the Battering Ram\nGRA02800 Two handed swordsman attacking\nGRA02807 Two handed swordsman decaying\nGRA02803 Two handed swordsman dying\nGRA02810 Two handed swordsman moving\nGRA02806 Two handed swordsman standing\nGRA02606 Unidentified animation - green coloured boulder maybe?\nGRA02593 Unidentified animation - spear maybe?\nGRA02596 Unidentified object?\nGRA04500 Unknown - small ball?\nGRA04477 Unknown 135 frame animated sequence\nGRA03976 Unknown 135 frame sequence\nGRA04674 Unknown 20 frame shadow sequence\nGRA04665 Unknown 5 frame shadow sequence\nGRA04666 Unknown 5 frame shadow sequence\nGRA04667 Unknown 5 frame shadow sequence\nGRA04668 Unknown 5 frame shadow sequence\nGRA04669 Unknown 5 frame shadow sequence\nGRA04670 Unknown 5 frame shadow sequence\nGRA04671 Unknown 5 frame shadow sequence\nGRA04672 Unknown 5 frame shadow sequence\nGRA04673 Unknown 5 frame shadow sequence\nGRA04675 Unknown 5 frame shadow sequence\nGRA04676 Unknown 5 frame shadow sequence\nGRA04677 Unknown 5 frame shadow sequence\nGRA04678 Unknown 5 frame shadow sequence\nGRA04679 Unknown 5 frame shadow sequence\nGRA04680 Unknown 5 frame shadow sequence\nGRA03798 Unknown animation sequence - Some kind of burning missile?\nGRA03754 Unknown animation sequence?\nGRA04410 Unknown hero attacking\nGRA04276 Unknown hero decaying - appears identical to GRA03853\nGRA04284 Unknown hero decaying - same as GRA04276 etc\nGRA04417 Unknown hero decaying as above\nGRA04274 Unknown hero dying - appears identical to GRA03849\nGRA04282 Unknown hero dying - same as GRA04274 etc\nGRA04413 Unknown hero dying as above\nGRA04281 Unknown hero fighting - same as GRA04273 etc\nGRA03924 Unknown Hero knight attacking\nGRA03931 Unknown Hero knight decaying\nGRA03892 Unknown Hero knight decaying - appears identical to GRA03853\nGRA03970 Unknown Hero knight decaying - appears identical to GRA03853\nGRA03853 Unknown Hero knight decaying?\nGRA03927 Unknown Hero knight dying\nGRA03888 Unknown Hero knight dying - appears identical to GRA03849\nGRA03966 Unknown Hero knight dying - appears identical to GRA03849\nGRA03849 Unknown Hero knight dying?\nGRA03885 Unknown Hero knight fighting - appears identical to GRA03846\nGRA03963 Unknown Hero knight fighting - appears identical to GRA03846\nGRA03846 Unknown Hero knight fighting?\nGRA03934 Unknown Hero knight moving\nGRA03895 Unknown Hero knight moving - appears identical to GRA03856\nGRA03973 Unknown Hero knight moving - appears identical to GRA03856\nGRA03856 Unknown Hero knight moving?\nGRA03930 Unknown Hero knight standing\nGRA03891 Unknown Hero knight standing - appears identical to GRA03852\nGRA03969 Unknown Hero knight standing - appears identical to GRA03852\nGRA03852 Unknown Hero knight standing?\nGRA04277 Unknown hero moving - appears identical to GRA03856\nGRA04285 Unknown hero moving - same as GRA04277 etc\nGRA04420 Unknown hero moving as above\nGRA04275 Unknown hero standing - appears identical to GRA03852\nGRA04283 Unknown hero standing - same as GRA04275 etc\nGRA04416 Unknown hero standing as above\nGRA04273 Unknown hero type - appears identical to GRA03846\nGRA03818 Unknown shadow dot?\nGRA02554 Unknown shadows?\nGRA04256 Unknown Ships hull\nGRA04681 VDML Cheat Guy\nGRA00689 Vikings Long Boat\nGRA00699 Vikings Long Boat  - apears identical to GRA00689 & GRA00695\nGRA00695 Vikings Long Boat - appears identical to GRA00689 & GRA00699\nGRA03536 Vikings Wonder\nGRA03569 Villager female fisher casting net\nGRA03567 Villager female fisher dying\nGRA03565 Villager Female fisher moving carrying fish\nGRA03570 Villager female fisher moving with net\nGRA03568 Villager female fisher standing\nGRA03575 Villager male fisher casting net\nGRA03573 Villager male fisher dying\nGRA03576 Villager male moving with net or fish\nGRA03574 Villager male standing with net or fish\nGRA00802 War elephant decaying\nGRA00798 War elephant dying\nGRA00795 War elephant fighting\nGRA00805 War elephant moving\nGRA00801 War elephant standing\nGRA00170 Warrior with Round Shield and Sword moving\nGRA00166 Warrior with Round Shield and Sword?\nGRA01885 Water shallows tiles - appears to be ROR - not used n AOK\nGRA03548 Water splash\nGRA04368 Waterline water effects for a ship\nGRA03404 Waypoint Flag\nGRA00745 West European animation for mill sails shadows? Castle Age & Imperial Age\nGRA00036 West European Archery Range Castle Age & Imperial Age\nGRA00024 West European Archery Range Feudal Age\nGRA04235 West European back of small sail sequence - combine with GRA04231?\nGRA00145 West European Barracks Castle Age & Imperial Age\nGRA00133 West European Barracks Feudal Age\nGRA00105 West European Blacksmith Castle Age & Imperial Age\nGRA00093 West European Blacksmith Feudal Age\nGRA02549 West European bombard tower\nGRA00305 West European Castle\nGRA02247 West European Castle Age & Imperial Age houses\nGRA01021 West European Castle Age & Imperial Age Stable\nGRA00412 West European Castle Age dock similar to GRA00400\nGRA03612 West European Castle Age town center left side roof\nGRA03620 West European Castle Age town center left side roof support\nGRA03616 West European Castle Age town center left side roof supports\nGRA04628 West European Castle Age Town Center, right side roof\nGRA04636 West European Castle Age Town Center, right side roof support\nGRA04632 West European Castle Age Town Center, right side roof supports\nGRA04172 West European Damaged Stone Wall\nGRA00400 West European Dock Castle Age & Imperial Age\nGRA00388 West European Dock Feudal Age\nGRA02235 West European Feudal Age houses\nGRA01009 West European Feudal Age Stable\nGRA03600 West European Feudal Age town center left side roof\nGRA03608 West European Feudal Age town center left side roof support\nGRA03604 West European Feudal Age town center left side roof supports\nGRA04616 West European Feudal Age Town Center right side roof\nGRA04620 West European Feudal Age Town Center right side roof supports\nGRA04176 West European Fortified Damaged Wall\nGRA04078 West European fortified gate building sequence -- position\nGRA03717 West European Fortified gate building sequence / position\nGRA03733 West European Fortified gate building sequence \\ position\nGRA04166 West European fortified gate building sequence | position\nGRA02442 West European Fortified Gate closed \\ position\nGRA04038 West European Fortified Gate open -- position\nGRA04126 West European Fortified Gate open | position\nGRA02370 West European Fortified Gate raised / position\nGRA02478 West European Fortified Gate raised \\ position\nGRA04014 West European Fortified gate shut -- position\nGRA04102 West European Fortified Gate shut | position\nGRA02406 West European Fortified gate tower\nGRA04192 West European Fortified severely Damaged Wall\nGRA02113 West European Fortified Wall\nGRA03753 West European Fortified wall building sequence\nGRA04664 West European fortified wall shadows?\nGRA04624 West European Fuedal Age Town Center far right roof support\nGRA04070 West European gate building sequence -- position\nGRA03709 West European gate building sequence / position\nGRA03725 West European gate building sequence \\ position\nGRA04158 West European gate building sequence | position\nGRA01929 West European Gate closed / position\nGRA02430 West European Gate closed \\ position\nGRA04026 West European Gate open -- position\nGRA04114 West European Gate open | position\nGRA02358 West European Gate raised / position\nGRA02466 West European Gate raised \\ position\nGRA04002 West European Gate shut -- position\nGRA04090 West European Gate shut | position\nGRA02667 West European Guard Tower\nGRA04184 West European heavily damaged Fortified Wall\nGRA04180 West European heavily Damaged Stone Wall\nGRA03797 West European Imperial Age Market\nGRA04640 West European Imperial Age Town Center, right side roof\nGRA04648 West European Imperial Age Town Center, right side roof support\nGRA04644 West European Imperial Age Town Center, right side roof supports\nGRA02541 West European Keep\nGRA03507 West European lumber camp\nGRA00820 West European Market Castle Age\nGRA02278 West European Market Feudal Age\nGRA00749 West European mill animation Feudal Age\nGRA00741 West European Mill Feudal Age without Animation\nGRA00753 West European Mill without Animation Castle Age & Imperial Age\nGRA03495 West European Mining Camp\nGRA00281 West European Monastery.\nGRA04248 West European sail sequences\nGRA04188 West European severely Damaged Stone Wall\nGRA04304 West European ships main sail sequence\nGRA04601 West European ships main sail sequence\nGRA04312 West European ships sail\nGRA04227 West European Ships sail sequence\nGRA04298 West European ships sail sequence\nGRA04320 West European ships sail.\nGRA04308 West European ships small triangular sail\nGRA00958 West European Siege Workshop\nGRA04239 West European small sail sequence\nGRA04231 West European Small ships sail sequence\nGRA04243 West European small triangular sail sequence\nGRA00101 West European Smoke from Blacksmith\nGRA02394 West European Stone gate tower\nGRA02101 West European Stone Wall\nGRA00915 West European top piece of Castle Age Town Center\nGRA00903 West European top piece of Feudal Age Town Center\nGRA00927 West European top piece of Imperial Age Town Center\nGRA00923 West European town center flooring Imperial age\nGRA03472 West European town center left side roof supports Imperial Age\nGRA03476 West European town center left side roof supports Imperial Age\nGRA00907 West European town center support shadows Castle Age\nGRA00895 West European town center support shadows Feudal Age\nGRA00919 West European town center support shadows Imperial age\nGRA03468 West European Towncenter roof left side Imperial Age\nGRA01136 West European Trade Workshop\nGRA03835 West European University Castle Age\nGRA03839 West European University Imperial Age\nGRA03745 West European wall building sequence\nGRA04660 West European wall shadows?\nGRA02655 West European Watch Tower\nGRA01941 West Europeans Fortified Gate closed / position\nGRA03577 Wild boar running\nGRA01599 Woad raider decaying\nGRA01595 Woad raider dying\nGRA01592 Woad raider fighting\nGRA01602 Woad raider moving\nGRA01598 Woad raider standing\nGRA01629 Wolf attacking\nGRA01637 Wolf decaying\nGRA01632 Wolf dying\nGRA01636 Wolf moving\nGRA01640 Wolf running\nGRA01635 Wolf standing\nGRA00241 Wonder size building sequence\nGRA00933 Wonder size rubble\nGRA03985 Wrecked fish trap\nGRA04490 Yurt door to left, thatched roof\nGRA04492 Yurt door to right, thatched roof\nGRA04491 Yurt with logs outside, thatched roof\nGRA04495 Yurt, canvas roof, door to left\nGRA04494 Yurt, canvas roof, door to right\nGRA04493 Yurt, no door visible, canvas roof\nGRA04923 Aztec Wonder\nGRA05098 Back of large South American triangular sail\nGRA05099 Back of medium South American triangular sail\nGRA05096 Back of South American triangular sail\nGRA04963 Beta Dock from AOK GRA00409?\nGRA04723 Conquistador decaying\nGRA04719 Conquistador dying\nGRA04716 Conquistador fighting\nGRA04726 Conquistador moving\nGRA04722 Conquistador standing\nGRA04912 Crashes Mod Pack Studio AND SLP Converter - what is it ! ! !\nGRA05175 Debris from sinking ship identical to GRA00495 etc\nGRA04829 Eagle Warrior decaying\nGRA04827 Eagle Warrior dying\nGRA04826 Eagle Warrior fighting\nGRA04830 Eagle Warrior Moving\nGRA04828 Eagle Warrior standing\nGRA05241 ES Flag\nGRA04883 Flag sequence - possibly for the sea gate?\nGRA05142 Flag sequence probably for bombard tower GRA05143\nGRA05136 Flag sequence probably for watchtower\nGRA05139 Flag sequence probably for watchtower\nGRA05242 Flag Shadows - possibly for GRA05241\nGRA04914 Flags - 45 frames but only 9 appear to be used?\nGRA04981 Flags possibly for the fortified gate towers?\nGRA04978 Flags possibly for the gate towers?\nGRA05295 Flower patches\nGRA05303 Flying Dog\nGRA04729 Footprints in snow\nGRA04902 Gate building sequence -- position - West European\nGRA04886 Gate building sequence / position - looks like the West European - sea gate?\nGRA04894 Gate building sequence \\ position - West European\nGRA04910 Gate building sequence | position - West European\nGRA04897 Gate closed -- position - West European\nGRA04878 Gate closed / position - probably sea gate? West European\nGRA04889 Gate closed \\ position - West European\nGRA04905 Gate closed | position - West European\nGRA04900 Gate open -- position - West European\nGRA04881 Gate open / position - probably sea gate? West European\nGRA04892 Gate open \\ position - West European\nGRA04908 Gate open | position - West European\nGRA04964 Graveyard monuments\nGRA05296 Haystacks\nGRA05035 Heads on posts\nGRA04838 Horse dead\nGRA04836 Horse dying\nGRA04839 Horse moving\nGRA04837 Horse standing\nGRA05211 Horses and the warwagon decaying\nGRA05207 Horses and the warwagon dying\nGRA05214 Horses and wheel from the warwagon moving\nGRA05204 Horses from the warwagon standing\nGRA05210 Horses from the warwagon standing identical to GRA05204\nGRA04919 Hun Tarkan decaying\nGRA04917 Hun Tarkan dying\nGRA04916 Hun Tarkan fighting\nGRA04920 Hun Tarkan moving\nGRA04918 Hun Tarkan standing\nGRA04819 Hun Wonder\nGRA04856 Hussar decaying\nGRA04854 Hussar dying\nGRA04853 Hussar fighting\nGRA04857 Hussar moving\nGRA04855 Hussar standing\nGRA04834 Jaguar decaying\nGRA04832 Jaguar dying\nGRA04831 Jaguar fighting\nGRA04835 Jaguar moving\nGRA04833 Jaguar standing\nGRA04861 Jaguar Warrior decaying\nGRA04859 Jaguar Warrior dying\nGRA04858 Jaguar Warrior fighting\nGRA04862 Jaguar Warrior moving\nGRA04860 Jaguar Warrior standing\nGRA04728 Jungle\nGRA05218 Korean Turtle ship\nGRA05219 Korean Turtle ship appears identical to GRA05218\nGRA05220 Korean Turtle ship appears identical to GRA05218 and GRA05219\nGRA05238 Korean War Wagon\nGRA05239 Korean War Wagon appears identical to GRA05238\nGRA05240 Korean War Wagon appears identical to GRA05238 and GRA05239\nGRA05217 Korean Wonder\nGRA04820 Mayan Wonder\nGRA04865 Missionary converting\nGRA04869 Missionary converting? Either this or GRA04865 is converting the other is healing?\nGRA04868 Missionary decaying\nGRA04866 Missionary dying\nGRA04870 Missionary moving\nGRA04867 Missionary standing\nGRA05298 Monkey exploding\nGRA05297 Monkey fighting\nGRA05301 Monkey moving\nGRA05299 Monkey standing\nGRA05143 North European Bombard Tower\nGRA05113 North European castle age stable same as GRA01018\nGRA05110 North European feudal age stable same as GRA01006\nGRA05084 North European Imperial age town center flooring\nGRA04915 Pallisade - appears to be identical to AOK palisade\nGRA05045 Parakeet flying\nGRA05043 Parakeet gliding\nGRA05063 Plants\nGRA04874 Plumed archer decaying\nGRA04872 Plumed archer dying\nGRA04871 Plumed archer fighting\nGRA04875 Plumed archer moving\nGRA04873 Plumed archer standing\nGRA04823 Possibly an arrow - but only one frame ?\nGRA04824 Possibly an arrow - but only one frame ?\nGRA04825 Possibly an arrow - but only one frame ?\nGRA05292 Roman Ruins\nGRA05293 Ruined stone heads\nGRA05294 Ruined trade cart\nGRA04950 Sail - North European style\nGRA04882 Shadow - looks like some kind of tower?\nGRA05083 Shadow - possibly from South American castle age town center\nGRA05074 Shadow - possibly from South American feudal age town center\nGRA04911 Shadow for a watch tower - appears to match the West European one?\nGRA04921 Shadow for Aztec Wonder\nGRA05141 Shadow for Bombard tower\nGRA05049 Shadow for Castle Age South American mill GRA05050\nGRA05046 Shadow for Feudal Age South American mill GRA05047\nGRA04909 Shadow for gate building sequence | position - for GRA04910\nGRA04895 Shadow for gate closed -- position - for GRA04897\nGRA04887 Shadow for gate closed \\ position for GRA04889\nGRA04903 Shadow for gate closed | position - for GRA04905\nGRA04890 Shadow for gate open \\ position for GRA04892\nGRA04906 Shadow for gate open | position for GRA04908\nGRA05044 Shadow for Parakeet flying GRA05045\nGRA05052 Shadow for rope stays for mill\nGRA04954 Shadow for South American Castle\nGRA05135 Shadow for South American Castle Age watchtower GRA05137\nGRA04965 Shadow for South American closed gate / position GRA04967\nGRA04959 Shadow for South American dock Castle Age\nGRA04957 Shadow for South American dock Feudal Age\nGRA05132 Shadow for South American Feudal age watchtower GRA05134\nGRA04968 Shadow for South American fortified gate / position GRA04970\nGRA05017 Shadow for South American fortified gate building sequence -- position GRA05018\nGRA04985 Shadow for South American fortified gate building sequence / position GRA04986\nGRA05001 Shadow for South American fortified gate building sequence \\ position GRA05002\nGRA05033 Shadow for South American fortified gate building sequence | position GRA05034\nGRA05006 Shadow for South American fortified gate closed -- position GRA05008\nGRA04990 Shadow for South American fortified gate closed \\ position GRA04992\nGRA05022 Shadow for South American fortified gate closed | position GRA05024\nGRA05012 Shadow for South American fortified gate open -- position GRA05014\nGRA04974 Shadow for South American fortified gate open / position GRA04976\nGRA04996 Shadow for South American fortified gate open \\ position GRA04998\nGRA05028 Shadow for South American fortified gate opn | position GRA05030\nGRA04980 Shadow for South American fortified gate tower GRA04982\nGRA05150 Shadow for South American fortified wall badly damaged GRA05151\nGRA05146 Shadow for South American fortified wall damaged GRA05147\nGRA05125 Shadow for South American fortified wall GRA05126\nGRA05154 Shadow for South American fortified wall very badly damaged GRA05155\nGRA05015 Shadow for South American gate building sequence -- position GRA05016\nGRA04983 Shadow for South American gate building sequence / position GRA04984\nGRA04999 Shadow for South American gate building sequence \\ position GRA05000\nGRA05031 Shadow for South American gate building sequence | position GRA5032\nGRA05003 Shadow for South American gate closed -- position GRA05005\nGRA04987 Shadow for South American gate closed \\ position GRA04989\nGRA05019 Shadow for South American gate closed | position GRA05021\nGRA05009 Shadow for South American gate open -- position GRA05011\nGRA04971 Shadow for South American gate open / position GRA04973\nGRA04993 Shadow for South American gate open \\ position GRA04995\nGRA05025 Shadow for South American gate open | position GRA05027\nGRA04977 Shadow for South American gate tower GRA04978\nGRA05148 Shadow for South American stone wall badly damaged GRA05149\nGRA05144 Shadow for South American stone wall damaged GRA05145\nGRA05123 Shadow for South American stone wall GRA05124\nGRA05152 Shadow for South American stone wall very badly damaged GRA05153\nGRA05138 Shadow for South American Watchtower Imperial Age GRA05140\nGRA05168 Shadow for the Turtle ship ?\nGRA04813 Shadow from Hun Wonder\nGRA05215 Shadow from Korean wonder\nGRA04814 Shadow from Mayan Wonder\nGRA05101 Shadow from siege workshop\nGRA04812 Shadow from Spanish Wonder\nGRA05176 Shadow from Turtle ship?\nGRA05182 Shadow from Turtle ship?\nGRA04876 Shadow of a gate closed / position - probably for sea gate? for GRA04878\nGRA04898 Shadow of gate open -- position - for GRA04900\nGRA04879 Shadow of Gate open / position - probably sea gate? for GRA04881\nGRA05042 Shadow of Parakeet gliding GRA05042\nGRA05130 Shadow of South American fortified wall building sequence GRA05131\nGRA05128 Shadow of South American wall building sequence GRA05129\nGRA05302 Shadow of the flying dog\nGRA04901 Shadows for gate building sequence -- position for GRA04902\nGRA04885 Shadows for Gate building sequence / position - possibly sea gate? for GRA04886\nGRA04893 Shadows for Gate building sequence \\ position - West European for GRA04894\nGRA04727 Shadows for the jungle\nGRA05100 Signpost\nGRA04933 Smoke for the South American blacksmith\nGRA04731 Snow covered trees\nGRA04700 Snow patch\nGRA04701 Snow patch\nGRA04702 Snow patch\nGRA04703 Snow patch\nGRA04704 Snow patch\nGRA04705 Snow patch\nGRA04699 Snow patch - possibly for a building roof?\nGRA05300 Some part of the Monkey?\nGRA04929 South American Archery Range Castle Age\nGRA04926 South American Archery range Feudal Age\nGRA04937 South American back of small triangualer sail\nGRA04939 South American back of small triangular sail\nGRA04947 South American Barracks Castle Age\nGRA04944 South American Barracks Dark Age\nGRA04934 South American Blacksmith Castle Age\nGRA04931 South American Blacksmith Feudal Age\nGRA05051 South American Casle Age Mill\nGRA04956 South American Castle\nGRA05050 South American Castle Age animated sail sequence for mill\nGRA04960 South American Dock Castle Age\nGRA04958 South American Dock Feudal Age\nGRA05307 South American empty trade cart decaying\nGRA05305 South American empty trade cart dying\nGRA05308 South American empty trade cart moving\nGRA05306 South American empty trade cart standing\nGRA05047 South American Feudal Age Animated sail sequence for mill\nGRA05048 South American Feudal Age Mill\nGRA04986 South American fortified gate building sequence\nGRA05018 South American fortified gate building sequence -- position\nGRA05002 South American fortified gate building sequence \\ position\nGRA05034 South American fortified gate building sequence | position\nGRA05008 South American fortified gate closed -- position\nGRA04970 South American fortified gate closed / position\nGRA04992 South American fortified gate closed \\ position\nGRA05024 South American fortified gate closed | position\nGRA05014 South American fortified gate open -- position\nGRA04976 South American fortified gate open / position\nGRA04998 South American fortified gate open \\ position\nGRA05030 South American fortified gate opn | position\nGRA04982 South American fortified gate tower\nGRA05126 South American Fortified Wall\nGRA05151 South American Fortified wall badly damaged\nGRA05131 South American fortified wall building sequence\nGRA05147 South American Fortified wall damaged\nGRA05155 South American Fortified wall very badly damaged\nGRA05016 South American gate building sequence -- position\nGRA04984 South American gate building sequence / position\nGRA05000 South American gate building sequence \\ position\nGRA05032 South American gate building sequence | position\nGRA05005 South American gate closed -- position\nGRA04967 South American gate closed / position\nGRA04989 South American gate closed \\ position\nGRA05021 South American gate closed | position\nGRA05011 South American gate open -- position\nGRA04973 South American gate open / position\nGRA04995 South American gate open \\ position\nGRA05027 South American gate open | position\nGRA04979 South American gate tower\nGRA05038 South American House Dark Age\nGRA05041 South American House Feudal Age\nGRA05311 South American loaded trade cart decaying\nGRA05312 South American loaded trade cart moving\nGRA05310 South American loaded trade cart standing\nGRA05106 South American lumber mill\nGRA05061 South American Market Castle Age\nGRA05058 South American Market Feudal Age\nGRA05062 South American Market Imperial Age\nGRA05095 South American medium triangular sail\nGRA05055 South American Mining Camp\nGRA05284 South American priest converting\nGRA05288 South American priest converting appears identical to GRA05284\nGRA05287 South American priest decaying\nGRA05285 South American priest dying\nGRA05286 South American priest standing\nGRA05290 South American priest standing with relic\nGRA05289 South American priest walking\nGRA05291 South American priest walking with relic\nGRA05103 South American Siege Workshop\nGRA04936 South American small triangular sail\nGRA04938 South American small triangular sail\nGRA05094 South American small triangular sail\nGRA05124 South American Stone Wall\nGRA05149 South American Stone wall badly damaged\nGRA05145 South American Stone wall damaged\nGRA05153 South American Stone wall very badly damaged\nGRA04953 South American Temple\nGRA05076 South American town center castle age left side roof\nGRA05078 South American town center castle age left side roof single support\nGRA05077 South American town center castle age left side roof two supports\nGRA05079 South American town center castle age right side roof\nGRA05080 South American town center castle age right side roof single support\nGRA05081 South American town center castle age right side roof single support\nGRA05082 South American town center center section castle age\nGRA05073 South American town center center section feudal age\nGRA05067 South American Town Center feudal age left side roof\nGRA05069 South American town center feudal age left side roof single support\nGRA05068 South American town center feudal age left side roof two supports\nGRA05070 South American town center feudal age right side roof\nGRA05071 South American town center feudal age right side roof single support\nGRA05072 South American town center feudal age right side roof single support\nGRA05091 South American town center imperial age center section\nGRA05085 South American town center imperial age left side roof\nGRA05087 South American town center imperial age left side roof three supports\nGRA05086 South American town center imperial age left side roof two supports\nGRA05088 South American town center imperial age right side roof\nGRA05090 South American town center imperial age right side roof single support\nGRA05089 South American town center imperial age right side roof three supports\nGRA04935 South American triangular sail\nGRA05097 South American triangular sail\nGRA05092 South American triangular sail sequence sail emptying\nGRA05093 South American triangular sail sequence sail filling\nGRA05119 South American University Castle Age\nGRA05122 South American University Imperial Age\nGRA05129 South American wall building sequence\nGRA05137 South American Watchtower Castle Age\nGRA05134 South American Watchtower Feudal Age\nGRA05140 South American Watchtower Imperial Age\nGRA05309 South Americant loaded trade cart dying\nGRA04818 Spanish Wonder\nGRA05107 Statue\nGRA04884 Tall Pallisade gate tower - from sea gate trick\nGRA04730 Tree/forest shadows\nGRA05166 Turkey  decaying\nGRA05164 Turkey running\nGRA05165 Turkey standing\nGRA05167 Turkey walking\nGRA05065 Unknown Shadow\nGRA04913 Unknown Shadow?\nGRA05118 Unknown small dot\nGRA05121 Unknown small dot\nGRA05127 Unused two headed lion sail\nGRA04710 Wagon Tracks in Snow\nGRA05116 West European trade workshop same as GRA01136\n```\n\nterrain.drs\n===========\n\n```\nTer15021 Bare Farm\nTer15010 Clear Dirt or Desert\nTer15005 Dead Farm\nTer15017 Desert? or Beach Like\nTer15000 Dirt 1\nTer15006 Dirt 2\nTer15004 Farm\nTer15022 Farm\nTer15023 Farm\nTer15001 Grass\nTer15008 Grass 1\nTer15009 Grass with dirt\nTer15024 Ice\nTer15011 Leaves\nTer15018 Road 1\nTer15019 Road 2\nTer15014 Shallow\nTer15007 Some Dirt\nTer15002 Water 1\nTer15015 Water 2\nTer15016 Water 3\nGRA15024 Ice\nGRA15031 Road with fungus\nGRA15030 Road with snow\nGRA15026 Snow\nGRA15029 Snow with ?????\nGRA15027 Snow with dirt\nGRA15028 Snow with grass\n```\n\ninterfac.drs\n============\n\n```\nINT50101 Blank sheet of parchment?\nINT50102 Blank sheet of parchment? Maybe different colors?\nINT50103 Blank sheet of parchment? Maybe different colors?\nINT50104 Blank sheet of parchment? Maybe different colors?\nINT50127 Possibly piece of  SCENARIOBKG.bmp\nINT50131 Maybe history section background\nINT50137 Maybe history background, possibly same as INT50137?\nINT50145 Background for insrcutions at the start of a custom scenario. (Also hints & history)\nINT50149 Score background\nINT50150 Blank grey strip???\nINT50155 The design prodution Page?\nINT50156 The design production page(no text)?\nINT50157 The programming page?\nINT50161 Possibly page from one of the ES cpn movies\nINT50162 Pics used in history section?\nINT50163 Conquerors load up screen\nINT50188 Possibly INT50156 with bottom cut off\nINT50189 Conquerors game screen when finished loading (town)\nINT50190 Possibly INT50127 with different colors?\nINT50202 ?????????????????????????????\nINT50204 ??? Possibly INT50204 with different colors ???\nINT50212 Section of wood?\nINT50213 Same as INT50212 with a few white distortions.\nINT50221 Victory/Loss background casting shadow over white ?? paper ??\nINT50222 Piece of parchment of wood, casting a shadow over a white ?? paper ?? Piece of above?\nINT50223 INT50221 with a compass on it??\nINT50224 INT50222 Enlarged?\nINT50225 Same as INT50221\nINT50226 Same as INT50224\nINT50230 Player colors (each color contained in it’s own frame)\nINT50231 Age of Kings game screen after startup (town)\nINT50232 Possibly page used in ES cpn movie? Not same as INT50161\nINT50233 ?????????????????????????????\nINT50235 Possibly page used in ES cpn movie?\nINT50236 Possibly page used in ES cpn movie?\nINT50237 Possibly page used in ES cpn movie?\nINT50238 Possibly page used in ES cpn movie?\nINT50242 Possibly page used in ES cpn movie?\nINT50259 ?????????????????????????????\nINT50260 Maybe INT50259 with the blue changed to black?\nINT50261 Maybe INT50259 or INT50260 with green changed to cyan?\nINT50262 Possibly same as INT50259?\nINT50266 Again, INT50259 with colors changed\nINT50269 And again\nINT50270 Possibly INT50204 with colors changed?\nINT50339 Box of player colors with a white box in them (each color contained in it’s own frame)\nINT50340 Same as INT50339 with the white bigger and no gray. Frames in different order\nINT50341 Parchment with ordnate lines on it\nINT50342 Info part of tech chart, no text\nINT50343 Possibly INT50127 enlarged?\nINT50400 Copyright symbol ©(I think)\nINT50403 Looks like a tiny black line\nINT50404 Copyright symbol ©(it think)\nINT50406 Reserved copyright ® symbol\nINT50601 Cotains little checks and x’s in it’s frames?\nINT50602 Same as INT50601?\nINT50603 Same as INT50601?\nINT50604 Same as INT50601, different background color\nINT50606 Same as above, different background color, few different icons\nINT50609 Same as above, different background color\nINT50610 Same as INT50606? Different colors\nINT50611 Same as INT50606? Different colors\nINT50612 Same as above with different colors & back background. Crashes SLP viewer\nINT50613 A red line with black spirals? Has a few strange icons in frames\nINT50688 Same as INT50606? Different colors\nINT50705 Building icons?\nINT50706 Same as INT50705?\nINT50707 Same as INT50705?\nINT50708 Same as INT50705?\nINT50713 Hieroglyphics?\nINT50714 Same as INT50713, different color stone?\nINT50715 Same as INT50713, different color stone?\nINT50716 Same as INT50713?\nINT50717 Weird bar?\nINT50718 Gray bar, same size as INT50717? Some type of button?\nINT50719 Blue bricks, same size as INT50717? Some type of button?\nINT50720 Brown bar, same size as INT50717? Some type of button?\nINT50721 Command Icons?\nINT50725 Mini map buttons?\nINT50726 Buttons at top (e.g. chat, menu, objectives, tech tree, etc.)\nINT50727 Same as INT50725?\nINT50728 Same as INT50725?\nINT50729 Tech icons?\nINT50730 Unit icons?\nINT50731 Little symbols (wood, food, gold, stone, goods, attack, armour, pierce armour, range)\nINT50732 Resource icons?\nINT50745 Health bar?\nINT50746 Possibly garrison flag?\nINT50747 Bar, similar to INT50717? Possibly a button?\nINT50748 Bar, similar to INT50718? Possibly a button?\nINT50749 Bar, similar to INT50719? Possibly a button?\nINT50750 Bar, similar to INT50720? Possibly a button?\nINT50751 Beta interface icons!\nINT50752 Mini map buttons (dimonds)\nINT50753 Possibly same as INT50726?\nINT50754 The final buttons that you see at the top (INT50726 in casings)\nINT50760 Resource icons & population icon at the top\nINT50761 Timeline icons\nINT50762 Probably garrison flag\nINT50763 ????????????????????????????????????????????\nINT50764 Building progress bar?\nINT50765 ????????????????????????????????????????????\nINT50766 Same as INT50761?\nINT50767 Shadow of something?\nINT50768 ??? Maybe a button? ???\nINT50769 Shields?\nINT50788 Final mini map buttons\nINT50789 Same as INT50788?\nINT50790 INT50788 enlarged?\nINT50791 Button from chat options?\nINT50792 Colored bar, probably player colors (each color has it’s own frame)\nINT51000 Mouse icons\nINT51001 Normal mouse icon, probably used if the user has “NoMouse” command line selected\nINT51002 Same as INT51001 except pink\nINT51003 Same as INT51001?\nINT51004 Same as INT51001 except green\nINT51005 INT51001 with orange border?\nINT51007 Same as INT51001?\nINT51008 Same as INT51001?\nINT51009 Same as INT51001 with yellow border?\nINT51010 Same as INT51001 with blue border?\nINT51101 Top with no menu icons and beta food icon?\nINT51102 Same as INT51101 but different background?\nINT51103 Same as INT51101 but different background?\nINT51104 Same as INT51101 but different background?\nINT51105 Same as INT51101 but different background?\nINT51106 Same as INT51101 but different background?\nINT51107 Same as INT51101 but different background?\nINT51108 Same as INT51101 but different background?\nINT51109 Same as INT51101 but different background?\nINT51110 Same as INT51101 but different background?\nINT51111 Same as INT51101 but different background?\nINT51112 Same as INT51101 but different background?\nINT51113 Same as INT51101 but different background?\nINT51114 Top with no menu icons?\nINT51115 Same as INT51114 but different background?\nINT51116 Same as INT51114 but different background?\nINT51117 Same as INT51114 but different background?\nINT51118 Same as INT51114 but different background?\nINT51121 Same as INT51101 but different background?\nINT51122 Same as INT51101 but different background?\nINT51123 Same as INT51101 but different background?\nINT51124 Same as INT51101 but different background?\nINT51125 Same as INT51101 but different background?\nINT51126 Same as INT51101 but different background?\nINT51127 Same as INT51101 but different background?\nINT51128 Same as INT51101 but different background?\nINT51129 Same as INT51101 but different background?\nINT51130 Same as INT51101 but different background?\nINT51131 Same as INT51101 but different background?\nINT51132 Same as INT51101 but different background?\nINT51133 Same as INT51101 but different background?\nINT51134 Same as INT51114 but different background?\nINT51135 Same as INT51114 but different background?\nINT51136 Same as INT51114 but different background?\nINT51137 Same as INT51114 but different background?\nINT51138 Same as INT51114 but different background?\nINT51141 Same as INT51101 but different background?\nINT51142 Same as INT51101 but different background?\nINT51143 Same as INT51101 but different background?\nINT51144 Same as INT51101 but different background?\nINT51145 Same as INT51101 but different background?\nINT51146 Same as INT51101 but different background?\nINT51147 Same as INT51101 but different background?\nINT51148 Same as INT51101 but different background?\nINT51149 Same as INT51101 but different background?\nINT51150 Same as INT51101 but different background?\nINT51151 Same as INT51101 but different background?\nINT51152 Same as INT51101 but different background?\nINT51153 Same as INT51101 but different background?\nINT51154 Same as INT51114 but different background?\nINT51155 ????????????????????????????????????????????\nINT51156 Same as INT51155\nINT51157 Same as INT51114 but different background?\nINT51158 Same as INT51114 but different background?\nINT51159 Same as INT51114 but different background?\nINT51160 Same as INT51114 but different background?\nINT52064 Same as INT50606? Different colors\nINT52065 Same as INT52065? Different colors\nINT53001 Hourglass?\nINT53002 Yellow bar?\nINT53003 White box with border?\nINT53004 Two grey arrows (each arrow has it’s own frame)\nINT53005 Tab on the score page?\nINT53006 Box where unit info is displayed?\nINT53007 INT53007 enlarged?\nINT53008  Recorded game controls.\nINT53009 Same as INT52065? Different colors\nINT53010 Food icon (villager carrying icon)\nINT53011 More icons like INT52065?\nINT53012 Microsoft/ES label\nINT53014 Possibly used in ES cpn movie?\nINT53017 Same as INT50606?\nINT53101 Possibly used in ES cpn movie?\nINT53102 Possibly used in ES cpn movie?\nINT53103 Possibly used in ES cpn movie?\nINT53104 Possibly used in ES cpn movie?\nINT53131 Same as INT52065, different colors?\nINT53132 Same as INT52065, different colors?\nINT53133 Same as INT52065, different colors?\nINT53134 Same as INT52065, different colors?\nINT53161 AOE2: the conquerors sign?\nINT53162 Same as INT53162, different colors?\nINT53163 Same as INT53162, different colors?\nINT53164 Same as INT53162, different colors?\nINT53171 Same as INT53162, different colors?\nINT53172 Same as INT53162, different colors?\nINT53173 Same as INT53162, different colors?\nINT53174 Same as INT53162, different colors?\nINT53200 Microsoft button in TC about\nINT53201 Same as INT53200, less distortion\nINT53202 ES button in TC about\nINT53203 Same as INT53202, less distortion\nINT53204 Flashing Microsoft & ES text back and forth\nINT53206 Coloured boxes with white squares in them\nINT53207 TC Name enlarging\nINT53208 Victory/loss page?\nINT53209 Images from cpn movies\nINT53210 White box\nINT53211 A window, or X\n```\n"
  },
  {
    "path": "doc/media/blendomatic.md",
    "content": "Blendomatic\n===========\n\nor: how to merge terrain tile edges.\n\nwhy do we need that?\n--------------------\n\n`terrain.drs` stores all the terrains as slp files, see [doc/media/terrain.md](./terrain.md) for that.\n\nPlacing these tiles beside each other works fine, if you use only one terrain (e.g. grass).\n\nIf two different terrains are next to each other, they need to be blended. Water and land will be blended to water and shore, etc etc.\n\nThis procedure allows merging any terrain type with any other.\n\n\nBlending procedure\n------------------\n\nBlending terrain tiles works by using alpha masks.\nTo create a \"transition tile\", a base tile and an alpha-masked overlay tile are merged.\nThe transition tile is a mixture of the two adjacent terrain tiles,\nwhere one of them gets drawn over partially by the neighbor.\n\nEach terrain type has a priority level (see table at the bottom).\n\nThe masked higher level tile is copied on top of the lower level tile.\n\nThis means that parts of the highlevel tile are visible in the original area\nof the lower level tile, creating a smooth transition.\n\nExample:\n\n    ######@@@@@@\n    ######@@@@@@\n    ######@@@@@@\n    ######@@@@@@\n\nlet `#` be terrain with priority 8, and `@` has priority 42.\n\n`@` has a higher priority than `#`, meaning that `@` is alpha masked before copying.\n\n```\nMask:    Tile:\n\n000111   @@@@@@             @@@\n001111   @@@@@@  masked    @@@@\n000111   @@@@@@   ===>      @@@\n000001   @@@@@@               @\n```\n\n(note that the mask actually has byte values for alpha blending, not just binary \"draw\" and \"hide\")\n\nFor the real blending masks, see below in section \"blending directions\".\n\n\nThe alpha-masked `@`-terrain is then drawn at the neighbor's position, on top of it.\n\n    ###@@@@@@@@@\n    ##@@@@@@@@@@\n    ###@@@@@@@@@\n    #####@@@@@@@\n\nYou can see that this creates a transition from the two textures,\nwhere higher prioritized terrains \"flood\" on their neighbor tile.\n\nIf you imagine `@` as water, and `#` as shore, it's literally flooding...\n\n\nWhich alpha mask is determined by two conditions:\n\n1. the tile positioning\n    * what direction will the transition be?\n2. the blending mode\n    * how does the transition look like?\n    * associated to terrain class -> ice needs other edges than shore\n    * which mode is used does not depend on the terrain priority,\n       it depends on the meeting classes.\n       e.g. when blending with ice,\n       the ice blend mode is applied to the other blending terrain.\n    * existing blending modes (mask shapes):\n      * mode 0: rough transition, full, used for dirt, grass, ...\n      * mode 1: same as mode 0\n      * mode 2: smooth transition, full length\n      * mode 3: smooth transition, short\n      * mode 4: rough hard edges, spraylike\n      * mode 5: sharp edges\n      * mode 6: same as mode 4\n      * mode 7: same as mode 0\n      * mode 8: same as mode 0\n      * => you can see that we actually have 5 blending modes.\n    * reason likely for that redundancy:\n      * blending mode contains more information than it looks at first.\n      * the highest blendmode_id gets selected as mask.\n      * explains the double modes: same mask needed,\n        but other priority requested.\n    * determine the mask for a tile-tile transition:\n\n      tile type | stored mode\n      ----------|------------\n      grass     | 0\n      farm      | 1\n      beach     | 2\n      water     | 3\n      shallows  | 4\n      snow road | 5\n      ice       | 6\n      snow      | 7\n      unknown   | 8\n\n      The mode is then used as index into a lookup table:\n      Current tile mode selects the row, neighbor mode the column.\n\n      ``` c\n      char blend_mask_lookup[8][8] = {\n          { 2, 3, 2, 1, 1, 6, 5, 4 },\n          { 3, 3, 3, 1, 1, 6, 5, 4 },\n          { 2, 3, 2, 1, 1, 6, 1, 4 },\n          { 1, 1, 1, 0, 7, 6, 5, 4 },\n          { 1, 1, 1, 7, 7, 6, 5, 4 },\n          { 6, 6, 6, 6, 6, 6, 5, 4 },\n          { 5, 5, 1, 5, 5, 5, 5, 4 },\n          { 4, 3, 4, 4, 4, 4, 4, 4 }\n      };\n      ```\n\n\nblending directions\n-------------------\n\nnon-blended tile:\n\n          #\n        #####\n      #########\n    #############\n      #########\n        #####\n          #\n\n\nblendomatic stores the following alpha masks:\n\nid is the tile number for the 31 tiles within one blending mode.\n\n`0` means: keep the base tile pixel, don't overdraw (mask 0)\n`#` means: use this pixel from the dominant neighbor to draw over the base tile (mask 1)\n\nthe id's 0..15 describe only 4 directions, but have 16 tiles.\nthese were created to avoid the obviously repeating pattern.\nmethod to choose one of the 4:\n-> use lower 2 bit of tile destination x or y coordinates.\n\n    id: 0..3           4..7           8..11          12..15\n      lower right   upper right    lower left     upper left\n\n          0              #              0              #\n        00000          0####          00000          ####0\n      000000000      00000####      000000000      ####00000\n    000000000####  000000000####  ####000000000  ####000000000\n      00000####      000000000      ####00000      000000000\n        0####          00000          ####0          00000\n          #              0              #              0\n\n    id:  16             17             18             19\n        right          down            up            left\n\n          0              0              #              0\n        00000          00000          #####          00000\n      00000000#      000000000      000###000      #00000000\n    000000000####  0000000000000  0000000000000  ####000000000\n      00000000#      000###000      000000000      #00000000\n        00000          #####          00000          00000\n          0              #              0              0\n\n    id:  20             21             22             23             24             25\n      upperright     upperleft      onlyright      onlydown        onlyup        onlyleft\n      lowerleft      lowerright\n\n          #              #              #              #              0              #\n        0####          ####0          ####0          #####          00000          0####\n      00000####      ####00000      ######000      #########      000000000      000######\n    ####00000####  ####00000####  ########00000  #############  #############  00000########\n      ####00000      00000####      ######000      000000000      #########      000######\n        ####0          0####          ####0          00000          #####          0####\n          #              #              #              0              #              #\n\n    id:  26             27             28             29             30\n        keep           keep           keep           keep           all\n       upperleft     upperright     lowerright     lowerleft\n\n          #              #              #              #              #\n        0####          ####0          #####          #####          #####\n      00000####      ####00000      ####0####      ####0####      ####0####\n    ####00000####  ####00000####  ####00000####  ####00000####  ####00000####\n      ####0####      ####0####      ####00000      00000####      ####0####\n        #####          #####          ####0          0####          #####\n          #              #              #              #              #\n\n\nthese 31 tiles are used to alphamask all possible terrain junctions.\n\n\nBlending algorithm\n------------------\n\n`@` = tile to draw next\n\n        0\n      7   1      => 8 neighbours that have influence on\n    6   @   2         the mask id selection.\n      5   3\n        4\n\nAlgorithm:\n```python\nfor @ in alltiles:\n\n    #draw the base tile:\n    @.draw()\n\n    #storage for influences by neighbor tiles\n    influence = dict()\n\n    #first step: gather information about possible influences\n    #look at every neighbor tile for that\n    for i in [0..7]:\n\n        #neighbor only interesting if it's a different terrain than @\n        if i.terrain_type != @.terraintype:\n\n            #adjacent tile will draw onto @, as it's priority is higher\n            #else, ignore this neighbor\n            if i.priority > @.priority:\n\n                if i.is_diagonal_influence:\n                    #get the ids of the adjacent neighbors of the diagonal\n                    #influence:\n                    i_neighbors = map(lambda x: x % 8, [i - 1, i + 1])\n\n                    if any of i_neighbors have influence:\n                        #don't apply diagonal influence i, as any of its\n                        #neighbors already influences the tile.\n                        continue\n\n                #as tile i has influence for this priority\n                # => bit i is set to 1 by 2^i (== 1 << i)\n                #each priority is drawn seperately later.\n                influence[i.terrain_id] |= 2**i\n\n    #sort influences by priority, so that higher priorities get drawn last.\n    influence = sorted(influence, by=influence.priority)\n\n    #now: we got all influences, grouped by terrain priority.\n    #for each of these influences, we continue finding the blendomatic mask\n    # and apply it to the neighbors texture,\n    # then draw the masked tile on top of the base (@) tile.\n\n    #the terrain_id has influences coming from directions 'binf',\n    #so we can select directional masks for that terrain.\n    for terrain_id, binf in influence.items():\n\n        #there is exactly one adjacent mask id for all combinations\n        adjacent_mask_id  = []\n        diagonal_mask_ids = []\n\n        #find mask id by influencing neighbor tiles\n        #                           neighbor id: 76543210\n        adjacent_mask_id = [0 .. 3] if binf == 0b00001000\n                   .add()  [4 .. 7] if binf == 0b00000010\n                           [8 ..11] if binf == 0b00100000\n                           [12..15] if binf == 0b10000000\n                           20       if binf == 0b00100010\n                           21       if binf == 0b10001000\n                           22       if binf == 0b10100000\n                           23       if binf == 0b10000010\n                           24       if binf == 0b00101000\n                           25       if binf == 0b00001010\n                           26       if binf == 0b00101010\n                           27       if binf == 0b10101000\n                           28       if binf == 0b10100010\n                           29       if binf == 0b10001010\n                           30       if binf == 0b10101010\n\n        diagonal_mask_id.add(16)    if binf  & 0b00000100 > 0\n                             17     if binf  & 0b00010000 > 0\n                             18     if binf  & 0b00000001 > 0\n                             19     if binf  & 0b01000000 > 0\n\n\n        #which of the 9 blending modes to use?\n        #the selected blending mode is depending on which tiles meet.\n        # (water->shore != water->ice)\n        # terrain class => Land, Farm, Beach, Road, Water, Ice, ..\n        #this means to determine:\n        # use i.blendmode or @.blendmode\n        #  when i is drawn onto @ later.\n        blendmode    = get_blending_mode(priority, @)\n        neighbortile = get_terrain_by_priority(priority)\n\n        #all masks to draw: one adjacent mask xor 1 to 4 diagonal masks\n        draw_masks = adjacent_mask_id + diagonal_mask_ids\n\n        for maskid in draw_masks:\n            #do the tile blending:\n            #mask away pixels by applying the combined mask\n            maskdata = mask[blendmode][maskid]\n\n            #draw the masked terrain piece on top of our base (@) tile\n            overlay_data = apply_mask(neighbortile.data, maskdata)\n            overlay_data.draw()\n\n```\n\nThe described alpha masks, are stored in `blendomatic.dat`:\n\nBlendomatic.dat file format\n---------------------------\n\nThis file contains the alphamasks that are applied per-tile.\nThis results in masking away areas, so a transition between the\npartially-missing top tile, and the underlying base tile is possible.\n\n```cpp\nstruct {\n\tstruct {\n\t\tunsigned int nr_blending_modes;              // normally 9\n\t\tunsigned int nr_tiles;                       // normally 31\n\t} blendomatic_header;\n\n\tstruct {\n\t\tunsigned int  tile_size;                     // normally 2353\n\t\tunsigned char tile_flags[nr_tiles];\n\n\t\tstruct {\n\t\t\tuint8_t alpha_bitmask[tile_size / 8];    // with tile_size pixels, use the data bitwise.\n\t\t} tile_bitmasks[32];                         // why 32? maybe nr_tiles + 1?\n\n\t\tstruct {\n\t\t\tuint8_t alpha_bytemap[tile_size];        // 7-bit alpha value pixels\n\t\t} tile_bytemasks[nr_tiles];\n\t} blending_modes[nr_blending_modes]\n} blendomatic.dat;\n```\n\nThe `alpha_bytemap` array contains values to be drawn as `*`\nWe have to add the spaces (.) ourselves.\n\nWith the default `tile_size`, we end up drawing the rhombus with 49 rows.\n\nThe resulting tile is then used to overlay on a regular terrain tile, so that parts are alphamasked.\n\n`alpha_bitmasks` selects which bytes from each tile are used,\nthe `alpha_bytemap` determines how much to blend.\n\nThis is our drawing goal (of course a bit bigger):\n\n    ....*....\n    ..*****..\n    *********\n    ..*****..\n    ....*....\n\nEach `*` is a pixel, with a 7 or 1 bit value, which just is an alpha threshold.\n\nfor `alpha_bytemap`:\n--> 128 == when masking the dominant texture, draw the pixel fully opaque.\n-->   0 == keep the base tile pixel here.\n\nfor `alpha_bitmasks`:\n-->   1 == this pixel will be overdrawn by dominant neighbor.\n-->   0 == keep the base tile pixel.\n"
  },
  {
    "path": "doc/media/drs-files.md",
    "content": "# DRS files\n\n## DRS format\n\n*drs* files are archives containing other files, like a tar archive, but much\nsimpler. Helpfully, the format is sequential in nature, which helps speed up\ndecoding time.\n\n### DRS Header\n\nA DRS file starts with its main header:\n\nLength   | Type   | Description        | Example\n---------|--------|--------------------|--------\n40 bytes | string | Copyright info     | Copyright (c) 1997 Ensemble Studios\\032.\n4 bytes  | string | File version       | 1.00\n12 bytes | string | File type          | tribe\n4 bytes  | int32  | Number of tables   | 4, 0x00000004\n4 bytes  | int32  | Offset of 1st file | 2188, 0x0000088C\n\n```cpp\nstruct drs_header {\n\tchar copyright[40];\n\tchar version[4];\n\tchar ftype[12];\n\tint32 table_count;\n\tint32 file_offset;\n};\n```\nPython format: `Struct(\"< 40s 4s 12s i i\")`\n\n* It is important to note that in later versions of the DRS format\n  (used for SW:GB), it appears that `copyright` has a length of 60 instead of\n  the usual 40.\n* `copyright`, `version` & `ftype` seem to always have the same value.\n* `table_count` stores how many `drs_table_info` structs will follow the main header.\n* `file_offset` is the offset of the first actual file in the DRS.\n\n### DRS Table info\n\nOne `drs_table_info` struct is stored for each file type (Possible file\nextensions are `bina`, `slp `, `wav `).\n\nLength  | Type   | Description            | Example\n--------|--------|------------------------|--------\n4 bytes | string | File extension         | 'anib', ' pls'\n4 bytes | int32  | Table offset           | 112, 0x00000070\n4 bytes | int32  | Number of files        | 71, 0x00000047\n\n```cpp\nstruct drs_table_info {\n\tchar file_extension[4];\n\tint32 file_info_offset;\n\tint32 num_files;\n};\n```\nPython format: `Struct(\"< 4s i i\")`\n\n* `file_extension` is reversed and padded by spaces (`0x20`) to reach 4 bytes.\n  Internally, Age of Empires does something like this when reading files\n  from a DRS archive:\n\n  ```c\n  read_drs_file('bina', 50500);\n  ```\n\n  Note how it uses a C char with multiple bytes. This is stored on disk as a\n  little-endian integer, so the 'a' byte ends up at the lowest address,\n  and the 'b' byte ends up at the highest address.\n* `file_info_offset` holds the offset where the actual table resides.\n* `num_files` contains the amount of files in the table.\n\nThe `drs_table_info` tells us, how many files are stored for the `file_type`.\n\n### DRS File info\n\nEach table contains entries for the files in it. The table entry\nconsists of: the unique integer identifier of the embedded file,\nthe offset of the embedded file and the size of the embedded file.\n\n`drs_file_info` tables start at position\n`drs_table_info->file_info_offset` for the corresponding table:\n\nLength  | Type  | Description | Example\n--------|-------|-------------|--------\n4 bytes | int32 | File id     | 50001, 0x0000C351\n4 bytes | int32 | File offset | 2188, 0x0000088C\n4 bytes | int32 | File size   | 625, 0x00000271\n\n```cpp\nstruct drs_file_info {\n\tint32 file_id;\n\tint32 file_data_offset;\n\tint32 file_size;\n};\n```\nPython format: `Struct(\"< i i i\")`\n\nFor every file, the offset in the `.drs` is stored, and also its size.\nThe file's ID can be assumed to be its name.\n\n\n## Files\n\nDRS files that come with the base AoK game.\n\n### gamedata.drs\n\nContains random map scripts (RMS), as well as AI scripts and some graphic files.\nRandom Map scripts describe how the builtin maps should be generated.\nAI scripts tell the computer what to do, such as build units, gather resources,\nand generally make it \"smart\". These are all `bina` files.\nThe graphic files (`slp`) are described below.\n\n### graphics.drs\n\nContains almost every graphic you see in the game. This includes units,\nbuildings, resources, animals, cliffs and shadows. These are all `slp` graphic\n(animation) files.\n\n### interfac.drs\n\nContains the main interface graphics of the game.\nAll of the the borders, buttons, logos, main game screen, etc you see when\nentering the game to starting one can be found within this file as `slp`\ngraphics. The drawing color palettes are also stored in this file as `bina`.\nFinally there's a bunch of `wav` sounds, too.\n\n### sounds.drs\n\nContains every sound in Age of Kings. These are all `wav` files.\n\n### terrain.drs\n\nContains all the terrain, in diamond shaped pieces. For each terrain, there are\n100 pieces. These are all `slp` files.\n\n## References\n\n* [Age of Empires .drs format](http://artho.com/age/drs.html)\n"
  },
  {
    "path": "doc/media/evolution-openage-gource.md",
    "content": "# Reproducing the gource 'evolution video'\n\nYou can find the \"current\" video here:\nhttps://www.youtube.com/embed/CrccACCWQG0\n\nDownload, compile & install git, gource, ffmpeg + FLOSS video cutting tool.\n\n1. Set git directory to 'Master' branch\n2. Open terminal/command line and create a new folder for the video\n3. Copy the logo in \\*.png-format into the new folder (svg→png with Inkscape)\n4. Navigate to the new folder\n5. Use the following command\n\n```\ngource --camera-mode overview --title https://openage.dev --seconds-per-day 0.05 --auto-skip-seconds 0.1 --max-file-lag 0.1 --file-idle-time 0 --background 555544 --logo <logo.png> --font-size 15 --date-format \"%B %Y\" --hide filenames, mouse -1920x1080 -o <outputfile-name.ppm> <../path to git directory>\n```\n\n6. Use ffmpeg to convert the ppm file into a video container with the following command\n\n```\n<path-to-ffmpeg(.exe)> -y -r 60 -f image2pipe -vcodec ppm -i <gource-outputfile-name.ppm> -vcodec libx264 -preset fast -pix_fmt yuv420p -crf 1 -threads 4 -bf 0 <output-filename>.x264.avi\n```\n\n7. Use video cutting tool and music licensed under creative commons\n8. Render!\n"
  },
  {
    "path": "doc/media/filtermaps-dat.md",
    "content": "# FilterMaps File Format\n\nThe most annoying `.dat` file.\n\n`FilterMaps.dat` contains 17 rhombus tiles, each mostly 49 rows high,\nwith each row having a stride of columns as pixels, similar to Blendomatic tiles.\nEach pixel contains a reference to an ICM which uses corresponding LightMaps for shading.\n\n``` cpp\nstruct filter_maps {\n\n\tstruct filter_map {\n\n\t\tlong header_length;                // number of bytes to read this filter map\n\t\tlong tile_size_y;                  // seems to be 49, 25, or 73\n\n\t\twhile (tile_size_y--) {\n\t\t\tuint8 crap1;                   // read 1 byte\n\t\t\tint remaining = crap1 & 0xFF;  // bitwise for reading more crap\n\t\t\tdo {\n\t\t\t\tuint16 crap2;              // read 2 bytes\n\t\t\t\tint check2 = crap2 & 0x0F; // bitwise for reading further crap\n\t\t\t\tdo {\n\t\t\t\t\tuint24 crap3;          // read 3 bytes\n\t\t\t\t\t// some of this crap is then loaded into the graphics rendering\n\t\t\t\t\tcheck2--;\n\t\t\t\t} while (check2);\n\t\t\t\tcheck1--;\n\t\t\t} while (check1);\n\t\t}\n\t}\n}[17];\n```\n"
  },
  {
    "path": "doc/media/lzx_compression_info.md",
    "content": "some information about the format from the cabextract comments:\n------\n\nMicrosoft's LZX document (in cab-sdk.exe) and their implementation\nof the com.ms.util.cab Java package do not concur.\n\nIn the LZX document, there is a table showing the correlation between\nwindow size and the number of position slots. It states that the 1MB\nwindow = 40 slots and the 2MB window = 42 slots. In the implementation,\n1MB = 42 slots, 2MB = 50 slots. The actual calculation is 'find the\nfirst slot whose position base is equal to or more than the required\nwindow size'. This would explain why other tables in the document refer\nto 50 slots rather than 42.\n\nThe constant NUM_PRIMARY_LENGTHS used in the decompression pseudocode\nis not defined in the specification.\n\nThe LZX document does not state the uncompressed block has an\nuncompressed length field. Where does this length field come from, so\nwe can know how large the block is? The implementation has it as the 24\nbits following after the 3 blocktype bits, before the alignment\npadding.\n\nThe LZX document states that aligned offset blocks have their aligned\noffset huffman tree AFTER the main and length trees. The implementation\nsuggests that the aligned offset tree is BEFORE the main and length\ntrees.\n\nThe LZX document decoding algorithm states that, in an aligned offset\nblock, if an extra_bits value is 1, 2 or 3, then that number of bits\nshould be read and the result added to the match offset. This is\ncorrect for 1 and 2, but not 3, where just a huffman symbol (using the\naligned tree) should be read.\n\nRegarding the E8 preprocessing, the LZX document states 'No translation\nmay be performed on the last 6 bytes of the input block'. This is\ncorrect. However, the pseudocode provided checks for the *E8 leader*\nup to the last 6 bytes. If the leader appears between -10 and -7 bytes\nfrom the end, this would cause the next four bytes to be modified, at\nleast one of which would be in the last 6 bytes, which is not allowed\naccording to the spec.\n\nThe specification states that the huffman trees must always contain at\nleast one element. However, many CAB files contain blocks where the\nlength tree is completely empty (because there are no matches), and\nthis is expected to succeed.\n\nThe errors in LZX documentation appear have been corrected in the\nnew documentation for the LZX DELTA format.\n\nhttp://msdn.microsoft.com/en-us/library/cc483133.aspx\n\nHowever, this is a different format, an extension of regular LZX.\nI have noticed the following differences, there may be more:\n\nThe maximum window size has increased from 2MB to 32MB. This also\nincreases the maximum number of position slots, etc.\n\nThe format now allows for \"reference data\", supplied by the caller.\nIf match offsets go further back than the number of bytes\ndecompressed so far, that is them accessing the reference data.\n"
  },
  {
    "path": "doc/media/openage/blendmask_format_spec.md",
    "content": "# Blendmask Format Specification\n\n**Format Version:** 2\n\nThe openage blendmask format is a plaintext configuration file format for defining\na blending pattern using alpha masks. A blendmask defines a table of directional filters\nto blend a terrain texture into another texture. Standard tiles have 8 directional\nedges where they can border other textures. Thus, there are `2^8 = 256` total\ncombinations which can be used as filters.\n\nMore info on the blending mechanism can be found in the [blendomatic docs](/doc/media/blendomatic.md).\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters have default values and are optional. The preferred file extension is\n`.blmask`.\n\n\n## Quick Reference\n\n```\n# This is a blendtable configuration file\n# comments start with # and are ignored\n\n# file version\nversion 2\n\n# texture file reference, relative to this file's location\ntexture <texture_id> <filename>\n\n# the zoom level at which the animation is shown in full detail\n# e.g. scalefactor 0.5 -> full detail at 200% zoom\nscalefactor <factor>\n\n# selection of blendomatic borders\nmask <directions> <texture_id> <subtex_id>\n```\n\n\n## Data Type Formatting\n\nType     | Example | Description\n---------|---------|---------\nbits     | `0b01`  | Number represented as bits\nint      | `5`     | Signed Integer\nfloat    | `1.2`   | Float\nstring   | `\"bla\"` | String of characters enclosed by `\"`\ntoken    | `off`   | Alphanumeric predefined keyword\n\n\n## Attributes\n\n### `version`\n\nVersion of the blendmask format. Increments every time the syntax\nor keywords of the format change.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\nversion_no | int    | No       | -\n\n**version_no**<br>\nVersion number of the format.\n\n\n#### Example\n\n```\nversion 2\n```\n\n\n### `texture`\n\nTells the renderer which texture resources it has to load.\nThere has to be at least one `texture` defined.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\ntexture_id | int    | No       | -\nfilename   | string | No       | -\n\n**texture_id**<br>\nReference ID for the resource used in this file. IDs should start at `0`.\n\n**filename**<br>\nPath to the texture resource on the filesystem. The file must be a [texture format file](texture_format_spec.md).\nThe different methods of resource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\ntexture 0 \"grass.texture\"\ntexture 1 \"../../grass.texture\"\ntexture 2 \"/{aoe2_base}/graphics/grass.texture\"\n```\n\n\n### `scalefactor`\n\nDefines a downscaling factor for the texture.\n\nParameter | Type  | Optional | Default value\n----------|-------|----------|--------------\nfactor    | float | No       | -\n\n**factor**<br>\nFactor by which sprite images are scaled at default zoom level.\nThis allows for high resolution sprites to be displayed at an\narbitrary scale. It can be used for sprites that should retain\nhigh image quality with higher zoom levels.\n\n\n#### Example\n\n```\n# Assume the sprite image has a size of 100x100\n# Factors <1 result in downscaling\nscalefactor 1.0   # No scaling, 100x100 at default zoom\nscalefactor 0.5   # 50x50 at default zoom; 100x100 at 2x zoom\nscalefactor 0.25  # 25x25 at default zoom; 100x100 at 4x zoom\n\n# Factors >1 result in upscaling\nscalefactor 2.0  # 200x200 at default zoom; 100x100 at 2x zoom\n```\n\n\n### `mask`\n\nTells the renderer which mask it has to use for what adjacent directions.\n\nParameter  | Type      | Optional | Default value\n-----------|-----------|----------|--------------\ndirections | int, bits | No       | -\ntexture_id | int       | No       | -\nsubtex_id  | int       | No       | -\n\n**directions**<br>\nThe directions for which the *blending* terrain touches the *blended*\nterrain, stored as an 8-Bit bitfield value. Each bit position represents\na direction around the blended tile. A mask is used if this value\nmatches the positions around the adjacent tiles.\n\nThey blending terrain positions are assgned to indices using the\nfollowing logic.\n\n```\n@   = the blended tile\n0-7 = adjacent tiles\n\n7  0  1\n\n6  @  2\n\n5  4  3\n```\n\nFor every adjacent tile of `@` that is from the lending terrain, the bit\nat the respective index is set. For example, if `@` is blended by another\nterrain adjacent in directions north-west, north and north-east, the bits\nat index 7, 0 and 1 are set (`0b10000011`). This is stored as a decimal integer\nvalue or a bits value.\n\n```\nmask 131 ...   # 131 equals 0b10000011\n```\n\nThe full range of 256 patterns can be used as lookups. However, 20\nmandatory directions must be defined in every blendmask file to ensure\ncompatibility to Genie Engine blending style. The list of mandatory\nblend directions can be found in section [Mandatory Directions](#mandatory-directions).\nThese mandatory directions are also used as fallback if a pattern\nwas left undefined.\n\n**texture_id**<br>\nID of the texture resource that contains the sutexture referenced by\n`subtex_id`.\n\n**subtex_id**<br>\nID of the subtexture from the referenced texture that is used as a source\nfor the mask.\n\n\n#### Example\n\n```\nmask 131 0 0\nmask 0b00101010 0 0\n```\n\n## Mandatory Directions\n\n**Diagonal directions:**\n\nInteger value | Bits value   | Adjacent directions\n--------------|--------------|--------------------\n2             | `0b00000010` | north east\n8             | `0b00001000` | south east\n32            | `0b00100000` | south west\n128           | `0b10000000` | north west\n\n**Vertical/Horizontal one-sided adjacency:**\n\nInteger value | Bits value   | Adjacent directions\n--------------|--------------|--------------------\n1             | `0b00000001` | north\n4             | `0b00000100` | east\n16            | `0b00010000` | south\n64            | `0b01000000` | west\n\n**Vertical/Horizontal two-sided adjacency:**\n\nInteger value | Bits value   | Adjacent directions\n--------------|--------------|--------------------\n5             | `0b00000101` | north, east\n17            | `0b00010001` | north, south\n20            | `0b00010100` | east, south\n65            | `0b01000001` | north, west\n68            | `0b01000100` | east, west\n80            | `0b01010000` | south, west\n\n**Vertical/Horizontal three-sided adjacency:**\n\nInteger value | Bits value   | Adjacent directions\n--------------|--------------|--------------------\n21            | `0b00010101` | north, east, south\n69            | `0b01000101` | north, east, west\n81            | `0b01010001` | north, south, west\n84            | `0b01010100` | east, south, west\n\n**Vertical/Horizontal four-sided adjacency:**\n\nInteger value | Bits value   | Adjacent directions\n--------------|--------------|--------------------\n85            | `0b01010101` | north, east, south, west\n\n\nVisual representation ingame:\n\n```\n    id:   2              8             32             128\n          NE             SE            SW             NW\n\n          0              0              0              #\n        00000          00000          00000          #####\n      00000000#      000000000      #00000000      000###000\n    000000000####  0000000000000  ####000000000  0000000000000\n      00000000#      000###000      #00000000      000000000\n        00000          #####          00000          00000\n          0              #              0              0\n\n    id:   1              4              16             64\n          N              E              S              W\n\n          #              0              0              #\n        0####          00000          00000          ####0\n      00000####      000000000      000000000      ####00000\n    000000000####  00000000#####  ####000000000  ####000000000\n      000000000      00000####      ####00000      000000000\n        00000          0####          ####0          00000\n          0              #              #              0\n\n    id:   5             17             20             65             68             80\n         N-E            N-S            E-S            N-W            E-W            S-W\n\n          #              #              0              #              #              #\n        0####          0####          00000          #####          ####0          ####0\n      000######      00000####      000000000      #########      ####00000      ######000\n    00000########  ####00000####  #############  #############  ####0000#####  ########00000\n      000######      ####00000      #########      000000000      00000####      ######000\n        0####          ####0          #####          00000          0####          ####0\n          #              #              #              0              #              #\n\n    id:  21             69             81             84\n        N-E-S          N-E-W          N-S-W          E-S-W\n\n          #              #              #              #\n        0####          #####          #####          ####0\n      00000####      ####0####      ####0####      ####00000\n    ####00000####  ####00000####  ####00000####  ####00000####\n      ####0####      00000####      ####00000      ####0####\n        #####          0####          ####0          #####\n          #              #              #              #\n\n    id:  85\n       N-E-S-W\n\n          #\n        #####\n      ####0####\n    ####00000####\n      ####0####\n        #####\n          #\n```\n"
  },
  {
    "path": "doc/media/openage/blendtable_format_spec.md",
    "content": "# Blendtable Format Specification\n\n**Format Version:** 1\n\nThe openage blendtable format is a plaintext configuration file format for defining\na blending pattern lookup table. Using this table, the renderer selects a blending\npattern for two directly adjacent terrain textures. The blending pattern is further\ndefined by the [blendmask format](blendmask_format_spec.md).\n\nThe blending table is referenced by [terrain format files](terrain_format_spec.md).\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters have default values and are optional. The preferred file extension is\n`.bltable`.\n\n\n## Quick Reference\n\n```\n# This is a blendtable configuration file\n# comments start with # and are ignored\n\n# file version\nversion 1\n\n# table definitions (n x n matrix)\nblendtable [\n<a> <b> <c> ...\n<d> <e> <f> ...\n<g> <h> <i> ...\n... ... ... ...\n]\n\n# pattern definitions\npattern <pattern_id> <filename>\n```\n\n\n## Data Type Formatting\n\nType     | Example | Description\n---------|---------|---------\nint      | `5`     | Signed Integer\nfloat    | `1.2`   | Float\nstring   | `\"bla\"` | String of characters enclosed by `\"`\ntoken    | `off`   | Alphanumeric predefined keyword\n\n\n## Attributes\n\n### `version`\n\nVersion of the blendtable format. Increments every time the syntax\nor keywords of the format change.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\nversion_no | int    | No       | -\n\n**version_no**<br>\nVersion number of the format.\n\n\n#### Example\n\n```\nversion 1\n```\n\n\n### `blendtable`\n\nLookup table for blend patterns blending two adjacent terrains.\n\nThere has to be exactly one `blendtable` defined.\n\nParameter | Type    | Optional | Default value\n----------|---------|----------|--------------\nmatrix    | int[]   | No       | -\n\n**matrix**<br>\nA `n`x`n` matrix containing reference IDs for blending patterns. This\nmust be a square matrix. Columns are separated by spaces, rows\nare separated by newlines. IDs in the table must be defined in the\nsame file by using the `pattern` attribute.\n\nWhen two terrains are adjacent, the renderer selects the blending\npattern by looking it up in this table. For that purpose, it uses\nthe `priority` and `blend_mode` parameters of the `[blendtable](terrain_format_spec.md#frame)`\nattribute in the respective terrain format definitions.\n\nThe terrain with the higher priority uses its `blend_mode` value for\nthe row-index in the table, the terrain with the lower priority uses\nits `blend_mode` value for the column-index. The terrain with the higher\npriority is then blended onto the other terrain, using the blending pattern\ndefined by the ID at the index.\n\n\n#### Example\n\n```\nblendtable [\n0  0  0\n1  2  3\n23 42 1337\n]\n```\n\n\n### `pattern`\n\nDefines a blending pattern that is used to blend one terrain texture\ninto a different terrain texture that is directly adjacent. The\nblending mechanism is described in more detail in the [blendmask](blendmask_format_spec.md)\nformat documentation.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\npattern_id | int    | No       | -\nfilename   | string | No       | -\n\n**pattern_id**<br>\nReference ID for the blending pattern used in this file. IDs should start at `0`.\n\n**filename**<br>\nPath to the blending pattern definition file on the filesystem. The different methods of\nresource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\npattern 0 \"blend0.blmask\"\npattern 1 \"./blend3.blmask\"\npattern 2 \"/{aoe2_base}/blend8.blmask\"\n```\n"
  },
  {
    "path": "doc/media/openage/file_referencing.md",
    "content": "# File Referencing\n\nThere are two ways to specify a path: *relative* and *absolute*.\n\n## Relative Path References\n\nRelative paths are relative to the location of the file they are defined in.\nThey can only refer to file resources that are in the same modpack as\nthe referencing file.\n\n```\n\"idle.png\"         # idle.png is in the same folder as the referencing file\n\"./idle.png\"       # same as above, but more explicit\n\"media/attack.png\" # attack.png is in the subfolder 'media', relative to the referencing file\n```\n\n## Absolute Path References\n\nAbsolute paths start from the (virtual) modpack root (the path where all\nmodpacks are installed). They must begin with `/` followed by either\n* A modpack identifier **or**\n* a shortened modpack alias\n\nenclosed by `{}`. For information on modpack identifiers and aliases see\nthe [modpack](modpacks.md#alias-and-identifier) docs.\n\n```\n\"/{aoe2_base@openage}/idle.png\" # absolute path with modpack identifier\n\"/{aoe2_base}/idle.png\"         # absolute path with modpack alias\n```\n\nAbsolute paths are the only way to reference file resources from other\nmodpacks.\n"
  },
  {
    "path": "doc/media/openage/modpack_definition_file.md",
    "content": "# Modpack Definition File\n\n**Format Version:** 2\n\nThe modpack definition file is the header file for [modpacks](modpacks.md).\n\nTOML is used as the configuration language.\n\nThis document contains information about the parameters and sections that\ncan be interpreted by the engine.\n\n## File Header\n\nIt is recommended to start the file with the following comment.\n\n```\n# openage modpack definition file\n```\n\nThis is not mandatory, but will help others who don't know the format\nfind this specification document.\n\nThe following parameters have to be specified.\n\n| Parameter      | Data Type | Optional | Description                                    |\n| -------------- | --------- | -------- | ---------------------------------------------- |\n| `file_version` | String    | No       | Version of the modpack definition file format. |\n\n\n## [info] Section\n\n`[info]` contains general information about the modpack.\n\n| Parameter          | Data Type     | Optional | Description                                                             |\n| ------------------ | ------------- | -------- | ----------------------------------------------------------------------- |\n| `packagename`      | String        | No       | Name of the modpack.                                                    |\n| `version`\\*        | String        | No       | The modpack's internal version number. Must use [semver] format.        |\n| `versionstr`\\*     | String        | Yes      | Human-readable version string.                                          |\n| `repo`             | String        | Yes      | Name of the repo where the package is hosted.                           |\n| `alias`            | String        | Yes      | Alias of the modpack. Aliases can be used for replacing other modpacks. |\n| `title`            | String        | Yes      | Title used in UI.                                                       |\n| `description`      | String        | Yes      | Path to a file with a short description (max 500 chars).                |\n| `long_description` | String        | Yes      | Path to a file with a detailed description.                             |\n| `url`              | String        | Yes      | Link to the modpack's website.                                          |\n| `license`          | Array[String] | Yes      | License(s) of the modpack.                                              |\n\n[semver]: https://semver.org/\n\n\\* `version` is used by the engine to determine the most recent version of a modpack. Therefore, it should be bumped when something in the modpack changes (e.g. whenever a new version gets published). `versionstr` is what is displayed to the user and can contain any string, so it can be used to represent any sensible version format.\n\n## [assets] Section\n\n`[assets]` contains paths to assets in the modpack.\n\n| Parameter | Data Type     | Optional | Description                                                                                            |\n| --------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------ |\n| `include` | Array[String] | No       | List of paths to assets that should be mounted on load time. Paths are allowed to contain wildcards.   |\n| `exclude` | Array[String] | Yes      | List of paths to assets that should be excluded from mounting. Paths are allowed to contain wildcards. |\n\n\n## [dependency] Section\n\n`[dependency]` contains a list of other modpacks that the modpack depends on.\n\n| Parameter  | Data Type     | Optional | Description                             |\n| ---------- | ------------- | -------- | --------------------------------------- |\n| `modpacks` | Array[String] | Yes      | List of modpack aliases or identifiers. |\n\n\n## [conflict] Section\n\n`[conflict]` contains a list of other modpacks that the modpack conflicts with.\n\n| Parameter  | Data Type     | Optional | Description                             |\n| ---------- | ------------- | -------- | --------------------------------------- |\n| `modpacks` | Array[String] | Yes      | List of modpack aliases or identifiers. |\n\n\n## [authors] Section\n\n`[authors]` contains information about the creators and authors of the modpack.\nEvery author must have their own subtable `[authors.{authorname}]`. The\nsubtable can set the following parameters.\n\n| Parameter  | Data Type     | Optional | Description                                                           |\n| ---------- | ------------- | -------- | --------------------------------------------------------------------- |\n| `name`     | String        | No       | Nickname of the author. Must be unique for the modpack.               |\n| `fullname` | String        | Yes      | Full name of the author.                                              |\n| `since`    | String        | Yes      | Version number of the release where the author started to contribute. |\n| `until`    | String        | Yes      | Version number of the release where the author stopped to contribute. |\n| `roles`    | Array[String] | Yes      | List of roles of the author during the creation of the modpack.       |\n| `contact`  | Table         | Yes      | Contact information (see below).                                      |\n\nThe contact table can use the following parameters.\n\n| Parameter  | Data Type | Optional | Description        |\n| ---------- | --------- | -------- | ------------------ |\n| `discord`  | String    | Yes      | Discord username.  |\n| `email`    | String    | Yes      | Email address.     |\n| `github`   | String    | Yes      | GitHub username.   |\n| `gitlab`   | String    | Yes      | Gitlab username.   |\n| `irc`      | String    | Yes      | IRC username.      |\n| `mastodon` | String    | Yes      | Mastodon username. |\n| `matrix`   | String    | Yes      | Matrix username.   |\n| `reddit`   | String    | Yes      | Reddit username.   |\n| `twitter`  | String    | Yes      | Twitter username.  |\n| `youtube`  | String    | Yes      | YouTube username.  |\n\n\n## [authorgroups] Section\n\n`[authorgroups]` contains information about teams or groups of authors.\nIt can be used in addition to `[authors]` to signify that the modpack is\na team effort.\n\n\n| Parameter     | Data Type     | Optional | Description                                                                                                                                           |\n| ------------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `name`        | String        | No       | Group or team name.                                                                                                                                   |\n| `authors`     | Array[String] | No       | List of author identifiers. These must match up with subtable keys in the `[authors]` section, e.g. `\"xxbunny123\"` references `[authors.xxbunny123]`. |\n| `description` | String        | Yes      | Path to a file with a description of the team.                                                                                                        |\n"
  },
  {
    "path": "doc/media/openage/modpacks.md",
    "content": "# Modpacks\n\nInput assets for openage are organized in packages that we call **modpacks**.\nEverything you need to know about them is described in this document.\n\n\n## What does a modpack contain?\n\nModpacks contain anything that is considered an input for the engine, e.g.\ngamedata, graphics, sounds, scripts, translations or a combination of all\nof those. There are no limits on their scope. A modpack can contain\nanything from minor enhancements to standalone games.\n\nApart from game-related data, modpacks store configuration data for loading\nthem into the engine. Most of this data is located in the\n**[modpack definition file](#modpack-definition-file)**. Modpacks can define\ntheir interaction with other modpacks by specifying **dependencies** and\n**conflicts**.\n\nThe organization of files inside the modpack can be decided by its creators.\nopenage does not enforce a specific file structure, although a \"sensible\"\nstructure is of course recommended. The only file that is bound to some requirements\nis the modpack definition file (see its dedicated section for details).\n\n\n### Modpack Definition File\n\nThe modpack definition file acts as the header file for the modpack. It is\nthe only mandatory file in the modpack. The definition file must be placed\nin the modpack's root folder under the filename `modpack.toml`.\n\nThe main purpose of the file is to store configuration parameters such as:\n\n* Internal name, alias and version number\n* Paths to game assets that should be loaded\n* Descriptions and author information\n\nFurthermore, the file can specify the interaction with other modpacks:\n\n* Dependencies on other modpacks\n* Conflicts with other modpacks\n* Replacment of other modpacks\n* Specification of a preferred load order (in relation to other mods)\n\nA full list of configuration parameters can be found [here](modpack_definition_file.md).\n\n\n## Distribution\n\nModpacks can be distributed as standalone packages or via a package\nrepository. Each of these methods has their own advantages.\n\n\n### Standalone Distribution\n\nAny modpack can be distributed as a standalone package by publishing\na ZIP or tar.gz archive of the modpack on a hosting platform or a\nwebsite. The archive can be imported by the openage launcher.\n\nIt is the easiest way to publish a modpack for openage. However, updates\nand dependencies have to be done manually. Therefore, it is recommended\nto use a package repository to offer better convenience features for users.\n\nA note of advice: Allow everyone to download older versions of a modpack.\nOther modpacks might depend on a specific release version of your content.\n\n\n### Package Repositories\n\nA package repository (repo for short) is a dedicated hosting platform for\nmodpacks. They are inspired by the package management systems of Linux\ndistributions, Python, Rust and others. Package repositories have the following\nadvantages:\n\n* Can be checked regularly for updates\n* Automatically download and install dependencies if they are in the same repo\n* Can store the history of modpacks (previous versions)\n* They can be searched by using keywords or content filters\n* Can sign modpacks to indicate trust (i.e. check for exploits or malware)\n\nEveryone can set up a package repository. Users can add an unlimted number of\nrepositories in the openage launcher. Some repositories are added by default\nwhen openage is installed.\n\nRepositories are the preferred method of distributing mods.\n\n\n## Licensing\n\nOnce a mod is out in the open, it's good to clarify how and what other creators are\nallowed to do with it. This can be done by adding a **license** to the\nmodpack. Using a license is completely optional, but it makes sense to\nadd one in some cases. For example, a license can specify how to give credit\nor whether files from a modpack are allowed to be integrated into other modpacks.\n\nThe license can be chosen for every modpack individually by the creator.\nWhile the engine is licensed as GPLv3+, modpack licenses do not need to\nbe compatible with it. Repos may decide which licenses they allow and\nreject. Additionally, they may offer a default license.\n\nWhen you copy (not reference) assets from modpacks of other creators,\nalways check their license first. Copying files may be subject to terms set\nin the license. It is usually safer (and less frowned upon) to have the modpack\nreferenced as a dependency than to copy files over.\n\n\n## Addressing Modpacks\n\nThis section describes standards for addressing and referencing modpacks\nas well as administrative requirements for that purpose.\n\n\n### Naming Conventions\n\nModpack and repository names can only contain these characters:\n\n* Letters from the latin alphabet (`a-z`, `A-Z`)\n* Numbers (`0-9`)\n* Separators (`-`, `_`, `.`)\n\nIt is recommend to use at least 4 characters, but this is not mandatory.\nRepositories might enforce stricter rules on modpack names such as\nword filters or prefixes.\n\nSome repository names are reserved and cannot be used:\n\n* `openage`: Reserved for engine development by us and modpacks created by the converter\n* `local`: Internal repository name for modpacks that were not installed from a repository\n\n\n### Alias and Identifier\n\nThere are two ways to reference a modpack: By using the modpack **identifier**\nor by using an **alias**. It is recommended to use aliases wherever possible\nand only use the identifier when it is absolutely necessary.\n\nThe modpack identifier is a unique string that is composed of the modpack\nname and the repository name seperated by `@`.\n\n```\nmodname@reponame\n```\n\nMods that are not installed from a repository are referred to by using `local`\nas the repo name.\n\n```\nmodname@local\n```\n\n----\n\nAn alias is a string that is used as a quick reference for the modpack\ninternally. By default, the alias is the name of the modpack, but it can\nalso be assigned in the modpack definition file. Aliases must be unique\nat load time.\n\n```\nmodname\n```\n\nAliases can be utilized for replacing other modpacks. For example, an enhanced\ngraphics pack for a mod can replace a normal graphics pack by assigning itself\nthe same alias. References using the alias will then point to the enhancement\ngraphics pack.\n\n\n### Version Pinning\n\nSometimes a very specific version of a modpack can be required due to\ncompatibility issues. This can be done by apending the requested version\nnumber to the modpack identifier separated by `::`.\n\n```\nmodname@reponame::version\n```\n"
  },
  {
    "path": "doc/media/openage/palette_format_spec.md",
    "content": "# Palette Format Specification\n\n**Format Version:** 1\n\nThe openage palette format is a plaintext configuration file format for defining\ncolour palettes. It tells the openage renderer a table of predefined RGBA colour values\nthat can be used for special pixels such as player colour pixels.\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters have default values and are optional. The preferred file extension is\n`.opal`.\n\n\n## Quick Reference\n\n```\n# This is a palette configuration file\n# comments start with # and are ignored\n\n# file version\nversion 1\n\n# number of entries in the colour table\nentries <count>\n\n# Colour values\ncolours [\n<int> <int> <int> <int>\n...\n]\n```\n\n\n## Data Type Formatting\n\nType     | Example | Description\n---------|---------|---------\nint      | `5`     | Signed Integer\n\n\n## Attributes\n\n### `version`\n\nVersion of the palette format. Increments every time the syntax\nor keywords of the format change.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\nversion_no | int    | No       | -\n\n**version_no**<br>\nVersion number of the format.\n\n\n#### Example\n\n```\nversion 1\n```\n\n\n### `entries`\n\nDefines the number of colour values in the palette.\nThere has to be exactly one `entries` defined.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\ncount      | int    | No       | -\n\n**count**<br>\nNumber of colour values in the palette.\n\n\n#### Example\n\n```\nentries 256\n```\n\n\n### `colours`\n\nDefines the array containing the colour values.\nThere has to be exactly one `colours` defined.\n\nParameter  | Type    | Optional | Default value\n-----------|---------|----------|--------------\nvalues     | int[]   | No       | -\n\n**values**<br>\nAn array of integer values in the range of `0` to `255`. Every palette\nentry is a 4-tuple of integers, with each value representing a channel\nbyte value for an *RGBA* color.\n\nThe parameter array must contain exactly `count * 4` values where `count`\nis the parameter value from the `entries` attribute.\n\n\n#### Example\n\n```\ncolours [\n255 255 255 0\n4 3 2 1\n]\n```\n"
  },
  {
    "path": "doc/media/openage/sprite_format_spec.md",
    "content": "# Sprite Format Specification\n\n**Format Version:** 2\n\nThe openage sprite format is a plaintext configuration file format for defining 2D\nanimations. It tells the openage renderer which texture resources it has to load\nand how subtextures in these resources should be displayed.\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters have default values and are optional. The preferred file extension is\n`.sprite`.\n\n\n## Quick Reference\n\n```\n# This is a sprite configuration file\n# comments start with # and are ignored\n\n# file version\nversion 2\n\n# texture file reference, relative to this file's location\ntexture <texture_id> <filename>\n\n# the zoom level at which the animation is shown in full detail\n# e.g. scalefactor 0.5 -> full detail at 200% zoom\nscalefactor <factor>\n\n# layer definitions\n# all layers will be drawn\nlayer <layer_id> mode=off  position=<int>\nlayer <layer_id> mode=once position=<int> time_per_frame=<float>\nlayer <layer_id> mode=loop position=<int> time_per_frame=<float> replay_delay=<float>\n\n# define an angle where frames can be assigned to or mirror from an existing angle\nangle <degree> mirror-from=<existing_angle>\n\n# assign frames to their layers and angles.\n# angle is the direction in degrees, etc.\nframe <frame_idx> <angle> <layer_id> <image_id> <subtex_id>\n```\n\n\n## Data Type Formatting\n\nType     | Example | Description\n---------|---------|---------\nint      | `5`     | Signed Integer\nfloat    | `1.2`   | Float\nstring   | `\"bla\"` | String of characters enclosed by `\"`\ntoken    | `off`   | Alphanumeric predefined keyword\n\n\n## Attributes\n\n### `version`\n\nVersion of the sprite format. Increments every time the syntax\nor keywords of the format change.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\nversion_no | int    | No       | -\n\n**version_no**<br>\nVersion number of the format.\n\n#### Example\n\n```\nversion 2\n```\n\n\n### `texture`\n\nTells the renderer which texture resources it has to load.\nThere has to be at least one `texture` defined.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\ntexture_id | int    | No       | -\nfilename   | string | No       | -\n\n**texture_id**<br>\nReference ID for the resource used in this file. IDs should start at `0`.\n\n**filename**<br>\nPath to the texture resource on the filesystem. The file must be a [texture format file](texture_format_spec.md).\nThe different methods of resource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\ntexture 0 \"idle.texture\"\ntexture 1 \"../../attack.texture\"\ntexture 2 \"/{aoe2_base}/graphics/attack.texture\"\n```\n\n\n### `scalefactor`\n\nDefines a downscaling factor for the animation's sprites.\n\nParameter | Type  | Optional | Default value\n----------|-------|----------|--------------\nfactor    | float | No       | -\n\n**factor**<br>\nFactor by which sprite images are scaled at default zoom level.\nThis allows for high resolution sprites to be displayed at an\narbitrary scale. It can be used for sprites that should retain\nhigh image quality with higher zoom levels.\n\n\n#### Example\n\n```\n# Assume the sprite image has a size of 100x100\n# Factors <1 result in downscaling\nscalefactor 1.0   # No scaling, 100x100 at default zoom\nscalefactor 0.5   # 50x50 at default zoom; 100x100 at 2x zoom\nscalefactor 0.25  # 25x25 at default zoom; 100x100 at 4x zoom\n\n# Factors >1 result in upscaling\nscalefactor 2.0  # 200x200 at default zoom; 100x100 at 2x zoom\n```\n\n### `layer`\n\nDefines a layer for the rendered sprite. Layers allow sprites\nto be delegated into the foreground or background of the animation.\nAll frames are assigned to a specific layer. At least one `layer`\nhas to be defined.\n\nParameter      | Type  | Optional | Default value\n---------------|-------|----------|--------------\nlayer_id       | int   | No       | -\nmode           | token | Yes      | `off`\nposition       | int   | Yes      | `0`\ntime_per_frame | float | Yes      | `1.0`\nreplay_delay   | float | Yes      | `0.0`\n\n**layer_id**<br>\nReference ID for the layer used in this file. IDs should start at `0`.\n\n**mode**<br>\nTells the renderer how often the animation on this layer repeats.\n\nMode   | Description\n-------|------------\n`off`  | Layer is not animated. Only the first frame is shown.\n`once` | Animation is played once. After that, the last animation frame is shown.\n`loop` | Animation loops indefinitely. The renderer waits `replay_delay` seconds after every loop.\n\n**position**<br>\nPosition of the layer in the final animation. Layers with a higher position\noverdraw ones with a lower position.\n\n**time_per_frame**<br>\nLength of time (in milliseconds) that a frame is shown.\n\n**replay_delay**<br>\nHow long the renderer waits (in milliseconds) until the next animation loop is started.\n\n\n#### Example\n\n```\nlayer 0\nlayer 1 mode=off  position=10\nlayer 2 mode=once position=20 time_per_frame=300\nlayer 3 mode=loop position=15 time_per_frame=1200 replay_delay=200\n```\n\n\n### `angle`\n\nSpecifies an angle that frames can get assigned to. The renderer\ndecides which angle to draw based on the angle of the game world\nobject the animation is attached to. At least one `angle` has to\nbe defined.\n\nParameter   | Type  | Optional | Default value\n------------|-------|----------|--------------\ndegree      | int   | No       | -\nmirror_from | int   | Yes      | -\n\n**degree**<br>\nActs as the ID and center point for the angle. Has to be an integer between\n`0` and `359`.\n\nThe angle with center point `degree = 0` is facing the camera, i.e. the game object\ndirection should be *towards* the camera. Subsequent angles are ordered *clockwise*, e.g.\n`90` faces left from the camera's point of view, `180` faces up, and `270` faces right.\n\nFrames at an angle are drawn until the angle of the game world object is\ncloser to another defined angle.  In other words, as long as the game world\nobjects's angle is between\n`previous_angle_degree + (degree - previous_angle_degree) / 2`\nand `degree + (next_angle_degree - degree) / 2`.\n\n```\nangle 0\nangle 45  # Drawn when game world object's angle in interval [22.5,67.5]\nangle 90\n...\n```\n\n**mirror_from**<br>\nSpecifies another angle that this angle will use for drawing frames.\nThe frames are flipped vertically (= mirrored).\n\n\n#### Example\n\n```\nangle 0\nangle 90\nangle 180\nangle 270 mirror_from=90\n```\n\n\n### `frame`\n\nDefines a frame in the animation. Frames have to be defined\nfor every angle individually. Sprites displayed in the frame\nare taken from a texture resource (spritesheet) referenced by the\n`imagefile` attribute.\n\nParameter  | Type  | Optional | Default value\n-----------|-------|----------|--------------\nframe_idx  | int   | No       | -\nangle      | int   | No       | -\nlayer_id   | int   | No       | -\ntexture_id | int   | No       | -\nsubtex_id  | int   | No       | -\n\n**frame_idx**<br>\nIndex of the frame in the animation for the specified `angle`. The\nfirst usable index value is `0`. Frames are played in order of\ntheir assigned indices. If an index is skipped, the frame at this\nindex will be empty.\n\n**angle**<br>\nID of the angle at which the frame is displayed.\n\n**layer_id**<br>\nID of the layer on which the frame is drawn.\n\n**texture_id**<br>\nID of the texture resource that contains the sutexture referenced by\n`subtex_id`.\n\n**subtex_id**<br>\nID of the subtexture from the referenced texture that is used as a source\nfor the sprite displayed in this frame.\n\n\n#### Example\n\n```\nframe 0 90 1 0 2\n# frame_idx  = 0  -> first frame in the animation\n# angle      = 90 -> attached to angle 90\n# layer_id   = 1  -> drawn on layer 1\n# texture_id = 0  -> taken from texture resource with ID 0\n# subtex_id  = 2  -> uses subtexture with ID 2 as sprite\n```\n"
  },
  {
    "path": "doc/media/openage/terrain_format_spec.md",
    "content": "# Terrain Format Specification\n\n**Format Version:** 2\n\nThe openage terrain format is a plaintext configuration file format for defining\nterrain textures. It tells the openage renderer which texture resources it has to load\nand how these resources should be displayed.\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters have default values and are optional. The preferred file extension is\n`.terrain`.\n\n\n## Quick Reference\n\n```\n# This is a terrain configuration file\n# comments start with # and are ignored\n\n# file version\nversion 2\n\n# texture file reference, relative to this file's location\ntexture <texture_id> <filename>\n\n# selection of blending pattern\nblendtable <table_id> <filename>\n\n# the zoom level at which the animation is shown in full detail\n# e.g. scalefactor 0.5 -> full detail at 200% zoom\nscalefactor <factor>\n\n# layer and animation definitions\n# layers defined first will be overdrawn by later definitions\nlayer <layer_id> mode=off  position=<int>\nlayer <layer_id> mode=loop position=<int> time_per_frame=<float> replay_delay=<float>\n\n# definition of a terrain frames\n# these are iterated for an animation\nframe <frame_idx> <layer_id> <texture_id> <subtex_id> priority=<int> blend_mode=<int>\n```\n\n\n## Data Type Formatting\n\n| Type   | Example | Description                          |\n| ------ | ------- | ------------------------------------ |\n| int    | `5`     | Signed Integer                       |\n| float  | `1.2`   | Float                                |\n| string | `\"bla\"` | String of characters enclosed by `\"` |\n| token  | `off`   | Alphanumeric predefined keyword      |\n\n\n## Attributes\n\n### `version`\n\nVersion of the terrain format. Increments every time the syntax\nor keywords of the format change.\n\n| Parameter  | Type | Optional | Default value |\n| ---------- | ---- | -------- | ------------- |\n| version_no | int  | No       | -             |\n\n**version_no**<br>\nVersion number of the format.\n\n\n#### Example\n\n```\nversion 2\n```\n\n\n### `texture`\n\nTells the renderer which texture resources it has to load.\nThere has to be at least one `texture` defined.\n\n| Parameter  | Type   | Optional | Default value |\n| ---------- | ------ | -------- | ------------- |\n| texture_id | int    | No       | -             |\n| filename   | string | No       | -             |\n\n**texture_id**<br>\nReference ID for the resource used in this file. IDs should start at `0`.\n\n**filename**<br>\nPath to the texture resource on the filesystem. The file must be a [texture format file](texture_format_spec.md).\nThe different methods of resource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\ntexture 0 \"idle.texture\"\ntexture 1 \"../../attack.texture\"\ntexture 2 \"/{aoe2_base}/graphics/attack.texture\"\n```\n\n\n### `blendtable`\n\nDefines a blending table that is used for looking up a blending pattern. The\nblending mechanism is described in more detail in the [blendomatic](/doc/media/blendomatic.md)\ndocumentation. The blending table and blending patterns are further defined\nin the [blending table format](blendtable_format_spec.md) and\n[blendmask format](blendmask_format_spec.md) specifications.\n\nBlending patterns/tables are **optional** and do not need to be used.\n\n| Parameter | Type   | Optional | Default value |\n| --------- | ------ | -------- | ------------- |\n| table_id  | int    | No       | -             |\n| filename  | string | No       | -             |\n\n**table_id**<br>\nReference ID for the blending table used in this file. IDs should start at `0`.\n\n**filename**<br>\nPath to the blending table definition on the filesystem. The different methods of\nresource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\nblendtable 0 \"blend0.bltable\" 40 7\nblendtable 1 \"./blend3.bltable\" 10 1\nblendtable 2 \"/{aoe2_base}/blend8.bltable\" 90 1\n```\n\n\n### `scalefactor`\n\nDefines a downscaling factor for the texture.\n\n| Parameter | Type  | Optional | Default value |\n| --------- | ----- | -------- | ------------- |\n| factor    | float | No       | -             |\n\n**factor**<br>\nFactor by which sprite images are scaled at default zoom level.\nThis allows for high resolution sprites to be displayed at an\narbitrary scale. It can be used for sprites that should retain\nhigh image quality with higher zoom levels.\n\n\n#### Example\n\n```\n# Assume the sprite image has a size of 100x100\n# Factors <1 result in downscaling\nscalefactor 1.0   # No scaling, 100x100 at default zoom\nscalefactor 0.5   # 50x50 at default zoom; 100x100 at 2x zoom\nscalefactor 0.25  # 25x25 at default zoom; 100x100 at 4x zoom\n\n# Factors >1 result in upscaling\nscalefactor 2.0  # 200x200 at default zoom; 100x100 at 2x zoom\n```\n\n\n### `layer`\n\nDefines a layer for the rendered texture. Layers allow subtextures\nto be delegated into the foreground or background of the final result.\nAll frames are assigned to a specific layer. At least one `layer`\nhas to be defined.\n\n| Parameter      | Type  | Optional | Default value |\n| -------------- | ----- | -------- | ------------- |\n| layer_id       | int   | No       | -             |\n| mode           | token | Yes      | `off`         |\n| position       | int   | Yes      | `0`           |\n| time_per_frame | float | Yes      | `1.0`         |\n| replay_delay   | float | Yes      | `0.0`         |\n\n**layer_id**<br>\nReference ID for the layer used in this file. IDs should start at `0`.\n\n**mode**<br>\nTells the renderer how often the animation on this layer repeats.\n\n| Mode   | Description                                                                               |\n| ------ | ----------------------------------------------------------------------------------------- |\n| `off`  | Layer is not animated. Only the first frame is shown.                                     |\n| `loop` | Animation loops indefinitely. The renderer waits `replay_delay` seconds after every loop. |\n\n**position**<br>\nPosition of the layer in the final animation. Layers with a higher position\noverdraw ones with a lower position.\n\n**time_per_frame**<br>\nLength of time (in milliseconds) that a frame is shown.\n\n**replay_delay**<br>\nHow long the renderer waits (in milliseconds) until the next animation loop is started.\n\n\n#### Example\n\n```\nlayer 0\nlayer 1 mode=off  position=10\nlayer 3 mode=loop position=15 time_per_frame=1200 replay_delay=200\n```\n\n\n### `frame`\n\nDefines a frame in a texture animation. If the texture is not\nsupposed to be animated, only frame `0` has to be defined.\nTextures displayed in the frame are taken from an image resource\ndefined by the `imagefile` attribute.\n\n| Parameter  | Type | Optional | Default value |\n| ---------- | ---- | -------- | ------------- |\n| frame_idx  | int  | No       | -             |\n| layer_id   | int  | No       | -             |\n| texture_id | int  | No       | -             |\n| subtex_id  | int  | No       | -             |\n| priority   | int  | Yes      | -             |\n| blend_mode | int  | Yes      | -             |\n\n**frame_idx**<br>\nIndex of the frame in the animation. The first usable index value\nis `0`. Frames are played in order of their assigned indices. If\nan index is skipped, the frame at this index will be empty.\n\n**texture_id**<br>\nID of the texture resource that contains the sutexture referenced by\n`subtex_id`.\n\n**subtex_id**<br>\nID of the subtexture from the referenced texture that is used as a source\nfor the terrain graphics.\n\n**priority**<br>\nDecides which [blending table](blendtable_format_spec.md) of the two\nadjacent terrain textures is selected. The table referenced by the terrain\nwith the highest priority value will be picked.\n\n```\n# grass.terrain\n...\nframe 0 1 0 0 0 200 200 priority=5 blend_mode=0\n...\n\n# sand.terrain\n...\nframe 0 1 0 0 0 200 200 priority=1 blend_mode=0\n...\n\n-> grass.terrain's frame has a higher blending priority.\n   therefore, its blending table definition is used when\n   grass.terrain and sand.terrain are adjacent to each other.\n```\n\nIf two adjacent terrains have equal priority, the blending table of the terrain\nwith a lower x coordinate value is selected. If the x coordinate value is also\nequal, the blending table of the terrain with the lowest y coordinate is selected.\n\nIf no priority is defined, the renderer assigns the frame priority 0.\n\n**blend_mode**<br>\nUsed for looking up the blending pattern index in the blending table. If no\nblend mode is defined, then no blending pattern is used at all, even if\nadjacent terrain textures define one.\n\n\n#### Example\n\n```\nframe 0 1 0 0 0 200 200 priority=1 blend_mode=0\n# frame_idx  = 0  -> first frame in the animation\n# layer_id   = 1  -> drawn on layer 1\n# image_id   = 0  -> taken from image resource with ID 0\n# priority   = 1  -> this frame's blending table is selected with priority 1\n# blend_mode = 0  -> use blend mode 0\n# Texture is located at (0,0) in image resource\n# and has a size of (200,200).\n```\n"
  },
  {
    "path": "doc/media/openage/texture_format_spec.md",
    "content": "# Texture Format Specification\n\n**Format Version:** 1\n\nThe openage texture format is a plaintext configuration file format for defining 2D\ntextures and contained subtextures. It tells the openage renderer which image resource\nit has to load and how the resource s partioned into subtextures.\n\nAll attributes start with a defined keyword followed by parameter values. Some\nparameters may have default values and are optional. The preferred file extension is\n`.texture`.\n\nA `.texture` file should be placed next to the image file it references and have the same\nfilename stem.\n\n\n## Quick Reference\n\n```\n# This is a texture configuration file\n# comments start with # and are ignored\n\n# file version\nversion 1\n\n# image file reference, relative to this file's location\nimagefile <filename>\n\n# Image size\nsize <width> <height>\n\n# Pixel format representation\npxformat <format> cbit=<token>\n\n# Defines a subtexture inside the image resource\n# Subtextures have x and y coordinates, width and height\n# as well as anchor points.\nsubtex <xpos> <ypos> <xsize> <ysize> <xhotspot> <yhotspot>\n```\n\n\n## Data Type Formatting\n\nType     | Example | Description\n---------|---------|---------\nint      | `5`     | Signed Integer\nstring   | `\"bla\"` | String of characters enclosed by `\"`\ntoken    | `off`   | Alphanumeric predefined keyword\n\n\n## Attributes\n\n### `version`\n\nVersion of the texture format. Increments every time the syntax\nor keywords of the format change.\n\nParameter  | Type   | Optional | Default value\n-----------|--------|----------|--------------\nversion_no | int    | No       | -\n\n**version_no**<br>\nVersion number of the format.\n\n#### Example\n\n```\nversion 1\n```\n\n\n### `imagefile`\n\nTells the renderer which image resource it has to load.\nThere has to be exactly one `imagefile` defined.\n\nParameter | Type   | Optional | Default value\n----------|--------|----------|--------------\nfilename  | string | No       | -\n\n**filename**<br>\nPath to the image resource on the filesystem. The different methods of\nresource referencing are explained in the [file referencing](file_referencing.md)\ndocs.\n\n\n#### Example\n\n```\nimagefile \"idle.png\"\n```\n\n\n### `size`\n\nSize of the image loaded from the file.\n\nParameter | Type   | Optional | Default value\n----------|--------|----------|--------------\nwidth     | int    | No       | -\nheight    | int    | No       | -\n\n**width**<br>\nWidth of the image in pixels.\n\n**height**<br>\nHeight of the image in pixels.\n\n\n#### Example\n\n```\nsize 800 600\n```\n\n\n### `pxformat`\n\nPixel format used for pixels in the image.\n\nParameter | Type   | Optional | Default value\n----------|--------|----------|--------------\nformat    | token  | No       | -\ncbit      | token  | Yes      | `false`\n\n**format**<br>\nPixel format of the image.\n\nFormat    | Description\n----------|------------\n`rgba8`   | 32 bits per pixel, RGBA colours\n\n**cbit**<br>\nDetermines if the last significant bit of the pixel's alpha channel is reserved\nas a colour command bit. If the command bit is set and has value `1`, the pixel\nis handled as a colour command with special drawing rules.\n\n\n#### Example\n\n```\npxformat rgba8\npxformat rgba8 cbit=true\n```\n\n\n### `subtex`\n\nDefines a subtexture in the texture. Subtextures are located inside\nthe image resource (spritesheet) defined by the `imagefile` attribute.\n\nEvery subtexture has an implicit reference ID. The ID is the index of\nthe `subtex` attribute, starting from index 0.\n\nParameter | Type  | Optional | Default value\n----------|-------|----------|--------------\nxpos      | int   | No       | -\nypos      | int   | No       | -\nxsize     | int   | No       | -\nysize     | int   | No       | -\nxanchor   | int   | No       | -\nyanchor   | int   | No       | -\n\n**xpos**<br>\nHorizontal position of the subtexture inside the image resource. The\nsubtexture's position is an offset to the pixel in the upper left corner\nof the texture (i.e. an offset to coordinate `(0,0)` in the image).\n\nThis must be a non-negative value.\n\n**ypos**<br>\nVertical position of the subtexture inside the image resource. The\nsubtexture's position is an offset to the pixel in the upper left corner\nof the texture (i.e. an offset to coordinate `(0,0)` in the image).\n\nThis must be a non-negative value.\n\n**xsize**<br>\nWidth of the subtexture inside the image resource.\n\nThis must be a non-negative value.\n\n**ysize**<br>\nHeight of the subtexture inside the image resource.\n\nThis must be a non-negative value.\n\n**xanchor**<br>\nHorizontal position of the anchor in the subtexture relative to\nits width and height. The anchor is used for pinning the subtexture\nto a game world object, i.e. the actual *zero* position of the subtexture.\n\nThis can be a negative value.\n\n**yanchor**<br>\nVertical position of the anchor in the subtexture relative to\nits width and height. The anchor is used for pinning the subtexture\nto a game world object.\n\nThis can be a negative value.\n\n#### Example\n\n```\n# Subtexture is located at (200,200) in the texture,\n# has dimensions (20,20) and an anchor point at (9,10)\nsubtex 200 200 20 20 9 10\n```\n"
  },
  {
    "path": "doc/media/original-metadata.md",
    "content": "Original Metadata\n=================\n\nAll data relevant for the game (e.g. how much costs building the castle?\nWhat cultures exist? Can my priest overclock their \"Wololo?\")\nare stored in a binary format in the file `empires2_x1_p1.dat`.\n\nThe format is described in the [huge struct definition](/openage/convert/value_object/read/media/datfile/empiresdat.py).\n\n\nThese data files are basically a huge tree structure.\nWe don't handle cross-references to make this tree a graph in the convert\nscript (like id mappings, e.g. villager has creation sound id 42,\nsound 42 plays the 1337.wav).\n\nAll data values are exported (in Python) to human-readable formats,\nand get imported by the C++ Engine again.\nThe storage format is completely replaceable that way.\n\nFor simplicity the minimal format is `CSV`.\n\nThe data will get converted to [`nyan`](/doc/nyan), our custom data language.\nIt's designed to be redundancy-free, well readable and specially optimized for\nmoddability.\n"
  },
  {
    "path": "doc/media/patterns/sld.hexpat",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma endian little\n#pragma pattern_limit 500000\n\nu16 cur_total_draw_length = 0;\n\n// unknown layer tests\nu32 entries_size = 0;\nu32 cur_offset = 0;\nu16 cur_index = 0;\nu16 prev_loc = 0;\n\nu16 cur_main_height = 0;\nu16 cur_main_height_blocks = 0;\n\nfn pad_length() {\n    return (4 - $) & 0b11;\n};\n\nstruct sld_header {\n    char signature[4];\n    u16  version;\n    u16  num_frames;\n    u16  unkown1;\n    u16  unkown2;\n    u32  unkown3;\n} [[static, inline]];\n\nstruct sld_frame_header {\n    u16 canvas_width;\n    u16 canvas_height;\n    s16 canvas_hotspot_x;\n    s16 canvas_hotspot_y;\n    u8  frame_type;\n    u8  unknown5;\n    u16 frame_index;\n} [[static, inline]];\n\nstruct sld_graphics_header {\n    u16 offset_x1;\n    u16 offset_y1;\n    u16 offset_x2;\n    u16 offset_y2;\n    u8  flag1;\n    u8  unknown1;\n} [[static, inline]];\n\nstruct sld_mask_header {\n    u8  flag1;\n    u8  unknown1;\n} [[static, inline]];\n\nstruct cmd_pack {\n    u8 skip_length;\n    u8 draw_length;\n\n    cur_total_draw_length += draw_length;\n};\n\nstruct sld_command_array {\n    cur_total_draw_length = 0;\n\n    u16 len;\n    cmd_pack cmds[len];\n};\n\nstruct bc1_block {\n    u16 color0;\n    u16 color1;\n    u8 pixel_data[4];\n} [[static]];\n\nstruct bc4_block {\n    u8 color0;\n    u8 color1;\n    u8 pixel_data[6];\n} [[static]];\n\nstruct sld_layer {\n    u32 content_length;\n};\n\nstruct sld_main_layer : sld_layer {\n    sld_graphics_header header;\n    sld_command_array cmds;\n    bc1_block compr_blocks[cur_total_draw_length];\n};\n\nstruct sld_shadow_layer : sld_layer {\n    sld_graphics_header header;\n    sld_command_array cmds;\n    bc4_block compr_blocks[cur_total_draw_length];\n};\n\nstruct unknown_entry {\n    u16 entry_loc @ cur_offset + 2 [[hidden]];\n    u16 cur_entry_length = entry_loc - prev_loc;\n    if (cur_index == cur_main_height_blocks - 1) {\n        cur_entry_length = entries_size - prev_loc;\n    }\n\n    u8 entry[cur_entry_length];\n\n    cur_offset += 2;\n    cur_index += 1;\n    prev_loc = entry_loc;\n} [[inline]];\n\nstruct sld_unknown_layer : sld_layer {\n    u16 unknown0;\n\n    cur_offset = $;\n    cur_index = 0;\n    prev_loc = 0;\n\n    u8 offset_size = cur_main_height_blocks;\n    u16 offsets[offset_size];\n\n    entries_size = content_length - sizeof(unknown0) - sizeof(offsets) - 4;\n\n    unknown_entry content[offset_size];\n};\n\nstruct sld_dmg_mask_layer : sld_layer {\n    sld_mask_header header;\n    sld_command_array cmds;\n    bc1_block compr_blocks[cur_total_draw_length];\n};\n\nstruct sld_playcol_mask_layer : sld_layer {\n    sld_mask_header header;\n    sld_command_array cmds;\n    bc4_block compr_blocks[cur_total_draw_length];\n};\n\nstruct sld_frame {\n    sld_frame_header header;\n\n    if (header.frame_type & 0x01) {\n        sld_main_layer main_layer;\n\n        cur_main_height = main_layer.header.offset_y2 - main_layer.header.offset_y1;\n        cur_main_height_blocks = cur_main_height / 4;\n\n        padding[pad_length()];\n    }\n    if (header.frame_type & 0x02) {\n        sld_shadow_layer shadow_layer;\n\n        padding[pad_length()];\n    }\n    if (header.frame_type & 0x04) {\n        sld_unknown_layer unknown_layer;\n\n        padding[pad_length()];\n    }\n    if (header.frame_type & 0x08) {\n        sld_dmg_mask_layer dmg_mask_layer;\n\n        padding[pad_length()];\n    }\n    if (header.frame_type & 0x10) {\n        sld_playcol_mask_layer playcol_mask_layer;\n\n        padding[pad_length()];\n    }\n};\n\nstruct sld_file {\n    sld_header header;\n    sld_frame frames[header.num_frames];\n} [[inline]];\n\nsld_file file @ 0x00;\n"
  },
  {
    "path": "doc/media/patterns/slp.hexpat",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma endian little\n\n// only show a specific frame (at the specified index)\n// if this value is -1 all frames are shown\n#define SHOW_FRAME -1\n\nstruct slp_header {\n    char version[4];\n    s32 num_frames;\n    char comment[24];\n};\n\nstruct slp_header_v4 {\n    char version[4];\n    s32 num_frames;\n    s16 type;\n    s16 num_directions;\n    s16 frames_per_direction;\n    s32 palette_id;\n    s32 offset_main;\n    s32 offset_secondary;\n    padding[8];\n};\n\nstruct slp_frame_info {\n    u32 cmd_table_offset;\n    u32 outline_table_offset;\n    u32 palette_offset;\n    u32 properties;\n    s32 width;\n    s32 height;\n    s32 hotspot_x;\n    s32 hotspot_y;\n};\n\nu32 offset = 0x00;\n\nchar version[4] @ offset;\nslp_header header @ offset;\n\noffset += sizeof(header);\n\nslp_frame_info infos[header.num_frames] @ offset;\n\ns32 cur_frame = 0;\ns32 cur_height = infos[cur_frame].height;\ns32 cur_width = infos[cur_frame].width;\n\nchar cur_cmd = 0x00;\nchar low_crumb = 0;\nchar low_nibble = 0;\nchar high_nibble = 0;\nu16 cmd_len = 0;\nu16 payload_len = 0;\n\nfn next_frame() {\n    cur_frame += 1;\n    cur_height = infos[cur_frame].height;\n    cur_width = infos[cur_frame].width;\n};\n\nstruct slp_frame_row_edge {\n    u16 left_space;\n    u16 right_space;\n};\n\nstruct slp_cmd {\n    u8 cmd;\n\n    low_crumb = cmd & 0x03;\n    low_nibble = cmd & 0x0f;\n    high_nibble = cmd & 0xf0;\n\n    if (low_crumb == 0b00) {\n        cmd_len = cmd >> 2;\n        payload_len = cmd_len;\n    }\n    else if (low_crumb == 0b01) {\n        cmd_len = cmd >> 2;\n        payload_len = 0;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b0010) {\n        cmd_len = cmd << 4;\n        u8 next;\n        cmd_len += next;\n        payload_len = cmd_len;\n    }\n    else if (low_nibble == 0b0011) {\n        cmd_len = cmd << 4;\n        u8 next;\n        cmd_len += next;\n        payload_len = 0;\n    }\n    else if (low_nibble == 0b0110) {\n        cmd_len = cmd >> 4;\n        payload_len = cmd_len;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n            payload_len = cmd_len;\n        }\n    }\n    else if (low_nibble == 0b0111) {\n        cmd_len = cmd >> 4;\n        payload_len = 1;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b1010) {\n        cmd_len = cmd >> 4;\n        payload_len = 1;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b1011) {\n        cmd_len = cmd >> 4;\n        payload_len = 0;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (cmd == 0x5E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else if (cmd == 0x7E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else if (cmd == 0x9E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else {\n        cmd_len = 0;\n        payload_len = 0;\n    }\n\n    u8 payload[payload_len];\n\n    cur_cmd = cmd;\n};\n\nstruct slp_cmd_row {\n    slp_cmd commands[while(cur_cmd != 0x0F)];\n\n    cur_cmd = 0x00;\n};\n\nstruct slp_cmd_table {\n    slp_cmd_row rows[cur_height];\n};\n\nstruct slp_frame {\n    slp_frame_row_edge outline_offsets[cur_height];\n    u32 cmd_offsets[cur_height];\n    slp_cmd_table table;\n\n    if (cur_frame < header.num_frames - 1)\n        next_frame();\n\n    if (SHOW_FRAME >= 0 && cur_frame - 1 != SHOW_FRAME)\n        continue;\n};\n\noffset += sizeof(infos);\n\nslp_frame frames[header.num_frames] @ offset;\n"
  },
  {
    "path": "doc/media/patterns/slp_v4.hexpat",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma endian little\n#pragma pattern_limit 3000000\n\n// Decode the secondary offsets if set to 1\n// switch off if you have performance problems\n#define DECODE_SECONDARY 0\n\n// only show a specific frame (at the specified index)\n// if this value is -1 all frames are shown\n#define SHOW_FRAME -1\n\nstruct slp_header {\n    char version[4];\n    s16 num_frames;\n    s16 type;\n    s16 num_directions;\n    s16 frames_per_direction;\n    s32 palette_id;\n    s32 offset_main;\n    s32 offset_secondary;\n    padding[8];\n};\n\nstruct slp_frame_info {\n    u32 cmd_table_offset;\n    u32 outline_table_offset;\n    u32 palette_offset;\n    u32 properties;\n    s32 width;\n    s32 height;\n    s32 hotspot_x;\n    s32 hotspot_y;\n};\n\nenum pixel_type : u8 {\n    index,\n    index_modifier,\n    bgra,\n};\n\nu32 offset = 0x00;\n\nchar version[4] @ offset;\nslp_header header @ offset;\n\noffset += sizeof(header);\n\nslp_frame_info infos[header.num_frames] @ offset;\n\ns32 cur_frame = 0;\ns32 cur_height = infos[cur_frame].height;\ns32 cur_width = infos[cur_frame].width;\ns32 cur_outline_table_offset = infos[cur_frame].outline_table_offset;\ns32 cur_cmd_table_offset = infos[cur_frame].cmd_table_offset;\npixel_type cur_pixel_type = pixel_type::index;\nif (infos[cur_frame].properties & 0x07)\n    cur_pixel_type = pixel_type::bgra;\nelse if (version == \"4.1X\")\n    cur_pixel_type = pixel_type::index_modifier;\n\nchar cur_cmd = 0x00;\nchar low_crumb = 0;\nchar low_nibble = 0;\nchar high_nibble = 0;\nu16 cmd_len = 0;\nu16 payload_len = 0;\n\nfn next_frame() {\n    cur_frame += 1;\n    cur_height = infos[cur_frame].height;\n    cur_width = infos[cur_frame].width;\n    cur_outline_table_offset = infos[cur_frame].outline_table_offset;\n    cur_cmd_table_offset = infos[cur_frame].cmd_table_offset;\n\n    if (infos[cur_frame].properties & 0x07)\n        cur_pixel_type = pixel_type::bgra;\n    else if (version == \"4.1X\")\n        cur_pixel_type = pixel_type::index_modifier;\n    else\n        cur_pixel_type = pixel_type::index;\n};\n\nstruct slp_frame_row_edge {\n    u16 left_space;\n    u16 right_space;\n};\n\nstruct slp_cmd {\n    u8 cmd;\n\n    low_crumb = cmd & 0x03;\n    low_nibble = cmd & 0x0f;\n    high_nibble = cmd & 0xf0;\n\n    if (low_crumb == 0b00) {\n        cmd_len = cmd >> 2;\n        payload_len = cmd_len;\n    }\n    else if (low_crumb == 0b01) {\n        cmd_len = cmd >> 2;\n        payload_len = 0;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b0010) {\n        cmd_len = cmd << 4;\n        u8 next;\n        cmd_len += next;\n        payload_len = cmd_len;\n    }\n    else if (low_nibble == 0b0011) {\n        cmd_len = cmd << 4;\n        u8 next;\n        cmd_len += next;\n        payload_len = 0;\n    }\n    else if (low_nibble == 0b0110) {\n        cmd_len = cmd >> 4;\n        payload_len = cmd_len;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n            payload_len = cmd_len;\n        }\n    }\n    else if (low_nibble == 0b0111) {\n        cmd_len = cmd >> 4;\n        payload_len = 1;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b1010) {\n        cmd_len = cmd >> 4;\n        payload_len = 1;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (low_nibble == 0b1011) {\n        cmd_len = cmd >> 4;\n        payload_len = 0;\n        if (cmd_len == 0) {\n            u8 next;\n            cmd_len = next;\n        }\n    }\n    else if (cmd == 0x5E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else if (cmd == 0x7E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else if (cmd == 0x9E) {\n        u8 next;\n        cmd_len = next;\n        payload_len = 0;\n    }\n    else {\n        cmd_len = 0;\n        payload_len = 0;\n    }\n\n    if (cur_pixel_type == pixel_type::index_modifier)\n        u16 payload[payload_len];\n    else if (cur_pixel_type == pixel_type::bgra)\n        u32 payload[payload_len];\n    else\n        u8 payload[payload_len];\n\n    cur_cmd = cmd;\n};\n\nstruct slp_cmd_row {\n    slp_cmd commands[while(cur_cmd != 0x0F)];\n\n    cur_cmd = 0x00;\n};\n\nstruct slp_cmd_table {\n    slp_cmd_row rows[cur_height];\n};\n\nstruct slp_frame {\n    $ = cur_outline_table_offset;\n    slp_frame_row_edge outline_offsets[cur_height];\n\n    $ = cur_cmd_table_offset;\n    u32 cmd_offsets[cur_height];\n\n    $ = cmd_offsets[0];\n    slp_cmd_table table;\n\n    if (cur_frame < header.num_frames - 1)\n        next_frame();\n\n    if (SHOW_FRAME >= 0 && cur_frame - 1 != SHOW_FRAME)\n        continue;\n};\n\noffset += sizeof(infos);\n\nslp_frame frames[header.num_frames] @ offset;\n\noffset = header.offset_secondary;\n\nif (offset == 0x00 || !DECODE_SECONDARY)\n    return;\n\nslp_frame_info infos_secondary[header.num_frames] @ offset;\n\ncur_frame = 0;\ncur_height = infos_secondary[cur_frame].height;\ncur_width = infos_secondary[cur_frame].width;\ncur_outline_table_offset = infos_secondary[cur_frame].outline_table_offset;\ncur_cmd_table_offset = infos_secondary[cur_frame].cmd_table_offset;\ncur_pixel_type = pixel_type::index;\n\nfn next_frame_secondary() {\n    cur_frame += 1;\n    cur_height = infos_secondary[cur_frame].height;\n    cur_width = infos_secondary[cur_frame].width;\n    cur_outline_table_offset = infos_secondary[cur_frame].outline_table_offset;\n    cur_cmd_table_offset = infos_secondary[cur_frame].cmd_table_offset;\n};\n\nstruct slp_frame_secondary {\n    $ = cur_outline_table_offset;\n    slp_frame_row_edge outline_offsets[cur_height];\n\n    $ = cur_cmd_table_offset;\n    u32 cmd_offsets[cur_height];\n    slp_cmd_table table;\n\n    if (cur_frame < header.num_frames - 1)\n        next_frame_secondary();\n\n    if (SHOW_FRAME >= 0 && cur_frame - 1 != SHOW_FRAME)\n        continue;\n};\n\noffset += sizeof(infos_secondary);\n\nslp_frame_secondary frames_secondary[header.num_frames] @ offset;\n"
  },
  {
    "path": "doc/media/sld-files.md",
    "content": "# SLD files\n\nSLD is the sprite storage format introduced in Build 66692 of Age of Empires 2: Definitive Edition.\n\nUnlike the [SMX](smx-files.md) and [SLP](slp-files.md) formats, the SLD format does not use\nindexed palettes for pixel data. Instead, SLD image data utilizes lossy texture compression\nalgorithms [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).\n\n1. [SLD file format](#sld-file-format)\n   1. [Header](#header)\n   2. [SLD Frame](#sld-frame)\n      1. [SLD Frame Header](#sld-frame-header)\n   3. [SLD Layers](#sld-layers)\n      1. [Layer Content Length](#layer-content-length)\n      2. [SLD Main Graphics Layer](#sld-main-graphics-layer)\n         1. [Header](#header-1)\n         2. [Pixel Data](#pixel-data)\n            1. [Command Array](#command-array)\n            2. [Compressed Block Array](#compressed-block-array)\n      3. [SLD Shadow Graphics Layer](#sld-shadow-graphics-layer)\n         1. [Header](#header-2)\n         2. [Pixel Data](#pixel-data-1)\n            1. [Command Array](#command-array-1)\n            2. [Compressed Block Array](#compressed-block-array-1)\n      4. [SLD Damage Mask Layer](#sld-damage-mask-layer)\n         1. [Header](#header-3)\n         2. [Pixel Data](#pixel-data-2)\n            1. [Command Array](#command-array-2)\n            2. [Compressed Block Array](#compressed-block-array-2)\n      5. [SLD Playercolor Mask Layer](#sld-playercolor-mask-layer)\n         1. [Header](#header-4)\n         2. [Pixel Data](#pixel-data-3)\n            1. [Command Array](#command-array-3)\n            2. [Compressed Block Array](#compressed-block-array-3)\n2. [Excursion: DXT1/BC1 Block Compression](#excursion-dxt1bc1-block-compression)\n   1. [Building the Lookup Table](#building-the-lookup-table)\n   2. [Constructing the Pixel Block](#constructing-the-pixel-block)\n   3. [Example](#example)\n3. [Excursion: DXT4/BC4 Block Compression](#excursion-dxt4bc4-block-compression)\n   1. [Building the Lookup Table](#building-the-lookup-table-1)\n   2. [Constructing the Pixel Block](#constructing-the-pixel-block-1)\n   3. [Example](#example-1)\n4. [Processing the Command Array (Example)](#processing-the-command-array-example)\n\nIn addition to the format description found here, we also\nprovide a [pattern file](/doc/media/patterns/sld.hexpat) for the [imHex](https://imhex.werwolv.net/)\neditor which can be used to explore the SLP data visually.\n\n## SLD file format\n\nSLD files are hierarchically structured into frames, layers and (compressed) color data.\n\n### Header\n\nThe SLD file starts with a header:\n\n| Length  | Type   | Description      | Example           |\n| ------- | ------ | ---------------- | ----------------- |\n| 4 bytes | string | Signature        | SLDX              |\n| 2 bytes | uint16 | Version          | 4, 0x0004         |\n| 2 bytes | uint16 | Number of frames | 721, 0x02D1       |\n| 2 bytes | uint16 | Unknown          | always 0x0000     |\n| 2 bytes | uint16 | Unknown          | always 0x0010     |\n| 4 bytes | uint32 | Unknown          | always 0xFF000000 |\n\n```cpp\nstruct sld_header {\n  char   file_descriptor[4];\n  uint16 version;\n  uint16 num_frames;\n  uint16 unknown1;\n  uint16 unknown2;\n  uint32 unknown3;\n};\n```\nPython format: `Struct(\"< 4s 4H I\")`\n\n\n### SLD Frame\n\nFrame definitions start directly after the file header. There are `sld_header.num_frames` frame\nentries that can be read sequentially. Frames can contain up to 5 layers:\n\n* main graphic layer\n* shadow layer (optional)\n* ??? layer (optional)\n* damage mask layer (optional)\n* player color mask layer (optional)\n\nWhich of these layers are present in a frame is determined by the `frame_type` value\nin the frame header.\n\n#### SLD Frame Header\n\nThe frame header contains 7 values:\n\n| Length  | Type   | Description     | Example                   |\n| ------- | ------ | --------------- | ------------------------- |\n| 2 bytes | uint16 | Canvas Width    | 200, 0xC8                 |\n| 2 bytes | uint16 | Canvas Height   | 200, 0xC8                 |\n| 2 bytes | int16  | Canvas Center X | 100, 0x64                 |\n| 2 bytes | int16  | Canvas Center Y | 100, 0x64                 |\n| 1 bytes | uint8  | Frame Type      | 7, 0b00000111 (bit field) |\n| 1 bytes | uint8  | Unkown          | 1, 0x01                   |\n| 2 bytes | uint16 | Frame index     | 5, 0x0005                 |\n\n```cpp\nstruct sld_frame_header {\n  uint16  canvas_width;\n  uint16  canvas_height;\n  int16  canvas_hotspot_x;\n  int16  canvas_hotspot_y;\n  uint8   frame_type;\n  uint8   unknown1;\n  uint16  frame_index;\n};\n```\nPython format: `Struct(\"< 4H 2B H\")`\n\nFrames define a *canvas* which is a square texture that the layers get drawn\ninto. The canvas size is dertermined by `canvas_width` and `canvas_height`.\nEvery canvas also has a *hotspot* which is an anchor point for the ingame mouse\npointer. When a unit/building is placed (e.g. in the editor or for building foundation),\nthe frame texture is attached to the mouse pointer at this position.\n\n`frame_type` is a bit field. This means that every bit set to `1`\nindicates that the frame contains a specific type of layer.\n\nIndices are read from left to right:\n\n| Bit index | Description                                                 |\n| --------- | ----------------------------------------------------------- |\n| 7         | If set to `1`, the frame contains a main graphic layer      |\n| 6         | If set to `1`, the frame contains a shadow layer            |\n| 5         | If set to `1`, the frame contains a ??? layer               |\n| 4         | If set to `1`, the frame contains a damage mask layer       |\n| 3         | If set to `1`, the frame contains a player color mask layer |\n| 0-2       | Unused                                                      |\n\n**Example**\n\n```\nframe_type = 0x17 = 0b0001 0111\n```\n\nThis frame would contain a *main graphic layer*, a *shadow layer*, a *??? layer*\nand a *player color mask layer*.\n\n### SLD Layers\n\nThe frame header is immediately followed by the layers specified in the `frame_type`\nvalue. The order of layers is always the same:\n\n1. [main graphics layer](#sld-main-graphics-layer)\n1. [shadow graphics layer](#sld-shadow-graphics-layer)\n1. ??? layer\n1. [damage mask layer](#sld-damage-mask-layer)\n1. [playercolor mask layer](#sld-playercolor-mask-layer)\n\nLayers that are not indicated by `frame_type` are skipped.\n\nThe layers use different compression methods for the pixel data they store. Here\nyou can see an overview over the used compression methods per layer.\n\n| Layer            | Compression |\n| ---------------- | ----------- |\n| Main Graphics    | DXT1        |\n| Shadow Graphics  | DXT4        |\n| ???              | ???         |\n| Damage Mask      | DXT1        |\n| Playercolor Mask | DXT4        |\n\n#### Layer Content Length\n\nEvery layer starts with 4 Bytes values that signify the number of bytes\nin the layer (including the content length value).\n\n| Length  | Type   | Description    | Example     |\n| ------- | ------ | -------------- | ----------- |\n| 4 bytes | uint32 | Content Length | 618, 0x026A |\n\n```cpp\nstruct sld_layer_length {\n  uint32  content_length;\n};\n```\nPython format: `Struct(\"< I\")`\n\n**Important**: After the `content_length` bytes that contain data, each layer\nis padded with null bytes until its length is a multiple of 4. Therefore,\nthe actual length of the layer in bytes should be calculated like this:\n\n```\nactual_length = content_length + ((4 - content_length) % 4)\n```\n\n\n#### SLD Main Graphics Layer\n\nThe main graphics layer stores the main texture of the game entity that is\ndisplayed in the game.\n\nThis layer always exists and is not optional.\n\n##### Header\n\nThe main graphics layer header contains 6 values:\n\n| Length  | Type   | Description                    | Example   |\n| ------- | ------ | ------------------------------ | --------- |\n| 2 bytes | uint16 | Layer Offset X1 (top left)     | 72, 0x48  |\n| 2 bytes | uint16 | Layer Offset Y1 (top left)     | 72, 0x48  |\n| 2 bytes | uint16 | Layer Offset X2 (bottom right) | 132, 0x84 |\n| 2 bytes | uint16 | Layer Offset Y2 (bottom right) | 108, 0x6C |\n| 1 bytes | uint8  | Flag 1                         | 128, 0x80 |\n| 1 bytes | uint8  | Unknown                        | 1, 0x01   |\n\n```cpp\nstruct sld_graphics_header {\n  uint16  offset_x1;\n  uint16  offset_y1;\n  uint16  offset_x2;\n  uint16  offset_y2;\n  uint8   flag1;\n  uint8   unknown1;\n};\n```\nPython format: `Struct(\"< 4H 2B\")`\n\nThere are 4 offset values which define the position of the layer inside its\nframe's canvas. `offset_x1` and `offset_y1` signify the position of the top left corner\nof the layer, while `offset_x2` and `offset_y2` define the position of the\nbottom right corner.\n\nWidth and height of the layer can be calculated from the 4 offset values.\n\n```\nwidth = offset_x2 - offset_x1\nheight = offset_y2 - offset_y1\n```\n\nYou can also calculate the position of the canvas hotspot relative to the layer.\n\n```\nlayer_hotspot_x = canvas_hotspot_x - offset_x1\nlayer_hotspot_y = canvas_hotspot_y - offset_y1\n```\n\n`flag1` is (presumably) a bit field that contains settings for drawing the layer.\n\nIndices are read from left to right:\n\n| Bit index | Description                                                   |\n| --------- | ------------------------------------------------------------- |\n| 0         | If set to `1`, the pixel data from previous frames is reused. |\n| 1-6       | Unknown                                                       |\n| 7         | Set to `1` in playercolor layers.                             |\n\n##### Pixel Data\n\nThe pixel data in SLD main graphics is drawn block-wise, i.e. in blocks of 4x4 (= 16) pixels.\nPixel blocks are either fully transparent or colored with RGB/RGBA values from a compressed\n**DXT1** data block. Blocks are inserted from left to right, starting in the\ntop left corner (0,0) of the sprite.\n\nDrawing commands and compressed pixel data blocks for the sprite are stored\nin two separate arrays: The **command array** and the **compressed block array**.\n\n###### Command Array\n\nImmediately after the SLD main graphics layer header, there is a length field\ncontaining the number of commands for the command array:\n\n| Length  | Type   | Description          | Example    |\n| ------- | ------ | -------------------- | ---------- |\n| 2 bytes | uint16 | Command array length | 11, 0x000B |\n\nThe command array is then followed by `command_array_length` commands with length 2 Bytes\n(i.e. a total number of `2 * command_array_length` bytes).\n\nEach command is a pair of bytes that tells us how many 4x4 pixel blocks are skipped (i.e.\nfully transparent) and how many 4x4 pixel blocks are drawn using the compressed DXT1 data blocks\nfrom the **compressed block array**.\n\n**Important:** If bitfield `flag1` in the layer header has bit 7 set, skipping works\nslightly different. Instead of drawing a fully transparent block, the pixel block\nin the previous *frame* (same position, same layer) is copied.\n\n| Length  | Type  | Description          | Example |\n| ------- | ----- | -------------------- | ------- |\n| 1 bytes | uint8 | Skipped blocks count | 8, 0x08 |\n| 1 bytes | uint8 | Draw blocks count    | 3, 0x03 |\n\nBlocks from the compressed block array are read sequentially. The first *Draw*\ncall will start reading at index 0 of the array. The next *Draw* command will continue\nreading where the previous command stopped.\n\nNote that there is no \"end of row\" command or something similar which indicates that\nthe draw pointer has to be moved to the beginning of the next row of blocks in the sprite.\nYou have to manually check if the drawing offset is still in the sprite boundaries and\nmove the drawing offset to the next block row if necessary.\n\nSee [this section](#processing-the-command-array-example) for an example on how to\nprocess the command array.\n\n###### Compressed Block Array\n\nAn array of blocks that each contain pixel data for a 4x4 pixel block in the sprite.\n\nThe **compressed block array** follows immediately after the **command array**.\nThere should be exactly as many blocks as the cumulative number of *Draw* calls\nin the command array. However, there is no length field that contains the number of\nblocks.\n\nBlocks in the main graphics layer are compressed with **DXT1** compression.\nSee [the section on DXT1](#excursion-dxt1bc1-block-compression) for descriptions and\nexamples on how to decompress the data in the block.\n\n\n#### SLD Shadow Graphics Layer\n\nThe shadow graphics layer stores the shadows of the game entity that is\ndisplayed in the game.\n\nThis layer seems to be optional.\n\n##### Header\n\n[Same as in the SLD Main Graphics Layer](#header-1)\n\n##### Pixel Data\n\nThe pixel data in SLD shadow graphics is drawn block-wise, i.e. in blocks of 4x4 (= 16) pixels\n(same as in the main graphics layer). However, unlike the main graphics layer, the pixel blocks\nlayer are only grayscale colors (i.e. they only use one color channel). They also\nuse a different compression method: **DXT4**.\n\nThe rules for drawing blocks are the same as for the main graphics layer.\nBlocks are inserted from left to right, starting in the top left corner (0,0) of the sprite.\nDrawing commands and compressed pixel data blocks for the sprite are stored\nin two separate arrays: The **command array** and the **compressed block array**.\n\n\n###### Command Array\n\n[Same as in the SLD Main Graphics Layer](#command-array)\n\n###### Compressed Block Array\n\nAn array of blocks that each contain pixel data for a 4x4 pixel block in the sprite.\n\nBlocks in the shadow graphics layer are compressed with **DXT4** compression.\nSee [the section on DXT4](#excursion-dxt4bc4-block-compression) for descriptions and\nexamples on how to decompress the data in the block.\n\n\n#### SLD Damage Mask Layer\n\nThe damage mask layer is an overlay for the main graphis layer that stores a modifier value\nin each pixel. Internally, this mask is used to display the darkening effect\nof damaged buildings/units. Each pixel in the damage mask layer corresponds\nto a pixel in the main graphics layer.\nUsing the modifier value in the damage mask pixel and the current damage percentage\nof the unit, the game internally calculates a multiplier that is applied to the RGB\nvalues of the main graphics pixel.\n\nThis layer is optional.\n\n##### Header\n\nThe damage mask layer header contains 2 values:\n\n| Length  | Type  | Description | Example   |\n| ------- | ----- | ----------- | --------- |\n| 1 bytes | uint8 | Flag 1      | 128, 0x80 |\n| 1 bytes | uint8 | Unknown     | 1, 0x01   |\n\n```cpp\nstruct sld_mask_header {\n  uint8 flag1;\n  uint8 unknown1;\n};\n```\nPython format: `Struct(\"< 2B\")`\n\nThis presumably is a shortened version of the header for the main graphics layer\nand the shadow layer. There is no width and height information as it should have the exact same\nsize as the main graphics layer of the frame.\n\n`flag1` is (presumably) a bit field that contains settings for drawing the layer.\n\n\n##### Pixel Data\n\nThe pixel data in SLD damage mask is drawn block-wise, i.e. in blocks of 4x4 (= 16) pixels\n(same as in the main graphics layer). Technically, the damage mask is stored as an\nRGB/RGBA sprite, but it is never displayed as such. Instead, the game turns the RGBA\nvalues into a modifier that is then applied to the main graphics during rendering.\n\nPixel blocks are either fully transparent or colored with RGB/RGBA values from a compressed\n**DXT1** data block. Blocks are inserted from left to right, starting in the\ntop left corner (0,0) of the sprite.\n\nThe rules for drawing blocks are the same as for the main graphics layer.\nBlocks are inserted from left to right, starting in the top left corner (0,0) of the sprite.\nDrawing commands and compressed pixel data blocks for the sprite are stored\nin two separate arrays: The **command array** and the **compressed block array**.\n\n###### Command Array\n\n[Same as in the SLD Main Graphics Layer](#command-array)\n\n###### Compressed Block Array\n\nAn array of blocks that each contain pixel data for a 4x4 pixel block in the sprite.\n\nBlocks in the shadow graphics layer are compressed with **DXT1** compression.\nSee [the section on DXT1](#excursion-dxt1bc1-block-compression) for descriptions and\nexamples on how to decompress the data in the block.\n\n#### SLD Playercolor Mask Layer\n\nThe playercolor mask layer is an overlay for the main graphis layer that stores an\nindex for a playercolor value in each pixel.\nUsing the index in the playercolor mask pixel and the owner of the unit,\nthe game gets a playercolor RGBA value and displays it ingame at the position\nof the pixel.\n\nThis layer is optional.\n\n##### Header\n\n[Same as in the SLD Damage Mask Layer](#header-3)\n\n##### Pixel Data\n\nThe pixel data in SLD playercolor mask is drawn block-wise, i.e. in blocks of 4x4 (= 16) pixels\n(same as in the main graphics layer). Playercolor mask pixels only use one color channel,\nso they are technically greyscale images. However, the color channel value is never\ndirectly displayed and instead treated as an index for looking up the actual\nplayer color value from a palette. The compression method used for the pixel\nblocks is **DXT4**.\n\nThe rules for drawing blocks are the same as for the main graphics layer.\nBlocks are inserted from left to right, starting in the top left corner (0,0) of the sprite.\nDrawing commands and compressed pixel data blocks for the sprite are stored\nin two separate arrays: The **command array** and the **compressed block array**.\n\n###### Command Array\n\n[Same as in the SLD Main Graphics Layer](#command-array)\n\n###### Compressed Block Array\n\nAn array of blocks that each contain pixel data for a 4x4 pixel block in the sprite.\n\nBlocks in the shadow graphics layer are compressed with **DXT4** compression.\nSee [the section on DXT4](#excursion-dxt4bc4-block-compression) for descriptions and\nexamples on how to decompress the data in the block.\n\n\n## Excursion: DXT1/BC1 Block Compression\n\nDXT1 block compression is a method to store a 4x4 pixel block in 8 bytes of data.\nEach block can display up to 4 different 16-Bit RGB(A) colors. The 16 pixels in the\nblock are represented by 2-Bit indices for a lookup table referencing the 4 RGBA colors.\n\nThe 8 byte block is organized as follows:\n\n| Length  | Type   | Description       |\n| ------- | ------ | ----------------- |\n| 2 bytes | uint16 | Reference color 0 |\n| 2 bytes | uint16 | Reference color 1 |\n| 4 bytes | uint32 | Pixel indices     |\n\n```cpp\nstruct bc1_block {\n  uint16 color0;\n  uint16 color1;\n  uint32 pixel_indices;\n};\n```\nPython format: `Struct(\"< 2H I\")`\n\n`color0` and `color1` are 16-Bit colors using the R5G6B5 format, i.e. the red\nchannel uses 5 Bits, the green channel uses 6 Bits, and the blue channel uses\n5 Bits. You can get the invidual values for each channel with a bitmask and bitshift:\n\n```\nr0 = (color0 & 0xF800) >> 11  (5 Bits; bit index 0-4)\ng0 = (color0 & 0x07E0) >> 5   (6 Bits; bit index 5-10)\nb0 = (color0 & 0x001F)        (5 Bits; bit index 11-15)\n\nRGB16: (r0, g0, b0)\n\n(Optional conversion to RGB32 with 8 Bit channel size):\nRGB32: (r0 * 8, g0 * 4, b0 * 8)\n```\n\nUncompressing the 4x4 pixel block requires 2 steps. First, we have to build\nthe lookup table for the 4 RGB colors using the two reference colors. Second,\nwe can assign colors to the pixels in the block by looking up their\nindices.\n\n### Building the Lookup Table\n\nThe lookup table has 4 entries that map a 2-Bit index to a 16-Bit RGB color\nvalue. The first 2 entries are the reference colors `color0` and `color1`.\n\n| Index | color Value |\n| ----- | ----------- |\n| 00    | `color0`    |\n| 01    | `color1`    |\n\nUsing the reference colors, we have to interpolate the color values for the\nother two indices. The interpolation works differently depending on which of\nthe two reference color values (that means their 16-Bit integer values) is greater.\n\nIf `color0` is **greater than** `color1`, the color values for the remaining\nindices are calculated as follows:\n\n| Index | Color Value                                |\n| ----- | ------------------------------------------ |\n| 10    | `color2 = (2/3) * color0 + (1/3) * color1` |\n| 11    | `color3 = (1/3) * color0 + (2/3) * color1` |\n\n(The factors above are applied to each of the three color channels. Similarly,\nthe addition of coulour values is the addition of the colors' respective color\nchannels.)\n\nIf `color0` is **smaller than or equal to** `color1`, only the color value of `color2`\nis interpolated, while `color3` represents a color value with full transparency:\n\n| Index | Color Value                                |\n| ----- | ------------------------------------------ |\n| 10    | `color2 = (1/2) * color0 + (1/2) * color1` |\n| 11    | `color3 = (0, 0, 0, 0)`                    |\n\nThe latter case allows a pixel block to contain transparent pixels, although that\nsacrifices one of the possible color values.\n\n### Constructing the Pixel Block\n\nAfter we have created the lookup table, we can reconstruct the pixel block by looking\nup the pixel indices in the lookup table. There are 16 indices in each data block,\neach with a size of 2 Bit (= 32 Bit (4 Byte) in total). The position of an index\nin the data block also determines the position in the resulting 4x4 pixel block.\n\nThe easiest way to contruct the 4x4 pixel block is to fill it from left to right\nstarting in the top left corner. To do this, read the `pixel_indices` value\nas a 32-Bit unsigned integer with little endian byte order. You can then look up\nthe colors for all pixel indices one by one starting with the two least\nsignificant bits.\n\nA very simple implementation of this can be achieved\nby applying a `0b11` bitmask that extracts the current pixel index and a\nbitshift by 2 to get to the next pixel index (see below).\n\n```python\npixels = []\nbitmask = 0b11\nfor shift in range(16):\n    bc1_idx = pixel_indices & bitmask\n    col = lookup_table[bc1_idx]\n    pixels.append(col)\n    pixel_indices = pixel_indices >> 2\n```\n\nThe pixels in the generated list have the correct order if you want to insert them\ninto the pixel block from left to right and top to bottom.\n\n### Example\n\nLet's look at the the following compressed data block in the file:\n\n```\nBC1 block: 00 00 82 10 FB 37 17 77\n```\n\nFirst of all, we can extract the three values `color0`, `color1`, and `pixel_indices` in the block\nby reading them as integers in little endian format.\n\n```\ncolor0:        0x0000\ncolor1:        0x1082\npixel_indices: 0x771737FB\n```\n\nUsing `color0` and `color1`, we can construct the lookup table. The first step is to\nget the values for the three color channels.\n\n```\ncolor0: 0x0000\nr0 = (color0 & 0xF800) >> 11 = 0b00000  = 0    (5 Bits; bit index 0-4)\ng0 = (color0 & 0x07E0) >> 5  = 0b000000 = 0    (6 Bits; bit index 5-10)\nb0 = (color0 & 0x001F)       = 0b00000  = 0    (5 Bits; bit index 11-15)\n\nRGB16: (0, 0, 0)\nRGB32: (0, 0, 0)\n\ncolor1: 0x1082\nr1 = (color1 & 0xF800) >> 11 = 0b00010  = 2    (5 Bits; bit index 0-4)\ng1 = (color1 & 0x07E0) >> 5  = 0b000100 = 4    (6 Bits; bit index 5-10)\nb1 = (color1 & 0x001F)       = 0b00010  = 2    (5 Bits; bit index 11-15)\n\nRGB16: (2, 4, 2)\nRGB32: (16, 16, 16)\n```\n\nNext up, we have to determine the other 2 color values. Since the value of `color0` is\nsmaller than `color1`, we know that `color3` must be fully transparent. We only\nhave to interpolate `color2`.\n\n```\ncolor2 c= (1/2) * color0 + (1/2) * color1\n        = (1/2) * (0,0,0) + (1/2) * (2,4,2)\n        = (0,0,0)         + (1,2,1)\n        = (1,2,1)\n\nRGB16: (1, 2, 1)\nRGB32: (8, 8, 8)\n\ncolor3 = (0, 0, 0, 0)   (full transparency)\n```\n\nIn the end, our lookup table looks like this (using RGBA16 format):\n\n\n| Index | Color Value         |\n| ----- | ------------------- |\n| 00    | `c0 = (2, 4, 2, 1)` |\n| 01    | `c1 = (0, 0, 0, 1)` |\n| 10    | `c2 = (1, 2, 1, 1)` |\n| 11    | `c3 = (0, 0, 0, 0)` |\n\nAll we have to do now is to fill the pixel block using the `pixel_indices` values.\nUsing the algorithm from the previous section, we can read it from the back\nto get the lookup indices.\n\n```\npixel_indices (Hex):    0x771737FB\npixel_indices (Binary): 01110111000101110011011111111011\n    (split into 2 Bit): 01 11 01 11 00 01 01 11 00 11 01 11 11 11 10 11\n                                                                     ^\n                                                                     start here\n                                                                     (0,0)\n```\n\nWhen we assign the indices from left to right and top to bottom, the assignment\nlooks like this:\n\n| Coord  | x0   | x1   | x2   | x3   |\n| ------ | ---- | ---- | ---- | ---- |\n| **y0** | `11` | `10` | `11` | `11` |\n| **y1** | `11` | `01` | `11` | `00` |\n| **y2** | `11` | `01` | `01` | `00` |\n| **y3** | `11` | `01` | `11` | `01` |\n\nOr alternatively, using the color lookups:\n\n| Coord  | x0  | x1  | x2  | x3  |\n| ------ | --- | --- | --- | --- |\n| **y0** | c3  | c2  | c3  | c3  |\n| **y1** | c3  | c1  | c3  | c0  |\n| **y2** | c3  | c1  | c1  | c0  |\n| **y3** | c3  | c1  | c3  | c1  |\n\n\n## Excursion: DXT4/BC4 Block Compression\n\nDXT4 block compression is a method to store a 4x4 pixel block in 8 bytes of data.\nEach block can store up to 8 different 8-Bit single-channel colors (only the red\nchannel of an RGBA color used). You can also interpret them as greysale images since\nthese also only use 8-Bit. DXT4 compressed pixel values are usually not used for display,\nbut for storing other data necessary during rendering.\n\nThe 16 pixels in the block are represented by 3-Bit indices for a lookup table\nreferencing the 8 single-channel colors.\n\nThe 8 byte block is organized as follows:\n\n| Length  | Type    | Description       |\n| ------- | ------- | ----------------- |\n| 1 bytes | uint8   | Reference color 0 |\n| 1 bytes | uint8   | Reference color 1 |\n| 6 bytes | char[6] | Pixel indices     |\n\n```cpp\nstruct bc4_block {\n  uint8 color0;\n  uint8 color1;\n  uint8 pixel_indices[6];\n};\n```\nPython format: `Struct(\"< 8B\")`\n\n`color0` and `color1` are 8-Bit single-channel colors, i.e. they represent the\nred channel of an RGB color.\n\nUncompressing the 4x4 pixel block requires 2 steps. First, we have to build\nthe lookup table for the 8 single-channel colors using the two reference colors.\nSecond, we can assign colors to the pixels in the block by looking up their\nindices.\n\n### Building the Lookup Table\n\nThe lookup table has 8 entries that map a 3-Bit index to a 8-Bit red channel color\nvalue. The first 2 entries are the reference colors `color0` and `color1`.\n\n| Index | Color Value |\n| ----- | ----------- |\n| 000   | `color0`    |\n| 001   | `color1`    |\n\nUsing the reference colors, we have to interpolate the color values for the\nother 6 indices. The interpolation works differently depending on which of\nthe two reference color values (that means their 8-Bit integer values) is greater.\n\nIf `color0` is **greater than** `color1`, the color values for the remaining\nindices are calculated as follows:\n\n| Index | Color Value                                |\n| ----- | ------------------------------------------ |\n| 010   | `color2 = (6 * color0 + 1 * color1) / 7.0` |\n| 011   | `color3 = (5 * color0 + 2 * color1) / 7.0` |\n| 100   | `color4 = (4 * color0 + 3 * color1) / 7.0` |\n| 101   | `color5 = (3 * color0 + 4 * color1) / 7.0` |\n| 110   | `color6 = (2 * color0 + 5 * color1) / 7.0` |\n| 111   | `color7 = (1 * color0 + 6 * color1) / 7.0` |\n\nIf `color0` is **smaller than or equal to** `color1`, only the color value of 4 other colors (`color2`,\n`color3`, `color4`, `color5`) are interpolated. `color6` is set to `0` and `color7` is set to\n`255`.\n\n| Index | Color Value                                |\n| ----- | ------------------------------------------ |\n| 010   | `color2 = (4 * color0 + 1 * color1) / 5.0` |\n| 011   | `color3 = (3 * color0 + 2 * color1) / 5.0` |\n| 100   | `color4 = (2 * color0 + 3 * color1) / 5.0` |\n| 101   | `color5 = (1 * color0 + 4 * color1) / 5.0` |\n| 110   | `color6 = 0`                               |\n| 111   | `color7 = 255`                             |\n\n### Constructing the Pixel Block\n\nAfter we have created the lookup table, we can reconstruct the pixel block by looking\nup the pixel indices in the lookup table. There are 16 indices in each data block,\neach with a size of 3 Bit (= 48 Bit (6 Byte) in total). The position of an index\nin the data block also determines the position in the resulting 4x4 pixel block.\n\nThe easiest way to contruct the 4x4 pixel block is to fill it from left to right\nstarting in the top left corner. To do this, split the `pixel_indices` value\ninto two 3 Byte sections and convert them to 32-Bit unsigned integers\nwith little endian byte order. You can then look up the colors for all pixel indices\none by one, 3 bits at a time.\n\nA very simple implementation of this can be achieved\nby applying a `0b111` bitmask that extracts the current pixel index and a\nbitshift by 3 to get to the next pixel index (see below).\n\n```python\npixels = []\nbitmask = 0b111\npixel_indices0 = pixel_indices[0:3]\nfor shift in range(8):\n    bc4_idx = pixel_indices0 & bitmask\n    col = lookup_table[bc4_idx]\n    pixels.append(col)\n    pixel_indices0 = pixel_indices0 >> 3\n\npixel_indices1 = pixel_indices[3:6]\nfor shift in range(8):\n    bc4_idx = pixel_indices1 & bitmask\n    col = lookup_table[bc4_idx]\n    pixels.append(col)\n    pixel_indices1 = pixel_indices1 >> 3\n```\n\nThe pixels in the generated list have the correct order if you want to insert them\ninto the pixel block from left to right and top to bottom.\n\n### Example\n\nLet's look at the the following compressed data block in the file:\n\n```\nBC1 block: 10 82 33 45 FB 37 17 77\n```\n\nFirst of all, we can extract the three values `color0`, `color1`, and `pixel_indices` in the block.\nWE can already split `pixel_indices` into two 3 byte blocks that we convert to integers in\nlittle endian format.\n\n```\ncolor0:         0x10\ncolor1:         0x82\npixel_indices:  33 45 FB 37 17 77\npixel_indices0: 0xFB4533\npixel_indices1: 0x771737\n```\n\nUsing `color0` and `color1`, we can construct the lookup table. Since the value of `color0` is\nsmaller than `color1`, we know that `color6` must be 0 and `color7` must be 255. The rest\nof the colors has to be interpolated.\n\n```\ncolor0: 0x10 = 16\ncolor1: 0x82 = 130\ncolor2 = (4 * color0 + 1 * color1) / 5.0\n       = (4 * 16     + 1 * 130)    / 5.0\n       = 194 / 5.0\n       = 38\ncolor3 = (3 * color0 + 2 * color1) / 5.0\n       = (3 * 16     + 2 * 130)    / 5.0\n       = 308 / 5.0\n       = 61\ncolor4 = (2 * color0 + 3 * color1) / 5.0\n       = (2 * 16     + 3 * 130)    / 5.0\n       = 422 / 5.0\n       = 84\ncolor5 = (1 * color0 + 4 * color1) / 5.0\n       = (1 * 16     + 4 * 130)    / 5.0\n       = 536 / 5.0\n       = 107\ncolor6 = 0\ncolor7 = 255\n```\n\nIn the end, our lookup table looks like this:\n\n| Index | Color Value |\n| ----- | ----------- |\n| 000   | `c0 = 16`   |\n| 000   | `c1 = 130`  |\n| 010   | `c2 = 38`   |\n| 011   | `c3 = 61`   |\n| 100   | `c3 = 84`   |\n| 101   | `c3 = 107`  |\n| 110   | `c3 = 0`    |\n| 111   | `c3 = 255`  |\n\nAll we have to do now is to fill the pixel block using the `pixel_indices` values.\nUsing the algorithm from the previous section, we can read it from the back\nto get the lookup indices.\n\n```\npixel_indices0 (Hex):    0xFB4533\npixel_indices0 (Binary): 111110110100010100110011\n     (split into 3 Bit): 111 110 110 100 010 100 110 011\n                                                     ^\n                                                     start here\n                                                     (0,0)\n\npixel_indices1 (Hex):    0x771737\npixel_indices1 (Binary): 011101110001011100110111\n     (split into 3 Bit): 011 101 110 001 011 100 110 111\n                                                     ^\n                                                     (2,0)\n```\n\nWhen we assign the indices from left to right and top to bottom, the assignment\nlooks like this:\n\n| Coord  | x0    | x1    | x2    | x3    |\n| ------ | ----- | ----- | ----- | ----- |\n| **y0** | `011` | `110` | `100` | `010` |\n| **y1** | `100` | `110` | `110` | `111` |\n| **y2** | `111` | `110` | `100` | `011` |\n| **y3** | `001` | `110` | `101` | `011` |\n\nOr alternatively, using the color lookups:\n\n| Coord  | x0  | x1  | x2  | x3  |\n| ------ | --- | --- | --- | --- |\n| **y0** | c3  | c6  | c4  | c2  |\n| **y1** | c4  | c6  | c6  | c7  |\n| **y2** | c7  | c6  | c4  | c3  |\n| **y3** | c1  | c6  | c5  | c3  |\n\n\n## Processing the Command Array (Example)\n\n```\nWidth: 32px\nHeight: 12px\nCommand Array Length (Hex): 03 00\nCommand Array (Hex): 03 01  07 04  05 04\n```\n\nThe sprite in this example has the dimensions 32x12 (width x height). Since\nwe are operating on 4x4 blocks, that means the sprite demensions in blocks\nare 8x3.\n\n| Block | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   |\n| ----- | --- | --- | --- | --- | --- | --- | --- | --- |\n| **0** | -   | -   | -   | -   | -   | -   | -   | -   |\n| **1** | -   | -   | -   | -   | -   | -   | -   | -   |\n| **2** | -   | -   | -   | -   | -   | -   | -   | -   |\n\nWe start in the top left corner at block (0, 0). The first command is `03 01`\nwhich means we need 3 transparent blocks ((0, 0), (1, 0) and (2, 0)) and\nthen draw 1 block from the compressed block array at (3,0).\n\n| Block | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   |\n| ----- | --- | --- | --- | --- | --- | --- | --- | --- |\n| **0** |     |     |     | X   | -   | -   | -   | -   |\n| **1** | -   | -   | -   | -   | -   | -   | -   | -   |\n| **2** | -   | -   | -   | -   | -   | -   | -   | -   |\n\nThe next command is `07 04` which means we move the draw pointer\nforwards by 7 blocks. Since the current row only has 4 blocks remaining\n((4, 0) to (7, 0)), we wrap around to the next row. We then\nskip 3 blocks at the beginning of this row. Afterwards, we have\nto draw 4 blocks from the compressed block array starting at\n(3, 1).\n\n| Block | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   |\n| ----- | --- | --- | --- | --- | --- | --- | --- | --- |\n| **0** |     |     |     | X   |     |     |     |     |\n| **1** |     |     |     | X   | X   | X   | X   | -   |\n| **2** | -   | -   | -   | -   | -   | -   | -   | -   |\n\nThe final command is `05 04`, so 5 skipped blocks followed by\n4 draw blocks. We first skip the remaining block in the current\nrow, then move to the next one and skip another 4 blocks.\nThen we draw 4 blocks from the compressed block array. Since\nthis is the last command, there should be no more blocks to draw\nand the sprite is successfully decompressed.\n\n| Block | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   |\n| ----- | --- | --- | --- | --- | --- | --- | --- | --- |\n| **0** |     |     |     | X   |     |     |     |     |\n| **1** |     |     |     | X   | X   | X   | X   |     |\n| **2** |     |     |     |     | X   | X   | X   | X   |\n"
  },
  {
    "path": "doc/media/slp-files.md",
    "content": "# SLP files\n\nSLP files store graphics data for the AoE games. They contain all the (animation)\ntextures. In older releases, SLPs are packed into a DRS archive. Like the DRS format,\nit can also be read sequentially.\n\nThere are 5 known versions of SLP: `2.0N`, `3.0`, `4.0X`, `4.1X` and `4.2P`.\nBelow, you can find a table that shows which versions are used in which games.\n\nVersion | Games\n--------|-------------------\n`2.0N`  | AoE1 (1997), AoE2 (1999), SWGB (2000), AoE2:HD (2013)\n`3.0`   | AoE1:DE\n`4.0X`  | AoE1:DE\n`4.1X`  | AoE1:DE\n`4.2P`  | AoE1:DE (since Build 38862)\n\nAoE1, AoE2 (up to HD), and SWGB all use the `2.0N` version. With AoE1: Definitive\nEdition (AoE1:DE), three new versions were introduced; `3.0`, `4.0X`, and `4.1X`.\nVersion `3.0` isn't much different from `2.0N` SLPs and seems to be used\nfor rescaled SLPs and terrains.\n`4.0X` and `4.1X` SLPs have additional variables in the header and support multiple\nlayers for each frame. `4.1X` was introduced in a patch for AoE1 DE (probably Update 9).\nIt only seems to be used with decay SLPs and adds additional metadata to individual pixels,\ne.g. for displaying 'decay' of units.\n\n1. [SLP File Format](#slp-file-format)\n    1. [Header (up to version 3.0)](#header-up-to-version-30)\n    1. [Header (since version 4.0)](#header-since-version-40)\n    1. [Frame Info](#frame-info)\n    1. Frame Data\n        1. [Outline Table](#outline-table)\n        1. [Command Offset Table](#command-offset-table)\n        1. [Draw Commands](#draw-commands)\n1. [Palette Files](#palette-files)\n    1. [AoE1, AoE2, AoE2:HD, SWGB](#palette-files-in-older-versions-of-aoe1-and-aoe2-up-until-aoe2hd)\n    1. [AoE1:DE](#palette-files-in-aoe1de)\n1. [SLP types](#slp-types)\n    1. [Moving objects](#slp-files-for-moving-objects)\n    1. [Static objects](#slp-files-for-static-objects)\n    1. [Projectiles](#slp-files-for-projectiles)\n    1. [Shadows](#slp-files-for-shadows)\n\n## SLP file format\n\nSLPs from version `2.0` to `3.0` are mostly used by the games released before\nthe Definitive Editions as well as the beta and early releases of AoE1:DE.\n\nSLPs from version `4.0` onwards are used in AoE1:DE. There are a number of changes\nto the previous versions that were made to accomodate the new graphical features\nof the Definitive Edition. However, the basic structure remains similar to\nversions `2.0` and `3.0`.\n\nNote that SLPs with version `4.2P` need to be decompressed first (see section about the [compressed format](#compression-since-version-42p)).\n\nIn addition to the format description found here, we also\nprovide [pattern files](/doc/media/patterns/) for the [imHex](https://imhex.werwolv.net/)\neditor which can be used to explore the SLP data visually.\n\n### Compression (since version 4.2P)\nSLP version `4.2P` introduced in Update 38862 of Age of Empires 1: Definitive Edition\nis a container format that stores a compressed SLP file (regardless of version).\nThe format uses the [LZ4](https://en.wikipedia.org/wiki/LZ4_(compression_algorithm))\ncompression method. Additionally, there are two uncompressed header entries preceding\nthe compressed data stream. The full structure can be seen below.\n\nLength   | Type    | Description                   | Example\n---------|---------|-------------------------------|--------\n4 bytes  | string  | Version                       | 4.2P\n4 bytes  | uint32  | Uncompressed size of SLP file | 10637, 0x298D\nVariable | LZ4     | Compressed SLP file           | -\n\nDecompressing the LZ4 stream of `4.2P` SLPs must always yield a valid uncompressed\nSLP file.\n\n### Header (up to version 3.0)\n\nLength   | Type   | Description      | Example\n---------|--------|------------------|--------\n4 bytes  | string | Version          | 2.0N\n4 bytes  | int32  | Number of frames | 1, 0x00000001\n24 bytes | string | Comment          | ArtDesk 1.00 SLP Writer\n\n```cpp\nstruct slp_header {\n  char  version[4];\n  int32 num_frames;\n  char  comment[24];\n};\n```\nPython format: `Struct(\"< 4s i 24s\")`\n\n\n### Header (since version 4.0)\n\nLength   | Type    | Description             | Example\n---------|---------|-------------------------|--------\n4 bytes  | string  | Version                 | 4.0X\n2 bytes  | int16   | Number of frames        | 960, 0x000003C0\n2 bytes  | int16   | Type                    | 8, 0x08 (for VFX SLPs)\n2 bytes  | int16   | Number of directions    | 32, 0x0020 (always set to 1)\n2 bytes  | int16   | Frames per direction    | 45, 0x002D (always set to num_frames)\n4 bytes  | int32   | Palette ID              | always 0x00000000 (v4.1 uses it)\n4 bytes  | int32   | Offset for main graphic | 0x00000020\n4 bytes  | int32   | Offset for shadow/alphas| 0x0010C8C0, if 0x00000000 then there is no attached graphic data\n8 bytes  | Padding | Padding                 | - (possibly used for additional offsets)\n\n```cpp\nstruct slp_header_v4 {\n  char     version[4];\n  int16    num_frames;\n  int16    type;\n  int16    num_directions;\n  int16    frames_per_direction;\n  int32    palette_id;\n  int32    offset_main;\n  int32    offset_secondary;\n  pad byte padding[8];\n};\n```\nPython format: `Struct(\"< 4s H H H H i i i 8x\")`\n\nThe Type field designates what type of graphic the SLP is:\n\nValue    | Description\n---------|------------------------------------------------------------------------\n0x00     | \"Normal\"\n0x01     | \"Color Mask\" (Possibly unused, there are no examples of it being used)\n0x02     | \"Shadow Layer\"\n0x04     | \"Outline\" (Possibly unused, outlines were never implemented in AoE1:DE)\n0x08     | \"VFX Color\" (Used for effects, like fire or smoke)\n0x10     | \"VFX Alpha\" (Uses \"0x08\" instead as it's frame type marker when used in attached data)\n0x20     | \"Decay\" (Introduced in 4.1X SLPs)\n\nThe `num_directions` and `frames_per_direction` fields are for reference only as\nthese values are still determined within the .dat file. All official `4.0X` SLPs\ndon't appear to even use these fields correctly, since `num_directions` is always\nset to `0x0001` and `frames_per_direction` is always set to the same value as\n`num_frames`. In `4.1X`, these fields are not used at all.\n\n`palette_id` may also be for reference only and is always unused in `4.0X` SLPs.\nIn `4.1X`, an unknown value seems to have replaced it.\n\n`offset_main` points to the location of where frame data for the main graphic begins.\nThis value is usually set to `0x20` pointing to the location immediately after the header.\n\n`offset_secondary` points to the location of where secondary frame data begins. Usually,\nthis is where shadow data is stored, but is also used to store alpha values to the\nVFX type SLPs that use a palette. The secondary data essentially works like a SLP\nwithin a SLP as they contain their own frame info headers, edge outline tables,\ncommand offset tables, and draw commands. Shadows in `4.0X` SLPs don't use the\nshadow draw commands of previous version SLPs.\n\n### Frame info\nThere are `num_frames` entries of `slp_frame_info` after the header.\nEvery `slp_frame_info` stores meta-information about a single frame (texture)\nwithin the SLP.\n\nFor versions since `4.0`, if `slp_header_v4.offset_secondary` is non-zero,\nthere are an additional `num_frames` entries of `slp_frame_info` for the\nsecondary frame data at this offset.\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n4 bytes  | uint32 | Command table offset       | 2464, 0x000009A0\n4 bytes  | uint32 | Outline table offset       | 64, 0x00000040\n4 bytes  | uint32 | Palette offset (unused)    | 0, 0x00000000\n4 bytes  | uint32 | Properties                 | 16, 0x00000010\n4 bytes  | int32  | Width of image             | 800, 0x00000320\n4 bytes  | int32  | Height of image            | 600, 0x00000258\n4 bytes  | int32  | Centre of sprite (X coord) | 0, 0x00000000\n4 bytes  | int32  | Centre of sprite (Y coord) | 0, 0x00000000\n\n```cpp\nstruct slp_frame_info {\n  uint32 cmd_table_offset;\n  uint32 outline_table_offset;\n  uint32 palette_offset;\n  uint32 properties;\n  int32  width;\n  int32  height;\n  int32  hotspot_x;\n  int32  hotspot_y;\n};\n```\nPython format: `Struct(\"< I I I I i i i i\")`\n\nFollowing the `slp_frame_info` array is the data for each frame.\n`outline_table_offset` points to the position of the `slp_frame_row_edge` array\nand `cmd_table_offset` points to the position of the `slp_command_offset` array.\nBoth of these arrays are of length `height`.\n\nThe `height` and `width` values represent the size of the sprite while the\n`hotspot_X` and `hotspot_Y` values represent the center coordinates of the sprite.\n\nThe `palette_offset` is unused in all games. AoE1 SLPs have written values here,\nbut they are not read. It seems to be an early remnant of AoE1 development that\nwas abandoned.\n\nThe `properties` field is also an unused field (despite having some values such as\n0x10 written there) for AoE1, AoK, and SWGB, but it was later repurposed in AoK HD\nand AoE1 DE to include palette IDs for each frame. Values work as following:\n\n**AoK HD Palette IDs:**\n\nValue      | Description\n-----------|------------------------------------------------------------------------\n0x00       | Default (50500)\n0x07       | 32-bit / Or: (value & 7) == 0x07\n0x10       | Default (50500)\n0x18       | Default (50500)\n0x010000   | clf_pal (Cliff)\n0x020000   | pal_2 (Oak Trees)\n0x030000   | pal_3 (Palm Trees)\n0x040000   | pal_4 (Pine Trees)\n0x050000   | pal_5 (Snow Trees)\n0x060000   | pal_6 (Fire Effects)\n\n**AoE1 DE Palette IDs:**\n\nValue      | Description\n-----------|------------------------------------------------------------------------\n0x00       | Default (50500)\n0x07       | 32-bit / Or: (value & 7) == 0x07\n0x010000   | 01_units\n0x020000   | 02_nature\n0x030000   | 03_buildings_stonetool\n0x040000   | 04_buildings_greek\n0x050000   | 05_buildings_babylon\n0x060000   | 06_buildings_roman\n0x070000   | 07_nature_tree_conifer\n0x080000   | 08_nature_tree_palms\n0x090000   | 09_buildings_asian\n0x0A0000   | 10_buildings_egypt\n0x360000   | effects\n0x08000000 | effects\n\n### Outline Table\nAt `slp_frame_info.outline_table_offset`, an array of `slp_frame_row_edge` (of length `height`)\nstructs begins.\n\nNote that for version `4.1X` the order of the outline table and the [command offset table](#command-offset-table)\nmay be switched. Therefore, you should avoid trying to read them sequentially and always use\nthe respective offsets from the frame info struct.\n\nLength   | Type   | Description   | Example\n---------|--------|---------------|--------\n2 bytes  | uint16 | Left spacing  | 20, 0x0014\n2 bytes  | uint16 | Right spacing | 3, 0x0003\n\n```cpp\nstruct slp_frame_row_edge {\n  uint16 left_space;\n  uint16 right_space;\n};\n```\nPython format: `Struct(\"< H H\")`\n\nFor every row, `left_space` and `right_space` specify the number of transparent\npixels, from each side to the center. For example, in a 50 pixels wide row, with\na `slp_frame_row_edge` of `{ .left_space = 20, .right_space = 3 }`, the leftmost\n20 pixels will be transparent, the rightmost 3 will be transparent and there\nwill be 27 pixels of graphical data provided through some number of commands.\n\nIf the right or left value is `0x8000`, the row is completely transparent.\nNote that there are no command bytes for these rows, so will have to be skipped\n\"manually\".\n\n`width - left_space - right_space` = number of pixels in this line.\n\n\n### Command Offset Table\nAt `slp_frame_info.cmd_table_offset`, an array of uint32 offsets (of length `height`) begins.\n\nNote that for version `4.1X` the order of the [outline table](#outline-table) and the command offset table\nmay be switched. Therefore, you should avoid trying to read them sequentially and always use\nthe respective offsets from the frame info struct.\n\n```cpp\nstruct slp_command_offset {\n  uint32 offset;\n}\n```\nPython format: `Struct(\"< I\")`\n\nEach `offset` defines the offset (beginning) of the first command of a row.\nThe first `offset` in this array is the first drawing command for the image.\n\nThese are not actually necessary to use (but obviously are necessary to read),\nsince the commands can be read sequentially, although they can be used for\nvalidation purposes.\n\nThe actual command data starts after the end of the command offsets, or:\n`slp_frame_info.cmd_table_offset + 4 * slp_frame_info.height`.\n\n\n### Draw Commands\n\nThe image is drawn line by line, a line is finished with the \"End of line\"\ncommand (0x0F). A command is a one-byte number (`cmd_byte`), followed\nby command-specific data with a length (number of pixels) varying\ndepending on the command. The next command immediately follows the\nprevious command's data.\n\nCommands can also store meta information like the number of following pixels\nin the same byte. More complex commands store the pixel count in the following\nbyte.\n\nEach command triggers a drawing method for n = \"Count\" pixels.\n\nAll the commands tell you to draw a palette_index, for n pixels.\n\nCommands can be identified by examining the 4 least significant bits of `cmd_byte`.\n\nFor examples of drawing commands, see the [Examples](#examples) section.\n\n\n### Full drawing command list\n\nValue name     | Description\n---------------|------------\nnext           | The byte following `cmd_byte`\n`>> 2`         | `cmd_byte >> 2` (i.e., the 6 most significant bits used as data value)\n`>> n` or next | `pixel_count = cmd_byte >> n; if pixel_count == 0: pixel_count = next_byte`. i.e., the `8 - n` most significant bits of `cmd_byte` if they are `!= 0`, else the next byte.\n`<< 4 + next`  | `((cmd_byte & 0xf0) << 4) + next_byte`\n\nThe length of a pixel's `px_color_value` depends on the SLP version and whether\nit is a 32-bit SLP.\n\nSLP properties     | Length  | Color value\n-------------------|---------|-----------------------------------------------\nSLP version <= 4.0 | 1 bytes | 1-byte palette index\nSLP version >= 4.1 | 2 bytes | 1-byte display modifier, 1-byte palette index\n32-Bit SLP         | 4 bytes | BGRA color\n\nAn `X` signifies that the bit can have any value. These bits are often used for\nstoring the length (pixel count) of the command.\n\nCommand Name             | Byte value       | Pixel Count             | Description\n-------------------------|------------------|-------------------------|------------\nLesser draw              | `0bXXXXXX00`     | `cmd_byte >> 2`         | An array of length *Count* filled with `px_color_value`.\nLesser skip              | `0bXXXXXX01`     | `cmd_byte >> 2` or next | *Count* transparent pixels should be drawn from the current position.\nGreater draw             | `0bXXXX0010`     | `cmd_byte << 4 + next`  | An array of length *Count* filled with `px_color_value`.\nGreater skip             | `0bXXXX0011`     | `cmd_byte << 4 + next`  | *Count* transparent pixels should be drawn from the current position.\nPlayer color draw        | `0bXXXX0110`     | `cmd_byte >> 4` or next | An array of length \"Count\" filled with `px_color_value`. The real palette index is `player_color_palette_index + player * 16`, where `player` is the player ID you're drawing for (1-8).\nFill                     | `0bXXXX0111`     | `cmd_byte >> 4` or next | One `px_color_value` struct follows. This color should be drawn `pixel_count` times from the current position.\nFill player color        | `0bXXXX1010`     | `cmd_byte >> 4` or next | One player palette index byte follows. This color should be drawn `pixel_count` times. See `player_color_list (0x06)`\nShadow draw              | `0bXXXX1011`     | `cmd_byte >> 4` or next | Draw *Count* shadow pixels (The pixels to draw when a unit is behind another object).\nExtended command         | `0bXXXX1110`     | depends                 | Get the specific extended command by looking at the most significant bits of the command. (See the table below for details)\nEnd of row               | `0x0F`           | 0                       | End of commands for this row. If more commands follow, they are for the next row.\n\n**Command cases:**\n\n**Lesser Draw (Case 0x00, 0x04, 0x08, 0x0C):**\n\nThis case represents a short block of pixels up to a length of 64 and is good for\nsmall chunks of non-repeating pixels. The pixels to copy follows the command byte\nand is `length` bytes long.\n\nIn SLPs with version 4.0 and below, each of these bytes represents an index within\nthe color palette.\n\nSLPs with version 4.1 store an additional *display modifier* byte before the\npalette index. This modifier defines how long the pixel should be shown on screen\nafter the animation has started. The display time is `display_modifier / 2` seconds.\nAfter this time has passed, the pixel is drawn transparently.\n\nFor 32-bit SLPs, each 4 bytes is read for \"length\" bytes long in\nBGRA order and the alpha value here is always 255.\n```\nlength = command >> 2\n(ex: 0x04 = 1, 0x08 = 2, 0x0C = 3, 0x10 = 4, 0x14 = 5, 0x18 = 6, 0x1C = 7, 0x20 = 8, 0x24 = 9)\n```\n\n**Lesser Skip (Case 0x01, 0x05, 0x09, 0x0D):**\n\nThis case represents a short skip up to a length of 64 pixels. This command is\nmainly used for an empty/transparent space within the sprite, as it just moves\nthe pointer to the drawing buffer forward.\n```\nlength = command >> 2\n(ex: 0x05 = 1, 0x09 = 2, 0x0D = 3, 0x11 = 4, 0x15 = 5, 0x19 = 6, 0x1D = 7, 0x21 = 8,  0x25 = 9)\n```\n\n**Greater Draw (Case 0x02):**\n\nThis case represents a long block of pixels greater than a length of 64 and is\ngood for big chunks of non-repeating pixels. The pixels to copy follows the\ncommand byte and is \"length\" bytes long.\n\nIn SLPs with version 4.0 and below, each of these bytes represents an index within\nthe color palette.\n\nSLPs with version 4.1 store an additional *display modifier* byte before the\npalette index. This modifier defines how long the pixel should be shown on screen\nafter the animation has started. The display time is `display_modifier / 2` seconds.\nAfter this time has passed, the pixel is drawn transparently.\n\nFor 32-bit SLPs, each 4 bytes is read for \"length\" bytes long in\nBGRA order and the alpha value here is always 255.\n```\nlength = ((command & 0xf0) << 4) + next_byte\n(ex: 0x12 + 0x00 = 256, 0x12 + 0x0A = 266, 0x22 + 0x00 = 512)\n```\n\n**Greater Skip (Case 0x03):**\n\nThis case represents a long skip of pixels greater than 64 pixels.\nlength = ((command & 0xf0) << 4) + next_byte\n```\n(ex: 0x13 + 0x00 = 256, 0x13 + 0x0A = 266, 0x23 + 0x00 = 512)\n```\n\n**Player Color Draw (Case 0x06):**\n\nThis case represents a block of player color pixels that transform based on the\ncolor selected by the player. The pixels to copy follows the command byte and is\n`length` bytes long. Each of these bytes represents an index within the player color\nrange and is different for each game. For AoE1, index values are between 0x00 (brightest)\nand 0x09 (darkest). In AoK and SWGB, index values are between 0x00 (darkest) and\n0x07 (brightest). And in AoE1 DE, index values are between 0x00 and 0x7F (0-127)\nand use separate player color palettes. In-game, the colors are drawn from the start\nindex in the palette for the player + the index within the player color range. For AoK\nand SWGB, blue player colors begin at index 16 in the palette, red at index 32, green\nat index 48, yellow at index 64, orange at index 80, cyan/teal at index 96, purple at\nindex 112, and grey at index 128.\n```\nlength = command >> 4. If 0, the next byte is read and used as the length.\n(ex: 0x16 = 1, 0x26 = 2, 0xA6 = 10, 0xF6 = 15, 0x06 + 0x10 = 16)\n```\n\n**Fill (Case 0x07):**\n\nThis case represents a block of the same color pixels. This command is useful for\nshortening the amount of bytes needed for a line that consists of the same exact color\nfor more than 2 pixels straight and shrink it down to just 2 or 3 bytes (depending on\nlength). This command does not work in AoE1 and will break terrain SLPs in AoK and\nSWGB due to how elevation and blending are generated.\n\nIn SLPs with version 4.0 and below, each of these bytes represents an index within\nthe color palette.\n\nSLPs with version 4.1 store an additional *display modifier* byte before the\npalette index. This modifier defines how long the pixel should be shown on screen\nafter the animation has started. The display time is `display_modifier / 2` seconds.\nAfter this time has passed, the pixel is drawn transparently.\n\nFor 32-bit SLPs, each 4 bytes is read for \"length\" bytes long in\nBGRA order and the alpha value here is always 255.\n```\nlength = command >> 4. If 0, the next byte is read and used as the length.\n(ex: 0x17 = 1, 0x27 = 2, 0xA7 = 10, 0xF7 = 15, 0x07 + 0x10 = 16)\n```\n\n**Fill Player Color (Case 0x0A):**\n\nThis case represents a block of the same player color pixels. This command is useful\nfor shortening the amount of bytes needed for a line that consists of the same exact\nplayer color for more than 2 pixels straight and shrink it down to just 2 or 3 bytes\n(depending on length). This command does not work in AoE1.\n```\nlength = command >> 4. If 0, the next byte is read and used as the length.\n(ex: 0x1A = 1, 0x2A = 2, 0xAA = 10, 0xFA = 15, 0x0A + 0x10 = 16)\n```\n\n**Shadow Draw (Case 0x0B):**\n\nThis case represents a block of shadow pixels. The pixels underneath these shadow\npixels are identified within a shadow palette and used to draw into the buffer. The\nshadow palette is essentially a darkened variation of the graphic color palette (50500).\nIt's also used to draw things like the red-tinted checkerboard when placing buildings\nat forbidden places.\n```\nlength = command >> 4. If 0, the next byte is read and used as the length.\n(ex: 0x1B = 1, 0x2B = 2, 0xAB = 10, 0xFB = 15, 0x0B + 0x10 = 16)\n```\n\n**Extended Commands (Case 0x0E):**\n\nCommand name        | Byte value | Pixel Count | Description\n--------------------|------------|-------------|------------\nForward Draw        | `0x0E`     | 0           | Draw the following command if sprite is not flipped right to left.\nReverse Draw        | `0x1E`     | 0           | Draw following command if this sprite is x-flipped.\nNormal Transform    | `0x2E`     | 0           | Set color transform table to normal.\nAlternate Transform | `0x3E`     | 0           | Set color transform table to alternate.\nOutline 1           | `0x4E`     | 1           | `palette_index = player_index = player * 16`, if obstructed, draw player color, else transparent. This is the player color outline you see when a unit is behind a building. (special color = 1)\nOutline 1 Fill      | `0x5E`     | next        | `palette_index = player_index = player * 16`, can be >=1 pixel\nOutline 2           | `0x6E`     | 1           | `palette_index = 243`, shield outline. Used in SWGB for when shielded units are hit, a yellow outline is drawn surrounding the unit. (special color = 2)\nOutline 2 Fill      | `0x7E`     | next        | `palette_index = 243`, shield outline, can be >=1 pixel.\nDither              | `0x8E`     | ?           | ?\nPremultiplied Alpha | `0x9E`     | next        | Draws a line of semi-transparent pixels\nOriginal Alpha      | `0xAE`     | ?           | ?\n\nForward Draw, Reverse Draw, Normal Transform, and Alternate Transform are all\npossibly unused, obsolete, or abandoned commands.\n\nOutline 1 is the outline that surrounds a unit and is seen when a unit is obstructed,\nsuch as when it is behind a building or tree in AoK and SWGB. Its color is determined\nby the team color of the player. For AoK, Outline 2 does not have a real purpose. It\nmay have originally been intended as a black outline when obstructed, but in HD it draws\na player color outline the same as Outline 1. In SWGB, Outline 2 is used for the shield\noutline. This outline is the yellow outline that surrounds a unit and is seen when a\nshielded unit takes damage. When a unit is behind an object, only the outline of the\ncorresponding part of the unit is drawn, the rest of the unit is transparent. So, the\nleft outline is one pixel more to the left than the first color. Usually, these outline\npixels should be stored to a second spritesheet, which is rendered on top of the\ntree/building by the fragment shader.\n\nDither is unknown and was possibly an unused or abandoned command. There are no known\nexamples of it being used.\n\nPremultiplied Alpha (32-bit Only) draws pixels that contain alpha values lesser than\n255. The next pixel after the command determines the `length`. After the `length` byte,\neach 4 bytes therafter is read for `length` bytes long in BGRA order to determine the\ncolor. The alpha value is also inverted `(ex: 255 - alpha)`.\n\nOriginal Alpha is unknown and was possibly abandoned. There are no known examples of\nit being used.\n\n\nFor later drawing, a graphics file needs flags for:\n\n* object has outline\n* can show objects behind by outline\n* both of them\n\nNow the palette indices for all the colors of the unit are known, but a palette\nis needed for them to be drawn.\n\n\n### Secondary Frame info (since version 4.0X)\nSLPs with versions of 4.0X or higher store an attached secondary graphic data located\nafter all the main graphic draw commands. This secondary frame data can be used for\nshadow layers, alpha values for 8-bit pixels, and potentially player outlines\n(although unused in AoE1 DE). If the main graphic draw commands end before the end\nof the line, the rest of the line is padded with null bytes so that the attached\ndata can begin at a position ending in 0.\n\nAttached data begins with the `secondary_frame_info` array. This array is of size\n`num_frames` and contains structures with info for each of it's frames. Each\nstructure should be 32 bytes in length.\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n4 bytes  | uint32 | Command table offset       | 43328, 0x0000A940\n4 bytes  | uint32 | Outline table offset       | 43072, 0x0000A840\n4 bytes  | ?      | Unused                     | 0, 0x00000000\n3 bytes  | uint24 | Properties? (Unknown)      | 0, 0x000000 (sometimes 0x010010)\n1 byte   | uint8  | Frame Type                 | 8, 0x08 (VFX Alpha)\n4 bytes  | int32  | Width of image             | 800, 0x00000320\n4 bytes  | int32  | Height of image            | 600, 0x00000258\n4 bytes  | int32  | Centre of sprite (X coord) | 0, 0x00000000\n4 bytes  | int32  | Centre of sprite (Y coord) | 0, 0x00000000\n\n```cpp\nstruct secondary_frame_info {\n  uint32 sec_cmd_table_offset;\n  uint32 sec_outline_table_offset;\n  uint32 sec_null;\n  uint24 sec_properties;\n  uint8  sec_frame_type;\n  int32  sec_width;\n  int32  sec_height;\n  int32  sec_hotspot_x;\n  int32  sec_hotspot_y;\n};\n```\nThe Frame Type field designates what type of graphic the secondary frame is:\n\nValue    | Description\n---------|------------------------------------------------------------------------\n0x01     | \"Color Mask\" (Possibly unused, there are no examples of it being used)\n0x02     | \"Shadow Layer\"\n0x04     | \"Outline\" (Possibly unused, outlines were never implemented in AoE1 DE)\n0x08     | \"VFX Alpha\" (Not 0x10)\n\nJust like with the main graphic data, secondary graphics also contain their own frame\ninfo headers, edge outline tables, command offset tables, and draw commands.\n\n### Secondary Draw Commands (since version 4.0X)\nSecondary draw commands use the same command syntax, but only use the *lesser draw*,\n*lesser skip* and *fill* commands.\n\nCommand Name             | Byte value       | Pixel Count             | Description\n-------------------------|------------------|-------------------------|------------\nLesser draw              | `0bXXXXXX00`     | `cmd_byte >> 2`         | An array of length *Count* filled with 1-byte \"shadow\" values (see below)\nLesser skip              | `0bXXXXXX01`     | `cmd_byte >> 2` or next | *Count* transparent pixels should be drawn from the current position.\nGreater draw             | `0bXXXX0010`     | `cmd_byte << 4 + next`  | An array of length *Count* filled with 1-byte \"shadow\" values follows, 1 value per pixel.\nGreater skip             | `0bXXXX0011`     | `cmd_byte << 4 + next`  | *Count* transparent pixels should be drawn from the current position.\nPlayer color draw        | `0bXXXX0110`     | `cmd_byte >> 4` or next | An array of length \"Count\" filled with `px_color_value`. The real palette index is `player_color_palette_index + player * 16`, where `player` is the player ID you're drawing for (1-8).\nFill                     | `0bXXXX0111`     | `cmd_byte >> 4` or next | One palette index byte follows. This color should be drawn `pixel_count` times from the current position.\n\nFor shadows, the values read have to be converted to an alpha mask by left shifting\nby 2 and and flipping the bits (i.e. subtracting from 255):\n\n```\nshadow_alpha = 255 - (shadow_value << 2)\n```\n\nFor VFX alphas, the values can be read as-is and are not inverted or altered in any way:\n\n## Examples\n\n### Lesser draw and skip\n\n```\nRow example: 0x08 0x55 0xF4 0x19 0x28 0x99 0x35 0xF4 0x6D 0x67 0x6E 0xA5 0x01 0x4D 0x8E 0x0F\n\nfirst cmd_byte = 0x08 = 0b00001000\n```\n\nThe first `cmd_byte` value has the 2 least significant bits set to `0b00`,\nso we know it has to be a *lesser draw*. We can now calculate the pixel\ncount by shifting the command byte to the right 2 times:\n\n```\npixel_count = cmd_byte >> 2 = 0b00000010 = 0x02 = 2\n```\n\nThe pixel count is 2 which tells us that an array of 2 indices will follow\n`cmd_byte`. Therefore, the bytes `0x55` and `0xF4` belong to the drawing\ncommand.\n\nThe next `cmd_byte` is `0x19`:\n\n```\nsecond cmd_byte = 0x19 = 0b00011001\n```\n\nThe 2 least significant bits of this command are `0b01`, so this can be\nidentified as a *lesser skip* command. Here, we also have to calculate\nthe pixel count by shifting 2 times to the right:\n\n```\npixel_count = cmd_byte >> 2 = 0b00000110 = 0x06 = 6\n```\n\nThis tells us that 6 transparent pixels have to be drawn. *Lesser skips* do\nnot reference any other bytes, so the following byte `0x28` is our next\ncommand byte.\n\n```\nthird cmd_byte = 0x28 = 0b00101000\n```\n\nThis again is a *lesser draw* command because the 2 least significant bits\nare set to `0b00`, albeit with a different pixel count.\n\n```\npixel_count = cmd_byte >> 2 = 0b00001010 = 0x0A = 10\n```\n\nThis time, the next 10 bytes are palette indices that belong to the drawing\ncommand (`0x99 0x35 0xF4 0x6D 0x67 0x6E 0xA5 0x01 0x4D 0x8E`).\n\nOur next command byte is `0x0F`. This is the *end of row* command which tells\nus that the row is finished.\n\n\n## Palette Files\n\nThe drawing palette is stored inside `interfac.drs` (until AoE2: HD), while\nAoE1:DE and AoE2:DE store their palettes in separate files with a `.pal` or\n`.palx` extension. It's basically an array of `(r, g, b)` tuples.\n\nThe palette is a (text-based) JASC Paint Shop Pro file, starting with\n`JASC-PAL\\r\\n`. Read this line at the very start of a .BIN file to see if it's a\npalette file. The second line stores version information, should be `0100`. The\nthird line stores the number of entries, as text.\n\nThe rest is that many `r g b\\r\\n` lines. Again, `r`, `g` and `b` are stored as\ntext (range `0-255`).\n\nColors from the palette are referenced in the SLP files with an index.\nThe index refers to a line in the palette, so line 3=>index 0,\nline 4=>index 1 etc.\n\n\n### Palette files in older versions of AoE1 and AoE2 (up until AoE2:HD)\n\nThe file id is `50500+x`, the palettes (color tables) are stored the same way\nas SLPs are. (see [DRS Files](drs-files.md)) Here `x` is the palette index,\nwhich should be 0, experiment with `[1,10]`...\n\n`interfac.drs` contains many of these files, but the ingame art uses id 50500.\n\n\n### Palette files in AoE1:DE\n\nPalettes are stored as plain human-readable palette files with the suffixes\n`.pal` or `.palx`. Palette numbers are stored in a `palettes.conf` file that\ncontains a bunch of lines with assignments in the form of `palette_number,filename`.\n\n\n## SLP types\n\n### SLP files for moving objects\n\nIn Age of Empires 1, Age of Empires 2 and Star Wars: Galactic Battlegrounds,\neach animation has 10 keyframes and 5 directions. The other 3 directions are\ngenerated later by flipping the sprite on the y axis.\n\nOne SLP stores one animation -> 50 frames per SLP.\n\nMilitary units have 5 states that have animations:\n\n1. Idle\n2. Move\n3. Attack\n4. Die\n5. Decay\n\nVillagers have way more than the 5 states (for the different resources),\nboats have 2 separate animations for boat & sails.\n\n\n### SLP files for static objects\n\nContain 1 frame for the building. Some static objects have multiple chunks,\nlike the Town Center, where units can be under one arm and 'in front of' the\nmain building.\n\n\n### SLP files for projectiles\n\nArrows and projectiles have 35 keyframes and 5 directions for their animation\nbecause they need a smoother transition between up- and downwards motion.\n-> 175 frames per SLP.\n\n\n### SLP files for shadows\n\nEvery object in game has a shadow. Up until SLP v3.0, moving units store\ntheir shadow in the same frame as the main graphic, but buildings and\nother objects have their shadows in separate SLPs.\n\nSince version 4.0, shadows are stored in separate frames in the same SLP file.\n"
  },
  {
    "path": "doc/media/smp-files.md",
    "content": "# SMP Files\n\nSMP files are a successor format to SLP files. Like SLP files,\nthey contain animations, shadows and outlines for units. SMPs\nwere introduced in the beta release of Age of Empires 2: Definitive Edition.\nIt was mostly phased out before the final release in favor of a\ncompressed version of the format that is called SMX.\nYou can read more about SMX files [here](smx-files.md).\n\n1. [SMP File Format](#smp-file-format)\n    1. [Header](#header)\n    1. [Frame Header](#smp-frame-header)\n    1. [Layer Header](#smp-layer-header)\n    1. Layer Data\n        1. [Outline Table](#outline-table)\n        1. [Command Offset Table](#command-offset-table)\n        1. [Draw Commands](#draw-commands)\n        1. [SMP Pixel Data](#smp-pixel)\n            1. [Palette Info](#palette-info)\n            1. [Damage Info](#damage-info)\n1. [Examples](#examples)\n    1. [Retrieving a color value](#retrieving-a-color-value-from-a-smp-pixel)\n    1. [Calculating an RGB damage modifier value](#calculating-an-rgb-damage-modifier-value-for-an-smp-pixel)\n\n## SMP File Format\n\nSMPs share a lot of structural similarities to SLPs. However,\nall of the drawing commands have changed, so the formats are not\ncompatible to each other.\n\n\n### Header\n\nThe SMP file starts with a header:\n\nLength   | Type   | Description          | Example\n---------|--------|----------------------|--------\n4 bytes  | string | Signature            | SMP$\n4 bytes  | uint32 | Version              | 0x00000100\n4 bytes  | uint32 | Number of frames     | 721, 0x000002D1\n4 bytes  | uint32 | Number of facets     | 1, 0x0000001 (almost always 0x00000001)\n4 bytes  | uint32 | Frames per facet     | 721, 0x000002D1\n4 bytes  | uint32 | possibly checksum    | 0x8554F6F3\n4 bytes  | uint32 | File size in bytes   | 0x003D5800\n4 bytes  | uint32 | Source format        | 0x0B (SLP) or 0x0C (PSD)\n32 bytes | string | Comment              | Apparently the file path on FE's machines\n\n\n```cpp\nstruct smp_header {\n  char   signature[4];\n  uint32 version;\n  uint32 num_frames;\n  uint32 num_animations;\n  uint32 frames_per_animation;\n  uint32 checksum;\n  uint32 file_size;\n  uint32 source_format;\n  char   comment[32];\n};\n```\nPython format: `Struct(\"< 4s 7I 32s\")`\n\nRemarks:\n\n* *Source format* refers to the format of the file used as input for the\nofficial asset conversion tool `DEAssetTool.exe` when creating the SMP.\n\n### SMP Frame Offsets\n\nSMP frames can have up to 3 layers.\n\n* main graphic layer\n* shadow layer (optional)\n* outline layer (optional)\n\nAfter the file header, there are `num_frames` entries of `smp_frame_offset`.\nEvery `smp_frame_offset` stores the offset to a frame within the SMP\nfile.\n\n```cpp\nstruct smp_frame_offset {\n  uint32 offset;\n}\n```\nPython format: `Struct(\"< I\")`\n\n\n### SMP Frame Header\n\nAt every `smp_frame_offset` there is a 32 bytes long frame header\nthat stores the number of layers for the frame in a 4 byte length\nfield at the end.\n\n```cpp\nstruct smp_frame_header {\n  28 bytes unused; # stores frame header info for source_format = 0x0B\n  uint32   length;\n}\n```\n\nSMPs converted from SLPs (source format 0x0B), the frame header has\nthe same structure as the layer headers (see below), except that\n`outline_table_offset` and `cmd_table_offset` are set to zero.\nFor SMPs created from PSD files, all fields except the length field\nare set to zero.\n\n\n### SMP Layer Header\n\nAfter the frame header, there are `length` entries of `smp_layer_header`.\nThese struct are similar to the SLP Frame Info struct in that they store\nmetadata about the frame.\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n4 bytes  | uint32 | Width of image             | 168, 0x000000A8\n4 bytes  | uint32 | Height of image            | 145, 0x00000091\n4 bytes  | uint32 | Centre of sprite (X coord) | 88, 0x00000058\n4 bytes  | uint32 | Centre of sprite (Y coord) | 99, 0x00000063\n4 bytes  | uint32 | Layer type                 | 0x02, 0x04 or 0x08\n4 bytes  | uint32 | Outline table offset       | 600, 0x00000258\n4 bytes  | uint32 | Command table offset       | 0, 0x00000000\n4 bytes  | uint32 | Flags                      | 0x01, 0x02, 0x80 or 0xA0\n\n```cpp\nstruct smp_layer_header {\n  uint32 width;\n  uint32 height;\n  uint32 hotspot_x;\n  uint32 hotspot_y;\n  uint32 layer_type;\n  uint32 outline_table_offset;\n  uint32 cmd_table_offset;\n  uint32 flags;\n};\n```\nPython format: `Struct(\"< 8I\")`\n\nRemarks:\n\n* Layer types can be `0x02` (main graphic), `0x04` (shadow) or `0x08`\n(outline). In SMPs with source format 0x0B, outlines use a different layer type: `0x10`.\n* Outline and command table offsets **are always relative to the frame offset**.\n\n\n### Outline Table\n\nAt `outline_table_offset` (after the `smp_layer_header` structs), an array of\n`smp_layer_row_edge` (of length `height`) structs begins.\n\nLength   | Type   | Description   | Example\n---------|--------|---------------|-----------\n2 bytes  | uint16 | Left spacing  | 20, 0x0014\n2 bytes  | uint16 | Right spacing | 3, 0x0003\n\n```cpp\nstruct smp_layer_row_edge {\n  uint16 left_space;\n  uint16 right_space;\n};\n```\nPython format: `Struct(\"< H H\")`\n\nFor every row, `left_space` and `right_space` specify the number of transparent\npixels, from each side to the center. For example, in a 50 pixels wide row, with\na `smp_layer_row_edge` of `{ .left_space = 20, .right_space = 3 }`, the leftmost\n20 pixels will be transparent, the rightmost 3 will be transparent and there\nwill be 27 pixels of graphical data provided through some number of commands.\n\nIf the right or left value is `0xFFFF`, the row is completely transparent.\nNote that there are no command bytes for these rows, so it has to be skipped\n\"manually\".\n\n`width - left_space - right_space` = number of pixels in this line.\n\n\n### Command Offset Table\n\nAt `smp_layer_header.cmd_table_offset`, an array of\nuint32 (of length `height`) begins:\n\n```cpp\nstruct smp_command_offset {\n  uint32 offset;\n}\n```\nPython format: `Struct(\"< I\")`\n\nEach `offset` defines the offset (beginning) of the first command of a row.\nThe first `offset` in this array is the first drawing command for the image.\nAll offsets are relative to their respective `smp_frame_offset`.\n\nThese are not actually necessary to use (but obviously are necessary to read),\nsince the commands can be read sequentially, although they can be used for\nvalidation purposes.\n\n\n### Draw Commands\n\nThe image is drawn line by line, a line is finished with the *End of Row*\ncommand (0x03). A command is a one-byte number (`cmd_byte`), followed\nby command-specific data with a length (number of pixels) varying\ndepending on the command. The next command immediately follows the\nprevious command's data.\n\nIn contrast to SLPs, the SMP format uses a much more simplified command set\nthat only contains four commands: *Skip*, *Draw*, *Player Color Draw*\nand *End of Row*. The type of command is stored in the 2 least significant\nbits of the command byte. The 6 most significant bytes define the length of the\ncommand.\n\nEach command triggers a drawing method for n = \"Count\" pixels.\n\nFor examples of drawing commands, see the [Examples](#examples) section.\n\n\n### Full Command List\n\nAn `X` signifies that the bit can have any value. These bits are used for\nstoring the length (pixel count) of the command.\n\nThe commands works slightly different for each layer type.\n\n\n#### Main Graphics type\n\nCommand Name     | Byte value    | Pixel Count              | Description\n-----------------|---------------|--------------------------|------------\nSkip             | `0bXXXXXX00`  | `(cmd_byte >> 2) + 1`    | *Count* transparent pixels should be drawn from the current position.\nDraw             | `0bXXXXXX01`  | `(cmd_byte >> 2) + 1`    | An array of length `pixel_count * 4` bytes filled with 4-byte SMP pixels follows (see [SMP Pixel](#smp-pixel))\nPlayercolor Draw | `0bXXXXXX10`  | `(cmd_byte >> 2) + 1`    | An array of length `pixel_count * 4` bytes filled with 4-byte SMP pixels follows (see [SMP Pixel](#smp-pixel))\nEnd of Row       | `0bXXXXXX11`  | 0                        | End of commands for this row. If more commands follow, they are for the next row.\n\n* When converting the main graphics, the alpha values from the palette are\nignored by the game.\n\n\n#### Shadow type\n\nCommand Name     | Byte value    | Pixel Count              | Description\n-----------------|---------------|--------------------------|------------\nSkip             | `0bXXXXXX00`  | `(cmd_byte >> 2) + 1`    | *Count* transparent pixels should be drawn from the current position.\nDraw             | `0bXXXXXX01`  | `(cmd_byte >> 2) + 1`    | An array of length `pixel_count * 4` bytes filled with 1-byte alpha values follows.\nEnd of Row       | `0bXXXXXX11`  | 0                        | End of commands for this row. If more commands follow, they are for the next row.\n\n* Shadow layers (layer type `0x04`) sometimes do not explicitely draw the last\npixel in a row. If that happens, the openage converter draws the last *Draw* command\nagain.\n\n\n#### Outline type\n\nCommand Name     | Byte value    | Pixel Count              | Description\n-----------------|---------------|--------------------------|------------\nSkip             | `0bXXXXXX00`  | `(cmd_byte >> 2) + 1`    | *Count* transparent pixels should be drawn from the current position.\nDraw             | `0bXXXXXX01`  | `(cmd_byte >> 2) + 1`    | *Count* player color pixels should be drawn from the current position.\nEnd of Row       | `0bXXXXXX11`  | 0                        | End of commands for this row. If more commands follow, they are for the next row.\n\n* SMP files do not specify a color from a palette for outlines. The openage converter\nalways uses the color from index 0 in the player color palette for these *Draw* commands.\n\n\n### SMP Pixel\n\nSMP pixels store a palette index, palette number and section as well\nas a modifier for darkening the pixel if the corresponding unit is damaged.\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n1 byte   | uint8  | Palette index              | 20, 0x0014\n1 byte   | uint8  | Palette number and section | 7, 0x0007\n2 byte   | uint16 | Damage modifier            | 0x29D0 (bits [0,1] and [12,15] are unused)\n\n```cpp\nstruct smp_pixel {\n  uint8  px_index;\n  uint8  px_palette;\n  uint16 px_damage_modifier;\n};\n```\nPython format: `Struct(\"< B B H\")`\n\n#### Palette Info\n\nColors are stored in JASC palettes with 1024 colors. The palettes are assigned an index\nwhich is stored in a `palette.conf` file. To find the encoded color of a SMP pixel,\nthe *palette index* and *palette section* have to be determined from the `px_palette`\nvalue. The palette index is stored in the 6 most significant bits, while the palette\nsection is stored in the 2 least significant bits.\n\n```\npalette_index = px_palette >> 2\n\npalette_section = px_palette & 0b00000011\n```\n\n`px_index` has to be added to `256 * palette_section` to retrieve the actual\nindex for the palette. This index can then be used to get the color value from\nthe palette with the index `palette_index`.\n\n#### Damage Info\n\nThe last two bytes of the pixel (read with little endian byte order) contain a\nmodifier value that is internally used to realize the darkening effect of damaged/buidings\nunits.\nUsing this modifier and the current damage percentage, the game internally calculates\na multiplier that is applied to the RGB values of the pixel. More on how that works\nis described further below.\n\nThe modifier only effectively uses 10 of the uint16's bits (bits with index [2,11]).\nOf these 10 bits, the most significant bit seems to be a usage flag that indicates\nwhether the modifier should be applied to the pixels. The other 9 bits contain the\nmodifier value. You can calculate the effective value by shifting `px_damage_modifier`\nby 4 to the right and then masking with `0x01FF` to get rid of the usage flag.\n\n```\ndamage_modifier = (px_damage_modifier >> 4) & 0x01FF\n```\n\nThis yields a value between 0 and 511. From this value, the game creates an\napproximation of a [sigmoid curve](https://en.wikipedia.org/wiki/Sigmoid_function)\n(\"S\"-shaped curve) that, given the current damage percentage, returns a multiplier\nwhich is then applied to the RGB values of the retrieved palette color. Visually\nthis means that the darkening effect is small at first, then rapidly increases until 50%\ndamage has been reached. Afterwards, the increase will slow down until it levels off\nat a maximum value.\n\nYou can retrieve the multiplier by using this function:\n\n```python\ndef calculate_rgb_multiplier(damage_modifier, current_damage_percent):\n    damage_window_99_to_50 = current_damage_percent * 2\n    damage_window_74_to_25 = damage_window_99_to_50 - 0.5\n    damage_window_49_to_0 =  damage_window_74_to_25 - 0.5\n\n    damage_window_99_to_50 = min(max(damage_window_99_to_50, 0.0), 1.0)\n    damage_window_74_to_25 = min(max(damage_window_74_to_25, 0.0), 1.0)\n    damage_window_49_to_0 = min(max(damage_window_49_to_0, 0.0), 1.0)\n\n    a = math.floor(damage_modifier / 64)\n    temp = damage_modifier - 64 * a + 0.5\n    b = math.floor(temp / 8)\n    c = temp - 8 * b\n\n    a = a * damage_window_99_to_50\n    b = b * damage_window_74_to_25\n    c = c * damage_window_49_to_0\n\n    sigmoid_result = (a + b + c) / 7\n    sigmoid_result = min(max(sigmoid_result, 0.0), 0.65)\n\n    rgb_multiplier = 1 - sigmoid_result\n\n    return rgb_multiplier\n```\n\nRemarks:\n\n* Pixel's RGB values can at maximum get 65% darker than their original color. This\nbehaviour is hardcoded into the game and cannot be changed.\n* Adding `0.5` to `temp` is not strictly necessary, but will solve problems with\nfloating point rounding errors.\n* The three `damage_window_X_Y` values represent how far health of a building\nhas gone down relative to a health interval between `X` and `Y` (both representing\npercentages of health left). For example, if the building is currently at 70% health,\nthen we have progressed by 60% in the health interval that monitors 99% to 50% health.\nTherefore, `damage_window_99_to_50` would be `0.6`.\n\n\n#### Examples\n\n##### Retrieving a color value from a SMP pixel\n\nLet's assume we have a single SMP pixel and want to find the correct palette for it.\n\n```\nSMP pixel example: 0xEF 0x57 0x50 0x20\n```\n\nThe second byte value `0x57` contains the palette information.\n\nWe can retrieve the *palette index* by shifting `0x57` by 2 to the right. Alternatively,\nyou can also divide the value by 4 and floor the result.\n\n```\npalette_index = 0x57 >> 2 = 0b01010111 >> 2 = 0b00010101 = 21\n```\n\nThe *palette index* is 21 which maps to the palette `b_west.pal` in the `palettes.conf`\nof Age of Empires 2: Definitive Edition.\n\nNow we have to determine the section of the palette that is used  for the pixel. To\ndo that, we can either look at the 2 most significant or calculate\n`px_palette mod 4`.\n\n```\npalette_section = 0x57 & 0b00000011 = 0b01010111 & 0b00000011 = 0b00000011 = 3\n```\n\nHere, the *palette section* is 3 which would cover the indexes 512 to 767 in\n`b_west.pal`. From the retrieved values we can now determine the actual index\nin the palette by adjusting `px_index = 0xEF` to the palette section.\n\n```\ncolor_index = px_index + 256 * palette_section\n            = 0xEF + 256 * 0x03\n            = 239  + 256 * 3\n            = 1007\n```\n\nFinally, we can use this index to look up the color value in `b_west.pal`.\nIn our example, the RGBA value is (5,19,4,255).\n\n#### Calculating an RGB damage modifier value for an SMP pixel\n\nWe will calculate the RGB modifier for an SMP pixel of a building that has **70%**\nof its HP left.\n\n```\nSMP pixel example: 0x90 0x56 0x30 0x33\n```\n\nThe two latter bytes are relevant for our calculation. We have to read them as a 16-Bit\nunsigned integer with little endian byte order.\n\n```\npx_damage_modifier = 0x3330\n```\n\nTo get the actual modifier value we have to shift the value by 4 to the right\nand then mask the result with `0x01FF`.\n\n```\ndamage_modifier_value = (px_damage_modifier >> 4) & 0x01FF\n                      = (0x3330 >> 4) & 0x1FF\n                      = 0x0333 & 0x1FF\n                      = 0x233\n                      = 307\n```\n\nThe result should always be a value between 0 and 511.\n\nIn the next step we have to determine the variables `damage_window_99_to_50`,\n`damage_window_74_to_25`, `damage_window_49_to_0`. The values of these variables\ndepends on the current percentage of HP the unit has left or the current\npercentage of damage to its HP, respectively.\n\nSince we know the building is currently at 70% HP (= 30% damage), we can derive\nthe following values for the variables. The current percentage of damage is\nrepresented as a float between 0.0 (= 0% damage) and 1.0 (= 100% damage).\n\n```\ndamage_window_99_to_50 = current_damage_percent * 2\n                       = 0.3 * 2\n                       = 0.6\n```\n\n```\ndamage_window_74_to_25 = damage_window_99_to_50 - 0.5\n                       = 0.6 - 0.5\n                       = 0.1\n```\n\n```\ndamage_window_49_to_0 =  damage_window_74_to_25 - 0.5\n                       = 0.1 - 0.5\n                       = -0.4 (has to be clamped between 0.0 and 1.0)\n                       = 0.0\n```\n\nAs described in the [Damage Info](#damage-info) section, you can also\n*visualize* the result as a progression in three intervals. We have prepared\na figure for this example.\n\n![Damage Windows Visualization](images/damage_window_visualization.svg)\n\nThe three variables are three overlapping progress intervals within the\noverarching HP interval of the building. The red marker labelled with 70%\nshows the current HP percentage. You can see that we have progressed 60%\ninto the top (green) interval, therefore the value for its variable is 0.6.\nSimultaneously, we have progressed 10% into the middle (blue) interval and\nwill assign its variable the value 0.1 as a result. The bottom interval has not\nbeen encountered yet, so its variable gets assigned the value 0.0 for 0%\nprogression.\n\nNow that we know all relevant values, we can input them into the sigmoid\nfunction.\n\nFirst, we calculate the results for the temporary values `a`, `b` and `c`.\n\n```\na = math.floor(damage_modifier / 64)\n  = math.floor(307 / 64)\n  = math.floor(4.796875)\n  = 4\n\ntemp = damage_modifier - 64 * a + 0.5\n     = 307 - 64 * 4 + 0.5\n     = 51.5\n\nb = math.floor(temp / 8)\n  = math.floor(51.5 / 8)\n  = math.floor(6.4375)\n  = 6\n\nc = temp - 8 * b\n  = 51.5 - 8 * 6\n  = 3.5\n```\n\nIn the next step, we multiply the values with the damage window\nvariables.\n\n```\na = a * damage_window_99_to_50 = 4 * 0.6   = 2.4\nb = b * damage_window_74_to_25 = 6 * 0.1   = 0.6\nc = c * damage_window_49_to_0 = 3.5 * 0.0  = 0.0\n```\n\nThen we can divide by 7 and clamp the resulting value\nto the interval [0.0,0.65].\n\n```\nsigmoid_result = (a + b + c) / 7\n               = 3 / 7\n               = 0.429\n```\n\nThe RGB multiplier is retrieved by subtracting the sigmoid result\nfrom 1.\n\n```\nrgb_multiplier = 1 - sigmoid_result\n               = 0.571\n```\n\nFinally, the RGB multiplier can be applied to the RGB color value of\nthe pixel. In our case the unmodified value is (85,80,71). Every\none of these values is multiplied with `rgb_multiplier` and floored.\n\n```\nfloor(85 * 0.571) = 48\nfloor(80 * 0.571) = 45\nfloor(71 * 0.571) = 40\n```\n\nIn the game the pixel will therefore be shown with the color (48,45,40).\n"
  },
  {
    "path": "doc/media/smx-files.md",
    "content": "# SMX files\n\nSMX is a sprite storage format of Age of Empires 2: Definitive Edition.\nThe SMX format is a compressed version of the [SMP format](smp-files.md) that was\nused during the Beta release of the game.\n\nThe current release of Age of Empires 2: Definitive Edition uses a different\ngraphics format called [SLD](sld-files.md).\n\n1. [SMX File Format](#smx-file-format)\n    1. [Header](#header)\n    1. [Frame Header](#smx-frame-header)\n    1. [Layer Header](#smx-layer-header)\n    1. Layer Data\n        1. [Outline Table](#outline-table)\n        1. [SMX Pixel Data](#smx-pixel-data)\n            1. [Main Graphics](#main-graphics-type)\n            1. [Shadow](#shadow-type)\n            1. [Outline](#outline-type)\n1. [Compression Methods](#compression-algorithms)\n    1. [4plus1 method](#4plus1-method)\n    1. [8to5 method](#8to5-method)\n\n## SMX file format\n\nSMX files are basically a trimmed version of SMPs that remove unnecessary\nmetadata and padding from the file. They also do not define explicit\noffsets for each frame and its layers, so the file has to be read sequentially.\nCompression is only used for pixel data.\n\n\n### Header\n\nThe SMX file starts with a header:\n\nLength   | Type   | Description                 | Example\n---------|--------|-----------------------------|--------\n4 bytes  | string | Signature                   | SMPX\n2 bytes  | int16  | Version                     | 2, 0x0002 (for almost all units, some have 0x0001)\n2 bytes  | int16  | Number of frames            | 961, 0x03C1\n4 bytes  | int32  | File size SMX (this file)   | 2706603, 0x000294CAB (size without header)\n4 bytes  | int32  | File size SMP (source file) | 6051456, 0x0005C5680 (size without header)\n16 bytes | string | Comment                     | Always empty\n\n```cpp\nstruct smx_header {\n  char  file_descriptor[4];\n  int16 version;\n  int16 num_frames;\n  int32 file_size_comp;\n  int32 file_size_uncomp;\n  char  comment[16];\n};\n```\nPython format: `Struct(\"< 4s 2H 2I 16s\")`\n\n\n### SMX Frame\n\nThe frame definitions start directly after the file header. Like in the\nSMP format, the frames consist of up to 3 layers:\n\n* main graphic layer\n* shadow layer (optional)\n* outline layer (optional)\n\nWhich of these layers are present in a frame is determined by the value\n`frame_type` from the frame header.\n\n\n#### SMX Frame Header\n\nThe frame header contains 3 values:\n\nLength   | Type   | Description       | Example\n---------|--------|-------------------|--------\n1 bytes  | uint8  | Frame Type        | 3, 0b00000011 (bit field)\n1 bytes  | uint8  | Palette number    | 21, 0x15\n4 bytes  | uint32 | Uncompressed size | 6272, 0x1880\n\n```cpp\nstruct smx_frame_header {\n  uint8  frame_type;\n  uint8  palette_number;\n  uint32 uncomp_size;\n};\n```\nPython format: `Struct(\"< 2B I\")`\n\n`frame_type` is a bit field. This means that every bit set to `1`\nindicates that the frame contains a specific type of layer.\n\nBit index | Description\n----------|------------\n7         | If set to `1`, the frame contains a main graphic layer\n6         | If set to `1`, the frame contains a shadow layer\n5         | If set to `1`, the frame contains an outline layer\n4         | Determines the compression algorithm for the main graphic layer. `0` = 4plus1; `1` = 8to5 (see the [Compression Algorithms](#compression-algorithms) section)\n3         | If set to `1`, other animations' shadows will be cast over the animation.\n0-2       | Unused\n\n**Example**\n\n```\nframe_type = 0x0B = 0b0000 1011\n```\n\nThis frame would contain a *main graphic layer*, a *shadow layer*\nand use the *8to5* compression algorithm for its main graphic layer.\n\n\n#### SMX Layer Header\n\nAfter the frame header the layer definitions start. Every layer begins\nwith a layer header that stores metadata about the layer.\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n2 bytes  | uint16 | Width of image             | 168, 0x00A8\n2 bytes  | uint16 | Height of image            | 145, 0x0091\n2 bytes  |  int16 | Centre of sprite (X coord) | 88, 0x0058\n2 bytes  |  int16 | Centre of sprite (Y coord) | 99, 0x0063\n4 bytes  | uint32 | Length of layer in bytes   | 1848, 0x00000738\n4 bytes  | uint32 | Unknown                    | 950, 0x000003B6\n\n```cpp\nstruct smx_layer_header {\n  uint16 width;\n  uint16 height;\n  uint16 hotspot_x;\n  uint16 hotspot_y;\n  uint32 layer_len;\n  uint32 ??;\n};\n```\nPython format: `Struct(\"< 4H 2I\")`\n\n#### Outline Table\n\nDirectly after the layer header, an array of `smp_layer_row_edge`\n(of length `height`) structs begins. These work exactly like the row edges\nin the [SMP files](smp-files.md#outline-table).\n\n\n#### SMX Pixel data\n\n##### Main Graphics type\n\nIn the SMX format, drawing commands and pixel data for the\nmain graphic image are stored **in two separate arrays**.\n\nImmediately after the SMX layer row edge definition there are two values\nthat define the length of these arrays in bytes:\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|-----------------\n4 bytes  | uint32 | Command array length       | 354, 0x00000162\n4 bytes  | uint32 | Pixel data array length    | 1270, 0x000004F6\n\nThe commands are the same as in the SMP files except that their pixel\ndata has to be read from the pixel data array. Data from the\npixel data array has to be read sequentially. The first *Draw*\ncommand will start reading at index 0 of the pixel data array.\nThe next *Draw* command will continue reading where the previous command\nstopped.\n\nPixels in the pixel data array are compressed in chunks using one of\nthe two [compression algorithms](#compression-algorithms).\nEach chunk can store information for multiple pixels.\n\n\n**Command Reference Sheet**\n\n(The commands are the same as in the SMP format.)\n\nCommand Name     | Byte value    | Pixel Count              | Description\n-----------------|---------------|--------------------------|------------\nSkip             | `0bXXXXXX00`  | `(cmd_byte >> 2) + 1`    | *Count* transparent pixels should be drawn from the current position.\nDraw             | `0bXXXXXX01`  | `(cmd_byte >> 2) + 1`    | Read *Count* entries from the pixel data array as normal pixels.\nPlayercolor Draw | `0bXXXXXX10`  | `(cmd_byte >> 2) + 1`    | Read *Count* entries from the pixel data array as playercolor pixels.\nEnd of Row       | `0bXXXXXX11`  | 0                        | End of commands for this row. If more commands follow, they are for the next row.\n\n\n##### Shadow type\n\nUnlike the main graphics type layers, the shadow type layers\n**only use one array** for drawing commands and pixel data. They can be\nread exactly like [SMP shadow layers](#shadow-type) and are\n**not compressed**.\n\nHowever, they still store the length of the unified array immediately\nafter the SMX layer row edge definitions end:\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n4 bytes  | uint32 | Unified array length       | 354, 0x00000162\n\n\n##### Outline type\n\nOutline types also **only use one array** for drawing commands\nand pixel data. The information is stored exactly as in the\n[SMP outline layers](#outline-type) and **does not use compression**.\n\nLike the SMX shadow type layers, they store the length of the\nunified array immediately after the SMX layer row edge definitions end:\n\nLength   | Type   | Description                | Example\n---------|--------|----------------------------|--------\n4 bytes  | uint32 | Unified array length       | 354, 0x00000162\n\n\n## Compression Algorithms\n\nEvery SMX frame uses one of two available compression methods\nfor the main graphic sprite. Each of the compression methods\nwill store pixel data in chunks of 5 byte which can either contain\n2 (8to5 compression) or 4 compressed pixels (4plus1 compression).\nThe chunks can be read independently.\n\nBoth compression methods only remove metadata of the pixels and\nhave no effect on the RGBA values of the resulting ingame sprites.\n\n\n### 4plus1 method\n\nThe 4plus1 compression method stores the data of 4 pixels in a\n5 byte chunk.\n\nInformation lost (compared to [SMP pixel](smp-files.md#smp-pixel)):\n\n* `palette_index`: instead stored in SMX frame header\n* `px_damage_modifier`: completely removed\n\nLength   | Type   | Description                  | Example\n---------|--------|------------------------------|--------\n1 bytes  | uint32 | Palette index `pixel0`       | 43, 0x2B\n1 bytes  | uint32 | Palette index `pixel1`       | 43, 0x2B\n1 bytes  | uint32 | Palette index `pixel2`       | 43, 0x2B\n1 bytes  | uint32 | Palette index `pixel3`       | 43, 0x2B\n1 bytes  | uint32 | Palette sections             | 255, 0xFF (bit field)\n\nThe last byte stores the palette sections as bit field values.\n\nBit index | Description\n----------|------------\n6-7       | Palette section `pixel0`\n4-5       | Palette section `pixel1`\n2-3       | Palette section `pixel2`\n0-1       | Palette section `pixel3`\n\nThis compression method is used for anything that does\nnot require the damage modifier values (basically everything that is\nnot a building).\n\n\n### 8to5 method\n\nThe 8to5 compression method stores the data of 2 pixels in a\n5 byte chunk.\n\nInformation lost (compared to [SMP pixel](smp-files.md#smp-pixel)):\n\n* `palette_index`: instead stored in SMX frame header\n\nThe chunk is basically a giant bitfield, but the values are not that\nhard to extract.\n\nWe will first show where the values of `pixel0` and `pixel1` are stored\nin the bit field before discussing the extraction method as it might help\nunderstanding of the compression.\n\n**Example**\n\n```\nCompressed:\nHex:        90 1E 32 73 AA\nBin:        10010000 00011110 00110010 01110011 10101010\n\nUncompressed:\npixel0 Hex: 90 02 30 33\npixel0 Bin: 10010000 00000010 00110000 00110011\n\npixel1 Hex: 87 00 90 2A\npixel1 Bin: 10000111 00000000 10010000 00101010\n```\n\nValue                       | Bit indices\n----------------------------|------------\n`pixel0` palette index      | 0-7\n`pixel0` palette section    | 14-15\n`pixel0` damage modifier 1  | 16-19\n`pixel0` damage modifier 2  | 26-31\n`pixel1` palette index      | 22-23, 8-13\n`pixel1` palette section    | 20-21\n`pixel1` damage modifier 1  | 38-39,24-25\n`pixel1` damage modifier 2  | 32-37\n\nWhile this might look confusing at first, the two pixels can be easily\nextracted. If you look closely, you will notice that the bit positions\nin the compressed chunk for the values of `pixel0` are at the exact\nsame positions as in the uncompressed version of the pixel.\n\n```\nCompressed:\nBin:        10010000 00011110 00110010 01110011 10101010\n\nUncompressed:\npixel0 Bin: 10010000 XXXXXX10 0011XXXX XX110011\n\nX = irrelevant to pixel0\n```\n\nThus we can extract `pixel0` by bitmasking the first 4 bytes of the\nchunk. The mask is `0xFF03F03F`.\n\n```\nchunk[0:3] = 90 1E 32 73\n\npixel0 = chunk[0:3] & 0xFF03F03F = 90 02 30 33\n\n  10010000 00011110 00110010 01110011\n& 11111111 00000011 11110000 00111111\n-------------------------------------\n  10010000 00000010 00110000 00110011 = pixel0\n```\n\nFor the second pixel, two extra steps are needed. What we have to do\nis to take the second and third byte of the chunk and [rotate](https://en.wikipedia.org/wiki/Bitwise_operation#Rotate)\nthis value by 2 to the right. We also do the same\nto the fourth and fifth byte of the chunk.\n\n```\nrot_0 = chunk[1:2] ROTR 2\n      = 00011110 00110010 ROTR 2\n      = 10000111 10001100\n\nrot_1 = chunk[3:4] ROTR 2\n      = 01110011 10101010 ROTR 2\n      = 10011100 11101010\n```\n\nIf we now append the results, we get a similar situation for `pixel1`\nas we had for `pixel0`. The bit positions of the appended results align\nwith the bit positions in the uncompressed version of `pixel1`.\n\n```\n[rot_0,rot_1] = 10000111 10001100 10011100 11101010\n\nUncompressed:\nBin pixel1:     10000111 XXXXXX00 1001XXXX XX101010\n\nX = irrelevant to pixel1\n```\n\nAs a result, we can apply the mask on these rotated values again\nto extract `pixel1`.\n\n```\n[rot_0,rot_1] = 10000111 10001100 10011100 11101010 = 87 8C 9C EA\n\npixel1 = [rot_0,rot_1] & 0xFF03F03F = 87 00 90 2A\n\n  10000111 10001100 10011100 11101010\n& 11111111 00000011 11110000 00111111\n-------------------------------------\n  10000111 00000000 10010000 00101010 = pixel1\n```\n"
  },
  {
    "path": "doc/media/sound.md",
    "content": "Sound documentation\n===================\n\nKnown sound files:\n\nYou may add sounds here, but most of them are already referenced in the\ndat file structure.\n\ninterfac.drs\n------------\n\n    50313.wav - One of your units is being converted\n    50315.wav - Your military units are attacked\n    50316.wav - A villager or building of yours is attacked\n    50357.wav - City center bell\n    50366.wav - Wild animal attacks your units (not played for boar)\n"
  },
  {
    "path": "doc/media/stemplet-dat.md",
    "content": "# STemplet.dat File Format\n\nUsed for elevation tiles.\n\n\n```\nstruct templets {\n    struct templet {\n        int32_t header_length;         // number of bytes to read this Templet\n        int32_t tile_size_x;           // seems to be always 97\n        int32_t tile_size_y;           // mainly 49, but 25 for shorter tiles, and 73 for longer tiles.\n        int32_t hotspot_x;             // rendering offset\n        int32_t hotspot_y;             //\n        int32_t tiles_total;           // used for allocating rhombus tile count in bytes\n        int32_t offset_start1;         // colors1 start from header\n        int32_t offset_start2;         // colors2 start from header\n\n        // Now follows an outline table, exactly how they are found in SLP files.\n        struct {\n            uint16_t outline_left;     // number of transparent pixels at the start of this row (0x8000 must be interpreted as 0)\n            uint16_t outline_right;    // number of transparent pixels at the end of this row (item dito)\n        }[tile_size_y];\n\n        // Now follows a command table, exactly how they are found in SLP files.\n        // the next tile_size_y ints, (0 <= i < tile_size_y) indicate the offsets where the commands for row i are found,\n        // within an imaginary SLP file that could be generated from this data.\n        // The offsets would be 0x40 (header) + tile_size_y * 4 (length of outline table) + command offset\n        int32_t slp_command_offsets[tile_size_y];\n\n        // Normally, this is where the commands follow in slp files, but these are not included.\n        // The game is procedurally generating the missing commands.\n        // For example, at offset 0x00419AAB\n        // It doesn't make much sense because I don't think it updates the slp_command_offsets, but I may be wrong.\n    }[17];                                 // always seems to be 17\n};\n```\n"
  },
  {
    "path": "doc/media/terrain.md",
    "content": "terrain documentation\n=====================\n\nThis file describes the research done on terrain rendering.\n\n\nContents of terrain.drs\n-----------------------\n\n27 slp files for terrain data, each slp stores one terrain type with all tiles.\n\nid | filename   | priority | tilecount | description\n---|------------|---------:|-----------|------------\n 0 | 015000.slp |       70 | 100 tiles | brown dirty earth\n 1 | 015001.slp |      102 | 100 tiles | more dry grass\n 2 | 015002.slp |      139 | 100 tiles | light water\n 3 | 015004.slp |      155 |  36 tiles | full grown farm field\n 4 | 015005.slp |      157 |  36 tiles | decaying farm field\n 5 | 015006.slp |      101 | 100 tiles | dirt with little grass\n 6 | 015007.slp |      106 | 100 tiles | dry grass\n 7 | 015008.slp |       90 | 100 tiles | dark grass\n 8 | 015009.slp |      100 | 100 tiles | normal grass\n 9 | 015010.slp |       80 | 100 tiles | desert\n10 | 015011.slp |       92 | 100 tiles | sandy dry grass\n11 | 015014.slp |       60 | 100 tiles | swamp\n12 | 015015.slp |      140 | 100 tiles | deep water\n13 | 015016.slp |      141 | 100 tiles | ocean\n14 | 015017.slp |      110 | 100 tiles | sandy sand\n15 | 015018.slp |      122 | 100 tiles | ancient building fundaments\n16 | 015019.slp |      123 | 100 tiles | dirt ancient building fundaments\n17 | 015021.slp |      150 |   9 tiles | ready and empty farm field\n18 | 015022.slp |      151 |   9 tiles | stage 0 plants on farm field\n19 | 015023.slp |      152 |   9 tiles | stage 1 plants on farm field\n20 | 015024.slp |       40 | 100 tiles | ice\n21 | 015026.slp |      130 | 100 tiles | snow\n22 | 015027.slp |      132 | 100 tiles | dirt with snow\n23 | 015028.slp |      134 | 100 tiles | grass with snow\n24 | 015029.slp |      136 | 100 tiles | grass with snow\n25 | 015030.slp |      162 | 100 tiles | ancient building fundaments with snow\n26 | 015031.slp |      120 | 100 tiles | grass ancient building fundaments\n\n\nOne terrain tile is a parallelogram with equal side lengths, and the same angles in edges on opposing sides. (This is also known as a rhombus.)\n\nTile image height = 97px\nTile image width  = 49px\n\n\nHalf-tile structure: (`#` is any pixel, `@` is the center pixel for the entire tile)\n\n\t\t                                                #\n\t\t                                              #####\n\t\t                                            #########\n\t\t                                          #############\n\t\t                                        #################\n\t\t                                      #####################\n\t\t                                    #########################\n\t\t                                  #############################\n\t\t                                #################################\n\t\t                              #####################################\n\t\t                            #########################################\n\t\t                          #############################################\n\t\t                        #################################################\n\t\t                      #####################################################\n\t\t                    #########################################################\n\t\t                  #############################################################\n\t\t                #################################################################\n\t\t              #####################################################################\n\t\t            #########################################################################\n\t\t          #############################################################################\n\t\t        #################################################################################\n\t\t      #####################################################################################\n\t\t    #########################################################################################\n\t\t  #############################################################################################\n\t\t################################################@################################################ <| 97 pixels, 25th row\n\n\nDrawing smooth uniform terrain is simple:\n\n`#` is a single terrain tile.\nthis illustration shows the coordinate grid we use for the tile drawing.\n\n\t\t         3\n\t\t       2   #\n\t\t     1   #   #\n\t\tx= 0   #   *   #\n\t\t     #   #   #   #\n\t\ty= 0   #   #   #\n\t\t     1   #   #\n\t\t       2   #\n\t\t         3\n\n\nTile `*` is at (2, 1).\nThe formula for selecting the correct terrain .SLP frame index at position (x, y):\n\n```python\ntc = sqrt(terraintilecount) #=10 for regular terrains\nframe_id = (x % tc) + ((y % tc) * tc)\n```\n\n\nTerrain blending modes\n----------------------\n\nblending mode: see the [blendomatic docs](/doc/media/blendomatic.md) for\nmore information about terrain blending.\n\nblending mode id correction:\n\n\nstored mode | terrain type  | corrected mode\n-----------:|---------------|---------------\n0           | dirt, grass   | 1\n1           | farms         | 3\n2           | beach         | 2\n3           | water         | 0\n4           | shallows      | 1\n5           | roads         | 4\n6           | ice           | 5\n7           | snow          | 6\n8           | not assigned  | 4\n\nTerrain elevations\n------------------\n\nDrawing and blending in flat elevation.\n\n### Sloped tiles\ngenerated by texture mapping the flat tiles and lighting them (in HSV color space)\nblend RGB space, maybe blending them in HSV space could reduce the amount of grey.\nlift up the vertices by half of their diamond height.\n\n\n\nview_icm.dat\n------------\n\n\"inverse color map\"\n\n=> get best color_palette entry\nrgb to palette index transformation table with 10 variations of brightness\n\n=> 10 color maps, 32x32x32 lookup table, 1 byte indices.\n=> 10 32 kilobyte structures\n\nbrightness variation: use row 0..9\n\nfetches the closest color index in the color_table for a given rgb (3 x 8 bit) value.\nset each \"axis\" (the 32 bit position) to the upper 5 bits of r, g, b (shift >> 3)\nselect one row => coordinates in the table:\n\n```python\noffset = i * (32^3) + r * 32^2 + g * 32 + b\ni == [0,9] = icm index\npalette_index = byte_at(offset)\n```\n\nsloped terrain lighting brightness levels\n1 ICM:  normal/unlit pixels (flat tiles)\n9 ICMs: lighting\n=> 4 step of darkening and 4 steps of brightening, 1 neutral.\none ICM maps to the 50500 palette\nthe other 9 adjust the pixel brightness.\n\ngenerating the other ICMs:\nlighting in HSV color space, modify S and V separately.\nfor each RGB value in the ICM, convert to HSV, multiply (clamped) S and V by modifiers\nconvert the resulting HSV color back to RGB.\nlook up this color by the standard ICM, store back palette indices for the modified light to new ICM.\n\nlighting on sloped tiles:\nsloped/elevation tiles are dynamically generated by texturemapping flat tile data onto the sloped format\nfiltermaps.dat does that\ntexture mapping operates on the RGB values |> weighted bilinear filtering of the source pixels\n=> to convert the resultant R G B back to a palette_index ==> ICM lookup\nmultiple ICMs: pixel lighting step done in higher quality HSV color,\nno performance impact: lighting data is precalculated.\n\nICMs may not contain system reserved colors at the beginning and end of the palette\n=> some colors are not used/present in the ICMs\n\n\ntileedge.dat / blkedge.dat format\n---------------------------------\n\n`blkedge.dat` contains bitmasks that are used for drawing edges for fog of war and unexplored regions.\n\n```cpp\nstruct tile_edge {\n\tuint32_t elevation_offsets[17];\n\tstruct {\n\t\tuint32_t tile_offsets[94];\n\t\tstruct {\n\t\t\tstruct {\n\t\t\t\tuint8 y;   // 0 <= y <= 72\n\t\t\t\tuint8 x0;  // 0 <= x0 <= 96\n\t\t\t\tuint8 x1;  // 0 <= x1 <= 96 && x0 <= x1\n\t\t\t} spans[n];    // repeat until y == 255\n\t\t} tiles[94];       // each starting at tile_offsets[i]\n\t} elevations[17];      // each starting at elevation_offsets[i]\n}\n```\n\nblkedge.dat: replace 94 with 47, otherwise same format.\n\neach 'spans' == pixels from (x0,y) to (x1,y) (inclusive) to set to 1;\n=> 17*94 tiles, each 97x73 1-bit pixels\n\n\n\npatternmasks.dat format\n-----------------------\n\nNo idea what this data is for, also the format's not verified yet.\nfilesize = 164000 bytes = 2^5 * 5^3 * 41 bytes\n\n164000 bytes / 16 = 10250 bytes\n\n64 * 64 * 40 bytes = 163840 bytes, 160 bytes left then.\n(64 * 64 + 4) * 40 bytes = 164000 => win.\n\n```cpp\nstruct patternmasks {\n\tstruct {\n\t\tuint_32 unknown;  //4 bytes: 0x00100000 => if uint32: 1048576\n\t\tchar data[64*64];\n\t} mask[40];\n}\n```\n"
  },
  {
    "path": "doc/media_convert.md",
    "content": "How to use the original game assets?\n------------------------------------\n\nOpenage currently depends on the original game assets, so you need a valid copy of the *original game files*.\n\nCurrently we *support* conversion for these games:\n\n* **RoR**: Age of Empires 1 (1997) + Rise of Rome\n* **AoC**: Age of Empires 2 (1999) + The Conqueror's\n* **SWGB**: Star Wars: Galactic Battlegrounds + Clone Campaigns\n* **HD**: Age of Empires 2 (2013) (formerly: Age of Empires 2: HD Edition)\n* **DE1**: Age of Empires 1: Definitive Edition\n* **DE2**: Age of Empires 2: Definitve Edition\n\nOn Linux, you may have to use additional measures to download DE2 versions from Steam:\n\n* Use [Proton](https://github.com/ValveSoftware/Proton) which is integrated into [Steam Play](https://store.steampowered.com/linux) to download the DE2 assets (and even play it).\n* Use [Wine](https://www.winehq.org/) to run Steam for Windows.\n* Download the game assets using [SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD).\n  1. Install SteamCMD by following the instructions on [Valve's developer wiki](https://developer.valvesoftware.com/wiki/SteamCMD).\n  2. Download the assets with the following command (replacing `USERNAME` `GAME_APP_ID` and `ASSET_DIR` as appropriate):\n\n`./steamcmd.sh +@sSteamCmdForcePlatformType windows +login USERNAME +force_install_dir ASSET_DIR +app_update GAME_APP_ID validate +quit`\n\nThe game version vs Game_ID table such as below:\n Age of Empires (2013): 221380\n Age of Empires: Definitive Edition: 1017900\n Age of Empires II (Rise of the Rajas): 488060\n Age of Empires II: Definitive Edition: 813780\n\n\n## Conversion\n\nThe original AoC game asset formats are, lets say, \"special\", so they need to be converted in order to be usable from openage.\nThat conversion is performed by the `openage.convert` python module when the game is run for the first time.\n\nThe game will ask for your AoE II installation folder; examples include:\n\n    ~/.wine-age/drive_c/programs/ms-games/aoe2\n    /var/run/media/windisk/Program Files (x86)/Steam/SteamApps/common/Age2HD\n    ~/.steam/steam/SteamApps/common/Age2HD\n    ~/Library/Application Support/Steam/steamapps/common/Age2HD\n\nAlternatively, if your game installation is not found, you can trigger the conversion manually:\n\n```\npython3 -m openage convert --force --source-dir /path/to/game/install\n```\n\nYou will find the converted files in `assets/converted`.\n"
  },
  {
    "path": "doc/nyan/README.md",
    "content": "# nyan\n\nnyan - yet another notation\n\nhttps://github.com/SFTtech/nyan\n\n\n## Idea\n\nnyan is the data storage format for openage.\n\nWe developed it because of the lack of any format suitable for storing\nthe enormous complexity of the Age of Empires data in a sane and readable way.\n\nYes, the data could be written in `yaml`, `json`, `xml`, `csv` or a `docx`\nand contain the exact same information.\nBut you could also use a hexeditor to write the bytes yourself,\nor even [use butterflies](https://xkcd.com/378/) to change the bits on disk.\n\nSo we try to maximize the experience for both developers and modders.\nUsers give a shit about how things work internally,\nand as we're doing this project for fun and not profit and quick results,\nwe developed a data language to suit our needs.\n\nnyan allows us to have a typesafe hierarchical data storage,\nso all content data can be verified to be compatible at load time.\n\nIt is easy to read, write and modify, and most importantly,\nit can actually store unit upgrades, unit abilities, research,\nciv bonuses and age upgrades.\n\n\n## Technical specification\n\n[nyan specification](https://github.com/SFTtech/nyan/blob/master/doc/README.md)\n\n\n## openage specifics\n\nnyan is a general purpose data language,\nthe following describes the openage specific usage of it.\n\n* [data conversion](conversion.md)\n* [openage engine nyan interface](openage-lib.md)\n\n## Examples\n\nFor the beginning, you can look at these resources to find examples of the nyan notation:\n\n* [nyan docs](https://github.com/SFTtech/nyan/blob/master/doc/nyan.md)\n* converted game assets from [our converter](/doc/convert/)\n* [openage modding guide for nyan](https://github.com/SFTtech/openage-modding/tree/master/tutorials/nyan)\n"
  },
  {
    "path": "doc/nyan/aoe2_nyan_tree.uxf",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<diagram program=\"umlet\" version=\"15.1\">\n  <help_text>// Uncomment the following line to change the fontsize and font:\n// fontsize=10\n// fontfamily=SansSerif //possible: SansSerif,Serif,Monospaced\n\n\n//////////////////////////////////////////////////////////////////////////////////////////////\n// Welcome to UMLet!\n//\n// Double-click on elements to add them to the diagram, or to copy them\n// Edit elements by modifying the text in this panel\n// Hold Ctrl to select multiple elements\n// Use Ctrl+mouse to select via lasso\n//\n// Use +/- or Ctrl+mouse wheel to zoom\n// Drag a whole relation at its central square icon\n//\n// Press Ctrl+C to copy the whole diagram to the system clipboard (then just paste it to, eg, Word)\n// Edit the files in the \"palettes\" directory to create your own element palettes\n//\n// Select \"Custom Elements &gt; New...\" to create new element types\n//////////////////////////////////////////////////////////////////////////////////////////////\n\n\n// This text will be stored with each diagram;  use it for notes.</help_text>\n  <zoom_level>9</zoom_level>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1080</x>\n      <y>3249</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Object*\nbg=red</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>378</x>\n      <y>3231</y>\n      <w>270</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>*GameEntity*\n\nbg=pink\n--\ntypes : set(GameEntityType)\nabilities : set(Ability)\nmodifiers : set(Modifier)\nvariants : set(Variant)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4446</x>\n      <y>2826</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*DropSite*\nbg=green\n\n--\naccepts_from : set(ResourceContainer)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>639</x>\n      <y>3267</y>\n      <w>459</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>490.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>774</x>\n      <y>2817</y>\n      <w>270</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>*ResourceSpot*\n\nbg=pink\n--\nresource : Resource\nmax_amount : int\nstarting_amount : int\ndecay_rate : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>828</x>\n      <y>2979</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Resource*\nbg=pink\n\n--\nname : TranslatedString\nmax_storage : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>9</x>\n      <y>2412</y>\n      <w>243</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TranslatedString*\nbg=pink\n\n--\ntranslations : set(LanguageTextPair)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>2556</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TranslatedObject*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>270</x>\n      <y>2412</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TranslatedMarkupFile*\nbg=pink\n\n--\ntranslations : set(LanguageMarkupPair)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>549</x>\n      <y>2412</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TranslatedSound*\nbg=pink\n\n--\ntranslations : set(LanguageSoundPair)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>396</x>\n      <y>2475</y>\n      <w>27</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;90.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>117</x>\n      <y>2475</y>\n      <w>306</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>320.0;40.0;10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>396</x>\n      <y>2475</y>\n      <w>297</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;40.0;310.0;40.0;310.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2943</x>\n      <y>2187</y>\n      <w>288</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*ResearchableTech*\nbg=pink\n\n--\ntech : Tech\ncost : Cost\nresearch_time : float\nresearch_sounds : set(Sound)\ncondition : set(LogicElement)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1206</x>\n      <y>3348</y>\n      <w>288</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>*Terrain*\nbg=pink\n\n--\nname : TranslatedString\ntypes : set(TerrainType)\nterrain_graphic : Terrain\nsound : Sound\nambience : set(TerrainAmbient)\npath_costs : dict(PathType, int)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>3519</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TerrainAmbient*\nbg=pink\n\n--\nobject : GameEntity\nmax_density : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1647</x>\n      <y>2934</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Sound*\nbg=pink\n\n--\nplay_delay : float\nsounds : orderedset(file)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1449</x>\n      <y>1134</y>\n      <w>324</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Modifier*\nbg=pink\n\n--\nproperties : dict(ModifierProperty, ModifierProperty) = {}</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1206</x>\n      <y>3186</y>\n      <w>306</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Patch*\nbg=pink\n\n--\nproperties : dict(PatchProperty, PatchProperty) = {}\npatch : NyanPatch</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1647</x>\n      <y>3168</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Mod*\nbg=pink\n\n--\npriority : int\npatches : orderedset(Patch)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1161</x>\n      <y>3267</y>\n      <w>2295</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;2530.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3438</x>\n      <y>3249</y>\n      <w>306</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Ability*\nbg=pink\n\n--\nproperties : dict(AbilityProperty, AbilityProperty) = {}</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3618</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Animated*\nbg=pink\n\n--\nanimations : set(Animation)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3537</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*CommandSound*\nbg=pink\n\n--\nsounds : set(Sound)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3888</x>\n      <y>3465</y>\n      <w>162</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Move*\nbg=green\n\n--\nspeed : float\nmodes : set(MoveMode)\npath_type : PathType</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3672</x>\n      <y>3861</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Formation*\nbg=green\n\n--\nformations : set(GameEntityFormation)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4383</x>\n      <y>3897</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*RegenerateAttribute*\nbg=green\n\n--\nrate : AttributeRate</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5373</x>\n      <y>1764</y>\n      <w>288</w>\n      <h>279</h>\n    </coordinates>\n    <panel_attributes>*ShootProjectile*\nbg=green\n\n--\nprojectiles : orderedset(GameEntity)\nmin_projectiles : int\nmax_projectiles : int\n// time until the ability can be used again\nreload_time : float\n// time to wait between the start of the ability\n// and the application of the attack\nspawn_delay : float\n// time between the firing of each projectile\n// if projectiles is &gt; 1\nprojectile_delay : float\nrequire_turning : bool\nmanual_aiming_allowed : bool\nspawning_area_offset_x : float\nspawning_area_offset_y : float\nspawning_area_offset_z : float\nspawning_area_width : float\nspawning_area_height : float\nspawning_area_randomness : float\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4356</x>\n      <y>4077</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Resistance*\nbg=green\n\n--\nresistances : set(Resistance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3690</x>\n      <y>3465</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Fly*\nbg=green\n\n--\nheight : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>468</x>\n      <y>2574</y>\n      <w>675</w>\n      <h>693</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>730.0;750.0;730.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>2763</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Cheat*\nbg=pink\n\n--\nactivation_message : text\nchanges : orderedset(Patch)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>981</x>\n      <y>3015</y>\n      <w>162</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>900</x>\n      <y>2916</y>\n      <w>27</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1035</x>\n      <y>2844</y>\n      <w>108</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;100.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1431</x>\n      <y>3474</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>2871</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1332</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;90.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5913</x>\n      <y>3240</y>\n      <w>261</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*PassiveTransformTo*\nbg=green\n\n--\ncondition : set(LogicElement)\ntransform_time : float\ntarget_state : StateChanger\ntransform_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4122</x>\n      <y>2610</y>\n      <w>270</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>*Harvestable*\nbg=green\n\n--\nresources : ResourceSpot\nharvest_progress : set(Progress)\nrestock_progress : set(Progress)\ngatherer_limit : int\nharvestable_by_default : bool</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4464</x>\n      <y>4176</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Live*\nbg=green\n\n--\nattributes : set(AttributeSetting)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>2844</y>\n      <w>225</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Taunt*\nbg=pink\n\n--\nactivation_message : text\ndisplay_message : TranslatedString\nsound : Sound</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1728</x>\n      <y>3231</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>1197</y>\n      <w>27</w>\n      <h>2097</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;2310.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1332</x>\n      <y>3249</y>\n      <w>27</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>2790</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5787</x>\n      <y>3375</y>\n      <w>279</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*Despawn*\nbg=green\n\n--\nactivation_condition : set(LogicElement)\ndespawn_condition : set(LogicElement)\ndespawn_time : float\nstate_change : optional(StateChanger) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>576</x>\n      <y>3951</y>\n      <w>279</w>\n      <h>144</h>\n    </coordinates>\n    <panel_attributes>*PlayerSetup*\nbg=pink\n\n--\nname : TranslatedString\ndescription : TranslatedMarkupFile\nlong_description : TranslatedMarkupFile\nleader_names : set(TranslatedString)\nmodifiers : set(Modifier)\nstarting_resources : set(ResourceAmount)\ngame_setup : orderedset(Patch)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>702</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>702</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;760.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1557</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>378</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;400.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5787</x>\n      <y>3312</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Idle*\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1404</x>\n      <y>1224</y>\n      <w>189</w>\n      <h>144</h>\n    </coordinates>\n    <panel_attributes>Modifier should only be used\nin cases where Patches don't\nwork. For example, if the\nbonus is a percentage value\nor continuously stacks (like\nresources from the Feitoria).\nModifier objects can still be\npatched.\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1503</x>\n      <y>999</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ContinuousResource*\nbg=yellow\n\n--\nrates : set(ResourceRate)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>828</x>\n      <y>3096</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceAmount*\nbg=pink\n\n--\ntype : Resource\namount : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>486</x>\n      <y>3078</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*GoldAmount*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>486</x>\n      <y>3141</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*StoneAmount*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>486</x>\n      <y>3015</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*WoodAmount*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>486</x>\n      <y>2952</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FoodAmount*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>981</x>\n      <y>3123</y>\n      <w>162</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>900</x>\n      <y>3042</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>603</x>\n      <y>3123</y>\n      <w>243</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>250.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>585</x>\n      <y>2970</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0;30.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>585</x>\n      <y>3033</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0;30.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>585</x>\n      <y>3114</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;60.0;30.0;60.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>585</x>\n      <y>3096</y>\n      <w>45</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0;30.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>1062</y>\n      <w>27</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;80.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1476</x>\n      <y>891</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FeitoriaBonus*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1521</x>\n      <y>936</y>\n      <w>108</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>100.0;70.0;100.0;40.0;10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>882</x>\n      <y>918</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeSettingsValue*\nbg=yellow\n\n--\nattribute : Attribute</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>855</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MoveSpeed*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>855</x>\n      <y>630</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*GatheringRate*\nbg=yellow\n\n--\nresource_spot : ResourceSpot</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>441</y>\n      <w>540</w>\n      <h>684</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>580.0;740.0;10.0;740.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>873</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>657</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1638</x>\n      <y>891</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*RelicBonus*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>936</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;40.0;100.0;40.0;100.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3258</x>\n      <y>2331</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Create*\nbg=green\n\n--\ncreatables : set(CreatableGameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3942</x>\n      <y>2412</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Trade*\nbg=green\n\n--\ntrade_routes : set(TradeRoute)\ncontainer : ResourceContainer\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3258</x>\n      <y>2187</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Research*\nbg=green\n\n--\nresearchables : set(ResearchableTech)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>3402</y>\n      <w>135</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*RandomVariant*\n\nbg=pink\n--\nchance_share : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>684</x>\n      <y>3429</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4167</x>\n      <y>1485</y>\n      <w>234</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Storage*\nbg=green\n\n--\ncontainer : EntityContainer\n// container is emptied if condition is fulfilled\nempty_condition : set(LogicElement)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4491</x>\n      <y>1341</y>\n      <w>279</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>*StorageElementDefinition*\nbg=pink\n\n--\nstorage_element : GameEntity\nelements_per_slot : int\nconflicts : set(StorageElementDefinition)\nstate_change : optional(StateChanger) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4275</x>\n      <y>1449</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3987</x>\n      <y>1782</y>\n      <w>270</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*CollectStorage*\nbg=green\n\n--\ncontainer : EntityContainer\nstorage_elements : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3987</x>\n      <y>1692</y>\n      <w>270</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*RemoveStorage*\nbg=green\n\n--\ncontainer : EntityContainer\nstorage_elements : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>657</x>\n      <y>4104</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>game_setup patches the unique\nfeatures into the objects\n(graphics, techs, boni, abilities,\netc.)\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1836</x>\n      <y>4311</y>\n      <w>333</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*Progress*\nbg=pink\n\n--\nproperties : dict(ProgressProperty, ProgressProperty) = {}\ntype : ProgressType\nleft_boundary : float\nright_boundary : float\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1935</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>1062</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;1160.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1953</x>\n      <y>4221</y>\n      <w>171</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>Stores what happens after\na percentage of\nconstruction, damage,\ntransformation, etc. is\nreached\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>2331</x>\n      <y>3807</y>\n      <w>180</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>In AoE2 there is only\none HarvestProgress State\nin the interval [0,100],\nbut AoM had more.\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>5670</x>\n      <y>1854</y>\n      <w>216</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>If min_projectiles is greater than\nthe number of Projectiles in\nprojectiles, the last projectile\nin the orderedset should be used\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1602</x>\n      <y>3393</y>\n      <w>117</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>Abilities and\nStorageElements\ncan use these\nOverride types\nto change any\nother ability's\nanimation.\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>4446</x>\n      <y>3123</y>\n      <w>189</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>Villager Gather abilities can\noverride the graphics of\nIdle,Move,Die and Despawn\nvia CarryProgress objects with\nAnimationOverrides\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>3006</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Animation*\nbg=pink\n\n--\nsprite : file</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1809</x>\n      <y>2961</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>405</x>\n      <y>225</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Flyover*\nbg=yellow\n\n--\nrelative_angle : float\nflyover_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>297</y>\n      <w>333</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>350.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>315</y>\n      <w>288</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>*ElevationDifferenceLow*\nbg=yellow\n\n--\nmin_elevation_difference : optional(float) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>621</x>\n      <y>252</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>621</x>\n      <y>342</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>9</x>\n      <y>99</y>\n      <w>171</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>Pink elements:\n\nBasic nyan API objects\n\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>9</x>\n      <y>171</y>\n      <w>171</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>Green elements:\n\nAbilities (handled by engine\nimplementation)\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>9</x>\n      <y>243</y>\n      <w>171</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>Yellow elements:\n\nModifiers (handled by engine\nimplementation)\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>9</x>\n      <y>405</y>\n      <w>171</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>White elements:\n\nAoE2 specific objects</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3402</x>\n      <y>2466</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*RallyPoint*\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5526</x>\n      <y>3483</y>\n      <w>207</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Selectable*\nbg=green\n\n--\nselection_box : SelectionBox</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3402</x>\n      <y>4050</y>\n      <w>297</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*ActiveTransformTo*\nbg=green\n\n--\ntarget_state : StateChanger\ntransform_time : float\ntransform_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>504</x>\n      <y>3402</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Variant*\nbg=pink\n\n--\nchanges : orderedset(Patch)\npriority : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>468</x>\n      <y>3429</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1422</x>\n      <y>3843</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TechResearched*\nbg=pink\n\n--\ntech : Tech</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1404</x>\n      <y>3924</y>\n      <w>207</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*GameEntityProgress*\nbg=pink\n\n--\ngame_entity : GameEntity\nstatus : ProgressStatus</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1620</x>\n      <y>3807</y>\n      <w>27</w>\n      <h>810</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;880.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4023</x>\n      <y>1593</y>\n      <w>234</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*TransferStorage*\nbg=green\n\n--\nstorage_element : GameEntity\nsource_container : EntityContainer\ntarget_container : EntityContainer</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1557</x>\n      <y>3321</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4311</x>\n      <y>1782</y>\n      <w>288</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*SendBackToTask*\nbg=green\n\n--\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3672</x>\n      <y>3942</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*GameEntityStance*\nbg=green\n\n--\nstances: set(GameEntityStance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3969</x>\n      <y>3951</y>\n      <w>306</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*GameEntityStance*\nbg=pink\n\n--\nsearch_range : float\nability_preference : orderedset(Ability)\ntype_preference : orderedset(GameEntityType)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3888</x>\n      <y>3969</y>\n      <w>99</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>3465</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Patrol*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>3528</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Follow*\nbg=pink\n\n--\nrange : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3429</y>\n      <w>27</w>\n      <h>369</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;390.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3555</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3942</x>\n      <y>2331</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TradePost*\nbg=green\n\n--\ntrade_routes : set(TradeRoute)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3933</x>\n      <y>4050</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Aggressive*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3933</x>\n      <y>4113</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Defensive*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3915</x>\n      <y>4176</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*StandGround*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3933</x>\n      <y>4239</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Passive*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4014</x>\n      <y>4131</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4041</x>\n      <y>4023</y>\n      <w>27</w>\n      <h>261</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;270.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4014</x>\n      <y>4194</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4014</x>\n      <y>4257</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>2745</y>\n      <w>243</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>*Restock*\nbg=green\n\n--\nauto_restock : bool\ntarget : ResourceSpot\nrestock_time : float\nmanual_cost : Cost\nauto_cost : Cost\namount : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>3483</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*PerspectiveVariant*\n\nbg=pink\n--\nangle : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>486</x>\n      <y>3465</y>\n      <w>126</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>120.0;10.0;120.0;60.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5418</x>\n      <y>4293</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Foundation*\nbg=green\n\n--\nfoundation_terrain : Terrain</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>3897</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Pathable*\nbg=green\n\n--\nhitbox : Hitbox\npath_costs : dict(PathType, int)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5787</x>\n      <y>3186</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>// Returns unit to Idle state\n\n*Stop*\nbg=green</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5787</x>\n      <y>3546</y>\n      <w>288</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Herdable*\nbg=green\n\n--\nadjacent_discover_range : float\nmode : HerdableMode</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4446</x>\n      <y>2907</y>\n      <w>234</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*DropResources*\nbg=green\n\n--\ncontainers : set(ResourceContainer)\nsearch_range : float\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4284</x>\n      <y>3798</y>\n      <w>261</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Named*\nbg=green\n\n--\nname : TranslatedString\ndescription : TranslatedMarkupFile\nlong_description : TranslatedMarkupFile</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>3987</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Collision*\nbg=green\n\n--\nhitbox : Hitbox</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5139</x>\n      <y>4293</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeChangeTracker*\nbg=green\n\n--\nattribute : Attribute\nchange_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5373</x>\n      <y>1611</y>\n      <w>288</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>*Projectile*\nbg=green\n\n--\n// arc in degrees\narc : int\naccuracy : set(Accuracy)\n// Ballistics in AoE2\ntarget_mode : TargetMode\n// ignore these game entities for hit detection\nignored_types : set(GameEntityType)\nunignore_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1458</x>\n      <y>3744</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Literal*\nbg=pink\n\n--\nscope : LiteralScope</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>639</x>\n      <y>2979</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceContingent*\nbg=pink\n\n--\nmin_amount : int\nmax_amount : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>792</x>\n      <y>3006</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4446</x>\n      <y>2664</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ProvideContingent*\nbg=green\n\n--\namount : set(ResourceAmount)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4446</x>\n      <y>2745</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*UseContingent*\nbg=green\n\n--\namount : set(ResourceAmount)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2943</x>\n      <y>2313</y>\n      <w>288</w>\n      <h>144</h>\n    </coordinates>\n    <panel_attributes>*CreatableGameEntity*\nbg=pink\n\n--\ngame_entity : GameEntity\nvariants : set(Variant)\ncost : Cost\ncreation_time : float\ncreation_sounds : set(Sound)\ncondition : set(LogicElement)\n// determines how a game entity is placed after creation\nplacement_modes : set(PlacementMode)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2358</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>3168</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Terrain*\nbg=pink\n\n--\nsprite : file</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>2790</y>\n      <w>27</w>\n      <h>504</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;540.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>3033</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2223</x>\n      <y>4203</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Animated*\nbg=pink\n\n--\noverrides : set(AnimationOverride)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2223</x>\n      <y>4365</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TerrainOverlay*\nbg=pink\n\n--\n// overlays the terrain below game entity\nterrain_overlay : Terrain\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4230</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4392</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4473</x>\n      <y>4302</y>\n      <w>198</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*AttributeSetting*\nbg=pink\n\n--\nattribute : Attribute\nmin_value : int\nmax_value : int\nstarting_value : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4473</x>\n      <y>4554</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ProtectingAttribute*\nbg=pink\n\n--\nprotects : Attribute</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4239</y>\n      <w>27</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4518</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4509</x>\n      <y>4662</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Shield (SWGB)*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4617</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4725</x>\n      <y>4455</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Faith*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4671</x>\n      <y>4464</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;60.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4383</x>\n      <y>3978</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LineOfSight*\nbg=green\n\n--\nrange : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>3798</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Cloak (SWGB)*\nbg=green\n\n--\ninterrupted_by : set(Ability)\ninterrupt_cooldown : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5706</x>\n      <y>1575</y>\n      <w>243</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Accuracy*\nbg=pink\n\n--\naccuracy : float\n// radius around target where a missed shot lands\naccuracy_dispersion : float\ndispersion_dropoff : DropOffType\ntarget_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5652</x>\n      <y>1611</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4257</x>\n      <y>4410</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeAmount*\nbg=pink\n\n--\ntype : Attribute\namount : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4392</x>\n      <y>4455</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>684</x>\n      <y>3204</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*GameEntityType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>639</x>\n      <y>3231</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4725</x>\n      <y>4518</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Health*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4698</x>\n      <y>4464</y>\n      <w>45</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;90.0;30.0;90.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>738</x>\n      <y>3249</y>\n      <w>27</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;30.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>1917</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;2110.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>5157</y>\n      <w>378</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;400.0;10.0;400.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1071</x>\n      <y>5193</y>\n      <w>351</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Resistance*\nbg=pink\n\n--\nproperties : dict(ResistanceProperty, ResistanceProperty) = {}</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>369</x>\n      <y>5193</y>\n      <w>297</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Effect*\nbg=pink\n\n--\nproperties : dict(EffectProperty, EffectProperty) = {}</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>513</x>\n      <y>5157</y>\n      <w>387</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>410.0;10.0;10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1098</x>\n      <y>5121</y>\n      <w>126</w>\n      <h>36</h>\n    </coordinates>\n    <panel_attributes>Resistors' side\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>522</x>\n      <y>5121</y>\n      <w>126</w>\n      <h>36</h>\n    </coordinates>\n    <panel_attributes>Effectors' side\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>612</x>\n      <y>5400</y>\n      <w>315</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*FlatAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType\nmin_change_value : optional(AttributeAmount) = None\nmax_change_value : optional(AttributeAmount) = None\nchange_value : AttributeAmount\nignore_protection : set(ProtectingAttribute)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1341</x>\n      <y>5400</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*FlatAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType\nblock_value : AttributeAmount</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>945</x>\n      <y>4302</y>\n      <w>171</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AttributeChangeType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>612</x>\n      <y>5760</y>\n      <w>270</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Convert*\nbg=orange\n\n--\ntype : ConvertType\nmin_chance_success : optional(float) = None\nmax_chance_success : optional(float) = None\nchance_success : float\ncost_fail : optional(Cost) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1341</x>\n      <y>5760</y>\n      <w>261</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Convert*\nbg=orange\n\n--\ntype : ConvertType\nchance_resist : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>675</x>\n      <y>5886</y>\n      <w>216</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*AoE2Convert*\nbg=orange\n\n--\nskip_guaranteed_rounds : int\nskip_protected_rounds : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>5886</y>\n      <w>261</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*AoE2Convert*\nbg=orange\n\n--\n// How man applications of the effect\n// will always be resisted:\nguaranteed_resist_rounds : int\n// After these rounds resistance drops to 0\nprotected_rounds : int\n// How long it takes for the protection\n// to recharge\nprotection_round_recharge_time : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>297</x>\n      <y>5310</y>\n      <w>162</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ContinuousEffect*\nbg=orange\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>162</x>\n      <y>5400</y>\n      <w>297</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*FlatAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType\nmin_change_rate : optional(AttributeRate) = None\nmax_change_rate : optional(AttributeRate) = None\nchange_rate : AttributeRate\nignore_protection : set(ProtectingAttribute)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>612</x>\n      <y>5310</y>\n      <w>153</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*DiscreteEffect*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1341</x>\n      <y>5310</y>\n      <w>162</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*DiscreteResistance*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>828</x>\n      <y>3177</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceRate*\nbg=pink\n\n--\ntype : Resource\nrate : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>981</x>\n      <y>3204</y>\n      <w>162</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>160.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4257</x>\n      <y>4491</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeRate*\nbg=pink\n\n--\ntype : Attribute\nrate : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>513</x>\n      <y>5256</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>369</x>\n      <y>5283</y>\n      <w>171</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>170.0;10.0;10.0;10.0;10.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>513</x>\n      <y>5283</y>\n      <w>189</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;190.0;10.0;190.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1062</x>\n      <y>5283</y>\n      <w>180</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>180.0;10.0;10.0;10.0;10.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1224</x>\n      <y>5256</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1215</x>\n      <y>5283</y>\n      <w>216</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;220.0;10.0;220.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>990</x>\n      <y>5310</y>\n      <w>162</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ContinuousResistance*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>4320</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>963</x>\n      <y>5400</y>\n      <w>189</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*FlatAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType\nblock_rate : AttributeRate</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>576</x>\n      <y>5328</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>5328</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>576</x>\n      <y>5697</y>\n      <w>54</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;110.0;40.0;110.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>5868</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0;30.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1359</x>\n      <y>5868</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0;30.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1305</x>\n      <y>5427</y>\n      <w>54</w>\n      <h>297</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;310.0;40.0;310.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1305</x>\n      <y>5328</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0;10.0;120.0;40.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>5328</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0;40.0;120.0;10.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5607</x>\n      <y>2475</y>\n      <w>288</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>*ApplyDiscreteEffect*\nbg=green\n\n--\nbatches : set(EffectBatch)\n// time until the effects can be applied again\nreload_time : float\n// time to wait between the start of the ability\n// and the application of the effects\napplication_delay : float\n// the types the effects can be applied on\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5607</x>\n      <y>2313</y>\n      <w>288</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>*ApplyContinuousEffect*\nbg=green\n\n--\neffects : set(ContinuousEffect)\n// time to wait between the start of the ability\n// and the application of the effects\napplication_delay : float\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5634</x>\n      <y>2214</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MonkHeal (Ranged)*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>2340</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Repair*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>576</x>\n      <y>5427</y>\n      <w>54</w>\n      <h>297</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;310.0;40.0;310.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>2880</y>\n      <w>243</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Gather*\nbg=green\n\n--\nauto_resume : bool\nresume_search_range : float\ntargets : set(ResourceSpot)\ngather_rate : ResourceRate\ncontainer : ResourceContainer</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>612</x>\n      <y>5670</y>\n      <w>234</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*MakeHarvestable*\nbg=orange\n\n--\nresource_spot : ResourceSpot</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1341</x>\n      <y>5670</y>\n      <w>306</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*MakeHarvestable*\nbg=orange\n\n--\nresource_spot : ResourceSpot\nresist_condition : set(LogicElement)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5688</x>\n      <y>2259</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5886</x>\n      <y>2358</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>2484</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*SelfDestruct*\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5886</x>\n      <y>2502</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5634</x>\n      <y>2646</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Convert (Ranged)*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5688</x>\n      <y>2592</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>945</x>\n      <y>4365</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ConvertType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>4383</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1305</x>\n      <y>5697</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;120.0;40.0;120.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>2547</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Hunt*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4311</x>\n      <y>1593</y>\n      <w>288</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*EnterContainer*\nbg=green\n\n--\nallowed_containers : set(EntityContainer)\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4311</x>\n      <y>1692</y>\n      <w>234</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*ExitContainer*\nbg=green\n\n--\nallowed_containers : set(EntityContainer)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>2943</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*DiplomaticStance*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>2961</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3699</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Diplomatic*\nbg=pink\n\n--\nstances : set(DiplomaticStance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>954</x>\n      <y>36</y>\n      <w>162</w>\n      <h>432</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>160.0;460.0;10.0;460.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2241</x>\n      <y>2997</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Self*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2043</x>\n      <y>2961</y>\n      <w>261</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;270.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>189</y>\n      <w>288</w>\n      <h>279</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>300.0;10.0;170.0;10.0;170.0;290.0;10.0;290.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>171</y>\n      <w>288</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ElevationDifferenceLow*\nbg=yellow\n\n--\nmin_elevation_difference : optional(float) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>216</x>\n      <y>5670</y>\n      <w>243</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Lure*\nbg=orange\n\n--\ntype : LureType\n// where the game entity is lured to\ndestination : set(GameEntity)\n// at what range the luring is stopped\nmin_distance_to_destination : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>5427</y>\n      <w>54</w>\n      <h>288</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;300.0;10.0;300.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5670</y>\n      <w>225</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Lure*\nbg=orange\n\n--\ntype : LureType\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>5427</y>\n      <w>54</w>\n      <h>288</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;300.0;10.0;300.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>54</x>\n      <y>2295</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LanguageTextPair*\nbg=pink\n\n--\nlanguage : Language\nstring : text</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>315</x>\n      <y>2295</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LanguageMarkupPair*\nbg=pink\n\n--\nlanguage : Language\nmarkup_file : file</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>603</x>\n      <y>2295</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LanguageSoundPair*\nbg=pink\n\n--\nlanguage : Language\nsound : Sound</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>117</x>\n      <y>2358</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>396</x>\n      <y>2358</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>666</x>\n      <y>2358</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>522</x>\n      <y>2619</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Language*\nbg=pink\n\n--\nietf_string : text</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>585</x>\n      <y>2574</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3690</x>\n      <y>3546</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Turn*\nbg=green\n\n--\nturn_speed : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>4077</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Visibility*\nbg=green\n\n--\nvisible_in_fog : bool</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4122</x>\n      <y>1341</y>\n      <w>315</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*EntityContainer*\nbg=pink\n\n--\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)\nstorage_element_defs : set(StorageElementDefinition)\nslots : int\ncarry_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4428</x>\n      <y>1386</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1116</x>\n      <y>4761</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Cost*\nbg=pink\n\n--\ncost : Cost</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1071</x>\n      <y>4869</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1602</x>\n      <y>3294</y>\n      <w>216</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*AnimationOverride*\nbg=pink\n\n--\nability : AnimatedAbility\nanimations : set(Animation)\npriority : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>3069</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Diplomatic*\nbg=pink\n\n--\nstances : set(DiplomaticStance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>675</x>\n      <y>4239</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*DropoffType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>612</x>\n      <y>4257</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>612</x>\n      <y>4257</y>\n      <w>54</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;80.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>612</x>\n      <y>4194</y>\n      <w>54</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;80.0;40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>4176</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*NoDropoff*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>4239</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Linear*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1161</x>\n      <y>4302</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>// This type is _only_ evaluated if all FlatAttributeChange Effects with other types are outside of the \n// range defined in the FlatAttributeChange Effect with type Fallback.\n//\n// can be used to model the minimum damagge of 1 in AoE\n\n*Fallback*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1107</x>\n      <y>4320</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;60.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2205</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>945</x>\n      <y>3699</y>\n      <w>243</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Tech*\nbg=pink\n\n--\ntypes : set(TechType)\nname : TranslatedString\ndescription : TranslatedMarkupFile\nlong_description : TranslatedMarkupFile\nupdates : orderedset(Patch)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>3294</y>\n      <w>27</w>\n      <h>423</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;450.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4266</x>\n      <y>2340</y>\n      <w>216</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>// Determines traded resource and resource amount\n*TradeRoute*\nbg=pink\n\n--\ntrade_resource : Resource\nstart_trade_post : GameEntity\nend_trade_post : GameEntity</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4140</x>\n      <y>2421</y>\n      <w>189</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>190.0;10.0;190.0;40.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4545</x>\n      <y>2340</y>\n      <w>153</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AoE2TradeRoute*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>657</x>\n      <y>5535</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>657</x>\n      <y>5598</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>630</x>\n      <y>5508</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>30.0;60.0;10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>630</x>\n      <y>5553</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;80.0;10.0;80.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1395</x>\n      <y>5535</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1395</x>\n      <y>5598</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1359</x>\n      <y>5472</y>\n      <w>27</w>\n      <h>171</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;170.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1359</x>\n      <y>5553</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>216</x>\n      <y>5535</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>216</x>\n      <y>5598</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>405</x>\n      <y>5553</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>405</x>\n      <y>5508</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;60.0;30.0;60.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5535</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5598</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>5553</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>5472</y>\n      <w>45</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;100.0;30.0;100.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1350</x>\n      <y>2853</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Formation*\nbg=pink\n\n--\nsubformations : set(Subformation)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1566</x>\n      <y>2880</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1395</x>\n      <y>2952</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Subformation*\nbg=pink\n\n--\nordering_priority : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3969</x>\n      <y>3861</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*GameEntityFormation*\nbg=pink\n\n--\nformation : Formation\nsubformation : Subformation</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3924</x>\n      <y>3888</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1548</x>\n      <y>2979</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1467</x>\n      <y>2916</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1269</x>\n      <y>1503</y>\n      <w>189</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Scoped*\nbg=pink\n\n--\nstances : set(DiplomaticStance)\n// the scope for which the Bonus applies, e.g.\n// only for the game entity (default), for every\n// matching game entity owned by the player or \n// every matching game entity in the game\nscope : ModifierScope</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>612</x>\n      <y>5985</y>\n      <w>225</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*SendToContainer*\nbg=orange\n\n--\ntype : SendToContainerType\nstorages : set(EntityContainer)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>576</x>\n      <y>5787</y>\n      <w>54</w>\n      <h>252</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;260.0;40.0;260.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1341</x>\n      <y>5985</y>\n      <w>234</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*SendToContainer*\nbg=orange\n\n--\ntype : SendToContainerType\nsearch_range : float\nignore_containers : set(EntityContainer)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1305</x>\n      <y>5796</y>\n      <w>54</w>\n      <h>243</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;250.0;40.0;250.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3501</x>\n      <y>2484</y>\n      <w>54</w>\n      <h>783</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>40.0;850.0;40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3501</x>\n      <y>2358</y>\n      <w>54</w>\n      <h>153</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;150.0;40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3501</x>\n      <y>2214</y>\n      <w>54</w>\n      <h>171</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;170.0;40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3735</x>\n      <y>3267</y>\n      <w>2196</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;2420.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3339</y>\n      <w>27</w>\n      <h>576</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;620.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3564</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3645</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3528</x>\n      <y>3321</y>\n      <w>27</w>\n      <h>747</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;810.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3627</x>\n      <y>3888</y>\n      <w>63</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;100.0;50.0;100.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3528</x>\n      <y>3888</y>\n      <w>162</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;160.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3825</x>\n      <y>3492</y>\n      <w>54</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;100.0;40.0;100.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3825</x>\n      <y>3267</y>\n      <w>54</w>\n      <h>252</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;260.0;40.0;260.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3852</x>\n      <y>3492</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4194</x>\n      <y>2358</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4473</x>\n      <y>2358</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4509</x>\n      <y>2358</y>\n      <w>27</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;100.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3528</x>\n      <y>2259</y>\n      <w>432</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>460.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3906</x>\n      <y>2259</y>\n      <w>54</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;120.0;10.0;120.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3906</x>\n      <y>2358</y>\n      <w>54</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;100.0;10.0;100.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4392</x>\n      <y>4500</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>927</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;1010.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4536</x>\n      <y>3825</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4536</x>\n      <y>3924</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4014</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4536</x>\n      <y>4104</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4104</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4536</x>\n      <y>4005</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>3825</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5382</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>1080</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;1180.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5355</x>\n      <y>4320</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5382</x>\n      <y>4320</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>3924</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5751</x>\n      <y>3204</y>\n      <w>54</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5751</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>333</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;350.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5751</x>\n      <y>3573</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5571</x>\n      <y>2412</y>\n      <w>189</w>\n      <h>882</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>190.0;10.0;190.0;40.0;10.0;40.0;10.0;960.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5886</x>\n      <y>2565</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5733</x>\n      <y>2439</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>2664</y>\n      <w>27</w>\n      <h>630</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;680.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>2691</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4383</x>\n      <y>2664</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4383</x>\n      <y>2772</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>2772</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>2853</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>2934</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4383</x>\n      <y>2907</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4275</x>\n      <y>1557</y>\n      <w>900</w>\n      <h>1737</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>980.0;1910.0;980.0;360.0;10.0;360.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5148</x>\n      <y>1872</y>\n      <w>243</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;250.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5337</x>\n      <y>1692</y>\n      <w>54</w>\n      <h>207</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0;10.0;210.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4275</x>\n      <y>1620</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4275</x>\n      <y>1719</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4275</x>\n      <y>1800</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4248</x>\n      <y>1800</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4248</x>\n      <y>1719</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4248</x>\n      <y>1620</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5706</x>\n      <y>1710</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TargetMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5652</x>\n      <y>1728</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5895</x>\n      <y>1710</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*CurrentPosition*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5823</x>\n      <y>1728</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5895</x>\n      <y>1773</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ExpectedPosition*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5859</x>\n      <y>1728</y>\n      <w>54</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;80.0;40.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1089</x>\n      <y>1512</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ModifierScope*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1206</x>\n      <y>1530</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>3690</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AttackMove*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3708</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4068</x>\n      <y>3384</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MoveMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3960</x>\n      <y>3402</y>\n      <w>126</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>120.0;10.0;10.0;10.0;10.0;70.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>270</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>252</y>\n      <w>117</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Stray*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2160</x>\n      <y>702</y>\n      <w>270</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AbsoluteProjectileAmount*\nbg=yellow\n\n--\namount : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>405</y>\n      <w>531</w>\n      <h>720</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;780.0;570.0;780.0;570.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>837</x>\n      <y>711</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*GatheringEfficiency*\nbg=yellow\n\n--\nresource_spot : ResourceSpot</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>738</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>792</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ReloadTime*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>810</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;60.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>918</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*CreationTime*\nbg=yellow\n\n--\ncreatables : set(CreatableGameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>945</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>837</y>\n      <w>270</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*CreationResourceCost*\nbg=yellow\n\n--\nresources : set(Resource)\ncreatables : set(CreatableGameEntity)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>873</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>594</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResearchResourceCost*\nbg=yellow\n\n--\nresources : set(Resource)\nresearchables : set(ResearchableTech)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>621</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>729</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2160</x>\n      <y>621</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>// Immediately unlocks a Tech as soon as the requirements are fulfilled\n*InstantTechResearch*\nbg=yellow\n\n--\ntech : Tech\ncondition : set(LogicElement)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>648</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4446</x>\n      <y>3015</y>\n      <w>225</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*Herd*\nbg=green\n\n--\nstrength : int\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4410</x>\n      <y>3042</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>801</x>\n      <y>468</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*EntityContainerCapacity*\nbg=yellow\n\n--\ncontainer : EntityContainer\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>792</x>\n      <y>549</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*StorageElementCapacity*\nbg=yellow\n\n--\nstorage_element : StorageElementDefinition\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>495</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>576</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>675</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResearchTime*\nbg=yellow\n\n--\nresearchables : set(ResearchableTech)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>702</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2160</x>\n      <y>459</y>\n      <w>252</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>// Reveal area around listed units\n*Reveal*\nbg=yellow\n\n--\nline_of_sight : float\naffected_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>486</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>9</x>\n      <y>315</y>\n      <w>171</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>Orange elements:\n\nEffects/Resistances that can\nbe applied on other game\nentities\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2160</x>\n      <y>783</y>\n      <w>288</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>// The change values and fire rate of the provider and receiver are compared (divided)\n// with the result being added to the projectile amount of the receiver\n*AoE2ProjectileAmount*\nbg=yellow\n\n--\n// Ability of the modifier provider\nprovider_abilities : set(ApplyDiscreteEffect)\n// Ability of the modifier receiver\nreceiver_abilities : set(ApplyDiscreteEffect)\n// Only change values from these change types are compared\nchange_types : set(AttributeChangeType)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4185</x>\n      <y>3087</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*RegenerateResourceSpot*\nbg=green\n\n--\nrate : ResourceRate\nresource_spot : ResourceSpot</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4383</x>\n      <y>3114</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1044</x>\n      <y>1593</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>// Only affect yourself (default for modifiers in GameEntity)\n\n*Standard*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>1557</y>\n      <w>63</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>50.0;10.0;50.0;80.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>1656</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>// Affect all game entities in the list\n*GameEntityScope*\nbg=pink\n\n--\naffected_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>1620</y>\n      <w>63</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;70.0;50.0;70.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Text</id>\n    <coordinates>\n      <x>9</x>\n      <y>9</y>\n      <w>207</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>openage nyan data API v0.6.0</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1287</x>\n      <y>2718</y>\n      <w>288</w>\n      <h>126</h>\n    </coordinates>\n    <panel_attributes>*StateChanger*\nbg=pink\n\n--\nenable_abilities : set(Ability)\ndisable_abilities : set(Ability)\nenable_modifiers : set(Modifier)\ndisable_modifiers : set(Modifier)\ntransform_pool : optional(TransformPool) = None\npriority : int\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1566</x>\n      <y>2763</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1791</x>\n      <y>459</y>\n      <w>279</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>// Apply effects when in a container\n*InContainerContinuousEffect*\nbg=yellow\n\n--\ncontainers : set(EntityContainer)\nability : ApplyContinuousEffect</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2061</x>\n      <y>486</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1791</x>\n      <y>558</y>\n      <w>279</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>// Apply effects when in a container\n*InContainerDiscreteEffect*\nbg=yellow\n\n--\ncontainers : set(EntityContainer)\nability : ApplyDiscreteEffect</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2061</x>\n      <y>585</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>3753</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Normal*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3771</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>144</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Terrain*\nbg=yellow\n\n--\nterrain : Terrain</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>621</x>\n      <y>171</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>9</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Terrain*\nbg=yellow\n\n--\nterrain : Terrain</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>36</y>\n      <w>27</w>\n      <h>324</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;340.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2160</x>\n      <y>378</y>\n      <w>252</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>// Reveal area around listed units\n*DiplomaticLineOfSight*\nbg=yellow\n\n--\ndiplomatic_stance : DiplomaticStance</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>405</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>945</x>\n      <y>4428</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*LureType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>4446</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>945</x>\n      <y>4491</y>\n      <w>180</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*SendToContainerType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>4509</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3114</x>\n      <y>2493</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PlacementMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3150</x>\n      <y>2448</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2772</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Eject*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2565</y>\n      <w>189</w>\n      <h>117</h>\n    </coordinates>\n    <panel_attributes>*Place*\nbg=pink\n\n--\ntile_snap_distance : float\nclearance_size_x : float\nclearance_size_y : float\nallow_rotation : bool\nmax_elevation_difference : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3177</x>\n      <y>2538</y>\n      <w>27</w>\n      <h>351</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;370.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3177</x>\n      <y>2592</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>3564</y>\n      <w>234</w>\n      <h>162</h>\n    </coordinates>\n    <panel_attributes>*AdjacentTilesVariant*\n\nbg=pink\n--\nnorth : optional(GameEntity)\nnorth_east : optional(GameEntity)\neast : optional(GameEntity)\nsouth_east : optional(GameEntity)\nsouth : optional(GameEntity)\nsouth_west : optional(GameEntity)\nwest : optional(GameEntity)\nnorth_west : optional(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>567</x>\n      <y>3510</y>\n      <w>45</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;30.0;100.0;10.0;100.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>4302</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*InverseLinear*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3456</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ExecutionSound*\nbg=pink\n\n--\nsounds : set(Sound)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3483</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3402</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3375</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AnimationOverride*\nbg=pink\n\n--\noverrides : set(AnimationOverride)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1791</x>\n      <y>378</y>\n      <w>279</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*RefundOnCondition*\nbg=yellow\n\n--\ncondition : set(LogicElement)\nrefund_amount : set(ResourceAmount)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2061</x>\n      <y>405</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1170</x>\n      <y>3978</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ProgressStatus*\nbg=pink\n\n--\nprogress_type : ProgressType\nprogress : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1368</x>\n      <y>3987</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>162</x>\n      <y>5778</y>\n      <w>297</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*TimeRelativeAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType\ntotal_change_time : float\nignore_protection : set(ProtectingAttribute)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>5688</y>\n      <w>54</w>\n      <h>153</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;150.0;10.0;150.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>189</x>\n      <y>5895</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>189</x>\n      <y>5958</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>414</x>\n      <y>5913</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>414</x>\n      <y>5868</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;60.0;30.0;60.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>234</x>\n      <y>6030</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*TimeRelativeProgressChange*\nbg=pink\n\n--\ntype : ProgressType\ntotal_change_time : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>5814</y>\n      <w>54</w>\n      <h>270</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;280.0;10.0;280.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5778</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*TimeRelativeAttributeChange*\nbg=pink\n\n--\ntype : AttributeChangeType</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>5688</y>\n      <w>54</w>\n      <h>144</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;140.0;10.0;140.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5895</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>5958</y>\n      <w>198</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*FlatAttributeIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>5913</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>5850</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>927</x>\n      <y>6030</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*TimeRelativeProgressChange*\nbg=pink\n\n--\ntype : ProgressType\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1143</x>\n      <y>5805</y>\n      <w>54</w>\n      <h>279</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;40.0;290.0;10.0;290.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2007</x>\n      <y>3771</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ProgressType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1935</x>\n      <y>3789</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4545</x>\n      <y>2412</y>\n      <w>243</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AoE1TradeRoute*\nbg=pink\n\n--\nexchange_resources : set(Resource)\ntrade_amount : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2223</x>\n      <y>4446</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*StateChange*\nbg=pink\n\n--\nstate_change : StateChanger</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4473</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2223</x>\n      <y>4284</y>\n      <w>135</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Terrain*\nbg=pink\n\n--\n// changes the underlying terrain below game entity (permanently)\nterrain : Terrain\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4311</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5382</x>\n      <y>4239</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5418</x>\n      <y>4212</y>\n      <w>234</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TerrainRequirement*\nbg=green\n\n--\nallowed_types : set(TerrainType)\nblacklisted_terrains : set(Terrain)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5418</x>\n      <y>4122</y>\n      <w>225</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*OverlayTerrain*\nbg=green\n\n--\nterrain_overlay : Terrain</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4464</x>\n      <y>4446</y>\n      <w>216</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Attribute*\nbg=pink\n\n--\nname : TranslatedString\nabbreviation : TranslatedString\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>4392</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;60.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1791</x>\n      <y>657</y>\n      <w>279</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>*DepositResourcesOnProgress*\nbg=yellow\n\n--\nprogress_type : ProgressType\nresources : set(Resource)\naffected_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2061</x>\n      <y>684</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3978</x>\n      <y>1926</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PriceMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4032</x>\n      <y>2169</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4095</x>\n      <y>1944</y>\n      <w>153</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;150.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4230</x>\n      <y>1926</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Fixed*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3177</x>\n      <y>2790</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4509</x>\n      <y>2439</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4194</x>\n      <y>1944</y>\n      <w>27</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;80.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4194</x>\n      <y>2007</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4230</x>\n      <y>1989</y>\n      <w>135</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Dynamic*\nbg=pink\n\n--\nchange_value : float\nmin_price : float\nmax_price : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4302</x>\n      <y>2106</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PricePool*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4239</x>\n      <y>2124</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>684</x>\n      <y>81</y>\n      <w>225</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeAttributeChange*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>900</x>\n      <y>99</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>702</x>\n      <y>18</y>\n      <w>207</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeProgressChange*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>900</x>\n      <y>36</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>495</x>\n      <y>459</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Unconditional*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>171</y>\n      <w>27</w>\n      <h>333</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;350.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>315</y>\n      <w>153</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Unconditional*\nbg=yellow</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>333</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>918</x>\n      <y>2475</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Cost*\nbg=pink\n\n--\npayment_mode : PaymentMode</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1188</x>\n      <y>2430</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceCost*\nbg=pink\n\n--\namount : set(ResourceAmount)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1188</x>\n      <y>2520</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeCost*\nbg=pink\n\n--\namount : set(AttributeAmount)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1125</x>\n      <y>2502</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1161</x>\n      <y>2457</y>\n      <w>45</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1161</x>\n      <y>2493</y>\n      <w>45</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;70.0;30.0;70.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>756</y>\n      <w>270</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*CreationAttributeCost*\nbg=yellow\n\n--\nattributes : set(Attribute)\ncreatables : set(CreatableGameEntity)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>783</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1044</x>\n      <y>945</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>513</y>\n      <w>261</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResearchAttributeCost*\nbg=yellow\n\n--\nattributes : set(Attribute)\nresearchables : set(ResearchableTech)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1089</x>\n      <y>540</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1017</x>\n      <y>2538</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>963</x>\n      <y>2385</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PaymentMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1017</x>\n      <y>2430</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>909</x>\n      <y>2187</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Advance*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>909</x>\n      <y>2313</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Arrear*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>909</x>\n      <y>2250</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Adaptive*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1035</x>\n      <y>2142</y>\n      <w>27</w>\n      <h>261</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;270.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>999</x>\n      <y>2331</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>999</x>\n      <y>2268</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>999</x>\n      <y>2205</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3618</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AttributeChange*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>3789</y>\n      <w>99</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;90.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>189</x>\n      <y>6129</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeProgressDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>189</x>\n      <y>6192</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeProgressIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>414</x>\n      <y>6147</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>414</x>\n      <y>6102</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;60.0;30.0;60.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>891</x>\n      <y>6129</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeProgressDecrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>891</x>\n      <y>6192</y>\n      <w>234</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TimeRelativeProgressIncrease*\nbg=orange</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>6147</y>\n      <w>45</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;80.0;30.0;80.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>6102</y>\n      <w>45</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=-&gt;&gt;</panel_attributes>\n    <additional_attributes>10.0;60.0;30.0;60.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>909</x>\n      <y>2124</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Shadow*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>999</x>\n      <y>2142</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5868</x>\n      <y>3663</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*HerdableMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5931</x>\n      <y>3627</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>3744</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ClosestHerding*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5904</x>\n      <y>3708</y>\n      <w>27</w>\n      <h>207</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;210.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5904</x>\n      <y>3762</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>3807</y>\n      <w>180</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*LongestTimeInRange*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5949</x>\n      <y>3870</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MostHerding*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5904</x>\n      <y>3825</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5904</x>\n      <y>3888</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1269</x>\n      <y>3519</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TerrainType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1314</x>\n      <y>3474</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1116</x>\n      <y>4842</y>\n      <w>234</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Stacked*\nbg=pink\n\n--\nstack_limit : int\ncalculation_type : CalculationType\ndistribution_type : DistributionType\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1071</x>\n      <y>4788</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1467</x>\n      <y>4860</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*CalculationType*\nbg=pink\n\n// this determines the factor that is multiplied with the\n// value determined by distribution_type. It depends on\n// the number of effectors.</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1341</x>\n      <y>4878</y>\n      <w>144</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>140.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1539</x>\n      <y>5094</y>\n      <w>144</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Hyperbolic*\nbg=pink\n\n--\nshift_x : int\nshift_y : int\nscale_factor : float\n\n\n// factor =  (scale_factor / (num_effectors - shift_x)) + shift_y</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1503</x>\n      <y>4905</y>\n      <w>27</w>\n      <h>234</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;240.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1359</x>\n      <y>5616</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1503</x>\n      <y>5112</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1539</x>\n      <y>4995</y>\n      <w>144</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Linear*\nbg=pink\n\n--\nshift_x : int\nshift_y : int\nscale_factor : float\n\n\n// factor = scale_factor * (num_effectors - shift_x) + shift_y</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1503</x>\n      <y>5013</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1539</x>\n      <y>4932</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*NoStack*\nbg=pink\n\n\n// factor = 1</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1503</x>\n      <y>4950</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2835</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*OwnStorage*\nbg=pink\n\n--\ncontainer : EntityContainer</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3177</x>\n      <y>2862</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3231</x>\n      <y>2106</y>\n      <w>279</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ProductionQueue*\nbg=green\n\n--\nsize : int\nproduction_modes : set(ProductionMode)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3501</x>\n      <y>2133</y>\n      <w>54</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;100.0;40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3312</x>\n      <y>2016</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ProductionMode*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3375</x>\n      <y>2061</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3555</x>\n      <y>2088</y>\n      <w>243</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Creatables*\nbg=pink\n\n--\nexclude : set(CreatableGameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3447</x>\n      <y>2034</y>\n      <w>126</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;120.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3555</x>\n      <y>2007</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Researchables*\nbg=pink\n\n--\nexclude : set(ResearchableTech)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3519</x>\n      <y>2034</y>\n      <w>54</w>\n      <h>108</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;100.0;40.0;100.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1485</x>\n      <y>3627</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LogicElement*\nbg=pink\n\n--\nonly_once : bool</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1800</x>\n      <y>3717</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AND*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3690</y>\n      <w>27</w>\n      <h>288</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;300.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1800</x>\n      <y>3780</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*OR*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1800</x>\n      <y>3843</y>\n      <w>126</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*SUBSETMIN*\nbg=pink\n\n--\nsize : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1755</x>\n      <y>3798</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3798</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3735</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5724</x>\n      <y>3510</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5751</x>\n      <y>3420</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5751</x>\n      <y>3330</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1215</x>\n      <y>3744</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LiteralScope*\nbg=pink\n\n--\nstances : set(DiplomaticStance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1395</x>\n      <y>3771</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;70.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1188</x>\n      <y>3834</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Any*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1287</x>\n      <y>3807</y>\n      <w>27</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;130.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1188</x>\n      <y>3897</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Self*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1269</x>\n      <y>3915</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1269</x>\n      <y>3852</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>3870</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>3951</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4014</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceSpotsDepleted*\nbg=pink\n\n--\nonly_enabled : bool</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4095</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Timer*\nbg=pink\n\n--\ntime : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4041</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4122</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4176</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeBelowValue*\nbg=pink\n\n--\nattribute : Attribute\nthreshold : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4419</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ProjectilePassThrough*\nbg=pink\n\n--\npass_through_range : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1440</x>\n      <y>4500</y>\n      <w>171</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ProjectileHitTerrain*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4203</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4446</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4518</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4851</x>\n      <y>3987</y>\n      <w>126</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*Hitbox*\nbg=pink\n\n--\nradius_x : float\nradius_y : float\nradius_z : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4788</x>\n      <y>4014</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>3699</y>\n      <w>243</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*DetectCloak (SWGB)*\nbg=green\n\n--\nallowed_types : set(GameEntityType)\nblacklisted_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>3726</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1287</x>\n      <y>4950</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*DistributionType*\nbg=pink\n\n// Becomes relevant if effector's have different change values\n// e.g. monk1 = heals 20 HP/s\n//      monk2 = heals 30 HP/s\n// if distribution_type = Mean the mean value 25 HP/s is used\n// for the calculation</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1233</x>\n      <y>4914</y>\n      <w>72</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;70.0;10.0;70.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1368</x>\n      <y>5022</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Mean*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1332</x>\n      <y>4995</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1332</x>\n      <y>5040</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5634</x>\n      <y>3681</y>\n      <w>126</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Rectangle*\nbg=pink\n\n--\nwidth : float\nheight : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5472</x>\n      <y>3681</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MatchToSprite*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5553</x>\n      <y>3591</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*SelectionBox*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5607</x>\n      <y>3555</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5643</x>\n      <y>3636</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5571</x>\n      <y>3636</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1305</x>\n      <y>1593</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Stacked*\nbg=pink\n\n--\nstack_limit : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1494</x>\n      <y>1467</y>\n      <w>27</w>\n      <h>261</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;270.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5382</x>\n      <y>4149</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1170</x>\n      <y>2997</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*NyanPatch*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1116</x>\n      <y>3015</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1647</x>\n      <y>4176</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeAbovePercentage*\nbg=pink\n\n--\nattribute : Attribute\nthreshold : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4284</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>3195</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1908</x>\n      <y>3087</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Palette*\nbg=pink\n\n--\npalette : file</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1845</x>\n      <y>3114</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3483</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4149</x>\n      <y>3609</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Guard*\nbg=pink\n\n--\nrange : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4104</x>\n      <y>3636</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3222</x>\n      <y>2691</y>\n      <w>189</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Replace*\nbg=pink\n\n--\ngame_entities : set(GameEntity)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3177</x>\n      <y>2718</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>972</x>\n      <y>3609</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TechType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1071</x>\n      <y>3627</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;60.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1017</x>\n      <y>3654</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;50.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>990</x>\n      <y>3528</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Any*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1026</x>\n      <y>3573</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1278</x>\n      <y>3600</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Any*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1314</x>\n      <y>3564</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>657</x>\n      <y>3141</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Any*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>738</x>\n      <y>3159</y>\n      <w>63</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>50.0;50.0;50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2223</x>\n      <y>4122</y>\n      <w>207</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AnimationOverlay*\nbg=pink\n\n--\noverlays : set(Animation)\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4149</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1647</x>\n      <y>4095</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeBelowPercentage*\nbg=pink\n\n--\nattribute : Attribute\nthreshold : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4257</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AttributeAboveValue*\nbg=pink\n\n--\nattribute : Attribute\nthreshold : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1620</x>\n      <y>4122</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1620</x>\n      <y>4203</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>387</y>\n      <w>288</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>*ElevationDifferenceHigh*\nbg=yellow\n\n--\nmin_elevation_difference : optional(float) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>621</x>\n      <y>414</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>621</x>\n      <y>477</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1386</x>\n      <y>90</y>\n      <w>288</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ElevationDifferenceHigh*\nbg=yellow\n\n--\nmin_elevation_difference : optional(float) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>117</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>342</x>\n      <y>3735</y>\n      <w>117</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MiscVariant*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>450</x>\n      <y>3582</y>\n      <w>162</w>\n      <h>198</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>160.0;10.0;160.0;200.0;10.0;200.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4176</x>\n      <y>3006</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ResourceStorage*\nbg=green\n\n--\ncontainers : set(ResourceContainer)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4383</x>\n      <y>3033</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3933</x>\n      <y>3006</y>\n      <w>216</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*ResourceContainer*\nbg=pink\n\n--\nresource : Resource\nmax_amount : int\ncarry_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4140</x>\n      <y>3033</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3960</x>\n      <y>3123</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*InternalDropSite*\nbg=pink\n\n--\nupdate_time : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4032</x>\n      <y>3087</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;40.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1143</x>\n      <y>2718</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*TransformPool*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1260</x>\n      <y>2736</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4014</x>\n      <y>4068</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2133</x>\n      <y>2997</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Any*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2169</x>\n      <y>2961</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2277</x>\n      <y>2961</y>\n      <w>27</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;40.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3879</x>\n      <y>2088</y>\n      <w>369</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>*ExchangeRate*\nbg=pink\n\n--\nbase_price : float\nprice_adjust : optional(dict(ExchangeMode, PriceMode)) = None\nprice_pool : optional(PricePool) = None</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3942</x>\n      <y>2223</y>\n      <w>234</w>\n      <h>99</h>\n    </coordinates>\n    <panel_attributes>*ExchangeResources*\nbg=green\n\n--\nresource_a : Resource\nresource_b : Resource\nexchange_rate : ExchangeRate\nexchange_modes : set(ExchangeMode)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4032</x>\n      <y>1971</y>\n      <w>27</w>\n      <h>135</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;130.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4212</x>\n      <y>2232</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*ExchangeMode*\nbg=pink\n\n--\nfee_multiplier : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4167</x>\n      <y>2259</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4428</x>\n      <y>2205</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Sell*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4428</x>\n      <y>2268</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Buy*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4347</x>\n      <y>2241</y>\n      <w>99</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;90.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4347</x>\n      <y>2268</y>\n      <w>99</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;90.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1548</x>\n      <y>3690</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1656</x>\n      <y>3654</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1701</x>\n      <y>3627</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LogicGate*\nbg=pink\n\n--\ninputs : set(LogicElement)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1800</x>\n      <y>3924</y>\n      <w>126</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*SUBSETMAX*\nbg=pink\n\n--\nsize : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3951</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1674</x>\n      <y>3717</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*NOT*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1755</x>\n      <y>3735</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1674</x>\n      <y>3780</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*XOR*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3870</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1674</x>\n      <y>3843</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MULTIXOR*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1755</x>\n      <y>3861</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3168</x>\n      <y>3294</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*AbilityProperty*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3267</y>\n      <w>27</w>\n      <h>45</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;30.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3726</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3780</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Lock*\nbg=pink\n\n--\nlock_pool : LockPool</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3807</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4599</x>\n      <y>3618</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Lock*\nbg=green\n\n--\nlock_pools : set(LockPool)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4563</x>\n      <y>3645</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>4806</x>\n      <y>3618</y>\n      <w>135</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*LockPool*\nbg=pink\n\n--\nslots : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>4761</x>\n      <y>3645</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1404</x>\n      <y>1422</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ModifierProperty*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1539</x>\n      <y>1440</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1449</x>\n      <y>1530</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1449</x>\n      <y>1620</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1305</x>\n      <y>1674</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Multiplier*\nbg=pink\n\n--\nmultiplier : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1449</x>\n      <y>1701</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2106</x>\n      <y>810</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>981</x>\n      <y>414</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>effect\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1152</x>\n      <y>414</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>resistance\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>666</x>\n      <y>270</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>flac\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1260</x>\n      <y>162</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>flac\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>630</x>\n      <y>4950</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*EffectProperty*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>747</x>\n      <y>4968</y>\n      <w>153</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;150.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>684</x>\n      <y>4626</y>\n      <w>27</w>\n      <h>342</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;360.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>4842</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Cost*\nbg=pink\n\n--\ncost : Cost</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>441</x>\n      <y>4761</y>\n      <w>216</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Diplomatic*\nbg=pink\n\n--\nstances : set(DiplomaticStance)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>810</x>\n      <y>4257</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>504</x>\n      <y>4680</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AreaEffect*\nbg=pink\n\n--\nrange : float\ndropoff : DropoffType</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>4707</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>4788</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>4869</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1017</x>\n      <y>4950</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ResistanceProperty*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>873</x>\n      <y>4968</y>\n      <w>162</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;160.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1071</x>\n      <y>4788</y>\n      <w>27</w>\n      <h>180</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;180.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2187</x>\n      <y>4086</y>\n      <w>27</w>\n      <h>414</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;440.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2097</x>\n      <y>4041</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ProgressProperty*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1935</x>\n      <y>4059</y>\n      <w>180</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;180.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1773</x>\n      <y>3375</y>\n      <w>27</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;60.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1737</x>\n      <y>3429</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Reset*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1161</x>\n      <y>2781</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Reset*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1242</x>\n      <y>2799</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1440</x>\n      <y>4563</y>\n      <w>171</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*StateChangeActive*\nbg=pink\n\n--\nstate_change : StateChanger</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4590</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1413</x>\n      <y>4338</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*OwnsGameEntity*\nbg=pink\n\n--\ngame_entity : GameEntity</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>4365</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5877</x>\n      <y>2628</y>\n      <w>297</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*EffectBatch*\nbg=pink\n\n--\neffects : set(DiscreteEffect)\nproperties : dict(BatchProperty, BatchProperty) = {}\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>6228</x>\n      <y>2637</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*BatchProperty*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>6291</x>\n      <y>2718</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Priority*\nbg=pink\n\n--\npriority : int\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>6255</x>\n      <y>2682</y>\n      <w>27</w>\n      <h>171</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;170.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>6255</x>\n      <y>2745</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>6291</x>\n      <y>2799</y>\n      <w>153</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Chance*\nbg=pink\n\n--\nchance : float\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>6255</x>\n      <y>2826</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>6165</x>\n      <y>2655</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5823</x>\n      <y>2592</y>\n      <w>72</w>\n      <h>90</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;80.0;10.0;80.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5967</x>\n      <y>2718</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*UnorderedBatch*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5931</x>\n      <y>2691</y>\n      <w>27</w>\n      <h>198</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;200.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5931</x>\n      <y>2736</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5967</x>\n      <y>2781</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*OrderedBatch*\nbg=pink\n\n// Order is defined by Priority(EffectProperty)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5931</x>\n      <y>2799</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5967</x>\n      <y>2844</y>\n      <w>135</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ChainedBatch*\nbg=pink\n</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5931</x>\n      <y>2862</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>477</x>\n      <y>4599</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Priority*\nbg=pink\n\n--\npriority : int</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>648</x>\n      <y>4626</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;50.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1251</x>\n      <y>3078</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PatchProperty*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>3096</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1224</x>\n      <y>3042</y>\n      <w>27</w>\n      <h>162</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;160.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1296</x>\n      <y>3123</y>\n      <w>27</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;70.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1458</x>\n      <y>3645</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1377</x>\n      <y>3672</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*True*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1377</x>\n      <y>3609</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*False*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1458</x>\n      <y>3672</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>30.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>36</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1350</x>\n      <y>198</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>153</x>\n      <y>3276</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Tree*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>153</x>\n      <y>3339</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Relic*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>279</x>\n      <y>3267</y>\n      <w>117</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>110.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>279</x>\n      <y>3168</y>\n      <w>27</w>\n      <h>279</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>252</x>\n      <y>3357</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>153</x>\n      <y>3213</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Swordsman*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>153</x>\n      <y>3150</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Barracks*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>252</x>\n      <y>3294</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>252</x>\n      <y>3231</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>252</x>\n      <y>3168</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>153</x>\n      <y>3402</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Projectile*</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>252</x>\n      <y>3420</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>18</x>\n      <y>3150</y>\n      <w>126</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>All ingame objects\nare game entities\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3744</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Construct*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3807</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Harvest*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3870</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Restock*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3681</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Carry*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2214</x>\n      <y>3933</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Transform*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3636</y>\n      <w>27</w>\n      <h>342</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;360.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3636</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3699</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3762</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3825</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3888</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2178</x>\n      <y>3951</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>5139</x>\n      <y>4212</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Constructable*\nbg=green\n\n--\nstarting_progress : int\nconstruction_progress : set(Progress)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>5355</x>\n      <y>4239</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1989</x>\n      <y>2016</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Node*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2034</x>\n      <y>2061</y>\n      <w>27</w>\n      <h>342</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;360.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1872</x>\n      <y>2097</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Start*\nbg=pink\n\n--\nnext : Node</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2007</x>\n      <y>2124</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1674</x>\n      <y>2097</y>\n      <w>135</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Activity*\nbg=pink\n\n--\nstart : Start</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1800</x>\n      <y>2124</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>80.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1917</x>\n      <y>2187</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*End*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2007</x>\n      <y>2205</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2070</x>\n      <y>2097</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*XORGate*\nbg=pink\n\n--\nnext : orderedset(Condition)\ndefault : Node</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2070</x>\n      <y>2259</y>\n      <w>198</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*XOREventGate*\nbg=pink\n\n--\nnext : dict(Event, Node)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1863</x>\n      <y>2259</y>\n      <w>153</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Ability*\nbg=pink\n\n--\nnext : Node\nability : Ability</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2034</x>\n      <y>2124</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2034</x>\n      <y>2286</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2007</x>\n      <y>2286</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3348</x>\n      <y>3033</y>\n      <w>162</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Activity*\nbg=green\n\n--\ngraph : Activity</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3501</x>\n      <y>3060</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLNote</id>\n    <coordinates>\n      <x>1620</x>\n      <y>2052</y>\n      <w>144</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>Unit behaviour graph\nbg=blue</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2349</x>\n      <y>2268</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Event*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2259</x>\n      <y>2286</y>\n      <w>108</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>100.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2367</x>\n      <y>2313</y>\n      <w>27</w>\n      <h>216</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;220.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2394</x>\n      <y>2340</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Wait*\nbg=pink\n\n--\ntime : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2394</x>\n      <y>2421</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*WaitAbility*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2367</x>\n      <y>2367</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2367</x>\n      <y>2439</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2394</x>\n      <y>2484</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*CommandInQueue*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2367</x>\n      <y>2502</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2196</x>\n      <y>1665</y>\n      <w>126</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Condition*\nbg=pink\n\n--\nnext : Node</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2142</x>\n      <y>1692</y>\n      <w>72</w>\n      <h>423</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>60.0;10.0;10.0;10.0;10.0;450.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2277</x>\n      <y>1755</y>\n      <w>144</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*CommandInQueue*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2250</x>\n      <y>1728</y>\n      <w>27</w>\n      <h>306</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;320.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2250</x>\n      <y>1773</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2277</x>\n      <y>1818</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*NextCommand*\nbg=pink\n\n--\ncommand : Command</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2250</x>\n      <y>1836</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>2124</y>\n      <w>90</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2250</x>\n      <y>1926</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3915</x>\n      <y>3591</y>\n      <w>108</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PathType*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3960</x>\n      <y>3546</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2070</x>\n      <y>2178</y>\n      <w>225</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*XORSwitchGate*\nbg=pink\n\n--\nswitch : SwitchCondition\ndefault : Node</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2034</x>\n      <y>2205</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2349</x>\n      <y>2187</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*SwitchCondition*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2286</x>\n      <y>2205</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2529</x>\n      <y>2178</y>\n      <w>180</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*NextCommand*\nbg=pink\n\n--\nnext : dict(Command, Node)</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2466</x>\n      <y>2205</y>\n      <w>81</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>70.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1656</x>\n      <y>2466</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Command*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1602</x>\n      <y>2484</y>\n      <w>72</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;60.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1719</x>\n      <y>2601</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Idle*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1719</x>\n      <y>2664</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Move*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1719</x>\n      <y>2538</y>\n      <w>99</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ApplyEffect*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1683</x>\n      <y>2511</y>\n      <w>27</w>\n      <h>198</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;200.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1683</x>\n      <y>2556</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1683</x>\n      <y>2619</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1683</x>\n      <y>2682</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;40.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>3267</x>\n      <y>3861</y>\n      <w>144</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*Ranged*\nbg=pink\n\n--\nmin_range : float\nmax_range : float</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>3222</x>\n      <y>3888</y>\n      <w>63</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>50.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2277</x>\n      <y>1899</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*TargetInRange*\nbg=pink\n\n--\nability : Ability</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1863</x>\n      <y>2349</y>\n      <w>153</w>\n      <h>81</h>\n    </coordinates>\n    <panel_attributes>*Task*\nbg=pink\n\n--\nnext : Node\ntask : Task</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2007</x>\n      <y>2376</y>\n      <w>54</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>40.0;10.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1926</x>\n      <y>2466</y>\n      <w>90</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*Task*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1962</x>\n      <y>2421</y>\n      <w>27</w>\n      <h>63</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;.</panel_attributes>\n    <additional_attributes>10.0;50.0;10.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1962</x>\n      <y>2511</y>\n      <w>27</w>\n      <h>198</h>\n    </coordinates>\n    <panel_attributes>lt=&lt;&lt;-</panel_attributes>\n    <additional_attributes>10.0;10.0;10.0;200.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1989</x>\n      <y>2538</y>\n      <w>162</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*PopCommandQueue*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1962</x>\n      <y>2556</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1989</x>\n      <y>2601</y>\n      <w>162</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*ClearCommandQueue*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1962</x>\n      <y>2619</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>1989</x>\n      <y>2664</y>\n      <w>126</w>\n      <h>54</h>\n    </coordinates>\n    <panel_attributes>\n*MoveToTarget*\nbg=pink</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>1962</x>\n      <y>2682</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n  <element>\n    <id>UMLClass</id>\n    <coordinates>\n      <x>2277</x>\n      <y>1980</y>\n      <w>162</w>\n      <h>72</h>\n    </coordinates>\n    <panel_attributes>*AbilityUsable*\nbg=pink\n\n--\nability : Ability</panel_attributes>\n    <additional_attributes/>\n  </element>\n  <element>\n    <id>Relation</id>\n    <coordinates>\n      <x>2250</x>\n      <y>2007</y>\n      <w>45</w>\n      <h>27</h>\n    </coordinates>\n    <panel_attributes>lt=-</panel_attributes>\n    <additional_attributes>10.0;10.0;30.0;10.0</additional_attributes>\n  </element>\n</diagram>\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_ability.md",
    "content": "# engine.ability\n\nReference documentation of the `engine.ability` module of the openage modding API.\n\n## ability.Ability\n\n```python\nAbility(Object):\n    properties : dict(abstract(AbilityProperty), AbilityProperty) = {}\n```\n\nGeneralization object for all abilities. Abilities define what game entities can *do* and what they *are*, respectively. They can be considered passive and active traits.\n\n**properties**\nFurther specializes the ability beyond the standard behaviour.\n\nThe engine expects objects from the namespace `engine.ability.property.type` as keys. Values must always be an instance of the object used as key.\n\nStandard behavior without properties:\n\n* Abilities in the `abilities` set of `GameEntity` are considered enabled as soon as the game entity is created, unless a `StateChager` object disables them.\n* No ability is explicitly required to be animated or to play a sound. For this purpose, ability properties are used.\n* A game entity's abilities are available to **all** players, not just the owner. This can be limited to players with specific diplomatic stances towards the owner by assigning the `Diplomatic` property.\n* An ability's execution does not block any other ability, unless explicitely defined by the API.\n\nProperties:\n\n* `Animated`: Assigns an animation that is played while the ability is active.\n* `AnimationOverride`: Overrides the animations of specified animated abilities temporarily.\n* `CommandSound`: Assigns a sound that is played once when the player orders the game entity to use the ability.\n* `Diplomatic`: Makes the ability only accessible for players when the owner of the game entity has the specified diplomatic stances towards them.\n* `ExecutionSound`: Assigns a sound that is played while the ability is active.\n* `Lock`: Assigns the ability to a lock pool.\n\n## ability.property.AbilityProperty\n\n```python\nAbilityProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of abilities.\n\n## ability.property.type.Animated\n\n```python\nAnimated(AbilityProperty):\n    animations : set(Animation)\n```\n\nPlay an animation while the ability is used. The animation itself is configured in the `.sprite` file linked in the `Animation` object.\n\n**animations**\nThe animation(s) played while the ability is used. If more than one animation is defined, the engine randomly picks one of them.\n\n## ability.property.type.AnimationOverride\n\n```python\nAnimationOverride(AbilityProperty):\n    overrides : set(AnimationOverride)\n```\n\nSpecifies a set of animation overrides that are applied when the ability is used.\n\n*Usage example*: Consider a unit with two attacks. One of them is animated by having the unit wield a sword, while the other one is an animation in which the unit uses a bow. What we want is that the animation of the `Move` and `Idle` abilities correspond to the attack that was used last, e.g. after a sword attack, the movement animation show the unit moving around with a sword in its hand, and after a bow attack, the movement animation show the unit moving around with a bow. To accomplish this, we would add the `AnimationOverride(AbilityProperty)` to both attack abilities and specify `AnimationOverride` objects for them which target `Move` and `Idle`.\n\n**overrides**\nOverrides initiated when using the ability.\n\n## ability.property.type.CommandSound\n\n```python\nCommandSound(AbilityProperty):\n    sounds : set(Sound)\n```\n\nAbilities with this property will play a sound once when the player orders the game entity to use them.\n\n**sounds**\nSound(s) played when the order to use the ability is given. If more than one sound is defined, the engine randomly picks one of them.\n\n## ability.property.type.Diplomatic\n\n```python\nDiplomatic(AbilityProperty):\n    stances : set(children(DiplomaticStance))\n```\n\nRestricts the players who can access the ability. Access is given to a player when the owner of the game entity has one of the specified stances towards this player. Note that when using this property, the access must also be explicitly allowed for the owner of the game entity by adding the `Self` stance to the set.\n\n*Usage example*: Consider a trade post like the marketplace from AoE games. Players can send trade carts between trade posts to generate resources. Trade post behavior is activated by assigning the `TradePost` ability to a game entity. Without the `Diplomatic` property, all player can trade with this trade post, including its owner. However, if we want to limit the trade post to only be accessible for allies, we assign `Diplomatic`as a property and add the `Allied` stance to the `stances` set.\n\n**stances**\nWhitelist of stances of the game entity owner towards other players. Only players that match these stances from the owner's perspective can use the ability.\n\n## ability.property.type.ExecutionSound\n\n```python\nExecutionSound(AbilityProperty):\n    sounds : set(Sound)\n```\n\nAbilities with this property will play a sound while they are active.\n\n**sounds**\nThe sound(s) played while the ability is used. If more than one sound is defined, the engine randomly picks one of them.\n\n## ability.property.type.Lock\n\n```python\nLock(AbilityProperty):\n    lock_pool : LockPool\n```\n\nAbilities with this property require a free slot in the specified lock pool to become active. While they are active, they occupy one slot in the lock pool. This property can be used to prevent abilities from specific abilities being executed simultaneously.\n\n*Usage example*: Consider a game entity that has the abilities `Create` for producing game entities and `Research` for researching techs. At runtime, only one of them should be active at the same time, i.e. the game entity should *either* produce a game entity *or* research a tech, not both. We can realize this by assigning both abilities the `Lock` property which references the same `LockPool` object (which has 1 slot). As a consequence, `Create` cannot be used together with `Research` as they both require 1 free slot to be used.\n\n**lock_pool**\nThe lock pool used by the ability. This lock pool should be assigned to a `Lock` ability that is assigned to the game entity.\n\nIf the lock pool\n\n* is not assigned to a `Lock` ability of the game entity **or**\n* the corresponding `Lock` ability is disabled **or**\n* all slots are occupied by other abilities at runtime\n\nthe ability with this property cannot become active.\n\n## ability.property.type.Ranged\n\n```python\nRanged(AbilityProperty):\n    min_range : float\n    max_range : float\n```\n\nAbilities with this property can only be used within a specified range around the game entity. The property mostly affects abilities that are *targeted*, i.e. that are used on other game entities or locations in the game world.\n\nIf the target of the ability is another game entity and said game entity has a `Collision` ability, the range check factors in the `Hitbox` boundaries of the targeted game entity when calculating the distance.\n\nWithout this property, abilities behave as if `min_range` and `max_range` are `0.0`.\n\n**min_range**\nMinimum distance to the target of the ability.\n\n**max_range**\nMaximum distance to the target of the ability.\n\n## ability.type.ActiveTransformTo\n\n```python\nActiveTransformTo(Ability):\n    target_state       : StateChanger\n    transform_time     : float\n    transform_progress : set(Progress)\n```\n\nActivates a state change for a game entity. Triggered by player input.\n\n**target_state**\nTarget state change that activates when the time defined in `transform_time` has passed.\n\n**transform_time**\nThe time for the transformation to complete.\n\n**transform_progress**\nA set of `Progress` objects that can activate state changes and animation overrides while the transformation progresses. The objects in the set must have progress type `Restock`.\n\n## ability.type.Activity\n\n```python\nActivity(Ability):\n    graph: Activity\n```\n\nDefines the behaviour of a game entity. The behaviour is modelled as a directed node graph. Nodes in the graph correspond to actions that execute for the game entity or conditional queries and event triggers that indicate which path to take next. By traversing the node graph along its paths, the game entities actions are determined. See the [activity control flow](/doc/code/game_simulation/activity.md) documentation for more information.\n\n**graph**\nNode graph that defines the behaviour of the game entity.\n\n## ability.type.ApplyContinuousEffect\n\n```python\nApplyContinuousEffect(Ability):\n    effects              : set(ContinuousEffect)\n    application_delay    : float\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nApplies a continuous effects on another game entity.\n\n**effects**\nThe applied continuous effect definitions.\n\n**application_delay**\nTime between the initiation of the ability and the application of the effects in seconds.\n\n**allowed_types**\nWhitelist of game entity types that can be targeted with the ability.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.ApplyDiscreteEffect\n\n```python\nApplyDiscreteEffect(Ability):\n    batches              : set(EffectBatch)\n    reload_time          : float\n    application_delay    : float\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nApplies batches of discrete effects on a game entity.\n\n**effects**\nThe batches of discrete effect which are applied when the ability is used.\n\n**reload_time**\nMinimum time between two applications of the effect batches in seconds.\n\n**application_delay**\nTime between the initiation of the ability and first application of the effects in seconds.\n\n**allowed_types**\nWhitelist of game entity types that can be targeted with the ability.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.AttributeChangeTracker\n\n```python\nAttributeChangeTracker(Ability):\n    attribute       : Attribute\n    change_progress : set(children(AttributeChangeProgress))\n```\n\nAllows the alteration of the state of the game entity when its attribute values increase or decrease.\n\n**attribute**\nThe attribute which is monitored.\n\n**change_progress**\nSet of `AttributeChangeProgress` objects that activate when the current attribute value enters their defined intervals.\n\n## ability.type.Cloak\n\n```python\nCloak(Ability):\n    interrupted_by     : set(Ability)\n    interrupt_cooldown : float\n```\n\nMakes the unit untargetable for other game entities.\n\n**interrupted_by**\nAbilities of the game entity that interrupt the cloak when used.\n\n**interrupt_cooldown**\nTime needed to reactivate the cloak after an ability from `interrupted_by` has been used.\n\n## ability.type.CollectStorage\n\n```python\nCollectStorage(Ability):\n    container        : EntityContainer\n    storage_elements : set(GameEntity)\n```\n\nAllows a game entity to insert specified game entities into one of its containers.\n\n**container**\nContainer the target game entity will be inserted into. A `Storage` ability with this container must be enabled.\n\n**storage_elements**\nGame entities that can be inserted into the container. The container must allow the `GameEntity` objects.\n\n## ability.type.Collision\n\n```python\nCollision(Ability):\n    hitbox : Hitbox\n```\n\nAdds collision behaviour to a game entity.\n\n**hitbox**\nDefines the size (x, y, z) of the collision hitbox.\n\n## ability.type.Constructable\n\n```python\nConstructable(Ability):\n    starting_progress     : int\n    construction_progress : set(Progress)\n```\n\nMakes the game entity constructable via `Effect` types.\n\n**starting_progress**\nThe construction progress when the game entity is created.\n\n**construction_progress**\nSet of `Progress` objects that activate when the current contruction progress value enters their defined intervals. The objects in the set must have progress type `Construct`.\n\n## ability.type.Create\n\n```python\nCreate(Ability):\n    creatables : set(CreatableGameEntity)\n```\n\nAllows a game entity to spawn new instances of game entities.\n\n**creatables**\nStores the reference to the creatable game entities as well as configuration options for the creation.\n\n## ability.type.Despawn\n\n```python\nDespawn(Ability):\n    activation_condition : set(LogicElement)\n    despawn_condition    : set(LogicElement)\n    despawn_time         : float\n    state_change         : optional(StateChanger) = None\n```\n\nPermanently removes the game entity from the game. By default, `Despawn` is inactive until triggered by `activation_condition`. Once activated, at least one of the elements in `despawn_condition` must be true to trigger the despawning.\n\n**activation_condition**\nActivates (i.e. *primes*) the ability when one of the listed conditions are true. After activation, the ability will wait for an element in `despawn_condition` to be true.\n\n**despawn_condition**\nTriggers the despawning process when of the conditions are true. `activation_condition` must have been fulfilled once before the despawn condition can be triggered.\n\n**despawn_time**\nTime until the game entity is removed from the game after the despawn condition has been fulfilled. The removal is permanent.\n\n**state_change**\nAlters the abilities and modifiers of a game entity after the despawn condition is fulfilled. The state change stays active until the game entity is removed from the game.\n\n## ability.type.DetectCloak\n\n```python\nDetectCloak(Ability):\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nEnables the game entity to decloak other game entities which use the `Cloak` ability.\n\n**allowed_types**\nWhitelist of game entity types that can be decloaked.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.DropResources\n\n```python\nDropResources(Ability):\n    containers           : set(ResourceContainer)\n    search_range         : float\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nAllows a game entity to drop off resources at other game entities that have the `DropSite` ability. The resources are transferred to the player's global resource storage. Game entities with the `ResourceStorage` ability will automatically use this ability when their resource container capacity is reached.\n\n**containers**\nResource containers whose resources are transferred.\n\n**search_range**\nRange in which the unit will search for a resource drop site if the ability is not used manually.\n\n**allowed_types**\nWhitelist of game entity types that can be used as drop sites. They must have a `DropSite` ability accepting the corresponding resource container.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.DropSite\n\n```python\nDropSite(Ability):\n    accepts_from : set(ResourceContainer)\n```\n\nAllows a game entity to act as a resource drop off point for game entities with the `DropResources` ability. The resources are transfered to the player's global resource storage.\n\n**accepts_from**\nResource containers that can be emptied at the game entity.\n\n## ability.type.EnterContainer\n\n```python\nEnterContainer(Ability):\n    allowed_containers   : set(EntityContainer)\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nAllows a game entity to enter specified containers of another game entity's `Storage` ability.\n\n**allowed_containers**\nContainers that can be entered.\n\n**allowed_types**\nWhitelist of game entity types that can be targeted with the ability.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.ExchangeResources\n\n```python\nExchangeResources(Ability):\n    resource_a     : Resource\n    resource_b     : Resource\n    exchange_rate  : ExchangeRate\n    exchange_modes : set(ExchangeMode)\n```\n\nExchanges a resource for another resource. The type of exchange depends on the exchange mode. Players can exchange resources with themselves or other players.\n\n**resource_a**\nFirst resource used for the exchange.\n\n**resource_b**\nSecond resource used for the exchange.\n\n**exchange_rate**\nExchange rate per unit of exchanged resources.\n\n**exchange_modes**\nDefines how the resources can be exchanged.\n\n## ability.type.ExitContainer\n\n```python\nExitContainer(Ability):\n    allowed_containers : set(EntityContainer)\n```\n\nAllows a game entity to exit specified containers of another game entity's `Storage` ability when they are in the container.\n\n**allowed_containers**\nContainers that can be exited.\n\n## ability.type.Fly\n\n```python\nFly(Ability):\n    height : float\n```\n\nAllows a game entity fly at a fixed height.\n\n**height**\nThe height at which the game entity flies. This value is always relative to the ground below.\n\n## ability.type.Formation\n\n```python\nFormation(Ability):\n    formations : set(GameEntityFormation)\n```\n\nAllows a game entity to be part of specified formations.\n\n**formations**\nFormation types that the game entity can be part of.\n\n## ability.type.Foundation\n\n```python\nFoundation(Ability):\n    foundation_terrain : Terrain\n```\n\nChanges the terrain under a game entity on its creation.\n\n**foundation_terrain**\nReplacement terrain that is placed under the space the game entity occupies.\n\n## ability.type.GameEntityStance\n\n```python\nGameEntityStance(Ability):\n    stances: set(GameEntityStance)\n```\n\nDefines activity stances for a game entity. These stances define which abilities will be used without player interaction when the game entity is idle.\n\n**stances**\nStance definitions for the game entity.\n\n## ability.type.Gather\n\n```python\nGather(Ability):\n    auto_resume         : bool\n    resume_search_range : float\n    targets             : set(ResourceSpot)\n    gather_rate         : ResourceRate\n    container           : ResourceContainer\n```\n\nAllows a game entity to gather from resource spots defined in the `Harvestable` abilities of other game entities.\n\n**auto_resume**\nDetermines whether the game entity will automatically resume gathering after the resource spot is depleted. When enabled, the game entity will check if it can refill the depleted resource spot with one of its `Restock` abilities. Otherwise, the game entity will search for a new resource spot it can access with this ability.\n\n**resume_search_range**\nRange in which the game entity searches for a new resource spot if `auto_resume` is enabled.\n\n**targets**\nResource spots that can be accessed and gathered from.\n\n**gather_rate**\nRate at which the game entity collects resources from the targeted resource spot.\n\n**container**\nResource container of the game entity where the gathered resources are stored. The resource container must be referenced by the game entity in a `ResourceStorage` ability.\n\n## ability.type.Harvestable\n\n```python\nHarvestable(Ability):\n    resources              : ResourceSpot\n    harvest_progress       : set(HarvestProgress)\n    restock_progress       : set(RestockProgress)\n    gatherer_limit         : int\n    harvestable_by_default : bool\n```\n\nAssigns a game entity a resource spot that can be harvested by other game entities with the `Gather` ability.\n\n**resource_spot**\nThe resource spot as a `ResourceSpot` object. It defines the contained resource type and resource capacity.\n\n**harvest_progress**\nCan alter the game entity when the percentage of harvested resources reaches defined progress intervals. The objects in the set must have progress type `Harvest`.\n\n**restock_progress**\nCan alter the game entity when the percentage of restocked resources reaches defined progress intervals. The objects in the set must have progress type `Restock`.\n\n**gatherer_limit**\nLimits the amount of gatherers that can access the resource spot simultaneously.\n\n**harvestable_by_default**\nDetermines whether the resource spot is harvestable when it is created. If `True`, the resource spot will be accessible without any conditions as long as the corresponding `Harvestable` ability of the game entity is enabled. When set to `False`, the resource spot must be activated with a `MakeHarvestable` effect. The conditions under which the activation succeeds are decided by the `MakeHarvestable` resistance of the game entity the ability belongs to.\n\n## ability.type.Herd\n\n```python\nHerd(Ability):\n    strength             : int\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nAllows a game entity to change the ownership of other game entities with the `Herdable` ability.\n\n**strength**\nComparison value for situations when the game entity competes with other game entities for a herdable. The game entity with the highest `strength` value will always be prefered, even if other game entities fulfill the condition set by `mode` in `Herdable` better.\n\n**allowed_types**\nWhitelist of game entity types that can be herded.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.Herdable\n\n```python\nHerdable(Ability):\n    adjacent_discover_range : float\n    mode                    : HerdableMode\n```\n\nMakes the game entity switch ownership when it is in range of another game entity with a `Herd` ability. Its new owner is owner of the herding game entity. If the herdable game entity is in range of two or more herding game entities, the ownership changes based on the `mode` set in the `Herdable` ability.\n\n**adjacent_discover_range**\nWhen other herdables are in this range around the herded game entity, they will also be herded and switch ownership.\n\n**mode**\nDetermines who gets ownership of the herdable game entity when multiple game entities using `Herd` are in range.\n\n## ability.type.Idle\n\n```python\nIdle(Ability):\n    pass\n```\n\nUsed for assigning animations and sounds to game entities in an idle state.\n\n## ability.type.LineOfSight\n\n```python\nLineOfSight(Ability):\n    range : float\n```\n\nReveals the area around the game entity on the ingame map.\n\n**range**\nRadius of the area around the game entity that is revealed.\n\n## ability.type.Live\n\n```python\nLive(Ability):\n    attributes : set(AttributeSetting)\n```\n\nAssigns attributes to a game entity.\n\n**attributes**\nConfigures the attribute values of the game entity.\n\n## ability.type.Lock\n\n```python\nLock(Ability):\n    lock_pools : set(LockPool)\n```\n\nDefines lock pools which abilities with the `Lock` property can use.\n\n**lock_pools**\nLock pools definitions.\n\n## ability.type.Move\n\n```python\nMove(Ability):\n    speed      : float\n    modes      : set(MoveMode)\n    path_type  : children(PathType)\n```\n\nAllows a game entity to move around the map.\n\n**speed**\nSpeed of movement.\n\n**modes**\nModes of movements that can be used.\n\n**path_type**\nPath type determining which pathfinding grid is searched to find a path from the start to the goal location.\n\n## ability.type.Named\n\n```python\nNamed(Ability):\n    name             : TranslatedString\n    description      : TranslatedMarkupFile\n    long_description : TranslatedMarkupFile\n```\n\nAssigns the game entity a translatable name and descriptions.\n\n**name**\nName of the game entity as a translatable string.\n\n**description**\nDescription of the game entity as a translatable markup file.\n\n**long_description**\nA longer description of the game entity as a translatable markup file.\n\n## ability.type.OverlayTerrain\n\n```python\nOverlayTerrain(Ability):\n    terrain_overlay : Terrain\n```\n\nTemporarily replace the map terrain the game entity is positioned on with a specified terrain.\n\n**terrain_overlay**\nTerrain that is temporily replaces the existing map terrain.\n\n## ability.type.PassiveTransformTo\n\n```python\nPassiveTransformTo(Ability):\n    condition          : set(LogicElement)\n    transform_time     : float\n    target_state       : StateChanger\n    transform_progress : set(Progress)\n```\n\nActivates a state change for a game entity when a condition is fulfilled.\n\n**condition**\nTriggers the transformation when one condition becomes true.\n\n**transform_time**\nTime to wait (in seconds) after the the transformation is triggered until the target state is activated.\n\n**target_state**\nState change activated after `transform_time` has passed.\n\n**transform_progress**\nCan alter the game entity while the transformation is in progress. The objects in the set must have progress type `Transform`.\n\n## ability.type.Pathable\n\n```python\nPathable(Ability):\n    hitbox     : Hitbox\n    path_costs : dict(children(PathType), int)\n```\n\nLets a game entity influence the pathing costs on the (static) pathfinding grid.\n\nThis ability should only be used for game entitie that never (or rarely) change positions as pathfinding grid recalculations are expensive. For dynamic pathfinding effects, using the `Collision` ability should be preferred.\n\n**hitbox**\nHitbox around the game entity that affects the underlying pathfinding grids. All grid cells that are covered by this hitbox ae influenced by the cost definitions in the `path_costs` attribute.\n\n**path_costs**\nCosts of traversing the area defined by the `hitbox` attribute on the pathfinding grid.\n\nKeys are `PathType` objects that are associated with a pathfinding grid in the pathfinder.\n\nValues represent the pathing cost for the terrain on the pathfinding grid. Each value must be an integer between `1` and `255`. `1` defines the *minimum* possible cost and `254` represents the *maximum* possible cost. `255` signifies that the terrain is impassable for the specified path type.\n\n## ability.type.ProductionQueue\n\n```python\nProductionQueue(Ability):\n    size             : int\n    production_modes : set(ProductionMode)\n```\n\nAllows a game entity to queue production of `CreatableGameEntity` and `ResearchableTech`.\n\n**size**\nMaximum number of production items in the queue.\n\n**production_modes**\nDefines the production items which can be added to the queue.\n\n## ability.type.Projectile\n\n```python\nProjectile(Ability):\n    arc               : int\n    accuracy          : set(Accuracy)\n    target_mode       : TargetMode\n    ignored_types     : set(children(GameEntityType))\n    unignore_entities : set(GameEntity)\n```\n\nGives a game entity projectile behaviour. A projectile is always spawned with another `GameEntity` object as a target. The engine calculates a parabolic path to the target, using the `arc`, `accuracy` and `target_mode` members. While moving along this path, the game entity can use other abilities.\n\n**arc**\nThe starting arc of the projectile as a value between 0 and 360.\n\n**accuracy**\nAccuracy of the projectile. Depending on the type of target entity, a different accuracy might be used.\n\n**target_mode**\nDetermines the end point of the projectile path.\n\n**ignore_types**\nThe projectile will not use abilities on game entities with these types and ignore their hitbox, while moving along the path. However, the projectile will always use abilities on its target, even if it has one of they types in this set.\n\n**unignore_entities**\nWhitelist of game entities who have a type that is listed in `ignore_types`, but should not ne ignored.\n\n## ability.type.ProvideContingent\n\n```python\nProvideContingent(Ability):\n    amount : set(ResourceAmount)\n```\n\nProvides a temporary resource amount to a `ResourceContingent`. The amount is provided until the ability is disabled.\n\n**amount**\nTemporary resource amount that is provided by the game entity.\n\n## ability.type.RallyPoint\n\n```python\nRallyPoint(Ability):\n    pass\n```\n\nAllows a game entity to set a rally point on the map. Game entities spawned by the `Create` ability or ejected from a container will move to the rally point location. The rally point can be placed on another game entity. In that case, the game entities moving there will try to use an appropriate ability on it.\n\n## ability.type.RegenerateAttribute\n\n```python\nRegenerateAttribute(Ability):\n    rate : AttributeRate\n```\n\nRegenerate attribute points at a defined rate. The game entity must have the attribute in its `Live` ability.\n\n**rate**\nRegeneration rate as an `AttributeRate` object.\n\n## ability.type.RegenerateResourceSpot\n\n```python\nRegenerateResourceSpot(Ability):\n    rate          : ResourceRate\n    resource_spot : ResourceSpot\n```\n\nRegenerate the available resources of a game entity's resource spot at a defined rate.\n\n**rate**\nResource regeneration rate.\n\n**resource_spot**\nResource spot that is refilled. The game entity must have a `Harvestable` ability that contains this resource spot.\n\n## ability.type.RemoveStorage\n\n```python\nRemoveStorage(Ability):\n    container        : EntityContainer\n    storage_elements : set(GameEntity)\n```\n\nAllows a game entity to remove specified game entities from one of its containers.\n\n**container**\nContainer the target game entity will be removed from. A `Storage` ability with this container must be enabled.\n\n**storage_elements**\nGame entities that can be removed from the container.\n\n## ability.type.Research\n\n```python\nResearch(Ability):\n    researchables : set(ResearchableTech)\n```\n\nAllows a game entity to research a `Tech` object. Initiating a research will lock the technology for other game entities that can research it. The lock is removed if the research process of the `Tech` is cancelled.\n\n**researchables**\nStores the reference to the researchable techs as well as configuration options for researching.\n\n## ability.type.Resistance\n\n```python\nResistance(Ability):\n    resistances : set(Resistance)\n```\n\nAssigns a game entity resistances to effects applied by other game entities.\n\n**resistances**\nResistances of the game entity against effects.\n\n## ability.type.ResourceStorage\n\n```python\nResourceStorage(Ability):\n    containers : set(ResourceContainer)\n```\n\nAssigns a game entity containers for storing resources until they are dropped at a drop site.\n\n**containers**\nContainers which specify how resources can be stored.\n\n## ability.type.Restock\n\n```python\nRestock(Ability):\n    auto_restock : bool\n    target       : ResourceSpot\n    restock_time : float\n    manual_cost  : Cost\n    auto_cost    : Cost\n    amount       : int\n```\n\nRefills a resource spot with a defined amount of resources.\n\n**auto_restock**\nDetermines whether the game entity will automatically restock a resource spot that it harvested and is depleted.\n\n**target**\nResource spot that can be restocked with this ability.\n\n**restock_time**\nTime until the restocking finishes in seconds.\n\n**manual_cost**\nCost of restocking when the ability is used manually by a player.\n\n**auto_cost**\nCost of restocking when the ability is automatically used by the game entity.\n\n**amount**\nResource amount that is added to the resource spot after restocking.\n\n## ability.type.Selectable\n\n```python\nSelectable(Ability):\n    selection_box : SelectionBox\n```\n\nMakes the game entity selectable by players.\n\n**selection_box**\nDefines the clickable area around the game entity which results in a selection.\n\n## ability.type.SendBackToTask\n\n```python\nSendBackToTask(Ability):\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nEmpties the containers of the `Storage` abilities of a game entity and makes the ejected game entities resume their previous tasks.\n\n**allowed_types**\nWhitelist of game entity types that will be affected.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.ShootProjectile\n\n```python\nShootProjectile(Ability):\n    projectiles              : orderedset(GameEntity)\n    min_projectiles          : int\n    max_projectiles          : int\n    reload_time              : float\n    spawn_delay              : float\n    projectile_delay         : float\n    require_turning          : bool\n    manual_aiming_allowed    : bool\n    spawning_area_offset_x   : float\n    spawning_area_offset_y   : float\n    spawning_area_offset_z   : float\n    spawning_area_width      : float\n    spawning_area_height     : float\n    spawning_area_randomness : float\n    allowed_types            : set(children(GameEntityType))\n    blacklisted_entities     : set(GameEntity)\n```\n\nSpawns projectiles that have a game entity as a target. Projectiles are essentially fire-and-forget objects and only loosely depend on the game entity they originate from.\n\n**projectiles**\nGame entities that can be spawned by the ability. Projectiles are spawned using the order in the set. The *last* `Projectile` is reused when the number of projectiles supposed to be fired is greater than the size of the set.\n\n**min_projectiles**\nMinimum amount of projectiles spawned.\n\n**max_projectiles**\nMaximum amount of projectiles spawned.\n\n**reload_time**\nTime until the ability can be used again in seconds. The timer starts after the *last* projectile has been fired.\n\n**spawn_delay**\nTime to wait between the initiation of the ability and spawning the first projectile in seconds.\n\n**projectile_delay**\nTime to wait until the next projectile is spawned when `min_projectiles > 1` in seconds.\n\n**require_turning**\nDetermines whether the game entity must face the target before it can use the ability.\n\n**manual_aiming_allowed**\nDetermines whether the ability can be aimed at a coordinate instead of a `GameEntity` object.\n\n**spawning_area_offset_x**\nPart of the spawn location coordinates. The spawn location is an offset from the anchor point of the game entity.\n\n**spawning_area_offset_y**\nPart of the spawn location coordinates. The spawn location is an offset from the anchor point of the game entity.\n\n**spawning_area_offset_z**\nPart of the spawn location coordinates. The spawn location is an offset from the anchor point of the game entity.\n\n**spawning_area_width**\nDetermines the spawn area in which the projectile can be spawned randomly.\n\n**spawning_area_height**\nDetermines the spawn area in which the projectile can be spawned randomly.\n\n**spawning_area_randomness**\nValue between 0.0 and 1.0 that determines how far the spawn location can be from the spawn area center. The engine chooses random (x,y) coordinates in the spawn area and multiplies them by this value in order to determine the spawn location.\n\n**allowed_types**\nWhitelist of game entity types that can be targeted with the ability.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.Stop\n\n```python\nStop(Ability):\n    pass\n```\n\nStops all current tasks and returns the game entity to an idle state.\n\n## ability.type.Storage\n\n```python\nStorage(Ability):\n    container       : EntityContainer\n    empty_threshold : set(LogicElement)\n```\n\nAllows a game entity to store other game entities. Stored game entities can influence the state of the storing game entity.\n\n**container**\nConfigres the storage container.\n\n**empty_threshold**\nEjects all game entities from the container if one of the conditions is true.\n\n## ability.type.TerrainRequirement\n\n```python\nTerrainRequirement(Ability):\n    allowed_types        : set(children(TerrainType))\n    blacklisted_terrains : set(Terrain)\n```\n\nMakes a game entity require specific terrains to be placed or moved on.\n\n**allowed_types**\nWhitelist of terrain types that the game entity can be placed or moved on.\n\n**blacklisted_terrains**\nBlacklist for specific terrains that would be covered by `allowed_types`, but should be explicitly excluded.\n\n## ability.type.Trade\n\n```python\nTrade(Ability):\n    trade_routes : set(TradeRoute)\n    container    : ResourceContainer\n```\n\nAllows a game entity to trade with other game entities that have the `TradePost` ability.\n\n**trade_routes**\nTrade routes that can be established with trade posts. The `TradeRoute` object defines rules and traded resources for the trade.\n\n**container**\nResource container of the game entity where the traded resources are stored. The resource container must be referenced by the game entity in a `ResourceStorage` ability.\n\n## ability.type.TradePost\n\n```python\nTradePost(Ability):\n    trade_routes : set(TradeRoute)\n```\n\nMakes a game entity a trade post that other game entities can trade with using their `Trade` ability.\n\n**trade_routes**\nTrade routes that can be established with this trade post. The `TradeRoute` object defines rules and traded resources for the trade.\n\n## ability.type.TransferStorage\n\n```python\nTransferStorage(Ability):\n    storage_element  : GameEntity\n    source_container : EntityContainer\n    target_container : EntityContainer\n```\n\nTransfers a game entity from one container to another.\n\n**storage_element**\nGame entity that is transferred.\n\n**source_container**\nContainer the transferable game entity is stored in.\n\n**target_container**\nContainer the game entity is inserted into. The target container can belong to the same game entity.\n\n## ability.type.Turn\n\n```python\nFly(Ability):\n    turn_speed : float\n```\n\nAllows a game entity to turn on the spot.\n\n**turn_speed**\nSpeed at which the game entity turns. `inf` can be assigned to make the game entity turn immediately.\n\n## ability.type.UseContingent\n\n```python\nUseContingent(Ability):\n    amount : set(ResourceAmount)\n```\n\nReserves a temporary resource amount of a `ResourceContingent`. The amount is freed when the ability is disabled.\n\n**amount**\nTemporary resource amount that is reserved by the game entity.\n\n## ability.type.Visibility\n\n```python\nVisibility(Ability):\n    visible_in_fog : bool\n```\n\nConfigures a game entity's visibility in the fog of war.\n\n**visible_in_fog**\nDetermines whether the game entity is visible in the fog of war.\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_effect.md",
    "content": "# engine.effect\n\nReference documentation of the `engine.effect` module of the openage modding API.\n\n## effect.Effect\n\n```python\nEffect(Object):\n    properties : dict(abstract(EffectProperty), EffectProperty) = {}\n```\n\nGeneralization object for all effects.\n\n**properties**\nFurther specializes the effect beyond the standard behaviour.\n\nThe engine expects objects from the namespace `engine.effect.property.type` as keys. Values must always be an instance of the object used as key.\n\nStandard behavior without properties:\n\n* Effects are **only applied** if the resistor has a **matching** `Resistance` object. Matching can be further defined depending on the concrete effect.\n* Applying them **costs nothing**.\n* Will be applied to targets **with any diplomatic stance**.\n* They only affect the target chosen with the `ApplyContinuousEffect`/`ApplyDiscretEffect` ability.\n* Effects have no preferred order of application.\n\nProperties:\n\n* `Area`: Effects are applied to all game entities in a circular area around the target.\n* `Cost`: Makes effects cost attribute points or resources.\n* `Diplomatic`: Effects only apply to game entities with specified diplomatic stances.\n* `Priority`: Sets the preferred order of application in comparison to other effects.\n\n## effect.property.EffectProperty\n\n```python\nEffectProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of effects.\n\n## effect.property.type.Area\n\n```python\nArea(EffectProperty):\n    range   : float\n    dropoff : DropoffType\n```\n\nApply the effect to game entities in a circular area around the target.\n\n**range**\nThe radius of the area in which game entities are affected.\n\n**dropoff**\nChanges the effectiveness of the effect based on the distance to the center of the circle (see `DropoffType`).\n\n## effect.property.type.Cost\n\n```python\nCost(EffectProperty):\n    cost : Cost\n```\n\nMake the effect cost attribute points or resources.\n\n**cost**\nThe costs for the effect as a `Cost` object.\n\n## effect.property.type.Diplomatic\n\n```python\nDiplomatic(EffectProperty):\n    stances : set(children(DiplomaticStance))\n```\n\nMake the effect only applicable to game entities of players with a specified diplomatic stance.\n\n**stances**\nIf the target is owned by a player has *any* of the specified diplomatic stances, the effect is applied.\n\n## effect.property.type.Priority\n\n```python\nPriority(EffectProperty):\n    priority : int\n```\n\nAssigns a priority to the effect. Priorities determine the order of application when multiple effects are applied via an `EffectBatch`.\n\n**priority**\nPriority value used for comparison with other effects. Effects with higher values are preferred before effects with lower values.\n\n## effect.continuous.ContinuousEffect\n\n```python\nContinuousEffect(Effect):\n    pass\n```\n\nGeneralization object for effects that are applied at a per-second rate.\n\n## effect.continuous.flat_attribute_change.FlatAttributeChange\n\n```python\nFlatAttributeChange(ContinuousEffect):\n    type              : children(AttributeChangeType)\n    min_change_rate   : optional(AttributeRate) = None\n    max_change_rate   : optional(AttributeRate) = None\n    change_rate       : AttributeRate\n    ignore_protection : set(ProtectingAttribute)\n```\n\nGeneralization object for effects that change the resistor's current attribute values at a flat per-second rate. The change value can optionally be limited to an interval with `min_change_rate` as the lower bound and `max_change_rate` as the upper bound.\n\nNote that you cannot use this effect object directly and have to choose one of the specializations `FlatAttributeChangeDecrease` or `FlatAttributeChangeIncrease`.\n\n**type**\nThe effect will be matched with a `resistance.continuous.flat_attribute_change.type` namespace object that stores the same `AttributeChangeType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**min_change_rate**\nThe applied change rate can never go lower than the specified rate.\n\n**max_change_rate**\nThe applied change rate can never go higher than the specified rate.\n\n**change_rate**\nThe gross per-second rate at which the attribute points of the resistor change. The net change rate (applied rate) is calculated by subtracting the resistor's `block_rate` from the effector's `change_rate`.\n\n```math\napplied\\_rate = change\\_rate - block\\_rate\n```\n\nThe applied rate is further bound by the interval defined by `min_change_rate` and `max_change_rate` if these members are set.\n\n**ignore_protection**\nIgnores the `ProtectingAttribute`s in the set when changing the attributes of the target.\n\n## effect.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\n\n```python\nFlatAttributeChangeDecrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `FlatAttributeChange` effect that *decreases* the resistor's current attribute value at a per-second rate.\n\n## effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\n\n```python\nFlatAttributeChangeIncrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `FlatAttributeChange` effect that *increases* the resistor's current attribute value at a per-second rate.\n\n## effect.continuous.lure.type.Lure\n\n```python\nLure(ContinuousEffect):\n    type                        : children(LureType)\n    destination                 : set(GameEntity)\n    min_distance_to_destination : float\n```\n\nMakes the target move to another game entity.\n\n**type**\nThe effect will be matched with a `resistance.continuous.lure.type` namespace object that stores the same `LureType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**destination**\nPossible destinations the target can move to. Whichever game entity is closest will be chosen.\n\n**min_range_to_destination**\nMinimum distance the target has to have to any destination.\n\n## effect.continuous.time_relative_attribute_change.TimeRelativeAttributeChange\n\n```python\nTimeRelativeAttributeChange(ContinuousEffect):\n    type              : children(AttributeChangeType)\n    total_change_time : float\n    ignore_protection : set(ProtectingAttribute)\n```\n\nGeneralization object for effects that change the resistor's current attribute values at a per-second rate relative to the `max_value` of their attribute settings. The change rate is scaled such that it would increase the attribute points of the resistor from `min_value` to `max_value` (or decrease them from `max_value` to `min_value`, respectively) in a fixed amount of time. The current attribute value of the target is not considered. Calculating the change rate can be done by using this formula:\n\n```math\napplied\\_rate = resistor\\_max\\_value / total\\_change\\_time\n```\n\n*Example*: Consider a resistor with 50 max HP / 0 min HP and an effector with a `TimeRelativeAttributeDecrease` time of 5 seconds. The per-second change rate is calculated by dividing the maximum HP value by the time requirement of the effect. Hence, the change rate is 10HP/s. This rate is fix as long as the maximum HP value does not change. If the resistor currently has 30 HP, it would arrive at 0 HP in 3 seconds.\n\n**type**\nThe effect will be matched with a `resistance.continuous.time_relative_attribute_change.type` namespace object that stores the same `AttributeChangeType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**total_change_time**\nThe total time needed to change the resistors attribute points from `max_value` to `min_value` (for `TimeRelativeAttributeDecrease`) or from `min_value` to `max_value` (for `TimeRelativeAttributeIncrease`).\n\n**ignore_protection**\nIgnores the `ProtectingAttribute`s in the set when changing the attributes of the target.\n\n## effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeDecrease\n\n```python\nTimeRelativeAttributeDecrease(TimeRelativeAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeAttributeChange` effect that *decreases* the resistor's current attribute value in a fixed amount of time relative to their attribute's `max_value`.\n\n## effect.continuous.time_relative_attribute_change.type.TimeRelativeAttributeIncrease\n\n```python\nTimeRelativeAttributeIncrease(TimeRelativeAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeAttributeChange` effect that *increases* the resistor's current attribute value in a fixed amount of time relative to their attribute's `max_value`.\n\n## effect.continuous.time_relative_progress_change.TimeRelativeProgressChange\n\n```python\nTimeRelativeProgressChange(ContinuousEffect):\n    type              : children(ProgressType)\n    total_change_time : float\n```\n\nGeneralization object for effects that changes a resistor's current progress amount at a per-second rate relative to 100%. The change rate is scaled such that it would increase the specified progress amount of the resistor from 0% to 100% (or decrease it from 100% to 0%, respectively) in a fixed amount of time. The current progress amount is not considered. Calculating the change rate can be done by using this formula:\n\n```math\napplied\\_rate = 100 / total\\_change\\_time\n```\n\n*Example*: Consider a constructable resistor and an effector with a `TimeRelativeProgressIncrease` time of 10 seconds. The per-second change rate is calculated by dividing 100% by the time requirement of the effect. Hence, the change rate is 10%/s. This rate is fix. If the resistor currently has 30% construction progress, it would be fully constructed in 7 seconds.\n\n**type**\nThe effect will be matched with a `resistance.continuous.time_relative_progress_change.type` namespace object that stores the same `ProgressType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**total_change_time**\nThe total time needed to change the resistors attribute points from 100% to 0% (for `TimeRelativeProgressDecrease`) or from 0% to 100% (for `TimeRelativeProgressIncrease`).\n\n## effect.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\n\n```python\nTimeRelativeProgressDecrease(TimeRelativeProgressChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeProgressChange` effect that *decreases* the resistor's progress amount in a fixed amount of time relative to 100%.\n\n## effect.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\n\n```python\nTimeRelativeProgressIncrease(TimeRelativeProgressChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeProgressChange` effect that *increases* the resistor's progress amount in a fixed amount of time relative to 100%.\n´\n## effect.discrete.DiscreteEffect\n\n```python\nDiscreteEffect(Effect):\n    pass\n```\n\nGeneralization object for effects that are applied immediately.\n\n## effect.discrete.convert.Convert\n\n```python\nConvert(DiscreteEffect):\n    type               : children(ConvertType)\n    min_chance_success : optional(float) = None\n    max_chance_success : optional(float) = None\n    chance_success     : float\n    cost_fail          : optional(Cost) = None\n```\n\nChange the owner of the target unit to the player who owns the effector game entity.\n\n**type**\nThe effect will be matched with a `resistance.discrete.convert.type` namespace object that stores the same `ConvertType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**min_chance_success**\nThe applied chance can never go lower than the specified percentage.\n\n**max_chance_success**\nThe applied chance can never go higher than the specified percentage.\n\n**chance_success**\nGross chance for the conversion to succeed as a percentage chance. The percentage should be stored as a float value between *0.0* and *1.0*. The net chance (applied chance) of success is calculated by subtracting the resistor's `chance_resist` from the effector's `chance_success`.\n\n```math\napplied\\_chance = chance\\_success - chance\\_resist\n```\n\nAny value below *0.0* is an automatic failure, while any value above *1.0* is an automatic success. The applied chance is further bound by the interval defined by `min_chance_success` and `max_chance_success`, if these members are set.\n\n**cost_fail**\nThe amount of attribute points or resources removed from the effector if the conversion fails.\n\n## effect.discrete.convert.type.AoE2Convert\n\n```python\nAoE2Convert(Convert):\n    skip_guaranteed_rounds : int\n    skip_protected_rounds  : int\n```\n\nSpecialized conversion effect that is implemented in AoE2. The convert chance at the start is guaranteed to be *0.0* for `guaranteed_resist_rounds` rounds and guaranteed to be *1.0* after `protected_rounds` rounds (both defined by the `resistance.discrete.convert.type.AoE2Convert` object).\n\nWhen the effector stops applying the effect, the resistor's protected rounds are increased until they reach their maximum value again. Running out of the range of the effector does not count as stopping the effect application.\n\n**skip_guaranteed_rounds**\nLowers the number of rounds that the resistor is guaranteed to resist every time.\n\n**skip_protected_rounds**\nLowers the number of rounds that are needed for the success chance to always be *1.0*.\n\n## effect.discrete.flat_attribute_change.FlatAttributeChange\n\n```python\nFlatAttributeChange(DiscreteEffect):\n    type              : children(AttributeChangeType)\n    min_change_value  : optional(AttributeAmount) = None\n    max_change_value  : optional(AttributeAmount) = None\n    change_value      : AttributeAmount\n    ignore_protection : set(ProtectingAttribute)\n```\n\nGeneralization object for effects that change a resistor's current attribute value by a flat amount. The change value can optionally be limited to an interval with `min_change_value` as the lower bound and `max_change_value` as the upper bound.\n\nNote that you cannot use this effect object directly and have to choose one of the specializations `FlatAttributeChangeDecrease` or `FlatAttributeChangeIncrease`.\n\n**type**\nThe effect will be matched with a `resistance.discrete.flat_attribute_change.type` namespace object that stores the same `AttributeChangeType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**min_change_value**\nThe applied change value can never go lower than the specified amount.\n\n**max_change_value**\nThe applied change value can never go higher than the specified amount.\n\n**change_value**\nThe gross amount by that the attribute points of the resistor change. The net change value (applied value) is calculated by subtracting the resistor's `block_value` from the effector's `change_value`.\n\n```math\napplied\\_value = change\\_value - block\\_value\n```\n\nThe applied rate is further bound by the interval defined by `min_change_value` and `max_change_value`, if these members are set.\n\n**ignore_protection**\nIgnores the `ProtectingAttribute`s in the set when changing the attributes of the target.\n\n## effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\n\n```python\nFlatAttributeChangeDecrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the discrete `FlatAttributeChange` effect that decreases the resistor's current attribute value by a flat amount.\n\n## effect.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\n\n```python\nFlatAttributeChangeIncrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the discrete `FlatAttributeChange` effect that increases the resistor's current attribute value by a flat amount.\n\n## effect.discrete.make_harvestable.type.MakeHarvestable\n\n```python\nMakeHarvestable(DiscreteEffect):\n    resource_spot : ResourceSpot\n```\n\nMakes a resource spot harvestable, if it is not already harbestable by default.\n\n**resource_spot**\nResource spot that should be made harvestable. The effect will be matched with a `resistance.discrete.make_harvestable.type` namespace object that stores the same `ResourceSpot` object in its `resource_spot` member. Additionally, the target needs to have a `Harvestable` ability that contains the resource spot.\n\n## effect.discrete.send_to_container.type.SendToContainer\n\n```python\nSendToContainer(DiscreteEffect):\n    type     : children(SendToStorageType)\n    storages : set(EntityContainer)\n```\n\nMakes the target move to and enter the nearest game entity where it can be stored. The resistor needs an `EnterContainer` ability for at least one of the containers for this to work.\n\n**type**\nThe effect will be matched with a `resistance.discrete.send_to_container.type` namespace object that stores the same `SendToStorageType` object in its `type` member. Otherwise, the effect will not be applied.\n\n**storages**\nContainers the target can enter. The target will choose the nearest game entity which it references one of these containers and for which it has a `EnterContainer` ability.\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_modifier.md",
    "content": "# engine.modifier\n\nReference documentation of the `engine.modifier` module of the openage modding API.\n\n## modifier.Modifier\n\n```python\nModifier(Object):\n    properties : dict(abstract(ModifierProperty), ModifierProperty) = {}\n```\n\nGeneralization object for all modifiers. Modifiers change the behavior of abilities at for general and edge cases. They can influence more than one ability at a time.\n\n**properties**\nFurther specializes the modifier beyond the standard behaviour.\n\nThe engine expects objects from the namespace `engine.modifier.property.type` as keys. Values must always be an instance of the object used as key.\n\nStandard behavior without properties:\n\n* Modifiers in the `modifiers` set of `GameEntity` are considered enabled as soon as the game entity is created, unless a `StateChager` object disables them.\n* If the modifier is assigned to a `GameEntity` object, the modifications only apply to this game entity.\n* If the modifier is assigned to a `Civilization` object, the modifications apply to all game entities of this civilization.\n* Modifiers stack by default.\n\nProperties:\n\n* `Multiplier`: The associated member value in the ability targeted by the modifier is multiplied with a defined factor.\n* `Scoped`: The modifier is applied to a defined set of game entities, regardless of where it is assigned.\n* `Stacked`: Allows defining a limit on how often the modifier effect is stacked.\n\n## modifier.property.ModifierProperty\n\n```python\nModifierProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of modifiers.\n\n## modifier.property.type.Multiplier\n\n```python\nMultiplier(ModifierProperty):\n    multiplier : float\n```\n\nMultiples the value of the associated ability member with a defined factor.\n\n**multiplier**\nMultiplication factor.\n\n## modifier.property.type.Scoped\n\n```python\nScoped(ModifierProperty):\n    stances : set(children(DiplomaticStance))\n    scope   : children(ModifierScope)\n```\n\nApplies the modifier to a defined set of game entities. The modifier affects these game entities as long as it stays enabled for the game entity it is assigned to.\n\n**diplomatic_stances**\nApplies the modifiers to players that the owner of the game entity has these diplomatic stances with.\n\n**scope**\nDefines the game entities affected by this modifier.\n\n## modifier.property.type.Stacked\n\n```python\nStacked(ModifierProperty):\n    stack_limit : int\n```\n\nDefines how often a modifier can be applied to the same game entity.\n\n**stack_limit**\nMaximum number of times the modifier can be stacked.\n\n## modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\n\n```python\nElevationDifferenceHigh(Modifier):\n    min_elevation_difference : optional(float) = None\n```\n\nChanges the cumulated *change value* of `FlatAtttributeChange` effects when the effector containing this modifier is located *higher* than the targeted resistor.\n\n**min_elevation_difference**\nThe minimum elevation difference between effector and resistor.\n\n## modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\n\n```python\nElevationDifferenceLow(Modifier):\n    min_elevation_difference : optional(float) = None\n```\n\nChanges the cumulated *change value* of `FlatAtttributeChange` effects when the effector containing this modifier is located *lower* than the targeted resistor.\n\n**min_elevation_difference**\nThe minimum elevation difference between effector and resistor.\n\n## modifier.effect.flat_attribute_change.type.Flyover\n\n```python\nFlyover(Modifier):\n    relative_angle       : float\n    flyover_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nChanges the accumulated *applied change value* of `FlatAtttributeChange` effects of a projectile's attack if the projectile path went over specified game entity types.\n\n**relative_angle**\nMaximum difference between the relative angle of the effector and the flyover entity in degrees.\n\n**flyover_types**\nWhitelist of game entity types that must be under the patch of the projectile. The game entities must have the `Hitbox` ability.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `flyover_types`, but should be excplicitly excluded.\n\n## modifier.effect.flat_attribute_change.type.Terrain\n\n```python\nTerrain(Modifier):\n    terrain : Terrain\n```\n\nChanges the accumulated *applied change value* of `FlatAtttributeChange` effects when the target game entity is on a specified terrain.\n\n**terrain**\nTerrain the targeted game entity must stand on.\n\n## modifier.effect.flat_attribute_change.type.Unconditional\n\n```python\nUnconditional(Modifier):\n    pass\n```\n\nChanges the accumulated *applied change value* of `FlatAtttributeChange` effects without any conditions.\n\n## modifier.effect.type.TimeRelativeAttributeChange\n\n```python\nTimeRelativeAttributeChange(Modifier):\n    pass\n```\n\nChanges the `total_change_time` member of `TimeRelativeAttributeChange` effects.\n\n## modifier.effect.type.TimeRelativeProgressChange\n\n```python\nTimeRelativeProgressChange(Modifier):\n    pass\n```\n\nChanges the `total_change_time` member of `TimeRelativeProgress` effects.\n\n## modifier.resistance.flat_attribute_change.type.ElevationDifferenceHigh\n\n```python\nElevationDifferenceHigh(Modifier):\n    min_elevation_difference : optional(float) = None\n```\n\nChanges the cumulated *change value* of `FlatAtttributeChange` resistances when the resistor containing this modifier is located *higher* than the effector.\n\n**min_elevation_difference**\nThe minimum elevation difference between effector and resistor.\n\n## modifier.resistance.flat_attribute_change.type.ElevationDifferenceLow\n\n```python\nElevationDifferenceLow(Modifier):\n    min_elevation_difference : optional(float) = None\n```\n\nChanges the cumulated *change value* of `FlatAtttributeChange` resistances when the resistor containing this modifier is located *lower* than the effector.\n\n**min_elevation_difference**\nThe minimum elevation difference between effector and resistor.\n\n## modifier.resistance.flat_attribute_change.type.Stray\n\n```python\nStray(Modifier):\n    pass\n```\n\nChanges the cumulated *applied change value* of `FlatAtttributeChange` resistances for a projectile when the resistor was not the intended target.\n\n## modifier.resistance.flat_attribute_change.type.Terrain\n\n```python\nTerrain(Modifier):\n    terrain : Terrain\n```\n\nChanges the cumulated *applied change value* of `FlatAtttributeChange` resistances when the resisting game entity is on a specified terrain.\n\n**terrain**\nThe terrain the game entity must stand on.\n\n## modifier.resistance.flat_attribute_change.type.Unconditional\n\n```python\nUnconditional(Modifier):\n    pass\n```\n\nChanges the accumulated *applied change value* of `FlatAtttributeChange` resistances without any conditions.\n\n## modifier.type.AbsoluteProjectileAmount\n\n```python\nAbsoluteProjectileAmount(Modifier):\n    amount : float\n```\n\nIncreases the projectile amount of `ShootProjectile` abilities by a fixed value. The total amount is limited by the `max_projectiles` member in `ShootProjectile`.\n\n**amount**\nAmount of projectiles added.\n\n## modifier.type.AoE2ProjectileAmount\n\n```python\nAoE2ProjectileAmount(Modifier):\n    provider_abilities : set(ApplyDiscreteEffect)\n    receiver_abilities : set(ApplyDiscreteEffect)\n    change_types       : set(children(AttributeChangeType))\n```\n\nCompares the raw change value of two sets of `ApplyDiscreteEffect` abilities. The final amount is calculated by using this formula:\n\n```math\namount = (change\\_value\\_provider / reload\\_time\\_provider) / change\\_value\\_receiver\n```\n\n**provider_abilities**\nThe abilities of the *provider*, i.e. the game entity that provides the projectile amount to a receiving game entity. These abilities do not have to be assigned to a game entity.\n\n**receiver_abilities**\nThe abilities of the *receiver*, i.e. the game entity that has its projectile amount changed. These abilities do not have to be assigned to a game entity.\n\n**change_types**\nThe change types of the abilities that are considered as `AttributeChangeType` objects.\n\n## modifier.type.AttributeSettingsValue\n\n```python\nAttributeSettingsValue(Modifier):\n    attribute : Attribute\n```\n\nChanges the `starting_value` **and** `max_value` members of an `AttributeSettings` object in the `Live` ability.\n\n**attribute**\n`AttributeSettings` objects with this attribute are considered.\n\n## modifier.type.EntityContainerCapacity\n\n```python\nEntityContainerCapacity(Modifier):\n    container : EntityContainer\n```\n\nChanges the `size` member of an `EntityContainer` object in a `Storage` ability. Resulting values are floored.\n\n**container**\nThe container which is considered.\n\n## modifier.type.ContinuousResource\n\n```python\nContinuousResource(Modifier):\n    rates : set(ResourceRate)\n```\n\nProvides a continuous trickle of resources while the modifier is enabled.\n\n**rates**\nThe resource rates as `ResourceRate` objects.\n\n## modifier.type.CreationAttributeCost\n\n```python\nCreationAttributeCost(Modifier):\n    attributes : set(Attribute)\n    creatables : set(CreatableGameEntity)\n```\n\nChanges the attribute amount of `AttributeCost` objects in `CreatableGameEntity` objects.\n\n**attributes**\nLimits the modifier to `AttributeAmount` objects referencing attributes from this set.\n\n**creatables**\n`CreatableGameEntity` objects that are considered.\n\n## modifier.type.CreationResourceCost\n\n```python\nCreationResourceCost(Modifier):\n    resources  : set(Resource)\n    creatables : set(CreatableGameEntity)\n```\n\nChanges the resource amount of `ResourceCost` objects in `CreatableGameEntity` objects.\n\n**resources**\nLimits the modifier to `ResourceAmount` objects referencing resources from this set.\n\n**creatables**\nThese `CreatableGameEntity` objects are considered.\n\n## modifier.type.CreationTime\n\n```python\nCreationTime(MultiplierModifier):\n    creatables : set(CreatableGameEntity)\n```\n\nChanges the `creation_time` member of a `CreatableGameEntity` object in the `Create` ability.\n\n**creatables**\n`CreatableGameEntity` objects that are considered.\n\n## modifier.type.DepositResourcesOnProgress\n\n```python\nDepositResourcesOnProgress(Modifier):\n    progress_status      : ProgressStatus\n    resources            : set(Resource)\n    affected_types       : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nDeposits resources of a game entity into the players resource pool if the game entity was involved in advancing the progress via `TimeRelativeProgressChange` effects.\n\n**progress_status**\nProgress status at which the resources are deposited. The `TimeRelativeProgress` effect must be applied at the exact same time as the progress is reached.\n\n**resources**\nResources that are dropped off.\n\n**affected_types**\nWhitelist of game entity types that can trigger the deposit action when the progress status is reached. The game entities do not need a `DropSite` ability.\n\n**blacklisted_game_entities**\nBlacklist for specific game entities that would be covered by `affected_types`, but should be excplicitly excluded.\n\n## modifier.type.DiplomaticLineOfSight\n\n```python\nDiplomaticLineOfSight(Modifier):\n    diplomatic_stance : children(DiplomaticStance)\n```\n\nActivates line of sight for game entities of players with the specified diplomatic stance are also visible to owner of the modifier.\n\n**diplomatic_stance**\nPlayers with these stances share ther line of sight with the modifier owner.\n\n## modifier.type.GatheringEfficiency\n\n```python\nGatheringEfficiency(Modifier):\n    resource_spot : ResourceSpot\n```\n\nChanges the amount of resources that are removed from a specific resource spot's resource amount while gathering.\n\n*Example*: Consider a gold resource spot containing 100 gold and a game entity with a `GatheringEfficiency` modifier for this resource spot with multiplier `0.8`. For an amount of 10 gold that the game entity gathers, the resource spot will remove only `0.8` times this amount, i.e. the resource spot only loses 8 gold. This effectively increases the yield of the resource spot to 125 gold for the game entity.\n\n**resource_spot**\nResource spot for which the efficiency is changed.\n\n## modifier.type.GatheringRate\n\n```python\nGatheringRate(Modifier):\n    resource_spot : ResourceSpot\n```\n\nChanges the gathering rate in a game entity's `Gather` ability for a specific resource spot.\n\n**resource_spot**\nResource spot for which the gathering rate is changed.\n\n## modifier.type.InContainerContinuousEffect\n\n```python\nInContainerContinuousEffect(Modifier):\n    containers : set(EntityContainer)\n    ability    : ApplyContinuousEffect\n```\n\nMakes a game entity apply continuous effects on itself while stored in specified containers.\n\n**containers**\nThe containers where the continuous effects are applied.\n\n**ability**\nAbility that is used to apply the effects. It does not have to be an ability of the game entity. Other modifiers of the game entity apply while using the ability.\n\n## modifier.type.InContainerDiscreteEffect\n\n```python\nInContainerDiscreteEffect(Modifier):\n    containers : set(EntityContainer)\n    ability    : ApplyDiscreteEffect\n```\n\nMakes a game entity apply discrete effects on itself while stored in specified containers.\n\n**containers**\nThe containers where the discrete effects are applied.\n\n**ability**\nAbility that is used to apply the effects. It does not have to be an ability of the game entity. Other modifiers of the game entity apply while using the ability.\n\n## modifier.type.InstantTechResearch\n\n```python\nInstantTechResearch(Modifier):\n    tech      : Tech\n    condition : set(LogicElement)\n```\n\nInstantly unlocks a `Tech` and applies its patches when the condition is fulfilled.\n\n**tech**\nTechnology that is researched.\n\n**condition**\nCondition that need to be fulfilled to trigger the research.\n\n## modifier.type.MoveSpeed\n\n```python\nMoveSpeed(MultiplierModifier):\n    pass\n```\n\nChanges the `move_speed` member of the `Move` ability.\n\n## modifier.type.RefundOnCondition\n\n```python\nRefundOnCondition(Modifier):\n    refund_amount : set(ResourceAmount)\n    condition     : set(LogicElement)\n```\n\nReturns a fixed amount of resources back to the player after the condition has been fulfilled.\n\n**refund_amount**\nAmount of resources that are added to the player's resource pool.\n\n**condition**\nCondition that triggers the refund.\n\n## modifier.type.ReloadTime\n\n```python\nReloadTime(Modifier):\n    pass\n```\n\nChanges the `reload_time` member of `ApplyDiscreteEffect` and `ShootProjectile` abilities.\n\n## modifier.type.ResearchAttributeCost\n\n```python\nResearchAttributeCost(Modifier):\n    attributes    : set(Attribute)\n    researchables : set(CreatableGameEntity)\n```\n\nChanges the attribute amount of `AttributeCost` objects in `ResearchableTech` objects.\n\n**attributes**\nLimits the modifier to `AttributeAmount` objects referencing `Attribute` objects from this set.\n\n**researchables**\n`ResearchableTech` objects that are considered.\n\n## modifier.type.ResearchResourceCost\n\n```python\nResearchResourceCost(Modifier):\n    resources     : set(Resource)\n    researchables : set(ResearchableTech)\n```\n\nChanges the resource amount of `ResourceCost` objects in `ResearchableTech` objects.\n\n**resources**\nLimits the modifier to `ResourceAmount` objects referencing `Resource` objects from this set.\n\n**researchables**\n`ResearchableTech` objects that are considered.\n\n## modifier.type.ResearchTime\n\n```python\nResearchTime(Modifier):\n    researchables : set(ResearchableTech)\n```\n\nChanges the `research_time` member of a `ResearchableTech` object in the `Research` ability.\n\n**researchables**\n`ResearchableTech` objects that are considered.\n\n## modifier.type.Reveal\n\n```python\nReveal(Modifier):\n    line_of_sight        : float\n    affected_types       : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nReveals an area around specified game entities.\n\n**line_of_sight**\nRadius of the visible area around the game entity.\n\n**affected_types**\nWhitelist of game entity types that the modifier should apply to.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `affected_types`, but should be excplicitly excluded.\n\n## modifier.type.StorageElementCapacity\n\n```python\nStorageElementCapacity(Modifier):\n    storage_element : StorageElementDefinition\n```\n\nChanges the `elements_per_slot` member of a `StorageElementDefinition` object in a container. Resulting values are floored.\n\n**storage_element**\nStorage element which is considered.\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_resistance.md",
    "content": "# engine.resistance\n\nReference documentation of the `engine.resistance` module of the openage modding API.\n\n## resistance.Resistance\n\n```python\nResistance(Object):\n    properties : dict(abstract(ResistanceProperty), ResistanceProperty) = {}\n```\n\nGeneralization object for all resistances.\n\n**properties**\nFurther specializes the resistance beyond the standard behaviour.\n\nThe engine expects objects from the namespace `engine.resistance.property.type` as keys. Values must always be an instance of the object used as key.\n\nStandard behavior without properties:\n\n* Effects are **only applied** if the resistor has a **matching** `Resistance` object. Matching can be further defined depending on the concrete effect.\n* Resisting **costs nothing**.\n* When multiple effectors are applying effects at the same time, results are accumulated without adjustments.\n\nProperties:\n\n* `Cost`: Makes resistances cost attribute points or resources.\n* `Stacked`: Adjust effect calculation when multiple effectors are applying effects simultaneously.\n\n## resistance.property.ResistanceProperty\n\n```python\nResistanceProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of resistances.\n\n## resistance.property.type.Cost\n\n```python\nCost(ResistanceProperty):\n    cost : Cost\n```\n\nMake the resistance cost attribute points or resources.\n\n**cost**\nThe amount of attribute points or resources removed from the resistor.\n\n## resistance.property.type.Stacked\n\n```python\nStacked(ResistanceProperty):\n    stack_limit       : int\n    calculation_type  : CalculationType\n    distribution_type : DistributionType\n```\n\nConfigure the calculation of the applied value when a resistor if affected by multiple effectors.\n\nThe final applied value is the product of\n\n* The accumulated and averaged change values of all effects (`dist_value`)\n* An influence factor based on the number of effectors (`stack_factor`).\n\n```math\napplied\\_value = stack\\_factor * dist\\_value\n```\n\nThe calculation method for both of these values is set in this property.\n\n**stack_limit**\nMaximum number of effectors that can apply their effects on the resistor. The effectors are chosen on a *first-come-first-serve* basis. Setting the stack limit to `inf` allows an unlimited number of effectors.\n\n**calculation_type**\nThe calculation method used to determine the *stack_factor*.\n\n**distribution_type**\nThe calculation method used to determine the *dist_value*.\n\n## resistance.continuous.ContinuousResistance\n\n```python\nContinuousResistance(Resistance):\n    pass\n```\n\nGeneralization object for resistances to continuous effects.\n\n## resistance.continuous.flat_attribute_change.FlatAttributeChange\n\n```python\nFlatAttributeChange(ContinuousResistance):\n    type       : children(AttributeChangeType)\n    block_rate : set(AttributeRate)\n```\n\nGeneralization object for resistances to continuous `FlatAttributeChange` effects.\n\nNote that you cannot use this resistance object directly and have to choose one of the specializations `FlatAttributeChangeDecrease` or `FlatAttributeChangeIncrease`.\n\n**type**\nEffects of type `effect.continuous.flat_attribute_change.FlatAttributeChange` are matched to this resistance if they store the same `AttributeChangeType` object in their `type` member. Otherwise, the effect will not be applied.\n\n**block_rate**\nThe per-second attribute rate that is blocked by the resistor. The net change rate (applied rate) is calculated by subtracting the resistor's `block_rate` from the effector's `change_rate`.\n\n```math\napplied\\_rate = change\\_rate - block\\_rate\n```\n\nThe applied rate is further bound by the interval defined by `min_change_rate` and `max_change_rate` in the effect, if these members are set.\n\n## resistance.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\n\n```python\nFlatAttributeChangeDecrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `FlatAttributeChange` resistance that blocks the effector's attribute decrease rate.\n\n\n## resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\n\n```python\nFlatAttributeChangeIncrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `FlatAttributeChange` resistance that blocks the effector's attribute increase rate.\n\n## resistance.continuous.lure.type.Lure\n\n```python\nLure(ContinuousResistance):\n    type : children(LureType)\n```\n\nResistance to the `Lure` effect.\n\n**type**\nEffects of type `effect.continuous.lure.type.Lure` are matched to this resistance if they store the same `LureType` object in their `type` member. Otherwise, the effect will not be applied.\n\n## resistance.continuous.time_relative_attribute_change.TimeRelativeAttributeChange\n\n```python\nTimeRelativeAttributeChange(ContinuousResistance):\n    type : children(AttributeChangeType)\n```\n\nGeneralization object for resistances to continuous `TimeRelativeAttributeChange` effects.\n\nNote that you cannot use this resistance object directly and have to choose one of the specializations `TimeRelativeAttributeDecrease` or `TimeRelativeAttributeIncrease`.\n\n**type**\nEffects of type `effect.continuous.time_relative_attribute_change.TimeRelativeAttributeChange` are matched to this resistance if they store the same `AttributeChangeType` object in their `type` member. Otherwise, the effect will not be applied.\n\n## resistance.continuous.time_relative_attribute_change.type.TimeRelativeAttributeDecrease\n\n```python\nTimeRelativeAttributeDecrease(TimeRelativeAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeAttributeChange` resistance that decreases the resistor's current attribute value in a fixed amount of time relative to their attribute's `max_value`.\n\n## resistance.continuous.time_relative_attribute_change.type.TimeRelativeAttributeIncrease\n\n```python\nTimeRelativeAttributeIncrease(TimeRelativeAttributeChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeAttributeChange` resistance that increases the resistor's current attribute value in a fixed amount of time relative to their attribute's `max_value`.\n\n## resistance.continuous.time_relative_progress.TimeRelativeProgressChange\n\n```python\nTimeRelativeProgressChange(ContinuousResistance):\n    type : children(ProgressType)\n```\n\nGeneralization object for resistances to continuous `TimeRelativeProgressChange` effects.\n\nNote that you cannot use this resistance object directly and have to choose one of the specializations `TimeRelativeProgressDecrease` or `TimeRelativeProgressIncrease`.\n\n**type**\nEffects of type `effect.continuous.time_relative_progress.TimeRelativeProgressChange` are matched to this resistance if they store the same `ProgressType` object in their `type` member. Otherwise, the effect will not be applied.\n\n## resistance.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\n\n```python\nTimeRelativeProgressDecrease(TimeRelativeProgressChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeProgressChange` resistance that decreases the resistor's progress amount in a fixed amount of time relative to 100%.\n\n## resistance.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\n\n```python\nTimeRelativeProgressIncrease(TimeRelativeProgressChange):\n    pass\n```\n\nSpecialization of the continuous `TimeRelativeProgressChange` resistance that increases the resistor's progress amount in a fixed amount of time relative to 100%.\n\n## resistance.discrete.DiscreteResistance\n\n```python\nDiscreteResistance(Resistance):\n    pass\n```\n\nGeneralization object for resistances to discrete effects.\n\n## resistance.discrete.convert.Convert\n\n```python\nConvert(DiscreteResistance):\n    type          : children(ConvertType)\n    chance_resist : float\n```\n\nResistance to the `Convert` effect.\n\n**type**\nEffects of type `effect.discrete.convert.Convert` are matched to this resistance if they store the same `ConvertType` object in their `type` member. Otherwise, the effect will not be applied.\n\n**chance_resist**\nPercentage amount subtracted from the effector's success chance. The percentage should be stored as a float value between *0.0* and *1.0*. The net chance (applied chance) of success is calculated by subtracting the resistor's `chance_resist` from the effector's `chance_success`.\n\n```math\napplied\\_chance = chance\\_success - chance\\_resist\n```\n\nAny value below *0.0* is an automatic fail, while any value above *1.0* is an automatic success. The applied chance is further bound by the interval defined by `min_chance_success` and `max_chance_success` in the effect, if these members are set.\n\n## resistance.discrete.convert.type.AoE2Convert\n\n```python\nAoE2Convert(Convert):\n    guaranteed_resist_rounds       : int\n    protected_rounds               : int\n    protection_round_recharge_time : float\n```\n\nResistance to the `AoE2Convert` effect.\n\n**guaranteed_resist_rounds**\nNumber of rounds at the start where the success chance is guaranteed to be *0.0*.\n\n**protected_rounds**\nNumber of rounds that are needed for the success chance to always be *1.0*.\n\n**protection_round_recharge_time**\nTime it takes until one protected round is added back to the number of protected rounds.\n\nWhen the effector stops applying the effect, the number of protected rounds is increased until they reach their maximum value again. Running out of the range of the effector does not count as stopping the effect application.\n\n## resistance.discrete.flat_attribute_change.FlatAttributeChange\n\n```python\nFlatAttributeChange(DiscreteResistance):\n    type        : children(AttributeChangeType)\n    block_value : set(AttributeAmount)\n```\n\nGeneralization object for resistances to discrete `FlatAttributeChange` effects.\n\nNote that you cannot use this resistance object directly and have to choose one of the specializations `FlatAttributeChangeDecrease` or `FlatAttributeChangeIncrease`.\n\n**type**\nEffects of type `effect.discrete.flat_attribute_change.FlatAttributeChange` are matched to this resistance if they store the same `AttributeChangeType` object in their `type` member. Otherwise, the effect will not be applied.\n\n**change_value**\nThe flat attribute amount that is blocked by the resistor. The net change value (applied value) is calculated by subtracting the resistor's `block_value` from the effector's `change_value`.\n\n```math\napplied\\_value = change\\_value - block\\_value\n```\n\nThe applied value is further bound by the interval defined by `min_change_value` and `max_change_value` in the effect, if these members are set.\n\n## resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\n\n```python\nFlatAttributeChangeDecrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the discrete `FlatAttributeChange` resistance that blocks the effector's attribute decrease value.\n\n\n## resistance.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\n\n```python\nFlatAttributeChangeIncrease(FlatAttributeChange):\n    pass\n```\n\nSpecialization of the discrete `FlatAttributeChange` resistance that blocks the effector's attribute increase value.\n\n## resistance.discrete.make_harvestable.type.MakeHarvestable\n\n```python\nMakeHarvestable(DiscreteResistance):\n    resource_spot     : ResourceSpot\n    resist_condition  : set(LogicElement)\n```\n\nResistance to the `MakeHarvestable` effect.\n\n**resource_spot**\nResource spot that should be made harvestable. Effects of type `effect.discrete.make_harvestable.type.MakeHarvestable` are matched to this resistance if they store the same `ResourceSpot` object in their `resource_spot` member. Additionally, the target needs to have a `Harvestable` ability that contains the resource spot.\n\n**resist_condition**\nCondition which must be fulfilled to make the resource spot harvestable.\n\n## resistance.discrete.send_to_container.type.SendToContainer\n\n```python\nSendToContainer(DiscreteResistance):\n    type              : children(SendToContainerType)\n    search_range      : float\n    ignore_containers : set(EntityContainer)\n```\n\nResistance to the `SendToContainer` effect.\n\n**type**\nEffects of type `effect.discrete.send_to_container.type.SendToContainer` are matched to this resistance if they store the same `SendToContainerType` object in their `type` member. Otherwise, the effect will not be applied.\n\n**search_range**\nThe range in which the resistor will search for a container.\n\n**ignore_containers**\nExcludes the containers from the set when searching for a target container.\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_root.md",
    "content": "# engine.root\n\nReference documentation of the `engine.root` module of the openage modding API.\n\n## root.Object\n\n```python\nObject():\n    pass\n```\n\nRoot object of the API. All other objects inherit from it, either explicitly or implicitly.\n"
  },
  {
    "path": "doc/nyan/api_reference/reference_util.md",
    "content": "# engine.util\n\nReference documentation of the `engine.util` module of the openage modding API.\n\n## util.accuracy.Accuracy\n\n```python\nAccuracy(Object):\n    accuracy             : float\n    accuracy_dispersion  : float\n    dispersion_dropoff   : DropOffType\n    target_types         : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nStores information for the accuracy calculation of a game entity with the `Projectile` ability.\n\n**accuracy**\nChance for the projectile to land at the \"perfect\" position to hit its target. The value represents a percentage and must be between 0.0 and 1.0.\n\n**accuracy_dispersion**\nMaximum accuracy dispersion when the projectile fails the accuracy check.\n\n**dispersion_dropoff**\nMultiplies the maximum dispersion with a dropoff factor. The dropoff depends on the distance of the projectile spawning game entity in relation to the `max_range` of its `ShootProjectile` ability.\n\n**target_types**\nGame entities types for which the accuracy value can be used.\n\n**blacklisted_entities**\nBlacklists game entities that have one of the types listed in `target_types`, but should not be covered by this `Accuracy` object.\n\n## util.activity.Activity\n\n```python\nActivity(Object):\n    start : Start\n```\n\nStores a node graph for the behaviour of a game entity. Activities are assigned to game entities with the `Activity` ability.\n\n**start**\nStarting node of the activity.\n\n## util.activity.condition.Condition\n\n```python\nCondition(Object):\n    node : Node\n```\n\nGeneralization object for conditions that can be used in `XORGate` nodes.\n\n**node**\nNode that is visited when the condition is true.\n\n## util.activity.condition.type.AbilityUsable\n\n```python\nAbilityUsable(Condition):\n    ability : abstract(Ability)\n```\n\nIs true when an ability can be used by the game entity when the node is visited.\n\n**ability**\nAbility definition used for the usability check. This can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity **and** be enabled for the check to pass. If an API object is referenced, at least one ability of the same type must be enabled for the check to pass.\n\n## util.activity.condition.type.CommandInQueue\n\n```python\nCommandInQueue(Condition):\n    pass\n```\n\nIs true when the game entity's command queue is not empty when the node is visited.\n\n## util.activity.condition.type.NextCommand\n\n```python\nNextCommand(Condition):\n    command : children(Command)\n```\n\nIs true when the next command in the game entity's command queue is of a specific type.\n\n**command**\nCommand type checked by the condition.\n\n## util.activity.condition.type.TargetInRange\n\n```python\nTargetInRange(Condition):\n    ability : abstract(Ability)\n```\n\nIs true when the target of the next command in the game entity's command queue is in range of an ability.\n\n**ability**\nAbility definition used for the range check.\n\nThis can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must be enabled. Otherwise, the range check fails. If an API object is referenced, the first active ability with the same type as the API object is executed.\n\nIf the ability has the property `Ranged`, the attributes of this property are utilized for the range check calculations. If the ability does not have a `Ranged` property, the condition is only true when the game entity is at the same position as the target.\n\n## util.activity.event.Event\n\n```python\nEvent(Object):\n    pass\n```\n\nGeneralization object for events that can be used in `XOREventGate` nodes.\n\n## util.activity.event.type.CommandInQueue\n\n```python\nCommandInQueue(Event):\n    pass\n```\n\nFires after a new command has been added to the game entity's command queue.\n\n## util.activity.event.type.Wait\n\n```python\nWait(Event):\n    time : float\n```\n\nFires after a certain amount of time has passed.\n\n**time**\nTime in seconds to wait.\n\nIf the value is zero or negative, the event fires immediately.\n\n## util.activity.event.type.WaitAbility\n\n```python\nWaitAbility(Event):\n    pass\n```\n\nFires at the exact time when a previously visited ability node has finished executing.\n\nIn other words, the event fires when the ability is done with the associated task. For example, in case of the `Move` ability, the event fires when the game entity has reached its destination.\n\n## util.activity.node.Node\n\n```python\nNode(Object):\n    pass\n```\n\nGeneralization object for nodes in an activity graph.\n\n## util.activity.node.type.Ability\n\n```python\nAbility(Node):\n    next    : Node\n    ability : abstract(Ability)\n```\n\nExecutes an ability of the game entity when the node is visited.\n\n**next**\nNext node in the activity graph.\n\n**ability**\nAbility that is executed.\n\nThis can reference a specific ability of the game entity or an abstract API object from the `engine.ability.type` namespace. If a specific ability is referenced, the ability must be assigned to the game entity and must be enabled. Otherwise, the ability is not executed. If an API object is referenced, the first active ability with the same type as the API object is executed.\n\n## util.activity.node.type.End\n\n```python\nEnd(Node):\n    pass\n```\n\nEnd of an activity. Does nothing.\n\n## util.activity.node.type.Start\n\n```python\nStart(Node):\n    next : Node\n```\n\nStart of an activity. Does nothing but pointing to the next node.\n\n**next**\nNext node in the activity graph.\n\n## util.activity.node.type.Task\n\n```python\nTask(Node):\n    next : Node\n    task : children(Task)\n```\n\nExecutes a task on the game entity when the node is visited.\n\n**next**\nNext node in the activity graph.\n\n**task**\nTask that is executed.\n\n## util.activity.node.type.XOREventGate\n\n```python\nXOREventGate(Node):\n    next : dict(Event, Node)\n```\n\nGateway that branches the activity graph when a certain event occurs. Events are registered immediately when the node is visited and cancelled when the node is left.\n\n**next**\nMapping of events to the next node in the activity graph. The first event that occurs is used to determine the next node.\n\n## util.activity.node.type.XORGate\n\n```python\nXORGate(Node):\n    next    : orderedset(Condition)\n    default : Node\n```\n\nGateway that branches the activity graph depending on the result of conditional queries. Queries are executed immediately when the node is visited.\n\n**next**\nMapping of conditional queries to the next node in the activity graph. The first query that evaluates to true is used to determine the next node. If no query evaluates to true, the `default` node is used as fallback.\n\n**default**\nDefault node that is used if no query evaluates to true.\n\n## util.activity.node.type.XORSwitchGate\n\n```python\nXORSwitchGate(Node):\n    switch  : children(SwitchCondition)\n    default : Node\n```\n\nGateway that branches the activity graph depending on the value of a runtime parameter. In comparison to `XORGate`, only one conditional query is done based on the value (similar to the behaviour of a [switch statement](https://en.wikipedia.org/wiki/Switch_statement)). The query is executed immediately when the node is visited.\n\n**switch**\nDefines which runtime parameter is checked as well as the mapping of parameter value to the next node in the activity graph. If a value is encountered at query execution time that is not associated with a node, the `default` node is used as fallback.\n\n**default**\nDefault node that is used if a value does not have an associated node.\n\n## util.activity.switch_condition.SwitchCondition\n\n```python\nSwitchCondition(Object):\n    pass\n```\n\nGeneralization object for conditions that can be used in `XORSwitchGate` nodes.\n\n## util.activity.switch_condition.type.NextCommand\n\n```python\nNextCommand(SwitchCondition):\n    next : dict(children(Command), Node)\n```\n\nSwitches branches based on the type of command that is in the queue of the game entity.\n\n**next**\nMapping of command types to the next node in the activity graph.\n\n## util.activity.task.Task\n\n```python\nTask(Object):\n    pass\n```\n\nGeneralization object for tasks that can be used in `Task` nodes.\n\n## util.activity.task.type.ClearCommandQueue\n\n```python\nClearCommandQueue(Task):\n    pass\n```\n\nClear the command queue of the game entity executing the activity.\n\n## util.activity.task.type.MoveToTarget\n\n```python\nMoveToTarget(Task):\n    pass\n```\n\nMove to the current target of the game entity. The target may be a position or another game entity. If the game entity has no target at time of execution, the task is skipped.\n\n## util.activity.task.type.PopCommandQueue\n\n```python\nPopCommandQueue(Task):\n    pass\n```\n\nPop the front command from the command queue of the game entity executing the activity.\n\n## util.animation_override.AnimationOverride\n\n```python\nAnimationOverride(Object):\n    ability    : abstract(Ability)\n    animations : set(Animation)\n    priority   : int\n```\n\nInternally overrides the animations used for an ability. The ability must have the `Animated` property when the override occurs. The override stops when\n\n* Another override of the same ability with a *greater than or equal to* (>=) priority is initiated **or**\n* The overridden ability is deactivated **or**\n* The `Animated` property is removed.\n\n**ability**\nAbility whose animations should be overridden. This member can reference a specific ability of the game entity or an API object from the `engine.ability.type` namespace. If an API object is referenced, all its instances of the ability defined for the game entity will have their animation overridden.\n\n**animations**\nReplacement animations of the override.\n\n**priority**\nPriority of the override. Overrides are only executed if their priority is *greater than or equal to* (>=) an already existing override. The default animation from an `Animated` property of an ability always has a priority of 0.\n\n## util.animation_override.type.Reset\n\n```python\nReset(AnimationOverride):\n    animations = {}\n    priority   = 0\n```\n\nResets the animation of the specified ability to the animation defined in the `Animated` property by removing the current animation override.\n\n## util.attribute.Attribute\n\n```python\nAttribute(Object):\n    name         : TranslatedString\n    abbreviation : TranslatedString\n```\n\nDefines an attribute that can be assigned a value range by `AttributeSetting`.\n\n**name**\nName of the attribute as a translated string.\n\n**abbreviation**\nShort version of the names as a translated string.\n\n## util.attribute.AttributeAmount\n\n```python\nAttributeAmount(Object):\n    type   : Attribute\n    amount : int\n```\n\nA fixed amount of a certain attribute.\n\n**type**\nAttribute reference.\n\n**amount**\nAmount of attribute points.\n\n## util.attribute.AttributeRate\n\n```python\nAttributeRate(Object):\n    type   : Attribute\n    rate   : float\n```\n\nA per-second rate of a certain attribute.\n\n**type**\nAttribute reference.\n\n**rate**\nRate of attribute points.\n\n## util.attribute.AttributeSetting\n\n```python\nAttributeSetting(Object):\n    attribute       : Attribute\n    min_value       : int\n    max_value       : int\n    starting_value  : int\n```\n\nAssigns an attribute to a game entity and specifies the range the attribute value can be in. The game entity has a *current attribute value* at runtime. Attribute values can be changed by abilities or effects.\n\n**attribute**\nAttribute that is configured by this object.\n\n**min_value**\nMinimum value the current attribute value can have.\n\n**max_value**\nMaximum value the current attribute value can have.\n\n**starting_value**\nThe current attribute value when the game entity is created.\n\n## util.attribute.ProtectingAttribute\n\n```python\nProtectingAttribute(Attribute):\n    protects : Attribute\n```\n\nProtects an attribute by preventing its value to be decreased by another game entity. Attribute increases by other game entities, costs and regeneration are not affected.\n\n**protects**\nThe attribute that is protected.\n\n## util.attribute_change_type.AttributeChangeType\n\n```python\nAttributeChangeType(Object):\n    pass\n```\n\nUsed by `FlatAttributeChange` effects and resistances for matching.\n\n## util.attribute_change_type.type.Fallback\n\n```python\nFallback(AttributeChangeType):\n    pass\n```\n\nA special type for `FlatAttributeChange`. Effects with this type are only evaluated if the accumulated applied value or applied rate of all other effects is outside of a specified interval. The interval is defined by the `FlatAttributeChange` object that has its `type` member set to `Fallback`. Upper and lower bounds are `[min_change_rate,max_change_rate]` (continuous effects) and `[min_change_value,max_change_value]` (discrete effects). The fallback effect is also evaluated if no other `FlatAttributeChange` effect is present or matched to any resistances. However, fallback effects still needs to be matched against a resistance object with `type` set to `Fallback`.\n\nFor example, effects that utilize fallback behaviour can be used to model minimum or maximum damage of a game entity.\n\n## util.calculation_type.CalculationType\n\n```python\nCalculationType(Object):\n    pass\n```\n\nGeneralization object for the calculation method for the influence factor of a resistance with the `Stacked` property.\n\n## util.calculation_type.type.Hyperbolic\n\n```python\nHyperbolic(CalculationType):\n    shift_x      : int\n    shift_y      : int\n    scale_factor : float\n```\n\nCalculates the influence factor using the hyperbolic equation. This will *decrease* the influence factor towards `shift_y` the more effectors are present.\n\n```math\ninfluence\\_factor = scale\\_factor \\ (num\\_effectors - shift\\_x) + shift\\_y\n```\n\n**shift_x**\nHorizontal shift in the hyperbola equation.\n\n**shift_y**\nVertical shift in the hyperbola equation.\n\n**scale_factor**\nScale factor in the hyperbola equation.\n\n## util.calculation_type.type.Linear\n\n```python\nLinear(CalculationType):\n    shift_x      : int\n    shift_y      : int\n    scale_factor : float\n```\n\nCalculates the influence factor using the linear equation. This will *increase* the influence factor the more effectors are present.\n\n```math\ninfluence\\_factor = scale\\_factor * (num\\_effectors - shift\\_x) + shift\\_y\n```\n\n**shift_x**\nHorizontal shift in the linear equation.\n\n**shift_y**\nVertical shift in the linear equation.\n\n**scale_factor**\nScale factor in the linear equation.\n\n## util.calculation_type.type.NoStack\n\n```python\nNoStack(CalculationType):\n    pass\n```\n\nSets the influence factor to 1 regardless of the number of effectors. Note that this is different to setting the stack limit to 1, since the distribution type can still be influenced by multiple effectors.\n\n## util.cheat.Cheat\n\n```python\nCheat(Object):\n    activation_message : text\n    changes            : orderedset(Patch)\n```\n\nCheats are a predefined gameplay change, often in favour and to the amusement of the player. They are deactivated by default in multiplayer. Advanced cheating behavior can be realized by attaching scripts to the `Cheat` object.\n\n**activation_message**\nThe activation message that has to be typed into the chat console.\n\n**changes**\nChanges to API objects.\n\n## util.command.Command\n\n```python\nCommand(Object):\n    pass\n```\n\nGeneralization object for commands of a game entity.\n\n## util.command.type.ApplyEffect\n\n```python\nApplyEffect(Command):\n    pass\n```\n\nGame entity command for using the `ApplyEffect` ability.\n\n## util.command.type.Idle\n\n```python\nIdle(Command):\n    pass\n```\n\nGame entity command for using the `Idle` ability.\n\n## util.command.type.Move\n\n```python\nMove(Command):\n    pass\n```\n\nGame entity command for using the `Move` ability.\n\n## util.container_type.SendToContainerType\n\n```python\nSendToContainerType(Object):\n    pass\n```\n\nUsed by `SendToContainer` effects and resistances for matching.\n\n## util.convert_type.ConvertType\n\n```python\nConvertType(Object):\n    pass\n```\n\nUsed by `Convert` effects and resistances for matching.\n\n## util.cost.Cost\n\n```python\nCost(Object):\n    payment_mode : PaymentMode\n```\n\nGeneralization object for resource and attribute costs.\n\n**payment_mode**\nDetermines how the costs have to be payed.\n\n## util.cost.type.AttributeCost\n\n```python\nAttributeCost(Cost):\n    amount : set(AttributeAmount)\n```\n\nDefines the cost as an amount of attribute points that is removed from a game entity's current attribute value.\n\n**amount**\nAmounts of attribute points.\n\n## util.cost.type.ResourceCost\n\n```python\nResourceCost(Cost):\n    amount : set(ResourceAmount)\n```\n\nDefines the cost as an amount of resources that is removed from the player's resource pool.\n\n**amount**\nAmounts of resources.\n\n## util.create.CreatableGameEntity\n\n```python\nCreatableGameEntity(Object):\n    game_entity     : GameEntity\n    variants        : set(Variant)\n    cost            : Cost\n    creation_time   : float\n    creation_sounds : set(Sound)\n    condition       : set(LogicElement)\n    placement_modes : set(PlacementMode)\n```\n\nDefines preconditions, placement and spawn configurations for a new instance of a game entity created by a `Create` ability.\n\n**game_entity**\nReference to the `GameEntity` object that is spawned.\n\n**variants**\nVariants can alter the game entity before they are created. The requirement and extent of the changes depends on the `Variant` object.\n\n**cost**\nAmount spent to initiate creation. Cancelling the creation results in a refund of the spent cost.\n\n**creation_time**\nTime to wait until the game entity is spawned from the creating game entity in seconds.\n\n**creation_sounds**\nSounds that are played when the game entity is spawned.\n\n**condition**\nConditions that unlock the creatable game entity for the creating game entity. Only one condition needs to be fulfilled to trigger the unlock. If the set is empty, the game entity is considered available from the start for the creating game entity.\n\n**placement_modes**\nDecides where and how the game entity instance is spawned.\n\n## util.diplomatic_stance.DiplomaticStance\n\n```python\nDiplomaticStance(Object):\n    pass\n```\n\nGeneralization object for diplomatic stances that can be used for diplomacy ingame. Diplomatic stances also define which player can use the abilities, modifiers and effects of a game entity.\n\n## util.diplomatic_stance.Any\n\n```python\nAny(DiplomaticStance):\n    pass\n```\n\nCan be used to address any diplomatic stance.\n\n## util.diplomatic_stance.type.Self\n\n```python\nSelf(DiplomaticStance):\n    pass\n```\n\nThe diplomatic stance of a player towards themselves.\n\n## util.distribution_type.DistributionType\n\n```python\nDistributionType(Object):\n    pass\n```\n\nGeneralization object for the calculation method for the distribution value of a resistance with the `Stacked` property.\n\n## util.distribution_type.type.Mean\n\n```python\nMean(DistributionType):\n    pass\n```\n\nCalculates the distribution value by using the mean of all change values of effectors.\n\n## util.dropoff_type.DropoffType\n\n```python\nDropoffType(Object):\n    pass\n```\n\nUsed for calculating the effectiveness over distance of `AreaEffect`s and `Accuracy` of projectiles. The dropoff modifier is always relational to the maximum range.\n\n## util.dropoff_type.type.InverseLinear\n\n```python\nInverseLinear(DropoffType):\n    pass\n```\n\nThe effectiveness starts at 0% (for zero distance) and linearly increases to 100% (for maximum distance).\n\n## util.dropoff_type.type.Linear\n\n```python\nLinear(DropoffType):\n    pass\n```\n\nThe effectiveness starts at 100% (for zero distance) and linearly decreases to 0% (for maximum distance).\n\n## util.dropoff_type.type.NoDropoff\n\n```python\nNoDropoff(DropoffType):\n    pass\n```\n\nThe effectiveness is constant and independent from the range to the target.\n\n## util.effect_batch.EffectBatch\n\n```python\nEffectBatch(Object):\n    effects    : set(DiscreteEffect)\n    properties : dict(abstract(BatchProperty), BatchProperty) = {}\n```\n\nGeneralization object for a collection of discrete effects. Batches combine the discrete effects to transactions. Batches - like effects - can have properties to configure the batch application.\n\n**effects**\nDiscrete effects applied by the batch.\n\n**properties**\nFurther specializes the batch beyond the standard behaviour.\n\nThe engine expects objects from the namespace `engine.util.effect_batch.property.type` as keys. Values must always be an instance of the object used as key.\n\nProperties:\n\n* `Chance`: Batches have a chance to be applied.\n* `Priority`: Sets the preferred order of application in comparison to other batches.\n\n## util.effect_batch.property.BatchProperty\n\n```python\nBatchProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of effect batches.\n\n## util.effect_batch.property.type.Chance\n\n```python\nChance(BatchProperty):\n    chance : float\n```\n\nThe batch has a random chance to be applied.\n\n**chance**\nChance of the batch to be applied. Muste be a value between 0.0 and 1.0.\n\n## util.effect_batch.property.type.Priority\n\n```python\nPriority(BatchProperty):\n    priority : int\n```\n\nAssigns a priority value to the batch that is used to determine the order of batch application in `ApplyDiscreteEffect`.\n\n**priority**\nThe priority of the batch. When comparing with other batches, the batch with higher values are applied first.\n\n## util.effect_batch.type.ChainedBatch\n\n```python\nChainedBatch(EffectBatch):\n    pass\n```\n\nThe effects in the batch are applied using the effect property `Priority` for ordering. If two or more effects have the same priority, it is assumed that the order for these effects does not matter. Effects are applied as long there are matching resistance objects stored by the resistor. If an effect cannot be applied because of a missing match, the application of the remaining effects is canceled.\n\n## util.effect_batch.type.OrderedBatch\n\n```python\nOrderedBatch(EffectBatch):\n    pass\n```\n\nThe effects in the batch are applied using the effect property `Priority` for ordering. If two or more effects have the same priority, it is assumed that the order for these effects does not matter.\n\n## util.effect_batch.type.UnorderedBatch\n\n```python\nUnorderedBatch(EffectBatch):\n    pass\n```\n\nThe effects in the batch are applied without any particular order.\n\n## util.exchange_mode.ExchangeMode\n\n```python\nExchangeMode(Object):\n    fee_multiplier : float\n```\n\nGeneralization object for exchange modes of the `ExchangeResource` ability.\n\n**fee_multiplier**\nShare of resource amounts that have to be payed as a fee for using the exchange mode. The fee is additional to the exchanged resources.\n\n## util.exchange_mode.type.Buy\n\n```python\nBuy(ExchangeMode):\n    pass\n```\n\nBuy an amount of `resource_a` by paying with an amount of `resource_b`. `resource_a * fee_multiplier` is charged as the additional fee.\n\n## util.exchange_mode.type.Sell\n\n```python\nSell(ExchangeMode):\n    pass\n```\n\nSell an amount of `resource_a` and receive an amount of `resource_b`. `resource_a * fee_multiplier` is charged as the additional fee.\n\n## util.exchange_rate.ExchangeRate\n\n```python\nExchangeRate(Object):\n    base_price   : float\n    price_adjust : optional(dict(ExchangeMode, PriceMode)) = None\n    price_pool   : optional(children(PricePool)) = None\n```\n\nDefines an exchange rate for the resources in the `ExchangeResources` ability.\n\n**base_price**\nPrice per unit at the start of the game.\n\n**price_adjust**\nMethod to adjust the current price of the exchange rate at runtime. This method will be used every time the `ExchangeResource` ability is used. Different types of adjustment can be configured for every exchange mode.\n\n**price_pool**\nCan be used to sync the current price at runtime across game entities. All exchange rates that use the same price pool share the same price at runtime. If `ExchangeRate` objects reference the same `PricePool`, but have different `base_price` values defined, the price at the start of the game is set to the lowest `base_price` value.\n\n## util.formation.Formation\n\n```python\nFormation(Object):\n    subformations : set(Subformation)\n```\n\nOrganizational structure for multiple game entities.\n\n**subformations**\nSubdivisions of the formation. Game entities are sorted into one of the subformations.\n\n## util.formation.Subformation\n\n```python\nSubformation(Object):\n    ordering_priority : int\n```\n\nSubdivision of a formation. It defines the structure and placement of game entities when the formation is formed.\n\n**ordering_priority**\nOrdering priority in relation to other subformations. Formations are ordered in with descending priority, i.e. the subformation with the highest priority is placed in front.\n\n## util.game_entity.GameEntity\n\n```python\nGameEntity(Object):\n    types     : set(children(GameEntityType))\n    abilities : set(Ability)\n    modifiers : set(Modifier)\n    variants  : set(Variant)\n```\n\nFor definition of all ingame objects, including units, buildings, items, projectiles and ambience. Their capabilities are handled through `Ability` and `Modifier` API objects stored in the members.\n\n**types**\nClassification of the game entity.\n\n**abilities**\nDefine what the game entity *does* and *is* (see the [ability module](reference_ability.md)).\n\n**modifiers**\nChange the stats of abilities belonging to the game entity. Mostly used to give boni or mali in certain situations (see the [modifier module](reference_modifier.md)).\n\n## util.game_entity_formation.GameEntityFormation\n\n```python\nGameEntityFormation(Object):\n    formation    : Formation\n    subformation : Subformation\n```\n\nDefines the placement of a game entity in a formation for the `Formation` ability.\n\n**formation**\nFormation the game entity is placed in.\n\n**subformation**\nSubformation inside the subformation that the game entity is inserted into.\n\n## util.game_entity_stance.GameEntityStance\n\n```python\nGameEntityStance(Object):\n    search_range       : float\n    ability_preference : orderedset(Ability)\n    type_preference    : orderedset(children(GameEntityType))\n```\n\nGeneralization object for activity stances for the `GameEntityStance` ability.\n\n**search_range**\nDefines the range in which the game entity will look for targets.\n\n**ability_preference**\nAbilities which the game entity will execute or search targets for. Their order in the set defines the priority of usage.\n\n**type_preference**\nDetermines which game entity types are prioritized as targets. Their order in the set defines the priority with which they are targeted. Game entities with types that are not in the set will be ignored.\n\n## util.game_entity_stance.type.Aggressive\n\n```python\nAggressive(GameEntityStance):\n    pass\n```\n\nThe game entity will use ranged abilities or move to the nearest target in its line of sight to use other abilities. If the target gets out of the line of sight, the game entity searches for a new target. When no new target can be found, the game entity stops moving and returns to an idle state.\n\n## util.game_entity_stance.type.Defensive\n\n```python\nDefensive(GameEntityStance):\n    pass\n```\n\nThe game entity will use ranged abilities or move to the nearest target in its line of sight to use other abilities. If the target gets out of range or the line of sight, the game entity searches for a new target. When no new target can be found, the game entity returns to its original position and returns to an idle state.\n\n## util.game_entity_stance.type.Passive\n\n```python\nPassive(GameEntityStance):\n    pass\n```\n\nThe game entity will stay at its current position and only reacts to manual commands given by players. Abilities in `ability_preference` will be ignored.\n\n## util.game_entity_stance.type.StandGround\n\n```python\nStandGround(GameEntityStance):\n    pass\n```\n\nThe game entity will stay at its current position. Abilities in `ability_preference` will be used if other game entities come in range of the game entity.\n\n## util.game_entity_type.GameEntityType\n\n```python\nGameEntityType(Object):\n    pass\n```\n\nClassification for a game entity.\n\n## util.game_entity_type.Any\n\n```python\nAny(GameEntityType):\n    pass\n```\n\nCan be used to address any game entity, even ones that have no `GameEntityType` assigned.\n\n## util.graphics.Animation\n\n```python\nAnimation(Object):\n    sprite : file\n```\n\nPoints to a openage sprite definition file in the `.sprite` format. The specified animation can be used by `AnimatedAbility` objects.\n\n## util.graphics.Palette\n\n```python\nPalette(Object):\n    palette : file\n```\n\nPoints to a openage palette definition file in the `.opal` format.\n\n## util.graphics.Terrain\n\n```python\nTerrain(Object):\n    sprite : file\n```\n\nPoints to a openage terrain definition file in the `.terrain` format. This object is used by the `terrain.Terrain` object that defines properties of ingame terrain.\n\n## util.herdable_mode.HerdableMode\n\n```python\nHerdableMode(Object):\n    pass\n```\n\nUsed by the `Herdable` ability to determine who gets ownership of the herdable.\n\n## util.herdable_mode.type.ClosestHerding\n\n```python\nClosestHerding(HerdableMode):\n    pass\n```\n\nThe player with the closest herding game entity gets ownership.\n\n## util.herdable_mode.type.LongestTimeInRange\n\n```python\nLongestTimeInRange(HerdableMode):\n    pass\n```\n\nThe player of the game entity which has been in range for the longest time gets ownership.\n\n## util.herdable_mode.type.MostHerding\n\n```python\nMostHerding(HerdableMode):\n    pass\n```\n\nThe player with the most herding game entities in range gets ownership.\n\n## util.hitbox.Hitbox\n\n```python\nHitbox(Object):\n    radius_x : float\n    radius_y : float\n    radius_z : float\n```\n\nDefines the hitbox of a game entity.\n\n**radius_x**\nWidth of the game entity.\n\n**radius_y**\nLength of the game entity.\n\n**radius_z**\nHeight of the game entity.\n\n## util.language.Language\n\n```python\nLanguage(Object):\n    ietf_string : text\n```\n\nA language definition. Languages are used for translated strings, markup files and sounds.\n\n**ietf_string**\nThe IETF identification tag of the language. [See here](https://tools.ietf.org/html/rfc4646) for more information on how the tags are established.\n\n## util.language.LanguageMarkupPair\n\n```python\nLanguageMarkupPair(Object):\n    language    : Language\n    markup_file : file\n```\n\nDefines the translation of a longer text from a markup file in a certain language.\n\n**language**\nLanguage used in the markup file.\n\n**markup_file**\nFile descriptor of the markup file. Has to be relative to the `.nyan` file where the `LanguageMarkupPair` is defined.\n\n## util.language.LanguageSoundPair\n\n```python\nLanguageSoundPair(Object):\n    language : Language\n    sound    : Sound\n```\n\nDefines the translation of a sound in a certain language.\n\n**language**\nLanguage used for the sound.\n\n**sound**\nReferences the `Sound` object for the translation.\n\n## util.language.LanguageTextPair\n\n```python\nLanguageTextPair(Object):\n    language : Language\n    string   : text\n```\n\nDefines the translation of a string in a certain language.\n\n**language**\nLanguage used for the string.\n\n**string**\nThe translated string.\n\n## util.language.translated.TranslatedObject\n\n```python\nTranslatedObject(Object):\n    pass\n```\n\nGeneralization object for any objects that are or should be different depending on the language. Currently we support translations for strings, markup files and sounds.\n\n## util.language.translated.type.TranslatedMarkupFile\n\n```python\nTranslatedMarkupFile(TranslatedObject):\n    translations : set(LanguageMarkupPair)\n```\n\nThe translated versions of a longer text stored in markup files.\n\n**translations**\nAll translations of the markup files as language-file pairs (see `LanguageMarkupPair`).\n\n## util.language.translated.type.TranslatedSound\n\n```python\nTranslatedSound(TranslatedObject):\n    translations : set(LanguageSoundPair)\n```\n\nThe translated versions of a sound.\n\n**translations**\nAll translations of the sound as language-sound pairs (see `LanguageSoundPair`).\n\n## util.language.translated.type.TranslatedString\n\n```python\nTranslatedString(TranslatedObject):\n    translations : set(LanguageTextPair)\n```\n\nThe translated versions of a string.\n\n**translations**\nAll translations of the string as language-text pairs (see `LanguageTextPair`).\n\n## util.lock.LockPool\n\n```python\nLockPool(Object):\n    slots : int\n```\n\nUsed by abilities to block each other. The lock pool has a defined number of slots available which are occupied by abilities with the `Lock` property when they execute. If no slots are available at runtime, the execution of the ability is blocked.\n\n**slots**\nMaximum number of abilities using the pool that can be active at the same time.\n\n## util.logic.LogicElement\n\n```python\nLogicElement(LogicElement):\n    only_once : bool\n```\n\nGeneralization object for a logical elements (literals, gates or constants) in the API. Logical elements are either true or false at a given point in time.\n\n**only_once**\nIf this is set, the logical element is always true if it has been true at least once during the game.\n\n## util.logic.const.False\n\n```python\nFalse(LogicElement):\n    pass\n```\n\nA logic element that is always false.\n\n## util.logic.const.True\n\n```python\nTrue(LogicElement):\n    pass\n```\n\nA logic element that is always true.\n\n## util.logic.gate.LogicGate\n\n```python\nLogicGate(LogicElement):\n    inputs : set(LogicElement)\n```\n\nGneralization object for a logic gate implementing a Boolean function. The Boolean value of the gate at runtime is evaluated using the defined inputs.\n\n**inputs**\nInputs for the gate. Some types of gate may require a minimum or maximum number of inputs. Gates with 0 inputs are always evaluated to false.\n\n## util.logic.gate.type.AND\n\n```python\nAND(LogicGate):\n    pass\n```\n\nEvaluates to true if *all* inputs are true.\n\n## util.logic.gate.type.MULTIXOR\n\n```python\nMULTIXOR(LogicGate):\n    pass\n```\n\nEvaluates to true if *an uneven number of* inputs is true.\n\n## util.logic.gate.type.NOT\n\n```python\nNOT(LogicGate):\n    pass\n```\n\nNegates the input.\n\nOnly one input is allowed for this type of gate. If more than one input is defined, the gate evaluates to false.\n\n## util.logic.gate.type.OR\n\n```python\nOR(LogicGate):\n    pass\n```\n\nEvaluates to true if *at least one* input is true.\n\n## util.logic.gate.type.SUBSETMAX\n\n```python\nSUBSETMAX(LogicGate):\n    size : int\n```\n\nEvaluates to true if *at most* `size` inputs are true.\n\n**size**\nMaximum number of inputs that should be true.\n\n## util.logic.gate.type.SUBSETMIN\n\n```python\nSUBSETMIN(LogicGate):\n    size : int\n```\n\nEvaluates to true if *at least* `size` inputs are true.\n\n**size**\nMinimum number of inputs that must be true.\n\n## util.logic.gate.type.XOR\n\n```python\nXOR(LogicGate):\n    pass\n```\n\nEvaluates to true if *exactly one* input is true.\n\n## util.logic.literal.Literal\n\n```python\nLiteral(LogicElement):\n    scope : LiteralScope\n```\n\nGeneralization object for a logical statement about the game world that is either true or false at a given point in time.\n\n**scope**\nScope in which the statement is checked.\n\n## util.logic.literal.type.AttributeAbovePercentage\n\n```python\nAttributeAbovePercentage(Literal):\n    attribute : Attribute\n    threshold : float\n```\n\nIs true when the current attribute value's share of its maximum value of a game entity is higher than a specified threshold.\n\n**attribute**\nAttribute that is checked.\n\n**threshold**\nValue threshold that must be passed for the literal to be true.\n\n## util.logic.literal.type.AttributeAboveValue\n\n```python\nAttributeAboveValue(Literal):\n    attribute : Attribute\n    threshold : float\n```\n\nIs true when the current attribute value of a game entity is higher than a specified threshold.\n\n**attribute**\nAttribute that is checked.\n\n**threshold**\nValue threshold that must be passed for the literal to be true.\n\n## util.logic.literal.type.AttributeBelowPercentage\n\n```python\nAttributeBelowPercentage(Literal):\n    attribute : Attribute\n    threshold : float\n```\n\nIs true when the current attribute value's share of its maximum value of a game entity is lower than a specified threshold.\n\n**attribute**\nAttribute that is checked.\n\n**threshold**\nValue threshold that must be passed for the literal to be true.\n\n## util.logic.literal.type.AttributeBelowValue\n\n```python\nAttributeBelowValue(Literal):\n    attribute : Attribute\n    threshold : float\n```\n\nIs true when the current attribute value of a game entity is lower than a specified threshold.\n\n**attribute**\nAttribute that is checked.\n\n**threshold**\nValue threshold that must be passed for the literal to be true.\n\n## util.logic.literal.type.GameEntityProgress\n\n```python\nGameEntityProgress(Literal):\n    game_entity : GameEntity\n    status   ´  : ProgressStatus\n```\n\nIs true when an instance of a game entity has reached a certain progress.\n\n**game_entity**\nThe game entity that should have progressed this far.\n\n**status**\nStatus of a game entity as a `ProgressStatus` object.\n\n## util.logic.literal.type.OwnsGameEntity\n\n```python\nOwnsGameEntity(Literal):\n    game_entity : GameEntity\n```\n\nTriggers when a game entity is owned by a player in the defined scope.\n\n**game_entity**\nGame entity that should be owned.\n\n## util.logic.literal.type.ProjectileHitTerrain\n\n```python\nProjectileHitTerrain(Literal):\n    pass\n```\n\nTriggers when a game entity's hitbox collides with the terrain of the map.\n\n## util.logic.literal.type.ProjectilePassThrough\n\n```python\nProjectilePassThrough(Literal):\n    pass_through_range : int\n```\n\nTriggers when the distance to the spawn location of the game entity becomes greater than a defined value.\n\n**pass_through_range**\nDistance to the spawn location. Only (x,y) coordinates are considered.\n\n## util.logic.literal.type.ResourceSpotsDepleted\n\n```python\nResourceSpotsDepleted(Literal):\n    only_enabled : bool\n```\n\nTriggers when all resource spots in `Harvestable` abilities have been depleted.\n\n**only_enabled**\nThe condition only considers resource spots of enabled `Harvestable` abilities.\n\n## util.logic.literal.type.StateChangeActive\n\n```python\nStateChangeActive(Literal):\n    state_change : StateChanger\n```\n\nTriggers when the defined state changer is active for the game entity.\n\n## util.logic.literal.type.TechResearched\n\n```python\nTechResearched(Literal):\n    tech : Tech\n```\n\nIs true when the specified technology has been researched by the player.\n\n**tech**\nTechnology that has to be researched.\n\n## util.logic.literal.type.Timer\n\n```python\nTimer(Literal):\n    time : float\n```\n\nTriggers after a specified amount of time has passed. When the timer is started depends on the object that uses the condition.\n\n**time**\nTime that has to pass after the activation.\n\n## util.logic.literal_scope.LiteralScope\n\n```python\nLiteralScope(Object):\n    stances : set(children(DiplomaticStance))\n```\n\nConfigures the scope in which the fulfillment of the literal is checked.\n\n**stances**\nDiplomatic stances defining the boundaries of the scope.\n\n## util.logic.literal_scope.type.Any\n\n```python\nAny(LiteralScope):\n    pass\n```\n\nCheck if the literal is true for any entity.\n\n## util.logic.literal_scope.type.Self\n\n```python\nSelf(LiteralScope):\n    pass\n```\n\nCheck if the literal is true for the game entity it is assigned to.\n\n## util.lure_type.LureType\n\n```python\nLureType(Object):\n    pass\n```\n\nUsed by `Lure` effects and resistances for matching.\n\n## util.mod.Mod\n\n```python\nMod(Object):\n    patches  : orderedset(Patch)\n    priority : int\n```\n\nDefines patches that will be automatically applied when the modpack is loaded.\n\n**patches**\nChanges the game state through patches.\n\n**priority**\nDetermines the application order of the mod in comparison to other `Mod` objects. `Mod` objects are applied starting with the object with the highest priority value.\n\n## util.modifier_scope.ModifierScope\n\n```python\nModifierScope(Object):\n    pass\n```\n\nGeneralization object for scopes of a `Scoped` property of a `Modifier`.\n\n## util.modifier_scope.type.GameEntityScope\n\n```python\nGameEntityScope(ModifierScope):\n    affected_types       : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n```\n\nDefines a scope of game entities the modifier should apply to.\n\n**affected_types**\nWhitelist of game entity types that the modifier should apply to.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `affected_types`, but should be excplicitly excluded.\n\n## util.modifier_scope.type.Standard\n\n```python\nStandard(ModifierScope):\n    pass\n```\n\nMakes the modifier behave as if standard rules would apply, i.e. as if the modifier had no `Scoped` property.\n\n## util.move_mode.MoveMode\n\n```python\nMoveMode(Object):\n    pass\n```\n\nGeneralization object for move modes for the `Move` ability.\n\n## util.move_mode.type.AttackMove\n\n```python\nAttackMove(MoveMode):\n    pass\n```\n\nMove to a position on the map. Stances from `GameEntityStance` ability are considered during movement.\n\n## util.move_mode.type.Follow\n\n```python\nFollow(MoveMode):\n    range : float\n```\n\nFollow another game entity at a defined range. The movement speed is adjusted to the followed game entity, but cannot go higher than the `speed` value from `Move`. Stances from `GameEntityStance` ability are ignored during movement. Following is stopped when the followed game entity gets out of the line of sight of the following game entity.\n\n**range**\nThe range at which the other game entity is followed.\n\n## util.move_mode.type.Guard\n\n```python\nGuard(MoveMode):\n    range : float\n```\n\nFollow another game entity at a defined range and defend it from other game entities. The movement speed is adjusted to the guarded game entity, but cannot go higher than the `speed` value from `Move`. Stances from `GameEntityStance` ability are ignored during movement. Guarding is stopped when the guarded game entity gets out of the line of sight of the guarding game entity.\n\n**range**\nThe range at which the other game entity is guarded.\n\n## util.move_mode.type.Normal\n\n```python\nNormal(MoveMode):\n    pass\n```\n\nMove to a position on the map. Stances from `GameEntityStance` ability are ignored during movement.\n\n## util.move_mode.type.Patrol\n\n```python\nPatrol(MoveMode):\n    pass\n```\n\nLets player set two or more waypoints that the game entity will follow. Stances from `GameEntityStance` ability are considered during movement.\n\n## util.patch.NyanPatch\n\n```python\nNyanPatch(Object):\n    pass\n```\n\nParent object for nyan patches used in the openage API. All nyan patches must inherit from this object.\n\n## util.patch.Patch\n\n```python\nPatch(Object):\n    properties : dict(abstract(PatchProperty), PatchProperty) = {}\n    patch      : children(NyanPatch)\n```\n\nWrapper for nyan patches with additional configuration options throuh properties.\n\n**properties**\nFurther specializes the patch beyond the standard behaviour.\n\nProperties:\n\n* `Diplomatic`: The patch is applied to the database view of players with defined diplomatic stances.\n\n**patch**\nNyan patch that gets applied by the wrapper.\n\n## util.patch.property.PatchProperty\n\n```python\nPatchProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of patches.\n\n## util.patch.property.type.Diplomatic\n\n```python\nDiplomatic(PatchProperty):\n    stances : set(children(DiplomaticStance))\n```\n\nThe patch is applied to all players that have the specified diplomatic stances.\n\n**stances**\nDiplomatic stances of the players the patch should apply to.\n\n## util.path_type.PathType\n\n```python\nPathType(Object):\n    pass\n```\n\nPath type that is associated with an internal pathfinding grid at runtime.\n\n## util.payment_mode.PaymentMode\n\n```python\nPaymentMode(Object):\n    pass\n```\n\nGeneralization object for the payment options of a `Cost` object.\n\n## util.payment_mode.type.Adaptive\n\n```python\nAdaptive(PaymentMode):\n    pass\n```\n\nThe cost is handled as *running costs*. Payment is progressive (relative to a value, e.g. time) while an action is executed. The action halts if the costs cannot be payed anymore.\n\n## util.payment_mode.type.Advance\n\n```python\nAdvance(PaymentMode):\n    pass\n```\n\nThe cost is handled as *payment in advance*. Actions require payment first before they can be executed.\n\n## util.payment_mode.type.Arrear\n\n```python\nArrear(PaymentMode):\n    pass\n```\n\nThe cost is handled as *payment in arrear*. Actions that require payment are executed first and payed afterwards.\n\n## util.payment_mode.type.Shadow\n\n```python\nShadow(PaymentMode):\n    pass\n```\n\nRequires the resources or attribute points to be available to the player, but does not require payment of the resources.\n\n## util.placement_mode.PlacementMode\n\n```python\nPlacementMode(Object):\n    pass\n```\n\nGeneralization object for all placement modes that are configurable for a `CreatableGameEntity` object.\n\n## util.placement_mode.type.Eject\n\n```python\nEject(PlacementMode):\n    pass\n```\n\nThe game entity is ejected from the creating game entity. Ejecting considers the game entity's `Hitbox` and `TerrainRequirement` abilities for the ejection location.\n\n## util.placement_mode.type.OwnStorage\n\n```python\nOwnStorage(PlacementMode):\n    container : EntityContainer\n```\n\nThe game entity is stored into a container of the creating game entity.\n\n**container**\nContainer used for storing the game entity. If the creating game entity does not have a `Storage` ability wth this container or the container is already full, the placement mode cannot be used.\n\n## util.placement_mode.type.Place\n\n```python\nPlace(PlacementMode):\n    tile_snap_distance       : float\n    clearance_size_x         : float\n    clearance_size_y         : float\n    allow_rotation           : bool\n    max_elevation_difference : float\n```\n\nThe game entity can be placed on the map after its creation. Placement considers the game entity's `Hitbox` and `TerrainRequirement` abilities for the placement location.\n\n**tile_snap_distance**\nDistance between the snap anchors when placing the game entity in the game world.\n\n**clearance_size_x**\nNecessary free space on the x-axis for placing the game entity.\n\n**clearance_size_y**\nNecessary free space on the y-axis for placing the game entity.\n\n**allow_rotation**\nThe player can cycle through the `PerspectiveVariant` variants of the game entity before placement.\n\n**max_elevation_difference**\nMaximum elevation difference between the lowest and hightest point in the placement area.\n\n## util.placement_mode.type.Replace\n\n```python\nReplace(PlacementMode):\n    game_entities : set(GameEntity)\n```\n\nReplaces one or more existing game entities. The replaced game entities are permanently removed from the game.\n\n**game_entities**\nGame entities that can be replaced.\n\n## util.price_mode.PriceMode\n\n```python\nPriceMode(Object):\n    pass\n```\n\nGeneralization object for price modes used for adjusting prices of exchange rates.\n\n## util.price_mode.type.Dynamic\n\n```python\nDynamic(PriceMode):\n    change_value : float\n    min_price    : float\n    max_price    : float\n```\n\nThe price is adjusted dynamically on every exchange using a fixed value.\n\n**change_value**\nThe amount y which the price is adjusted.\n\n**min_price**\nLower bound for the price. The price cannot be changed to a value lower than this.\n\n**min_price**\nUpper bound for the price. The price cannot be changed to a value higher than this.\n\n## util.price_mode.type.Fixed\n\n```python\nFixed(PriceMode):\n    pass\n```\n\nThe price is not adjusted by an exchange.\n\n## util.price_pool.PricePool\n\n```python\nPricePool(Object):\n    pass\n```\n\nAllows syncing the price (at runtime) of an exchange rate across several game entities. The price for exchanging resources is the same for every exchange rate using the same price pool.\n\n## util.production_mode.ProductionMode\n\n```python\nProductionMode(Object):\n    pass\n```\n\nGeneralization object for all production modes used by a `ProductionQueue` ability.\n\n## util.production_mode.type.Creatables\n\n```python\nCreatables(ProductionMode):\n    exclude : set(CreatableGameEntity)\n```\n\nThe queue can store production requests for `CreatableGameEntity` instances.\n\n**exclude**\n`CreatableGameEntity` instances that are blacklisted from being appended to the queue.\n\n## util.production_mode.type.Researchables\n\n```python\nResearchables(ProductionMode):\n    exclude : set(ResearchableTech)\n```\n\nThe queue can store production requests for `ResearchableTech` instances.\n\n**exclude**\n`ResearchableTech` instances that are blacklisted from being appended to the queue.\n\n## util.progress.Progress\n\n```python\nProgress(Object):\n    properties     : dict(abstract(ProgressProperty), ProgressProperty) = {}\n    type           : children(ProgressType)\n    left_boundary  : float\n    right_boundary : float\n```\n\nGeneralization object for progression types.\n\n*Progress* can refer to\n\n* The progress of ability actions **or**\n* The percentage of a value at runtime for values that operate in a min-max range\n\nIt is defined as a float between 0.0 (0%) and 100.0 (100%).\n\n`Progress` objects define what happens if the progress of an action or runtime value enters a specified interval. The direction at which the interval is entered does not matter. The boundaries of the interval are defined by `left_boundary` and `right_boundary`. The interval is *left-closed* and *right-open* which means the `Progress` object is associazed with a progress in this range:\n\n```math\nleft\\_boundary <= progress < right\\_boundary\n```\n\nIn the special case of `left_boundary = 0.0` the interval is considered as *left-open*. This is done to ensure that progress can be exclusively checked *during* an ability action (between 0% and 100%) without having to consider the *before* or *after*. If progress should additionally be checked for the case 0%, a `Progress` object with the exact values `left_boundary = 0.0` and `right_boundary = 0.0` must be defined. Similarly, If progress should additionally be checked for the case 100%, a `Progress` object with the exact values `left_boundary = 100.0` and `right_boundary = 100.0` must be defined.\n\n**properties**\nDefines what happens if the progress enters the defined interval.\n\nThe engine expects objects from the namespace `engine.util.progress.property.type` as keys. Values must always be an instance of the object used as key.\n\nProperties:\n\n* `AnimationOverlay`: Overlays the animation of an ability with other animations.\n* `Animated`: Overrides the animation of an ability.\n* `Terrain`: Changes the underlying terrain of the game entity.\n* `TerrainOverlay`: Changes terrain overlays of a game entity.\n* `StateChange`: Alters the base abilities and modifiers of the game entity through `StateChanger` objects.\n\n**type**\nClassification for the progress.\n\n**left_boundary**\nDefines the left boundary of the progression interval. Must be a float between 0.0 and 100.0 that represents a percentage of progression. Must be smaller than `right_boundary`.\n\n**right_boundary**\nDefines the left boundary of the progression interval. Must be a float between 0.0 and 100.0 that represents a percentage of progression. Must be larger than `left_boundary`.\n\n## util.progress.property.ProgressProperty\n\n```python\nProgressProperty(Object):\n    pass\n```\n\nGeneralization object for all properties of `Progress` objects.\n\n## util.progress.property.type.Animated\n\n```python\nAnimated(ProgressProperty):\n    overrides : set(AnimationOverride)\n```\n\nOverrides the animation of abilities when the specified progress interval has been reached.\n\n**overrides**\nThe overriding animations.\n\n## util.progress.property.type.AnimationOverlay\n\n```python\nAnimationOverlay(ProgressProperty):\n    overlays : set(Animation)\n```\n\nOverlays the animation of abilities with the specified animations.\n\n**overlays**\nThe overlay animations.\n\n## util.progress.property.type.StateChange\n\n```python\nStateChange(ProgressProperty):\n    state_change : StateChanger\n```\n\nAlters the base abilities and modifiers of a game entity when the specified progress interval has been reached.\n\n**state_change**\nThe state modifications as a `StateChanger` object.\n\n## util.progress.property.type.TerrainOverlay\n\n```python\nTerrainOverlay(ProgressProperty):\n    terrain_overlay : Terrain\n```\n\nChanges overlayed terrain of a game entity when the specified progress interval has been reached. The game entity needs an enabled `OverlayTerrain` ability for this to work.\n\n**terrain**\nOverrides the overlayed terrain of the currently enabled `OverlayTerrain` ability of the game entity. The override stops when another terrain overlay override is initiated, the overridden `OverlayTerrain` ability is deactivated or the override terrain is the same as the default overlayed terrain of the ability.\n\n## util.progress.property.type.Terrain\n\n```python\nTerrain(ProgressProperty):\n    terrain : Terrain\n```\n\nChanges the underlying terrain of a game entity when the specified progress interval has been reached.\n\n**terrain**\nThe new terrain that will be permanently placed under the game entity.\n\n## util.progress_status.ProgressStatus\n\n```python\nProgressStatus(Object):\n    progress_type : children(ProgressType)\n    progress      : float\n```\n\nGeneralization object for progress status objects used by `GameEntityProgress`.\n\n**progress_type**\nType of progress.\n\n**progress**\nMinimum amount of progress that has to be reached. Value can be between 0.0 and 100.0.\n\n## util.progress_type.ProgressType\n\n```python\nProgressType(Object):\n    pass\n```\n\nUsed by `Convert` effects and resistances for matching.\n\n## util.progress_type.type.AttributeChange\n\n```python\nAttributeChange(ProgressType):\n    pass\n```\n\nCompares the current attribute value in relation to the `max_value` of an attribute of a game entity. The `Progress` objects should be stored in a `AttributeChangeTracker` ability which specifies the type of attribute that is monitored. When the attribute value is equal to `max_value` of the attribute defined by the game entity, the progress is 100%. Once the attribute value reaches the `min_value`, the progress is 0%.\n\n## util.progress_type.type.Carry\n\n```python\nCarry(ProgressType):\n    pass\n```\n\nMonitors the occupied storage space of a `Storage` or `Gather` ability. An empty storage has a progress of 0% and a full storage a progress of 100%.\n\n## util.progress_type.type.Construct\n\n```python\nConstruct(ProgressType):\n    pass\n```\n\nMonitors the construction progress of a game entity with `Contructable` ability. An unconstructed game entity has a progress of 0% and a fully constructed game entity a progress of 100%.\n\n## util.progress_type.type.Harvest\n\n```python\nHarvest(ProgressType):\n    pass\n```\n\nMonitors the harvesting progress of a resource spot stored by a `Harvestable` ability. A resource spot at maximum capacity has a progress of 0% and a depleted resource spot a progress of 100%.\n\n## util.progress_type.type.Restock\n\n```python\nRestock(ProgressType):\n    pass\n```\n\nMonitors the restock progress of a restockable resource spot stored by a `Harvestable` ability. The restocking progress is initiated by the `Restock` ability of another game entity. At the start of the restocking process, the progress is 0%. After the restocking has finished, the progress is 100%.\n\n## util.progress_type.type.Transform\n\n```python\nTransform(ProgressType):\n    pass\n```\n\nMonitors the progress of a transformation initiated by the `ActiveTransformTo` or `PassiveTransformTo` ability. At the start of the transformation, the progress is 0%. After the transformation has finished, the progress is 100%.\n\n## util.research.ResearchableTech\n\n```python\nResearchableTech(Object):\n    tech            : Tech\n    cost            : Cost\n    research_time   : float\n    research_sounds : set(Sound)\n    condition       : set(LogicElement)\n```\n\nDefines preconditions for researching a technology with the `Research` ability.\n\n**tech**\nReference to the `Tech` object that is researched.\n\n**cost**\nResource amount spent to initiate the research. Cancelling the research results in a refund of the spent resources.\n\n**research_time**\nTime to wait until the `Tech` object's patches are applied in seconds.\n\n**research_sounds**\nSounds that are played when the research finishes.\n\n**condition**\nCondition that unlock the technology for the researching game entity. Only one condition needs to be fulfilled to trigger the unlock. If the set is empty, the technology is considered available from the start for the researching game entity.\n\n## util.resource.Resource\n\n```python\nResource(Object):\n    name        : TranslatedString\n    max_storage : int\n```\n\nDefines a resource that can be used in the game. Adding a resources will give an amount of 0 of that resource to all players. The current amount of resources can be influenced by the abilities and modifiers of game entities.\n\n**name**\nName of the resource.\n\n**max_storage**\nMaximum amount of resources that can be stored in the player's global resource storage.\n\n## util.resource.ResourceContingent\n\n```python\nResourceContingent(Resource):\n    min_amount : int\n    max_amount : int\n```\n\nA `Resource` that creates a *contingent* which is temporarily usable by game entities. The size of the contingent is determined by two values:\n\n* Static amounts can be acquired like normal resources\n* Temporary amounts that can be provided by game entities with `ProvideContingent`\n\nBy using the contingent (see `UseContingent` ability), the current amount of resources is not reduced. Instead, the game entity will reserve parts of the contingent until it loses the ability or dies. When the whole contingent is reserved, no more game entities using it can be created.\n\nContingents can be utilized to implement mechanics like Population Space (AoE2) or Supply (Starcraft).\n\nNote that it is also allowed to spend the static amounts determining the contingent size like normal resources.\n\n**min_amount**\nThe minimum contingent size. Static and temporary amounts will be added to this value.\n\n**max_amount**\nThe maximum contingent size.\n\n## util.resource.ResourceAmount\n\n```python\nResourceAmount(Object):\n    type   : Resource\n    amount : int\n```\n\nA fixed amount of a certain resource.\n\n**type**\nReference to the resource.\n\n**amount**\nAmount of the resource.\n\n## util.resource.ResourceRate\n\n```python\nResourceRate(Object):\n    type : Resource\n    rate : float\n```\n\nA per-second rate of a certain resource.\n\n**type**\nReference to the resource.\n\n**rate**\nRate of the resource.\n\n## util.resource_spot.ResourceSpot\n\n```python\nResourceSpot(Object):\n    resource        : Resource\n    max_amount      : int\n    starting_amount : int\n    decay_rate      : float\n```\n\nAmount of resources that is gatherable through the `Harvestable` ability of a game entity.\n\n**resource**\nType of resource that can be harvested.\n\n**max_amount**\nMaximum resource capacity of the resource spot.\n\n**starting_amount**\nGatherable amount when the resource spot is created.\n\n**decay_rate**\nDetermines how much resources are lost each second after the resource spot is activated (see `harvestable_by_default` for details).\n\n## util.selection_box.SelectionBox\n\n```python\nSelectionBox(Object):\n    pass\n```\n\nDefines the selection area for the `Selectable` ability.\n\n## util.selection_box.type.MatchToSprite\n\n```python\nMatchToSprite(SelectionBox):\n    pass\n```\n\nUse animation sprites of the game entity as the selection area. Transparent pixels are excluded.\n\n## util.selection_box.type.Rectangle\n\n```python\nRectangle(SelectionBox):\n    width  : float\n    height : float\n```\n\nUse a rectangular box as the selection area.\n\n**width**\nWidth of the selection box.\n\n**height**\nHeight of the selection box.\n\n## util.setup.PlayerSetup\n\n```python\nPlayerSetup(Object):\n    name               : TranslatedString\n    description        : TranslatedMarkupFile\n    long_description   : TranslatedMarkupFile\n    leader_names       : set(TranslatedString)\n    modifiers          : set(Modifier)\n    starting_resources : set(ResourceAmount)\n    game_setup         : orderedset(Patch)\n```\n\nPre-defined configuration for a specific player in a game. Patches are applied to the player's database view before the game starts.\n\n**name**\nName of the player setup.\n\n**description**\nDescription of the player setup.\n\n**long_description**\nA longer description of the player setup.\n\n**leader_names**\nNames for the leader of civilizations that are displayed with the score.\n\n**modifiers**\nModifiers for game entities of the player setup. By default, these modifiers apply to **all** game entities belonging to the player. For example, an `AttributeModifier` with `multiplier = 1.2` for the attribute `Health` will increase the maximum HP of every unit owned by the player by 20%. If you want the modifier to only affect specific game entities, you have to use the `Scoped` property or assign `Modifier` objects to individual game entities using `civ_setup`.\n\n**starting_resources**\nResources of the player at the start of a game.\n\n**game_setup**\nCustomizes the player setup through patches. Patches to other player setups can also be applied using the `Diplomatic` property for a `Patch`.\n\n## util.sound.Sound\n\n```python\nSound(Object):\n    play_delay : float\n    sounds     : orderedset(file)\n```\n\nA collection of sound files that can be played by abilities of a game entity.\n\n**play_delay**\nDelay when the sound loops.\n\n**sounds**\nA set of sound files that are played in the order they are stored in the set.\n\n## util.state_machine.StateChanger\n\n```python\nStateChanger(Object):\n    enable_abilities  : set(abstract(Ability))\n    disable_abilities : set(abstract(Ability))\n    enable_modifiers  : set(abstract(Modifier))\n    disable_modifiers : set(abstract(Modifier))\n    transform_pool    : optional(TransformPool) = None\n    priority          : int\n```\n\nState changes alter the *base state* of a game entity which is defined by the abilities and modifiers stored in a `GameEntity` object. They are allowed to enable new and disable existing abilities as well as modifiers. Multiple state changes can be applied at once. Only abilities and modifiers with a priority *lower than or equal to* (<=) the one defined in the `StateChanger` object will be disabled.\n\n**enable_abilities**\nAbilities that are enabled when the state change is active.\n\n**disable_abilities**\nAbilities that are disabled when the state change is active and the abilities have a priority *lower than or equal to* (<=) the `priority` in the disabling `StateChanger` object.\n\n**enable_modifiers**\nModifiers that are enabled when the state change is active.\n\n**disable_modifiers**\nModifiers that are disabled when the state change is active and the modifiers have a priority *lower than or equal to* (<=) the `priority` in the disabling `StateChanger` object.\n\n**transform_pool**\nTransform pool the state change uses. Only one state change can be active per transform pool. If this is `None`, the base state is changed.\n\n**priority**\nPriority of a state change. The value can be negative. Abilities and modifiers belonging to the base state have an implicit priority of 0.\n\n## util.state_machine.Reset\n\n```python\nReset(StateChanger):\n    enable_abilities  = {}\n    disable_abilities = {}\n    enable_modifiers  = {}\n    disable_modifiers = {}\n    transform_pool    = None\n    priority          = 0\n```\n\nResets the game entity to the *base* state. This means that all state changes are cleared immediately on activation.\n\n## util.storage.EntityContainer\n\n```python\nEntityContainer(Object):\n    allowed_types        : set(children(GameEntityType))\n    blacklisted_entities : set(GameEntity)\n    storage_element_defs : set(StorageElementDefinition)\n    slots                : int\n    carry_progress       : set(Progress)\n```\n\nUsed by the `Storage` ability to set the allowed game entities and store definitions of how the stored game entities influence the storing game entity.\n\n**allowed_types**\nWhitelist of game entity types that can be stored in this container.\n\n**blacklisted_entities**\nBlacklist for specific game entities that would be covered by `allowed_types`, but should be explicitly excluded.\n\n**storage_element_defs**\nContains further configuration settings for specific game entities.\n\n**slots**\nDefines how many slots for game entities the container has. Multiple game entities may be stacked in one slot depending on the `elements_per_slot` member in `StorageElementDefinition`.\n\n**carry_progress**\n`CarryProgress` objects that can alter the game entity when the container is filled. The objects in the set must have progress type `Carry`.\n\n## util.storage.ResourceContainer\n\n```python\nResourceContainer(Object):\n    resource       : Resource\n    max_amount     : int\n    carry_progress : set(Progress)\n```\n\nUsed by the `ResourceStorage` ability to define storage space for resources that can be carried by a game entity.\n\n**resource**\nResource stored in the container.\n\n**max_amount**\nMaximum amount of resources that can be stored in the container.\n\n**carry_progress**\n`CarryProgress` objects that can alter the game entity when the container is filled.  The objects in the set must have progress type `Carry`.\n\n## util.storage.resource_container.type.InternalDropSite\n\n```python\nInternalDropSite(ResourceContainer):\n    update_time    : float\n```\n\nDeposits the resources directly into the player's global resource amount. Instead of having to drop the resources at a drop site, the resource amount in the internal drop site is added to the player's resource amounts in defined intervals.\n\n**update_time**\nUpdate interval between the automatic deposition of the stored resources in seconds.\n\n## util.storage.StorageElementDefinition\n\n```python\nStorageElementDefinition(Object):\n    storage_element   : GameEntity\n    elements_per_slot : int\n    conflicts         : set(StorageElementDefinition)\n    state_change      : StateChanger\n```\n\nDefines how a stored game entity influences its container game entity.\n\n**storage_element**\nStored game entity to which this definition applies.\n\n**elements_per_slot**\nDefines how many game entities of the type referenced in `storage_element` can be stacked in one slot.\n\n**conflicts**\nStorage elements which cannot be in the container at the same time as this storage element.\n\n**state_change**\nAlters the base abilities and modifiers of the storing game entity when at least one game entity of the type referenced in `storage_element` is present in the container.\n\n## util.target_mode.TargetMode\n\n```python\nTargetMode(Object):\n    pass\n```\n\nGeneralization object for target modes used by projectiles.\n\n## util.target_mode.type.CurrentPosition\n\n```python\nCurrentPosition(TargetMode):\n    pass\n```\n\nMakes the projectile path end at the current position of the target when the projectile spawned.\n\n## util.target_mode.type.ExpectedPosition\n\n```python\nExpectedPosition(TargetMode):\n    pass\n```\n\nMakes the projectile path end at the position where the target is expected to be when the projectile is supposed to hit it.\n\n## util.taunt.Taunt\n\n```python\nTaunt(Object):\n    activation_message : text\n    display_message    : TranslatedString\n    sound              : Sound\n```\n\nA predefined message players can send to each other.\n\n**activation_message**\nActivation message that has to be typed into the chat console.\n\n**display_message**\nDisplayed message after the taunt is activated.\n\n**sound**\nSounds that are played after the taunt is activated.\n\n## util.tech.Tech\n\n```python\nTech(Object):\n    types            : set(children(TechType))\n    name             : TranslatedString\n    description      : TranslatedMarkupFile\n    long_description : TranslatedMarkupFile\n    updates          : orderedset(Patch)\n```\n\nAn object that can apply changes through patching. It follows the standard implementation from most other strategy games. The `Tech` object only stores the patches that change the game state, while cost, research time, and requirements are decided by the `Research` ability of a game entity. By default, technologies can only be applied once and researched by one game entity at a time. Afterwards, the engine sets a flag that the `Tech` was applied and automatically forbids researching it again.\n\n**types**\nClassification of the tech.\n\n**name**\nName of the technology.\n\n**description**\nDescription of the technology.\n\n**long_description**\nA longer description of the technology.\n\n**updates**\nChanges the game state through patches.\n\n## util.tech_type.TechType\n\n```python\nTechType(Object):\n    pass\n```\n\nClassification for a tech.\n\n## util.tech_type.Any\n\n```python\nAny(TechType):\n    pass\n```\n\nCan be used to address any tech, even ones that have no `TechType` assigned.\n\n## util.terrain.Terrain\n\n```python\nTerrain(Object):\n    types           : set(children(TerrainType))\n    name            : TranslatedString\n    terrain_graphic : Terrain\n    sound           : Sound\n    ambience        : set(TerrainAmbient)\n    path_costs      : dict(children(PathType), int)\n```\n\nTerrains define the properties of the ground which the game entities are placed on.\n\n**types**\nClassification of the terrain.\n\n**name**\nName of the terrain.\n\n**terrain_graphic**\nTexture of the terrain (see `util.graphics.Terrain`).\n\n**sound**\nAmbient sound played when the camera of the player is looking onto the terrain.\n\n**ambience**\nAmbient objects placed on the terrain.\n\n**path_costs**\nBase costs of traversing the pathfinding grid on map areas where the terrain is placed.\n\nKeys are `PathType` objects that are associated with a pathfinding grid in the pathfinder.\n\nValues represent the pathing cost for the terrain on the pathfinding grid. Each value must be an integer between `1` and `255`. `1` defines the *minimum* possible cost and `254` represents the *maximum* possible cost. `255` signifies that the terrain is impassable for the specified path type.\n\nFor `PathType` objects that exist in the modpack but are not keys in this dict, a default cost value of `1` is assumed.\n\n## util.terrain.TerrainAmbient\n\n```python\nTerrainAmbient(Object):\n    object      : GameEntity\n    max_density : int\n```\n\nAn ambient game entity that is placed randomly on a chunk of terrain (10x10 tiles).\n\n**object**\nGame entity placed on the terrain.\n\n**max_density**\nDefines how many ambient objects are allowed to be placed on a chunk at maximum.\n\n## util.terrain_type.TerrainType\n\n```python\nTerrainType(Object):\n    pass\n```\n\nClassification for a terrain.\n\n## util.terrain_type.Any\n\n```python\nAny(TerrainType):\n    pass\n```\n\nCan be used to address any terrain, even ones that have no `TerrainType` assigned.\n\n## util.trade_route.TradeRoute\n\n```python\nTradeRoute(Object):\n    trade_resource    : Resource\n    start_trade_post  : GameEntity\n    end_trade_post    : GameEntity\n```\n\nGeneralization object that defines a trade route between two game entities.\n\n**trade_resource**\nResource that is traded on this trade route. The traded amount depends on the trade route type.\n\n**start_trade_post**\nThe game entity where the traded resource is collected. The game entity must have a `TradePost` ability that contains this `TradeRoute` object.\n\n**end_trade_post**\nThe game entity the traded resource is delivered to. The game entity must have a `TradePost` ability that contains this `TradeRoute` object.\n\n## util.trade_route.type.AoE1TradeRoute\n\n```python\nAoE1TradeRoute(Object):\n    exchange_resources : set(Resource)\n    trade_amount       : int\n```\n\nUses Age of Empires 1 rules for trading.\n\n**exchange_resources**\nThe trading game entity exchanges `trade_amount` of a selected resource in this set by `trade_amount` of the resource defined by `trade_resource`.\n\n**trade_amount**\nAmount of resources traded each time.\n\n## util.trade_route.type.AoE2TradeRoute\n\n```python\nAoE2TradeRoute(Object):\n    pass\n```\n\nUses Age of Empires 2 rules for trading. The trading game entity chooses the nearest possible `end_trade_post` from the `start_trade_post`. Calculation of the traded resource amount is based on this formula:\n\n```math\ntrade\\_amount = 0.46 \\cdot tiles\\_distance \\cdot ((tiles\\_distance / map\\_size) + 0.3)\n```\n\n## util.transform_pool.TransformPool\n\n```python\nTransformPool(Object):\n    pass\n```\n\nDefines a pool for `StateChanger` objects. Only one state change can be active per transform pool.\n\n## util.variant.Variant\n\n```python\nVariant(Object):\n    changes  : orderedset(Patch)\n    priority : int\n```\n\nVariants can change the game entity when it is created. When variants are chosen depends on their type.\n\n**changes**\nThe changes to the game entity as a set of patches. Only the created game entity is affected.\n\n**priority**\nWhen many variants are chosen, this member influences the order in which the patches from their `changes` member are applied. Patches from variants with higher priority value are applied first.\n\n## util.variant.type.AdjacentTilesVariant\n\n```python\nAdjacentTilesVariant(Variant):\n    north      : optional(GameEntity)\n    north_east : optional(GameEntity)\n    east       : optional(GameEntity)\n    south_east : optional(GameEntity)\n    south      : optional(GameEntity)\n    south_west : optional(GameEntity)\n    west       : optional(GameEntity)\n    north_west : optional(GameEntity)\n```\n\nA variant that is chosen based on adjacent game entities. From all `AdjacentVariant` variants the one with the most matches in all directions is chosen.\n\n**north**\nThe desired game entity north of the created game entity.\n\n**north_east**\nThe desired game entity north-east of the created game entity.\n\n**east**\nThe desired game entity east of the created game entity.\n\n**south_east**\nThe desired game entity south-east of the created game entity.\n\n**south**\nThe desired game entity south of the created game entity.\n\n**south_west**\nThe desired game entity south-west of the created game entity.\n\n**west**\nThe desired game entity west of the created game entity.\n\n**north_west**\nThe desired game entity north-west of the created game entity.\n\n## util.variant.type.RandomVariant\n\n```python\nRandomVariant(Variant):\n    chance_share : float\n```\n\nFrom all variants of this type in the `variants` member of the game entity, one will be picked at random.\n\n**chance_share**\nThe relative chance of the variant to be picked. Note that this is **not** a percentage chance. The value defines how likely it is for the variant to be chosen relative to the other `RandomVariant` objects.\n\nExample:\n\n* Random variant 1 with `chance_share = 1.0`\n* Random variant 2 with `chance_share = 4.0`\n\nThe second variant is four times as likely to be picked. The absolute chances are:\n\n* Random variant 1: 20\\%\n* Random variant 2: 80\\%\n\n## util.variant.type.PerspectiveVariant\n\n```python\nPerspectiveVariant(Variant):\n    angle : int\n```\n\nVariant depending on the placement angle of the game entity. Currently only works with the `PlacementMode` of type `Place` with the `allow_ratation` member set to true.\n\n**angle**\nAngle of the game entity.\n"
  },
  {
    "path": "doc/nyan/openage-lib.md",
    "content": "# openage nyan library\n\nThe openage engine defines all kinds of `NyanObject`s so the nyan interpreter can verify data integrity. Together, the defined objects provide the API that can be used by the games running on the engine.\n\nThis interface will be described here.\n\n1. [Design principles](#design-principles)\n2. [API objects](#api-objects)\n   1. [Entity](#entity)\n   2. [GameEntity](#gameentity)\n   3. [Ability](#ability)\n   4. [Modifier](#modifier)\n   5. [Effect](#effect)\n3. [C++ Interface](#c-interface)\n\n\n## Design principles\n\nIn addition to the [nyan design goals](https://github.com/SFTtech/nyan/blob/master/doc/README.md#design-idea), the openage mod API follows its own design principles. It is recommended to follow these principles when extending the API.\n\n**Single API tree**\n\nThe API objects and every additional defined object are part of the same inheritance tree. To ensure this, all objects must inherit from the root object `Entity`, either explicitely or implicitely through its parents. By doing this, we can maintain a consistent object hierarchy, even if several mods are activated.\n\n**Define game entities through abilities**\n\nGame entities are nyan objects that are visibly present in the game world (e.g. units or buildings). Their data will mostly be defined by nested objects inheriting from `Ability`. An `Ability` object is basically a capsule for data and corresponds to an engine function. Therefore, abilities indirectly define what a unit can *do* (attack, gather, move, etc.) or what i can *be* (constructable, selectable, etc.). During a game, the corresponding engine function then decides *how* the data is going to be used.\n\n**No replacement of game entities within their life cycle**\n\nIn AoE2, game objects' attributes are mostly static and cannot represent different states. For example, tree objects are replaced with tree stump objects when they are chopped down by villagers, empty trade carts are replaced with full trade carts once they reach a market and villager objects change when they are assigned to a different resource spot. Because nyan objects can dynamically change at runtime, this strategy becomes unfeasible as it becomes increasingly difficult to determine a consistent target state. Instead, abilities will be used for modelling the various states a nyan object has. There also exists the possibility to model game entities as state machines.\n\n**No reliance on inheritance for specialization**\n\nInheritance cannot be removed from an object, so we have to be careful when introducing it to the API. Instead of inheritance an entity-component model is utilized, e.g. for abilities and modifers. If inheritance is used, it should follow these principles:\n\n* Avoid multi-inheritance in the API tree\n* Make it possible to configure the inherited members in such a way that the non-specialized behavior can be replicated\n\n**Genie engine functionality ⊆ nyan API functionality**\n\nThe functionality of Genie Engine abilities should always be a subset of the functionality of the openage API. Hardcoding should be removed and integrated into existing abilities.\n\n## API objects\n\nAn overview of the API design for the data of Age of Empires 2 is available as an UML diagram.\n\n![AoE2 API UML](aoe2_nyan_tree.svg)\n\n### Entity\n\nThe root object of the nyan tree. Stores no members. All API objects inherit from `Entity`.\n\n### GameEntity\n\n`GameEntity` defines every object that the player will be able to interact with in the game world. The `GameEntity` object is purposefully kept simple to make it versatile and allow as many unit configurations as possible. Characteristics of a game entity, including all stats and animations, are defined through its **abilities** and **modifiers**. Additionally, game entities can have **variants** that can be used for defining alternative graphics (e.g. for villagers or houses). Game entities are classified by assigning them **types** through `GameEntityType`.\n\n### Ability\n\nAn `Ability` gives a game entity a **defined behaviour** implemented in an engine function. Abilities are used for both passive (e.g. `Selectable`, `Constructable`) and active behaviour (e.g. `Move`, `Create`, `Gather`). Most abilities have members that further refine the parameters of the behaviour. For example, the `Move` ability enables a game entity to move around the map, while its `speed` member determines how fast the entity moves.\n\nThere are currently three ability subtypes available:\n\n* `SoundAbility`: While the ability is used, the game entity plays a sound. If more than one sound is defined in the set, the engine picks one at random.\n* `AnimatedAbility`: While the ability is used, the game entity plays an animation. The settings for the animation (speed, angles, frames, looping) are defined in the `.spite` file linked in the `Animation` object and are independent from the ability. If more than one animation is defined in the set, the engine picks one at random.\n* `DiplomaticAbility`: Determines whether the ability can be used to interact with other players with a specific diplomatic stance. Diplomatic abilities can only be used on (active abilities) or used by (passive abilities) players that have the diplomatic stance listed in the set. For example, an attack ability where `Enemy` is set in the stances is only able to use this attack on enemy units. If the attack has `Enemy` and `Neutral` in the set, it will also be able to attack neutral units. When `DiplomaticAbility` is not inherited, the ability is available for interaction with all stances by default.\n\nMost abilities have been reworked in comparison to AoE2 to provide extended functionality and make hardcoded parameters accessible to modding. The behaviour of the original game can be seen as a subset of the features in the openage nyan API.\n\n### Modifier\n\n`Modifier` objects alter parts of the behavior of abilities in certain situations. Their main applications are:\n\n* Covering edge cases that do not fit into the definition of an `Ability`. Examples include elevation and cliff attack bonuses as well as free tech unlocks by civs.\n* Providing bonuses that are percentage based (e.g. 20% more HP) because they are hard to model as patches.\n\nBy default, modifiers apply to the abilities of the game entity that stores them. With `ScopedModifier` the modifier can be applied to other game entities as well. The primary use for this are civilization bonuses.\n\n### Effect\n\n`Effect`s are used for (combat) interaction between two game entities. An effect is actively applied by one game entity (effector) with the `ApplyDiscreteEffect`/`ApplyContinuousEffect` abilities and met with a response by the affected game entity (resistor) using the `Resistance` ability. The use cases for effects are:\n\n* Combat (attacking, healing)\n* Conversion\n* Making resource spots accessible\n* Lure game entities (e.g. sheep)\n* Town bell\n\nThere are two major types of effects: Discrete and continuous. Discrete effects happen at a *specific point in time*, while continuous effects are applied *over time* at a per second rate. Any discrete effect can be combined with every other discrete effect. The same applies for continuous effects.\n\nFor an effect to apply, the effector's `Effect` needs a corresponding `Resistance` object on the resistor's side. Otherwise the effect is not evaluated.\n\n## C++ Interface\n\nopenage provides a thin nyan API layer to make interacing with nyan more accessible. This layer is implemented in `libopenage/gamestate/api` and\ncovers the most common use caes for retrieving objects and reading values. Using/extending the nyan API layer should be preferred when adding game logic that accesses nyan to the engine.\n"
  },
  {
    "path": "doc/project_structure.md",
    "content": "# Project Structure\n\nOne of the biggest problems for newcomers who want to contribute code to free\nsoftware projects is that they have no idea where to start.\n\nReading, understanding and finding the relevant code part is hard.\n\nThis file explains the modular structure of the project.\n\n1. [Architecture](#architecture)\n2. [Languages](#languages)\n3. [Folders](#folders)\n   1. [assets/](#assets)\n   2. [buildsystem/](#buildsystem)\n   3. [cfg/](#cfg)\n   4. [dist/](#dist)\n   5. [doc/](#doc)\n   6. [etc/](#etc)\n   7. [legal/](#legal)\n   8. [libopenage/](#libopenage)\n   9. [openage/](#openage)\n   10. [packaging/](#packaging)\n\n\n\n## Architecture\n\nThe [overall architecture](/doc/code/architecture.md) describes the conceptual overview.\n\n\n## Languages\n\nWe use Python, Cython and C++.\n\n| Extension     | Language | Usage                                                 |\n| ------------- | -------- | ----------------------------------------------------- |\n| `.py`         | Python   | Everything that does not crunch data                  |\n| `.pyx` `.pxd` | Cython   | Fast Python code, glue between C/C++ and Python       |\n| `.h`   `.cpp` | C++      | Data crunching code: simulation, graphics, sound, ... |\n\n\n## Folders\n\nEach folder in the root of the project contains fundamentally different\ncontents. This ensures separation of components and defines the base structure\nof the project.\n\n\n### assets/\n\nGame assets required at run time are placed in here. This includes everything\nthat is converted from the original assets (see [asset conversion](media_convert.md))\nand other input like shaders etc. The directory is `assets` for local builds or\ninstalled to `/usr/share/openage/assets` or whatever the platform recommends.\n\n\n### buildsystem/\n\nBuildsystem components, namely `cmake` scripts and configuration\ntemplates, are placed in this folder. This includes scripts for test\nintegration, python module definition, source file gathering and\nexecutable definitions are placed in this directory.\n\nThe code compliance checker also lives here.\n\n\n### cfg/\n\nContains the standard configuration of the engine, e.g. keybindings.\n\n\n### dist/\n\nFiles for package distribution.\n\n\n### doc/\n\nIn the `doc/` folder is conceptual documentation, ideas, algorithms, etc.\nThe code itself is commented with `/** doxygen comments */`.\n\nSee [doc/README.md](/doc/README.md) for documentation guidelines.\n\n### etc/\n\nAdditional resources for development tools, e.g. pylint and valgrind configs.\n\n\n### legal/\n\nOur license (GPLv3+) and licenses of other projects' components that are\nintegrated into openage.\n\n\n### libopenage/\n\nSource files written in **C++20** live here.\nAll engine components, data structures and C++ tests are located in this\nfolder, each subsystem resides in its own subfolder.\n\n\n### openage/\n\nContains the Python3 auxiliary components of openage.\nPython3 is used for non-data-crunching tasks, like converting original assets,\nscripting and modding.\n\nThe main entry point for openage is in `__main__.py`.\n\n\n### packaging/\n\nCMake script for creating a package for distribution.\n"
  },
  {
    "path": "doc/releasing.md",
    "content": "# Releasing\n\nopenage uses [Semver](https://semver.org).. People often seem to overlook the special rules for initial development versions. Please avoid prerelease versions (`-alpha`, `-beta`, `-rc.1`, ...) for now. We might decide to include build metadata like date or commit hash, that's up for discussion.\n\nRelease guide:\n\n1. Create a new changelog named `doc/changelogs/engine/$VERSION.md`.\n1. Bump the version number in `openage_version`. Given `0.y.z`, both `0.y.(z+1)` and `0.(y+1).0` are valid. The latter will probably be more common, but which of those you choose doesn't really matter.\n1. Commit, PR, merge, pull as usual.\n    * OPTIONAL: Set up [commit signing](https://help.github.com/en/articles/managing-commit-signature-verification), signing tags will make a \"verified\" badge show up on the release later.\n1. Tag the merge commit, something like `git tag -s v0.5.0`. The prefixed `v` is mandatory.\n1. Push the tag to GitHub.\n1. Use the GitHub web interface to publish a release from the tag. Make it look nice, see previous release description. If in doubt, save the draft and have someone else proof read it. Also include the changelog.\n1. In the future, kevin will automatically attach build artifacts like installers to the release. For now, you will need to do that manually. Have fun setting up a windows build environment or if you feel like being lazy, go bug someone who already has. Actually, taking care of where to build for windows before publishing the release might not be a bad idea.\n    * OPTIONAL: Brag on social media. /r/openage, /r/aoe2 and AoE2 Discords might be the right place to do so.\n"
  },
  {
    "path": "doc/reverse_engineering/aoc_structures_00_09_26_0809",
    "content": "# reversed engineered structures\n# for the age2_x1.exe version 00.09.26.0809\n\n\nRGE_Command\nRGE_Task_list\n\n\n\n\\Game (8240)\n\t0       VOID    Name\n\t4       CALL    005E4DA0\n\t8       CALL\n\t12      CALL\n\t24      CALL\n\t28      CALL    005E4720 (GetSetupStatus)\n\t32      VOID    hCampaign\n\t36      CALL    GetLanguageStringByID\n\t40      VOID    Settings\n\t44      VOID    HWND (WindowHandle)\n\t48      DWORD   WindowStatus\n\t56      DWORD\n\t72      DWORD   RoomTime\n\t76      DWORD\n\t80      DWORD   ???\n\t88      CALL    ???\n\t92      CALL    ???\n\t96      CALL    ???\n\t100     DWORD   SetupStatus\n\t104     FPN4    SetupTimer\n\t108     VOID    DirectDraw\n\t256     CALL    return world\n\t440     VOID\n\t476     VOID    Queries\n\t1060    VOID    World\n\t5092    DWORD   Unknown1\n\t5096    DWORD   Unknown2\n\t5108    9*BYTE  Player_Cultures\n\t5320    VOID    RMS_FileName\n\t4253    VOID\n\t4780    VOID    PlayerConfigs\n\n\\DirectDraw (1208)\n\t44      FLT4    ???\n\n\\Game\\Settings ()\n\t1040    DWORD\n\t1646    DWORD\n\t2196    DWORD   FullScreenEnabled\n\t2204    DWORD   DirectDrawEnabled\n\t2208    DWORD   SystemMemoryEnabled\n\t2276    DWORD   ScreenWidth\n\t2280    DWORD   ScreenHeight\n\t3396    VOID    DirectoryName\n\t3657    VOID    CampaignFileName\n\n\\PlayerConfigs (28)\n\n\\Game\\World[]\n\t12      VOID    Rendering\n\t16      DWORD   ???\n\t20\n\t24      DWORD   ???\n\t32      DWORD   Spawns_Identity\n\t36      FLT4    ???\n\t40      BYTE    ???\n\t41      BYTE    ???\n\t42      BYTE\n\t44      DWORD   Sounds_IncrementID\n\t48      DWORD   Graphics_IncrementID\n\t52      VOID    Terrains\n\t56      DWORD   Sounds_Count\n\t60      VOID    Sounds_Array\n\t64      DWORD   Graphics_Count\n\t68      VOID    Graphics_Array\n\t72      DWORD   PlayersCount\n\t76      VOID    PlayersArray\n\t76      CALL    0040AA80\n\t80      CALL    0040AAF0\n\t80      DWORD   Profiles_Count?\n\t84      VOID    Profiles_Array\n\t88      VOID    Modification\n\t92      WORD    Boundaries_Count\n\t94      WORD    Boundaries_Usage\n\t96      VOID    Boundaries_Array\n\t100     VOID\n\t104     VOID    9668 bytes unknown\n\t108     VOID    Library\n\t112     CALL    ???\n\t112     VOID\n\t116     DWORD   ???\n\t120     CALL    ??? Terrain\n\t120     DWORD   Colors_Count\n\t124     VOID    Colors_Array\n\t128     VOID    Spawns_Identity\n\t132     CALL\n\t136\n\t140     BYTE    ???\n\n\t144     VOID\n\t148     WORD    CurrentPlayerID\n\t152     FLT4    MapViewX\n\t156     FLT4    MapViewY\n\t160     FLT4    ???\n\t164     DWORD   Sounds_Unknown (sound_dvr)\n\t168     FLT4    ???\n\n\t180     CALL    ? destructor\n\n\t192     CALL    ?\n\t196     DWORD   CampaignIndex\n\t200     DWORD   CampaignPlayerIndex\n\t204     DWORD   CampaignScenarioIndex\n\t208\n\t212     9*DWORD ???\n\t248     VOID\n\t252     VOID\n\t256     VOID\n\t260     VOID\n\t264     VOID\n\t268     VOID\n\t272     VOID\n\t276     CALL    0040A9B0\n\n\t284     DWORD\n\n\t396     VOID    Objects_Array\n\t400     DWORD   Objects_Count\n\t404     DWORD   Difficulty\n\t408     DWORD   LockTeams\n\t412     VOID\n\n\t436     DWORD   ???\n\n\t464     DWORD\n\n\t472     VOID    ???\n\t476     DWORD   ???\n\t480     DWORD   ???\n\t484\n\t488     FLT4    UnknownTime timeGetTime()\n\t492     FLT4    timeGetTime()\n\t496     DWORD   Researches_Count\n\t500     VOID    Researches_Array\n\t504     VOID\n\t\t\t0       DWORD   [off_64923C]\n\t\t\t14      WORD    Counter\n\t\t\t15      VOID\n\t\t\t68      BYTE\n\t508     VOID    ???\n\t512     DWORD\n\t516     DWORD\n\t520     DWORD\n\t524     BYTE\n\t528     DWORD\n\t532     BYTE\n\t533     BYTE\n\t534     BYTE\n\t536     DWORD\n\t540     DWORD\n\t544     DWORD\n\t548     DWORD\n\t552     DWORD\n\t556     DWORD\n\t560     DWORD\n\t564     DWORD\n\t568     DWORD\n\t572     DWORD\n\t576     DWORD\n\t580     DWORD\n\t584     DWORD\n\t588     DWORD\n\t592     DWORD\n\t596     DWORD\n\t600     DWORD\n\t604     DWORD\n\t608     DWORD\n\t612     DWORD\n\t616     DWORD\n\n\t648     BYTE\n\t649     BYTE\n\t650     BYTE\n\t651     BYTE\n\t652     DWORD   [0x1010101]\n\t656     DWORD   [0x1010101]\n\t660     DWORD   [1]\n\t664     BYTE    ???\n\t668     FLT4 ??? [0x45ABE000]\n\t672     VOID    Criteria\n\t676     VOID    TechTree\n\n\t6088    VOID    Sound_Result\n\n\\UnitUnknown ()\n\t248     DWORD   TaskID\n\t308     BYTE    Gathering\n\n\\Spawns ()\n\t0       DWORD\n\t4       VOID    Spawns_Array\n\t8       DWORD   Spawns_Count\n\n\\Spawns\\Spawn ()\n\t0       DWORD   Instance\n\t4       DWORD   Identity\n\t8       VOID    \\Unit\n\t12      VOID    \\Player\n\t16      VOID    \\Sprite\n\t20      VOID\n\t24      VOID\n\t28      VOID\n\t32      VOID    Garrison\n\t36      VOID\n\t40      WORD    Unknown4\n\t42      WORD    Unknown5\n\t44      WORD    Unknown6\n\t46      WORD    Unknown7\n\t48      FLOAT   HitPoints\n\t52      BYTE    Unknown9\n\t53      BYTE    Frame\n\t54      BYTE    SelectionEffect\n\t55      BYTE    Discarded1 (some offset)\n\t56      FLOAT   PositionX\n\t60      FLOAT   PositionY\n\t64      FLOAT   PositionZ\n\t68      FLOAT   AttributeCapacity\n\t72      BYTE    Status\n\t73      BYTE    Hittable\n\t74      BYTE    Dopple\n\t75      BYTE    Unknown3\n\t76      WORD    AttributeIdentity\n\t78      BYTE    Type\n\t79      BYTE    Unknown8\n\t80      DWORD\n\t84      DWORD\n\t88      DWORD\n\t92      VOID    Unknown1_Array\n\t96      DWORD   Unknown1_Count\n\t100     DWORD   Unknown1_Usage\n\t100     CALL    Set_Status?\n\t104     BYTE\n\t108     VOID\n\t112     BYTE     [-1,&4]\n\n\t117     BYTE\n\t116     BYTE    [0,1,2,3]\n\t118     BYTE    Unknown10\n\t?\n\t120     DWORD   [-1]\n\t124     BYTE\n\t128     DWORD\n\t132     FLOAT   (deprecated)\n\t136     VOID    SpawnSelection\n\t140     DWORD   Unknown1Count2\n\n\t156     VOID\n\t160     DWORD\n\t164     DWORD\n\t168     DWORD\n\n\t188     FLOAT\n\t196     FLOAT\n\t192     FLOAT\n\t196\n\n\t228     CALL\n\t232\n\n\t240     CALL\n\t244     CALL\n\t248\n\n\t264     VOID\n\t268     BYTE\n\t270     WORD\n\n\t328     VOID    SalvageAttributes\n\n\t338     WORD\n\t340     WORD    SalvageDecay\n\t344     DWORD\n\t348     FLOAT\n\t352     VOID\n\t356\n\n\t377     BYTE\n\t378     BYTE    TypeFormatted\n\t380     FLOAT   MissileAmount1\n\t384     VOID\n\t388\n\t392     BYTE\n\n\t404     DWORD\n\t408     DWORD\n\t412     VOID\n\t416\n\n\t568     CALL\n\n\\Spawns\\Spawn\\Delta_1 ()\n\t0       DWORD   Instance\n\t4       VOID    Sprite\n\t8       BYTE    Type\n\t12      DWORD   DirectionX\n\t16      DWORD   DirectionY\n\t20      WORD    FrameMirror\n\t22      BYTE    Mode\n\nCOMMANDS_TYPES\n\t0       ?BYTE\n\t1       VOID    \\Sounds\n\n\\Actions\\Action (76)\n\t0       WORD    Enabled\n\t2       WORD    Identity\n\t4       BYTE    Unknown1\n\t6       WORD    Type\n\t8       WORD    Class\n\t10      WORD    Object\n\t12      BYTE    SelectorEnabler\n\t13      BYTE    Unknown7\n\t14      WORD    Terrain\n\t16      WORD    SelectorMode\n\t17      BYTE    RightClickMode\n\t18      BYTE    Unknown12\n\t20      WORD    AttributeCarried\n\t22      WORD    AttributeProduction\n\t24      WORD    AttributeDropped\n\t26      WORD    AttributeGeneric\n\t28      FPN4    AttributeMultiplier\n\t32      FPN4    RangeAlert\n\t36      FPN4    RangeExtra\n\t40      BYTE    Unknown4\n\t44      FPN4    Unknown5\n\t48      WORD    Plunder\n\t50      WORD    Unknown9\n\t52      VOID    Graphic1\n\t56      VOID    Graphic2\n\t60      VOID    Graphic3\n\t64      VOID    Graphic4\n\t68      VOID    SoundExecute\n\t72      VOID    SoundDeposit\n\n\\Unit_Static (200)\n\t0\n\t4       BYTE    Type\n\t8       VOID    \\Sounds\n\t12      WORD    LanguageName\n\t14      WORD    LanguageCreation\n\t16      WORD    Identity1\n\t18      WORD    Identity2\n\t20      WORD    Identity3\n\t22      WORD    Class\n\t24      VOID    GraphicStanding1\n\t28      VOID    GraphicStanding2\n\t32      VOID    GraphicDying1\n\t36      VOID    GraphicDying2\n\t40      BYTE    Revival\n\t42      WORD    HitPoints\n\t44      FPN4    LineOfSight\n\t48      BYTE    GarrisonCapacity\n\t50      WORD    Speed\n\t52      FPN4    RadiusX\n\t56      FPN4    RadiusY\n\t60      FPN4    RadiusZ\n\t64      VOID    SoundSelection\n\t68      VOID    TrainSound1\n\t72      VOID    SoundDying\n\t76      DWORD   TrainSound2\n\t80      WORD    ObjectDead\n\t82      BYTE    EditorLayer\n\t83      BYTE    Hovering\n\t84      WORD    Icon\n\t86      BYTE    EditorHidden\n\t88      WORD    Unknown1\n\t90      BYTE    Enabled\n\t91      BYTE    Disabled\n\t92      WORD    PlacementTerrainBypassX\n\t94      WORD    PlacementTerrainBypassY\n\t96      WORD    PlacementTerrainStaticX\n\t98      WORD    PlacementTerrainStaticY\n\t100     FPN4    EditorRadiusX\n\t104     FPN4    EditorRadiusY\n\t108     BYTE    BuildingMode\n\t109     BYTE    FogOfWar\n\t110     WORD    Boundary\n\t112     BYTE    Flying\n\t113\n\t114     3*WORD  CostsAttributes\n\t120     3*FLOAT CostsQuantities\n\t132     WORD    ResourceCapacity\n\t136     FPN4    ResourceDecay\n\t140     FLOAT   Unknown3\n\t144     3*BYTE  Costs\n\t147     BYTE    BlastType\n\t148     BYTE    Unknown2\n\t149     BYTE    Interaction\n\t150     BYTE    MinimapMode\n\t151     BYTE    CommandAttribute\n\t152     BYTE    MinimapColor\n\t153     BYTE    AttackMode\n\t154     BYTE    NewUnknown\n\t155     BYTE    Damages_Count\n\t156     VOID    Damages_Array\n\t160     BYTE    SelectionMask\n\t161     BYTE    SelectionType\n\t162     BYTE    SelectionShape\n\t163     ?\n\t164     DWORD   Attribute\n\t168     DWORD   LanguageHelp\n\t172     DWORD   LanguageHotkeyText\n\t176     DWORD   LanguageHotkey\n\t180     BYTE    SelectionDisabled\n\t181     BYTE    SelectionUnknown6\n\t182     BYTE    SelectionUnknown7 [0,1,2]\n\t183     BYTE    SelectionUnknown8\n\t184     BYTE    SelectionEffect\n\t185     BYTE    SelectionColor\n\t188     FPN4    SelectionRadiusX\n\t192     FPN4    SelectionRadiusY\n\t196     FPN4    SelectionRadiusZ\n\n\\Unit_Tree (200)\n\n\\Unit_Animated (204)\n\t200     FLOAT   Speed\n\n\\Unit_Moving (252)\n\t204     VOID    SpriteWalking\n\t208     VOID    SpriteRunning\n\t212     FLOAT   RotationRate\n\t216     BYTE    Unknown11\n\t?\n\t218     WORD    TrackingUnit\n\t220     BYTE    TrackingMode\n\t224     FLOAT   TrackingDensity\n\t228     BYTE    Unknown12\n\t229     ?\n\t231\n\t232     FLOAT   RotationAngle1\n\t236     FLOAT   RotationAngle2\n\t240     FLOAT   RotationAngle3\n\t244     FLOAT   RotationAngle4\n\t248     FLOAT   RotationAngle5\n\n\\Unit_Action (288)\n\t252     VOID    Actions\n\t256     WORD    SheepConversion\n\t260     FLOAT   SearchRadius\n\t264     FLOAT   WorkRate\n\t268     WORD    DropSite1\n\t270     WORD    DropSite2\n\t272     BYTE    TaskSwapGroupID\n\t276     VOID    SoundMove\n\t280     VOID    SoundStop\n\t284     BYTE    AnimalMode\n\n\\Unit_Combat (368)\n\t288     VOID    SpriteAttack\n\t292     WORD    ArmorDefault\n\t294     WORD    Armors_Count\n\t296     VOID    Armors_Array\n\t300     WORD    Attacks_Count\n\t304     VOID    Attacks_Array\n\t308     WORD    Boundary\n\t312     FLOAT   RangeMax\n\t316     FLOAT   BlastRadius\n\t320     BYTE    BlastLevel\n\t324     FLOAT   Reload\n\t328     WORD    ObjectProjectile\n\t330     WORD    Accuracy\n\t332     BYTE    TowerMode\n\t334     WORD    Delay\n\t336     FLOAT   DisplacementX\n\t340     FLOAT   DisplacementY\n\t344     FLOAT   DisplacementZ\n\t348     FLOAT   RangeMin\n\t352     WORD    DisplayedArmor\n\t354     WORD    DisplayedAttack\n\t356     FLOAT   DisplayedRange\n\t360     FLOAT   DisplayedReload\n\t364     FLOAT   GarrisonRecovery\n\n\\Unit_Missile (380)\n\t368     BYTE    Stretch\n\t369     BYTE    Smart\n\t370     BYTE    DropAnimation\n\t371     BYTE    Penetration\n\t372     BYTE    Unknown24\n\t376     FLOAT   ProjectileArc\n\n\\Unit_Combat (444)\n\t368     6*3     Costs\n\t386     WORD    Timer\n\t388     WORD    Location\n\t390     BYTE    Button\n\t392     WORD    DisplayedPierce\n\t396     FLOAT   Unknown26\n\t-------------------------\n\t400     FLOAT   Unknown27\n\t404     BYTE    MissileGraphicDelay\n\t408     FLOAT   MissileAmounts\n\t412     BYTE    MissileMaximum\n\t416     FLOAT   MissileSpawning1\n\t420     FLOAT   MissileSpawning2\n\t424     FLOAT   MissileSpawning3\n\t428     DWORD   MissileUnit\n\t432     DWORD   MissileGraphic\n\t436     BYTE    Unknown29\n\t437     BYTE    HeroMode\n\t440     VOID    SpriteGarrison\n\n\\Unit_Building (576)\n\t444     VOID    SoundConstruction\n\t448     VOID    SpriteConstruction\n\t452     VOID    SpriteSnow\n\t456     BYTE    Adjacent\n\t458     WORD    IconDisabler\n\t460     BYTE    Disappears\n\t462     WORD    StackUnit\n\t464     WORD    Terrain\n\t466     WORD    Resource\n\t468     WORD    Research\n\t470     BYTE    Unknown33\n\n\t512     WORD    ObjectHead\n\t514     WORD    ObjectTransform\n\t516     VOID    SoundUnknown\n\t520     BYTE    GarrisonType\n\t524     FLOAT   GarrisonHealing\n\t528     FLOAT   Unknown35\n\t532     WORD    Unknown36\n\t534     VOID    LootStorages\n\t538     WORD\n\n\t543     BYTE    Unknown\n\n\\Sprite (120)\n\t0       0x0D    FileName\n\t15\n\t16      DWORD   Resource\n\t20      BYTE    Status\n\t24      VOID    Color_Pointer\n\t28      VOID    Resource_Pointer\n\t32      VOID\n\t36      VOID\n\t40      BYTE    Unknown2\n\t41      BYTE    Layer\n\t42      WORD    Color\n\t44      BYTE    Replay\n\t46      WORD    Coordinate1\n\t48      WORD    Coordinate2\n\t50      WORD    Coordinate3\n\t52      WORD    Coordinate4\n\t54      WORD    Deltas_Count\n\t56      VOID    Deltas_Array\n\t60      DWORD   DeltasSound\n\t64      BYTE    AttackSoundsUsed\n\n\t68      VOID    Angles_Array\n\t72      0x15    Name\n\t94      WORD    Frames\n\t96      WORD    Angles\n\t100     RN32    Speed\n\t104     RN32    Rate\n\t108     RN32    Delay\n\t112     BYTE    Sequence\n\t113     ?\n\t114     WORD    Identity\n\t116     BYTE    Mirroring\n\t117     BYTE    Unknown3\n\t118     ?\n\t119     ?\n\n\\Sprite\\Delta [16]\n\t0       WORD    Graphic\n\t2       WORD    Unknown1\n\t4       VOID    Pointer\n\t8       WORD    DirectionX\n\t10      WORD    DirectionY\n\t12      WORD    Angle\n\t14      WORD    Unknown2\n\n\\DRS (276) dword_6B59E4\n\t0       ?\n\t2       VOID    DRS_File\n\n\t16      STR     FileName\n\n\\DRS\\File ()\n\t0               Copyright\n\t40      4       Signature\n\t44      12      Name\n\t56              Length\n\t72              Tables\n\n\\ResImage (36)\n\t0       VOID    Resource1\n\t4       VOID    BitmapInfoHeader\n\t8       VOID    BitmapColorsQuad\n\t12      VOID    Resource2\n\t16      DWORD   Version\n\t20      DWORD   SizeX\n\t24      DWORD   SizeY\n\t28      WORD    Orientation\n\n\\ResShape (28)\n\t0       BT32    FileFlag\n\t4       DWORD   FileSize\n\t12      VOID    FileData\n\n\\Profiles\n\t0       CALL    0040AB50\n\t4\n\n\\Profile (48)\n\t0       DWORD   off_642E2C\n\t4       20      Title\n\t20      CALL    Writer?\n\t24      DWORD   Objects_Count\n\t28      VOID    Objects_Array\n\t32      WORD    Attributes_Count\n\t36      VOID    Attributes_Array\n\t40      BYTE    Theme\n\t41      BYTE    ???\n\t42      WORD    TechTree\n\t44      WORD    TeamBonus\n\t46      ???\n\n\\SeededMaps (44)\n\t0\n\n\\Researches (12)\n\t0       VOID Array\n\t4       DWORD Count\n\t8       DWORD PlayerID\n\n\\Research (68)\n\t0\n\t2       VOID    Requirements\n\t16      WORD    Requisites\n\t18      WORD    Profile\n\t20      WORD    FullTechMode\n\t22      3*WORD  Attribute\n\t28      3*WORD  Quantity\n\t34      3*BYTE  Deducted\n\t38      WORD    Timer\n\t40      WORD    Modifier\n\t42      WORD    Type\n\t44      WORD    Icon\n\t46      BYTE    Button\n\t48      WORD    Location\n\t50      WORD    LanguageName\n\t52      WORD    LanguageDescription\n\t56      DWORD   LanguageHelp\n\t60      DWORD   LanguageName2\n\t64      DWORD   LanguageSlot\n\n\\Modificator (12)\n\t0       DWORD   Instance\n\t4       VOID    Modifiers_Array\n\t8       DWORD   Modifiers_Count\n\n\\Modifiers\\Modifier (44)\n\t0       ???\n\t1       ???     ???\n\t2       31      Title\n\t36      WORD    Controls_Count\n\t40      VOID    Controls_Array\n\n\\Modifier\\Controls\\Control (12)\n\t0       WORD    Type\n\t2       WORD    Argument1\n\t4       WORD    Argument2\n\t6       WORD    Argument3\n\t8       FPN4    Operand\n\n\\Lineages\\Lineage (16)\n\t0       WORD\n\t4       VOID\n\n\\Technologies (16)\n\t0       VOID    Array\n\t4       DWORD   Count\n\t8       VOID    Researches\n\t12      VOID    Player\n\n\\Technologies\\Technology ()\n\n\n\\TechTree (44)\n\t0       DWORD   Group_Ages_Count\n\t4       VOID    Group_Ages_Array\n\t8       DWORD   Group_Builds_Count\n\t12      VOID    Group_Builds_Array\n\t16      DWORD   Group_Units_Count\n\t20      VOID    Group_Units_Array\n\t24      DWORD   Group_Techs_Count\n\t28      VOID    Group_Techs_Array\n\t32      DWORD   Identity\n\t36      DWORD   Culture\n\t40      DWORD   Linear\n\n\\TechTree\\Group_Age (144)\n\t0       DWORD   Identity\n\t4       DWORD   Status\n\t8       DWORD   Linear\n\t12      DWORD   Buildings_Count\n\t16      VOID    Buildings_Array\n\t20      DWORD   Units_Count\n\t24      VOID    Units_Array\n\t28      DWORD   Researches_Count\n\t32      VOID    Researches_Array\n\t36      84      Items\n\t120     BYTE    Slots\n\t121             Unknown3\n\t131             Unknown4\n\t141             Unknown5\n\n\\TechTree\\Group_Build (136)\n\t0       DWORD   Identity\n\t4       DWORD   ResearchEnabler\n\t8       DWORD   Status\n\t12      DWORD   Linear\n\t16      DWORD   Builds_Count\n\t20      VOID    Builds_Array\n\t24      DWORD   Units_Count\n\t28      VOID    Units_Array\n\t32      DWORD   Techs_Count\n\t36      VOID    Techs_Array\n\t40      84      Items\n\t124     DWORD   Horizon\n\t125     BYTE*5  DominionsTotal\n\t130     BYTE*5  DominionsFirst\n\t135     BYTE    DominionsUnknown\n\n\\TechTree\\Group_Unit (124)\n\t0       DWORD   Identity\n\t4       DWORD   ResearchEnabler\n\t8       DWORD   Status\n\t12      DWORD   Linear\n\t16      DWORD   Origin\n\t20      DWORD   ResearchRequired\n\t24      DWORD   Units_Count\n\t28      VOID    Units_Array\n\t32      84      Items\n\t116     DWORD   Column\n\t120     DWORD   Horizon\n\n\\TechTree\\Group_Tech (132)\n\t0       DWORD   Identity\n\t4       DWORD   Status\n\t8       DWORD   Linear\n\t12      DWORD   Origin\n\t16      DWORD   Builds_Count\n\t20      VOID    Builds_Array\n\t24      DWORD   Units_Count\n\t28      VOID    Units_Array\n\t32      DWORD   Techs_Count\n\t36      VOID    Techs_Array\n\t40      84      Items\n\t124     DWORD   Column\n\t128     DWORD   Horizon\n\n\\Database\\Objects (12)\n\t0       DWORD   Instance\n\t4       VOID    Array\n\t8       DWORD   Count\n\n\\TerrainTile (0x0020)\n\t0x00    WORD    ???\n\t0x02    WORD    ???\n\t0x04    BYTE    [0-16]\n\t0x05    BYTE    Texture [0]\n\t0x06    BYTE    Elevation [1]\n\t0x07    BYTE    ??? [0] DWORD\n\t0x08    BYTE    ??? [-1]\n\t0x09    BYTE    ??? [0]\n\t0x0A    BYTE    ??? [0]\n\t0x0B    (BIT1)  ???\n\t0x0C    BYTE    ??? [0]\n\t0x0D    BYTE    ??? [0]\n\t0x0E    BYTE    ??? [-1]\n\t0x0F    BYTE    ??? [0]\n\t0x10    DWORD   ??? [0]\n\t0x14    DWORD   ??? [0]\n\t0x18    ???\n\t0x1C    ???\n\n\\Renderer ()\n\t0       DWORD   Instance\n\t4       VOID    Tiles_Data_1\n\t8       DWORD   Size_X\n\t12      DWORD   Size_Y\n\t16      DWORD   Size_X_Multiply\n\t20      DWORD   Size_Y_Multiply\n\n\t0x0024  (SFLT)  ??? [0x40400000]\n\t0x0038  VOID    PlayersData\n\n\t140     (42*436=18312) Terrains\n\t18452   (16*1440=23040) Borders\n\t22296\n\t41492   VOID    Tiles_Data_2\n\t41496   FLT4    MapMinX\n\t41500   FLT4    MapMinY\n\t41504   FLT4    MapMaxX\n\t41508   FLT4    MapMaxY\n\t41512   FLT4    MapMaxXPlus1\n\t41516   FLT4    MapMaxYPlus1\n\t41520   WORD    Terrains_Count\n\t41522   WORD    Borders_Count\n\t41524   WORD    MaxTerrains\n\t41526   WORD    TileSizeX\n\t41528   WORD    TileSizeY\n\t41530   WORD    TileHalfSizeY\n\t41532   WORD    TileHalfSizeX\n\t41534   WORD    TileSizeZ (ElevationHeight)\n\t41536   WORD    CurrentX\n\t41538   WORD    CurrentY\n\t41540   WORD    Block0X\n\t41542   WORD    Block1X\n\t41544   WORD    Block0Y\n\t41546   WORD    Block1Y\n\t41548   VOID    SearchMapX\n\t41552   VOID    SearchMapY\n\t41556   BYTE    AnyFrameChange\n\t41557   BYTE    Map_Visible_Flag\n\t41558   BYTE    Fog_Flag\n\t41559   BYTE    UMV_Data_List_Used\n\t41560   DWORD   UMV_Data_List_Size\n\t41564   VOID    UMV_Data_GUID\n\t41568   VOID    UMV_Data_Points\n\t41572   VOID    OB_Manager\n\t41576   VOID    ???\n\t41580   VOID    RandomMaps\n\t41584   VOID    GameWorld\n\t41588   VOID    MapZones (Players array)\n\t41592   VOID    InifiedVisMap\n\t41596   VOID    UnitManager\n\t41600   VOID    MapCopyData\n\t41604   DWORD   OL_SystemActive [0,1]\n\t41608   VOID    OL_ZoneNum\n\t41612   DWORD   OL_MaxListSize [2]\n\t41616   DWORD   OL_HeapSize [4]\n\t41620   DWORD   OL_Pool [12]\n\t41624   DWORD    [250]\n\t41628   DWORD    [x/8]\n\t41632   DWORD    [x/32]\n\t41636   DWORD    [32]\n\t41640   DWORD    [16]\n\t41644   VOID\n\n\t41916   VOID    Plugins (Blendomatic, ect... other DAT)\n\t41920   DWORD    [400]\n\t41924\n\t41928   DWORD    [0,1]\n\t41932   DWORD   Iterator\n\t41936   17*VOID ElevationsData1\n\t42004   17*VOID ElevationsData2\n\t42072   17*VOID ElevationsData3\n\t42140   17*VOID ElevationsData4\n\n\t42208\n\n\t42228   DWORD\n\t42232   DWORD\n\t42236   DWORD\n\t42240   DWORD\n\t42244\n\t42248   256*40  PatternMasks\n\n\\Renderer\\Plugins ()\n\t900     10*VOID ViewICM (inverse color maps, 1 for RGB, 9 for HSV brightness)\n\t940     17*VOID STemplet (elevations shape)\n\t1008    17*VOID FilterMaps (texture mapping bilinear filter, uses an ICM as reference)\n\t1076    17*VOID LoqMaps\n\t1144    18*VOID LightMaps\n\t1216    40*VOID PatternMasks\n\t1508    31*VOID Blendomatic (blending modes)\n\n\\RandomMaps (22304)\n\n\\RandomMaps_ScriptController (288412)\n\t259428  VOID\n\t259436  DWORD   CurrentType\n\t259440  DWORD   CurrentComment\n\t259444  DWORD   CurrentBracket\n\t259448  100*4   ArrayConditionStatus\n\t259848  BYTE    CurrentCondition\n\n\t285856  BYTE    CurrentRandom\n\t285860          ArrayRandomsStart\n\n\t253024  DWORD   MinNumberOfCliffs\n\t253028  DWORD   MaxNumberOfCliffs\n\t253032  DWORD   MinLengthOfCliff\n\t253036  DWORD   MaxLengthOfCliff\n\t253040  DWORD   CliffCurliness\n\t253048  DWORD   MinDistanceCliffs\n\t253044  DWORD   MinTerrainDistance\n\n\t259420  BYTE    CurrentGenStatElevation\n\t259421  BYTE    CurrentGenStatConnect\n\t259422  BYTE    CurrentGenStatTerrain\n\t259423  BYTE    CurrentGenStatObjects\n\t259424  BYTE    CurrentGenStatCliff\n\t259425  BYTE    CurrentGenStatLand\n\n\t286260          ArrayRandomsStatus\n\n\t287172  DWORD   ElevationGeneratorDefaultTerrain\n\t287176  DWORD   ElevationGeneratorReplaceTerrain\n\n\t287580  DWORD   ConnectionGeneratorReplaceTerrain\n\t287980  DWORD   GlobalRandomStart\n\t288396\n\n\\Player\n\t132     ?\n\n\\Condition\n\t0       BYTE    Type\n\t1               ObjectConstant\n\t2               ObjectSource\n\t8       DWORD   PlayerID\n\n\\Sounds\\Sound (16)\n\t0       WORD    Status\n\t2       WORD    Items_Count\n\t4       VOID    Items_Array\n\t8       DWORD   Timer\n\t12      DWORD   Identity\n\n\\Sounds\\Sound\\Items\\Item (40)\n\t0       0x0D    FileName\n\t16      DWORD   ResourceID\n\t20      WORD    Percent\n\t22      WORD    CultureID\n\t24      WORD    PlayerID\n\t28      VOID    ResourceStruct\n\t32      BYTE    Status [2]\n\t36      DWORD   Timer\n\n\\SoundResourceStruct (32)\n\t0       DWORD\n\t4       BYTE\n\t5       BYTE\n\n\t8       DWORD   ResourceID\n\t12      ?\n\t16      ?\n\t20      ?\n\t24      BYTE\n\t28      VOID\n\n\\Coloration (56)\n\t0       ?\n\n\\Terrain [436]\n\t0       WORD    enabled??? (-1)\n\t2       CNST    Title\n\t15      CNST    FileName\n\t28      DWORD   Resource\n\t32      DWORD\n\t36      VOID    Color_COL\n\t40      DWORD\n\t44      DWORD\n\t48      BYTE    Color1\n\t49      BYTE    Color2\n\t50      BYTE    Color3\n\t51      BYTE\n\t52      BYTE\n\t53      BYTE    Passable\n\t54      BYTE    Impassable\n\t55      BYTE    Animate\n\t56      WORD    SpriteFrames\n\t58      WORD    Pause\n\t60      FLT4    Rate\n\t64      FLT4    Delay\n\t68      WORD\n\t70      WORD\n\t72      DWORD\n\t76      BYTE\n\t77      BYTE\n\t140     ?\n\n\t172     DWORD\n\t180     DWORD   ???\n\t188     BYTE\n\t189     BYTE\n\t190     BYTE\n\t192     WORD\n\t194     WORD    DimensionX\n\t196     WORD    DimensionY\n\t198     CNST    Placements\n\t217     BYTE\n\t332     WORD\n\n\\Borders\\Border ()\n\t0\n\n\\RandomMaps\\Words\n\t26      DWORD   Arg2\n\t29      STR     Arg1 (Name)\n\t100     BYTE    Zero\n\t108     BYTE    Arg3\n\t109     BYTE    Arg4\n\t110     BYTE    Arg5\n\t111     BYTE    Arg6\n\n\\Behavior\\Script ()\n\t1012    VOID    CurrentName\n\t1016    DWORD   CurrentLine\n\t1020    VOID    Corruption\n\n# This structure is deprecated.\n# It was once used for debug purposes.\nBehavior\\Script\\Corruption (396)\n\t0       BYTE    Flag\n\t260     DWORD\n\t264     BYTE\n\t392     DWORD   Code\n\n\\Mission (20)\n\t0       VOID    Instance\n\t4\n\t8       VOID    Conditions_Array\n\t12      WORD    Conditions_Count\n\t14      WORD    Available\n\t16      FLOAT   Timeline\n\n\\Campaign ()\n\t?\n\n\\Victory ()\n\t32      DWORD   Unknown2\n\t36      DWORD   Unknown3\n"
  },
  {
    "path": "doc/reverse_engineering/civilizations.md",
    "content": "# Civilization attributes\n\n## Age of Empires I\n\n### Civilization specials\n\nCivilization | Bonus\n-------------|------\nAssyrian     | +40% Archery Range unit fire rate; Villagers 30% faster.\nBabylonian   | Double wall and tower hit points; +30% Priest rejuvenation rate; +30% stone mining.\nChoson       | +80 Long Swordsman and Legion hit points, +2 tower range, -30% Priest cost.\nEgyptian     | +20% gold mining; +33% Chariot and Chariot Archer hit points; +3 Priest range.\nGreek        | Hoplite, Phalanx and Centurion 30% faster; War ships 30% faster.\nHittite      | Double Stone Thrower, Catapult, Heavy Catapult hit points; +1 Archery Range unit attack; +4 war ship range.\nMinoan       | -30% ship cost; +2 Composite Bowman range; +25% Farm production.\nPersian      | +30% hunting; -30% Farm production; War Elephant and Elephant Archer 50% faster; +50% Trireme fire rate.\nPhoenician   | -25% War Elephant and Elephant Archer cost; +65% Catapult Trireme and Juggernaught fire rate.\nShang        | -30% Villager cost; Double wall hit points.\nSumerian     | +15 villager hit points; +50% Stone Thrower, Catapult, Heavy Catapult fire rate; Double Farm production.\nYamato       | -25% Horse Archers, Scout, Cavalry, Heavy Cavalry, Cataphract cost; Villagers 30% faster; +30% ship hit points.\n\n### Unit/Building creation table\n\nIf a unit or building is listed in the following table, that particular\ncivilization can produce that particular unit/building:\n\nInfantry:\n\nUnit/Build.     | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n----------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nBroad Swordsman | III |    x |     x |    x |        |       |    x |    x |    x |     x |     x |    x |\nLong Swordsman  |  IV |    x |     x |    x |        |       |      |    x |    x |     x |       |    x |\nLegion          |  IV |    x |     x |    x |        |       |      |      |    x |     x |       |      |\nHoplite         | III |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nPhalanx         |  IV |      |       |      |        |     x |    x |    x |      |     x |       |    x |    x\nCenturion       |  IV |      |       |      |        |     x |    x |    x |      |     x |       |    x |    x\n\nArchers:\n\nUnit/Build.        | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n-------------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nChariot Archer     | III |    x |     x |      |      x |       |    x |      |      |     x |     x |    x |\nImproved Bowman    | III |      |     x |    x |      x |       |      |    x |    x |     x |     x |      |    x\nComposite Bowman   |  IV |      |     x |      |      x |       |      |    x |    x |     x |     x |      |    x\nElephant Archer    |  IV |      |       |      |      x |       |    x |      |    x |     x |       |      |\nHorse Archer       |  IV |    x |     x |    x |        |       |    x |      |    x |       |     x |    x |    x\nHeavy Horse Archer |  IV |      |       |      |        |       |    x |      |    x |       |     x |    x |    x\n\nCavalry:\n\nUnit/Build.   | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n--------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nChariot       | III |    x |     x |      |      x |       |    x |      |      |     x |     x |    x |\nCavalry       | III |    x |     x |    x |        |     x |    x |    x |    x |     x |     x |      |    x\nHeavy Cavalry |  IV |    x |       |    x |        |     x |      |      |    x |       |     x |      |    x\nCataphract    |  IV |    x |       |    x |        |       |      |      |    x |       |     x |      |    x\nWar Elephant  |  IV |      |       |      |      x |       |    x |      |    x |     x |       |    x |\n\nSiege Weapons:\n\nUnit/Build.    | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n---------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nBallista       |  IV |    x |       |    x |        |     x |      |    x |      |       |     x |      |\nCatapult       |  IV |    x |     x |      |        |     x |    x |    x |    x |       |     x |    x |\nHelepolis      |  IV |    x |       |    x |        |     x |      |    x |      |       |     x |      |\nHeavy Catapult |  IV |    x |     x |      |        |     x |    x |    x |      |       |       |    x |\n\nBoats:\n\nUnit/Build.      | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n-----------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nFishing Ship     | III |    x |     x |    x |      x |     x |      |    x |    x |     x |     x |    x |    x\nMerchant Ship    | III |    x |     x |    x |      x |     x |    x |    x |    x |     x |     x |      |    x\nTrireme          |  IV |    x |       |    x |      x |     x |      |    x |    x |     x |       |    x |    x\nCatapult Trireme |  IV |      |       |      |      x |     x |      |    x |    x |     x |       |      |    x\nHeavy Transport  |  IV |      |       |      |      x |     x |      |    x |    x |     x |       |      |    x\nJuggernaught     |  IV |      |       |      |      x |     x |      |    x |    x |     x |       |      |    x\n\nBuildings:\n\nUnit/Build.    | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n---------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nAcademy        | III |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nFortification  | IV  |    x |     x |    x |      x |     x |    x |      |    x |     x |     x |    x |\nGuard Tower    | IV  |    x |     x |    x |      x |     x |    x |      |    x |     x |     x |    x |\nBallista Tower | IV  |    x |     x |    x |      x |     x |    x |      |      |     x |       |    x |\n\nTechnologies\n\nUnit/Build.   | Age | Ass. | Baby. | Cho. | Egypt. | Greek | Hit. | Min. | Per. | Phoe. | Shang | Sum. | Yama.\n--------------|-----|------|-------|------|--------|-------|------|------|------|-------|-------|------|------\nArchitecture  | III |      |     x |    x |      x |     x |    x |    x |    x |       |     x |    x |    x\nArtisanship   | III |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nAstrology     | III |    x |     x |    x |      x |     x |    x |      |    x |     x |     x |      |\nBronze Shield | III |      |     x |    x |        |     x |    x |    x |    x |     x |     x |    x |    x\nMysticism     | III |    x |     x |    x |      x |     x |      |      |    x |     x |     x |    x |\nNobility      | III |      |     x |      |      x |     x |    x |    x |    x |     x |     x |    x |    x\nPlow          | III |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nPolytheism    | III |    x |     x |    x |      x |     x |      |    x |    x |     x |     x |    x |    x\nWheel         | III |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nAfterlife     |  IV |    x |     x |    x |      x |     x |      |      |    x |     x |     x |      |    x\nAlchemy       |  IV |      |     x |      |      x |     x |    x |    x |    x |     x |       |    x |    x\nAristocracy   |  IV |      |     x |      |      x |     x |    x |    x |      |     x |       |    x |    x\nBallistics    |  IV |    x |     x |    x |      x |     x |    x |    x |      |     x |       |    x |    x\nChain mail    |  IV |      |       |      |      x |     x |    x |    x |    x |       |     x |    x |    x\nCoinage       |  IV |    x |     x |    x |        |     x |    x |    x |      |     x |       |      |    x\nCraftsmanship |  IV |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |      |    x\nEngineering   |  IV |      |     x |      |      x |     x |    x |    x |    x |     x |       |    x |    x\nFanaticism    |  IV |    x |     x |    x |      x |     x |      |      |    x |     x |     x |      |\nIrrigation    |  IV |    x |     x |    x |      x |     x |    x |    x |      |     x |     x |    x |    x\nIron Shield   |  IV |      |       |      |        |     x |    x |    x |    x |     x |     x |      |    x\nJihad         |  IV |    x |     x |    x |      x |       |      |      |    x |     x |     x |      |\nMetallurgy    |  IV |    x |       |    x |      x |       |    x |    x |    x |       |     x |      |    x\nMonotheism    |  IV |    x |     x |    x |      x |       |      |      |    x |     x |     x |      |\nSiegecraft    |  IV |    x |     x |    x |        |     x |    x |    x |      |       |       |    x |    x\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/accuracy.md",
    "content": "# Accuracy and Projectiles\n\nMechanics of accuracy.\n\n## Distance\n\n[Source](https://www.youtube.com/watch?v=HpzOTdItUGA)\n\nAccuracy depends on the distance to the intended target. The following figure shows accuracy of shooting a standing target at various distances. `o` represents the archer (in this case: Elite Longbowman), `X` represents the target. Every `.` symbolizes a tile between the two units.\n\n```\n......................................\n...1 Tile....oX.......................  100   % accuracy\n......................................\n...2 Tiles...o.X......................  100   % accuracy\n......................................\n...3 Tiles...o..X.....................  96.63 % accuracy\n......................................\n...4 Tiles...o...X....................  91.90 % accuracy\n......................................\n...6 Tiles...o.....X..................  87.82 % accuracy\n......................................\n...12 Tiles..o...........X............  83.17 % accuracy\n......................................\n...21 Tiles..o....................X...  81.77 % accuracy\n......................................\n```\nIf the target is standing 2 or less tiles away, arrows always seem to hit. For targets at range 3 or more, the archer's accuracy deteriorates. The deterioration slows down with every tile between archer and target and seems to approach a *minimum accuracy* (in this case: 80 %).\n\n### Inaccuracy\n\n[Source](https://www.youtube.com/watch?v=nh3GnmFSkeY)\n\nThe game probably determines first, if a shot is accurate (meaning it will 100 with a 100 % accuracy) or inaccurate. In the latter case the *accuracy error radius* comes into effect. This value is used by the game to calculate a margin between the target and the spot where the projectile lands.\n\nAlso, this margin is likely related to the distance between archer and target. In this case, the code for calculating the margin would look like this:\n\n    (margin) = (distance) * rand(0,accuracy_error_radius)\n\nBecause the margin is very low for most of the units a projectile can still hit its intended target despite being inaccurate.\n\n## Mechanics before and after \"Ballistics\"\n\n[Source](https://www.youtube.com/watch?v=1AXTuKJ1dLg)\n\n100 % accuracy in the game's sense must not be confused with hitting the target 100 % of the time.\n\nBefore researching Ballistics, units fire at the current positions of a target. If the target moves and is fast enough, it will not be hit by the projectile. This has the effect that even though the game determines a shot to be accurate, it doesn't mean the target has to be hit.\n\nAfter the player researched Ballistics, units tend to fire at the position where the target is supposed to be when the arrow lands. The targeted unit can still dodge the shot if it moves into another direction after the archer fires.\nBallistics does affect bombard towers but not other gunpowder or siege units.\n\n## Stray Arrows and Scorpions\n\n[Source2](https://www.youtube.com/watch?v=DZQn4-aAjGU)\n\nIf a ranged unit misses its target but the arrow happens to hit a different unit, this unit only takes **half** of the damage.\n\nThis behaviour also applies to scorpions and other units, which have arrows passing through units. Scorpions will only damage their targeted unit with full damage, even if the arrows move through other units first.\n[Source](https://www.youtube.com/watch?v=o4Z0BTmzRgg)\n\n## Target Size\n\n[Source3](https://www.youtube.com/watch?v=33MEpQKCTes)\n\nThe chance to hit also depends on the unit size. Bigger units seem to have a larger area to hit which in consequence counters the effect of the *accuracy error radius*.\n\n## Friendly Fire\n\nMost of the ranged units do not injure friendly units with their projectiles. The Onager is an exception because its \"area of effect\" attack hits all units regardless of their owner. However, other units with \"area of effect\"-damage such as petards and demolition ships do not injure friendlies.\n\n## Weird AoE2 Quirks\n\nWhen a ranged unit dies but the projectile is still in the air, modifiers for damage and effects might change. Some of the effects include:\n\n* When a trebuchet is packed before it has hit its target, the damage of the projectile will change to 1. If the trebuchet is destroyed before the projectile hits, it does no damage.\n* If a scorpion is destroyed, the bolt can suddenly destroy trees on its path.\n* The bonus for standing on a cliff or hill (+25 % damage) is removed after the units dies and all arrows that are still in the air will do normal damage.\n* Portuguese caravels fire in random directions, if attacking a melee unit at range 0. This seems to be a bug related to calculations concerning the *accuracy error radius*.\n\n[Source](https://www.youtube.com/watch?v=O6fD4AC5sFQ)\n\nOther things related to accuracy:\n\n* Trebuchets also have a hard coded 80 % accuracy against buildings [Source](https://youtu.be/nh3GnmFSkeY?t=5m46s)\n* British trebuchets after researching \"Warwolf\" have a 100 % accuracy against every unit except 1x1 tile buildings [Source](https://youtu.be/nh3GnmFSkeY?t=11m44s)\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/attacking_alarm.md",
    "content": "# Attacking Alarms\n\n[Source](https://www.youtube.com/watch?v=jmgD8WMlZtk)\n\n## The two different alarms/alerts\n\nThere are two alarms in the game files that are triggered when the player is attacked. One of them also has two variations that are played for special occasions.\n\nThe first one (`50315`) sounds like a horn and is triggered when one of your units is attacked by an enemy. `50316` has a bell sound and plays when a villager or building gets damaged by an enemy. Variations on the first sound are `50313` (warns that a unit is being converted) and `50366` (warns that a wild animal attacks a unit).\n\n    50313.wav  (Horn + monk) One of your units is being converted\n    50315.wav  (Horn) Your military units are attacked\n    50316.wav  (Bell) A villager or building of yours is attacked\n    50366.wav  (Horn + wolf) Wild animal attacks your units (not played for boar)\n\nAll the alarms only trigger if you are attacked and not if you kill enemy units that have not hit you yet.\n\n## Battles\n\nThere is only one alarm played per \"battle\". Battles start when the player is attacked, i.e. when the alarm sound plays. Their size is always 20x20 tiles with the center being at the place where the players' units were attacked first (i.e. a 10 tile radius). Units fighting in this area can not trigger more alarms until they either leave the battle field (they would have to move outside of the 20x20 area) or the battle is over (see **Cooldown**).\n\nMultiple battles can happen simultaneously. For example, units that start to fight outside of the 10 tile radius will create a new battle field and thus a separate alarm sound will be played.\n\n## Cooldown\n\nA battle is over when\n\n* fighting in the 20x20 area has stopped\n* a 10 second cooldown has passed\n\nIf fighting on the battle field breaks out again before the cooldown finishes, it resets.\n\nAfter the cooldown has passed, the game considers any new confrontation/fighting as a new battle. Therefore a new alarm sound will be played.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/build_speed.md",
    "content": "# Build speed\n\n[Source](https://www.youtube.com/watch?v=I-WvtZWLLc0)\n\nAdding villagers to a construction site decreases the time it takes to finish the building. However, the increase in build speed is not linear. The more villagers are assigned to the building, the lesser the increase is.\n\n## Math\n\n    (time for completion) = (3 * (build time)) / ((number of villagers) + 2)\n\n## Relation to the building's HP\n\nThe *status of completion* is independent from the HP of a building. This ensures that a building can be completed in the same amount of time, even if the construction site is constantly attacked by enemies.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/building_placement.md",
    "content": "Building placement constraints\n==============================\n\nTodo: everything\n\nBuilding | Constraint\n---------|-----------\nDocks    | shallow water at the middle block and at least on ground block\n\nRange to keep building\n======================\n\nWhen a builder is tasked with building a building and that is completed, it\nwill try to build another building. This not only applies to villagers present\nwhen the building is completed but also to villagers who only arrive after the\nbuilding is completed.\n\nIt appears to choose the closest building, where the distance is not rounded up\nto full tiles. When choosing whether or not a building is in range however,\nonly full tile distances are taken into account.\n\nThe range varies depending on the building size. They are however not just\nsimple circles generated with Bresenham or Midpoint. In the following ASCII art\npieces, each character represents one tile, where `o` is the tile the villager\nis in, `.` tiles are in range and `X` tiles are out of range. Only one tile of\nthe building has to be in range, not all.\n\nFor {1x1,3x3,5x5}-tile-buildings (all with odd size, 5x5 is wonder and largest\nbuilding possible):\n\n```\nXXXXXXX.....XXXXXXX\nXXXX...........XXXX\nXXX.............XXX\nXX...............XX\nX.................X\nX.................X\nX.................X\n...................\n...................\n.........o.........\n...................\n...................\nX.................X\nX.................X\nX.................X\nXX...............XX\nXXX.............XXX\nXXXX...........XXXX\nXXXXXXX.....XXXXXXX\n```\n\nFor {2x2,4x4(?)}-tile-buildings (all with even size):\n\n```\nXXXXXXXXXXXXXXXXXXX\nXXXXXXX.....XXXXXXX\nXXXXX.........XXXXX\nXXX.............XXX\nXXX.............XXX\nXX...............XX\nXX...............XX\nX.................X\nX.................X\nX........o........X\nX.................X\nX.................X\nXX...............XX\nXX...............XX\nXXX.............XXX\nXXX.............XXX\nXXXXX.........XXXXX\nXXXXXXX.....XXXXXXX\nXXXXXXXXXXXXXXXXXXX\n```\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/damage.md",
    "content": "# Damage\n\n[Source](http://ageofempires.wikia.com/wiki/Attack)\n\nClose combat and projectile damage calculations.\n\n## Measurement\n\n(dark age, no tech)\n\nunit            | enemy    | stated attack | stated defense | damage (uphill) | damage (level) | damage (downhill)\n----------------|----------|---------------|----------------|-----------------|----------------|------------------\nteutonic knight | house    | 17            | 0              | 16              | 21             | 26\nteutonic knight | villager | 17            | 0              | 13              | 17             | 22\nvillager        | villager | 3             | 0              | 2               | 3              | 4\ntrebuchet       | castle   | 200           | 0              | 323             | 431            | 539\nram             | castle   | 2             | 0              | 88              | 117            | 146\n\n## Attack damage formula\n\nThe damage in AoE1 and AoE2 is calculated with the following formula\n\n```\ndamage = max(1, ((max(0, melee_damage - melee_armor)\n                + max(0, pierce_damage - pierce_armor)\n                + sum(max(0, attack_bonus - attack_resist)))\n                * elevation_factor) * stray_factor)\n```\n\nExplanation:\n\n* `melee_damage`: Melee damage of the attacking unit\n* `melee_armor`: Melee armor of the defending unit\n* `pierce_damage`: Pierce damage of the attacking unit\n* `pierce_armor`: Pierce armor of the defending unit\n* `attack_bonus`: Attack bonus (flat additive) against a target's armor class.\n* `attack_resist`: Attack resist (flat additive) against an attack bonus.\n* `elevation_factor`: Multiplies damage by `1.25` (`1.33` in AoE1) if the attacking units is on higher elevated terrain or on top of a cliff. Similarly, units on lower ground receive a malus that multiplies their damage by `0.75` (`0.66` in AoE1). The malus doesn't apply when standing at the bottom of a cliff.\n* `stray_factor`: When a projectile misses its intended target but hits a bystanding unit, damage is halved. Check [Accuracy](accuracy.md) for more details.\n\nStanding on top of cliffs and additionally being higher elevated does not increase the damage bonus. Also, standing on higher elevated terrain while at the bottom of a cliff does not negate the bonus damage of the unit on top of a cliff. However, it does gives the unit at the bottom of the cliff the elevation damage bonus of `1.25`. The only requirement for the application of the cliff damage bonus is that there must be a cliff in between the units. The distance to the cliff does not matter.\n\n### Armor\n\n[Source](http://ageofempires.wikia.com/wiki/Armor)\n\nArmor is organized through *armor classes* of which there are 30 in AoC and 32 in AoE2:HD. Units can have several armor classes, but will always have the classes *melee* and *pierce* assigned. Units have a value for each of their armor classes which indicate the `attack_resist` against attacks directed at the armor class. All units (except trees, gregarious animals, hunting wolves and some units from beta versions) have a default value of **1000** for each armor class that they don't have.\n\nAn attack is always applied against one or more armor classes. We will look at an example to show how the damage is calculated.\n\nAn archer attack a spearman. The attack of the archer does `0` damage against the *melee* armor class, `4` damage against the *pierce* armor class and `3` damage against the *spearman* armor class. The spearman has the armor classes *melee* with value `0`, *pierce* with value `0`, *infantry* with value `0` and *spearman* with value `0`. Now we can calculate the outcome of the attack:\n\n```\ndamage =   max(0, archer_melee_damage - spearman_melee_armor)\n         + max(0, archer_pierce_damage - spearman_pierce_armor)\n         + max(0, archer_spearman_damage - spearman_spearman_armor)\n\n       = max(0, 0 - 0) + max(0, 4 - 0) + max(0, 3 - 0) = 7\n```\n\nThe attack does a total of 7 damage. The *infantry* armor class of the spearman is ignored because the archer's attack does not do damage against it.\n\nAs a second example, we will let a spearman attack a cataphract. This time, we will list attack and defense in a table.\n\nAttack class  | Attack value |\n--------------|--------------|\nmelee         | 3            |\npierce        | 0            |\ncavalry       | 15           |\nwar elephant  | 15           |\ncamel         | 12           |\nship          | 9            |\neagle warrior | 1            |\nbuilding      | 1            |\n\nDefense class | Attack resist |\n--------------|---------------|\nmelee         | 2             |\npierce        | 1             |\ncavalry       | 12            |\nunique unit   | 0             |\n\nThe matching armor classes are *melee*, *pierce* and *cavalry*. We can derive a formula for the damage from that.\n\n```\ndamage =   max(0, spearman_melee_damage - cataphract_melee_armor)\n         + max(0, spearman_pierce_damage - cataphract_pierce_armor)\n         + max(0, spearman_cavalry_damage - cataphract_cavalry_armor)\n\n       = max(0, 3 - 2) + max(0, 0 - 1) + max(0, 15 - 12) = 4\n```\n\nThe attack does a total of 4 damage.\n\nOnly the values for the melee class and the pierce class are visible in the interface.\n\n## Damage by Projectiles\n\nThe damage of a shot in AoE2 is most likely calculated after it has hit the target. This leads to weird behaviour when units die, including:\n\n* Losing environmental bonuses such as uphill or cliff bonus damage (+25 %)\n* Dealing no damage, e.g. trebuchets\n* Dealing only 1 damage, e.g. if japanese trebuchets are packed before they hit their target\n\nIt also has other consequences. Technologies will affect projectiles that were in the air when research finished.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/formations.md",
    "content": "# Formations\n\n## Principles of Formations in AoE2\n\nDave Pottinger's Articles: [1](http://www.gamasutra.com/view/feature/131720/coordinated_unit_movement.php) [2](http://www.gamasutra.com/view/feature/131721/implementing_coordinated_movement.php)\n\n1. Grouped units have the same speed\n\nThe speed of a formation is determined by its slowest unit. This will help the formation to stay together.\n\n2. Grouped units take the same path\n\nIf there is an obstruction in the units path, the group will stay together to avoid being attacked when separated.\n\n3. Grouped units arrive at the same time\n\nWhen a group is formed, the faster units should decrease their speed for slower units to catch up.\n\n## Structure\n\nWhen talking about formations, one has to differentiate between\n\n1. The selection of the player\n2. The 'actual' formation\n3. The subformations of a formation\n\nPlayers can select any units they own and add them to their selection. Therefore, a selection can consist of varying unit types and allows mixing of buildings, military units, villagers and others. Not all of these units are capable of forming a formation. Buildings are excluded from formations because they can't move and so are unpacked trebuchets. Villagers, trade carts and fishing ships will not be part of any formation. If they are moved, they don't abide Pottinger's rules and will move to the target at their normal speed while each of them takes their own path. Hence, when villagers are part of a selection of military units, the military units will form a formation and then move to the target while the villagers will independently move there.\n\nA formation always consists of 4 subformations (which can be empty). What unit type belongs to what subformation is hard coded into the game. A quick overview is shown below. The `^`-symbol shows the front of the formation.\n\n```\n...................\n...^^^^^^^.........\n1..Cavalry.........\n2..Infantry (melee)\n3..Ranged..........\n4..Siege/Support...\n...................\n```\n\nEvery military unit in the game is sorted into one of these subformations depending on their `GroupID` (in some cases their `LineID`). Bear in mind, that the descriptions next to the subformations in the above figure are not fully accurate. For example, *cavalry archers* are part of the \"Ranged\"-subformation instead of the \"Cavalry\"-subformation. To avoid confusion specific subformations will from now on be referenced by their index (e.g. `subformation[1]` equals \"Cavalry\"-subformation) The figure below shows to which subformation a `GroupID` belongs.\n\n```\n...................\n...^^^^^^^.........\n1..Cavalry......... 912, 947\n2..Infantry (melee) 906\n3..Ranged.......... 900, 923, 936, 944, 955\n4..Siege/Support... 902, 913, 918, 919, 920, 935, 943, 951, 959, Saboteur (UnitID: 706)\n...................\n5..Else............ 904, 921, 958, 961 (not part of any formation)\n```\n\nAs you can see, the only exception to sorting by `GroupID` is the saboteur. Ships are also sorted into a formation by their `LineID` (see the section about **Ship Formations**).\n\nFormations have a formation type which determines how the units are generally ordered (see **Formation Types**). The subformations inherit their formation type from the parent formation.\n\n### Ship Formations\n\nShips use the same formation system and are sorted into a subformation by similar rules. Instead of using `GroupID`, `LineID` determines which subformation is chosen (probably because all battle ships have the same `GroupID` 922). The figure below shows the subformations to which each class of ships is passed.\n\n```\nWARNING: These are LineIDs not GroupIDs\n...................\n...^^^^^^^.........\n1..Cavalry......... -294\n2..Infantry (melee) -293\n3..Ranged.......... -283, -284, -292\n4..Support......... -285\n...................\n5..Else............ Admiral Yun Shi (UnitID: 844)\n```\n\nBecause the same formation system is used for land and sea units it is possible to mix them inside a formation. This behavior can be observed when one selects ships and land units and then sends them into shallow water.\n\n## Subformations\n\nSubformations are the parts of the formation where the ordering of units into lines takes place. How units are ordered depends on a variety of factors and is also determined by other subformations in the parent formation.\n\n### Individual Subformation\n\nFor a start one has to look at the behavior of an individual subformation. Let's assume we have a single subformation consisting of ranged units (`subformation[3]`).\n\n```\n............\n...^^^^^^...\n...RRRRRR...\n...RRRRRR...\n............\n```\n\nThe number of lines (or `row_count`) in this subformation is always determined by the number of its units. Unit width and different unit types have no influence here. The `row_count` can be calculated by an algorithm. After the value has been calculated the units will be sorted into the lines starting from the top left.\n\n```\n............\n...^^^^^^...\n..>RRRRRR...\n...RRRRRR...\n............\n```\n\n#### Mixing Unit Types\n\nIf a subformation is formed with units of more than one unit type, AoE2 tries to evenly distribute them. This is achieved by alternating between unit types when sorting them into a line. To determine the unit type, the `UnitID` value is used.\n\n```\n............\n...^^^^^^...\n..>ASASSS...\n...SSSSSS...\n............\n```\n\nIn the above example, we have a subformation consisting of 2 archers `A` and 10 skirmishers `S`. Beginning from the top left the subformation alternates between archers and skirmishers until there are no more archers left. The other skirmishers fill up the remaining space.\n\nThe same rules apply, if more than two unit types are used. We will now have a look at a subformation containing 5 archers `A`, 3 skirmishers `S`, 6 longbowman `L` and 1 throwing axeman `T`.\n\n```\n..............\n...^^^^^^^^...\n..>LASTLASL...\n...LASLALA....\n..............\n```\n\nWhich unit type is sorted into the line first depends on the order in which the player selected the unit types (or how they are ordered inside the selection queue). In the first example of this section, the player selected the archers first and added the skirmishers to their selection. The second and above example would be the result of selecting longbowman first, archers second, skirmishers third and throwing axeman last.\n\n#### Distance Between Units\n\nHow far units in a subformation stay apart is determined by the width of the widest units and the length of the longest unit. All units inside the subformation will follow this rule, regardless of their own height or length. As an example, one can select a few archers and a scorpion. Scorpions are one of the widest units in `subformation[3]` and will therefore determine the distance between all of the units. Furthermore, one will notice that all rows of the subformation are affected and not solely the line the scorpion is in.\n\n### Influence of other Subformations\n\nSo far we have only examined the grouping of individual subformation but left out the behavior of the parent formation. The parent formation knows about the variables of each subformation, e.g. `row_count` and `distance_between_units`. We will now see, how the parent formation can influence the `row_count` of other subformations.\n\nLet's view two subformations, one with swordsman and one with rams, independently.\n\n```\n.............\n...SSSSSSS...\n...SSSSSSS...\n.............\n```\n```\n............\n...RRRRRR...\n............\n```\n\nWe would expect that the grouping of units inside the parent formation looks exactly the same, with the two subformation just joined together. However, this is not the case. Instead, the parent formation will look like this:\n\n```\n....................\n...SSSSSSSSSSSSSS...\n.......RRRRRR.......\n....................\n\n```\n\nThe reason for the change of the `row_count` for `subformation[2]` is the width of `subformation[4]`. Rams are about two times as wide as swordsman, hence the subformation they are in will also be much wider than `subformation[2]` if it is selected individually. To deal with the difference in width, the number of units per row of `subformation[2]` is extended. In general one can say that the widest subformation in the parent formation influences the `row_count` of all other subformations.\n\n## Formation Types\n\n### Line Formation\n\nThis is the standard formation whenever the player makes a selection. Units form lines (see all examples in **Subformations**) and are arranged with the weaker subformations in the back and the stronger ones in the front.\n\n### Staggered Formation\n\nBehaves like the Line Formation except that the space between units is doubled for every subformation.\n\n### Boxed Formation\n\nUnits form the outline of a square. The weaker units are in the center while stronger units are placed on the outside. When representing a subformation with its corresponding number, it will look like this:\n\n```\n...............\n.1.1.1.1.1.1.1.\n.1.2.2.2.2.2.1.\n.1.2.3.3.3.2.1.\n.1.2.3.4.3.2.1.\n.1.2.3.3.3.2.1.\n.1.2.2.2.2.2.1.\n.1.1.1.1.1.1.1.\n...............\n```\n\nIn comparison to the other formation types, the ordering of units with the boxed formation type in the subformations seems to be random and does not follow any specific rule. It is assumed that the units are grouped anew every time a movement order is placed.\n\nThis formation type is also rarely used in competitive games and presumably disfavored by players.\n\n### Flanked Formation\n\nThis type of formation leaves a big vertical gap between the rows of a line formation and effectively splits it in half. Otherwise it behaves exactly like a Line Formation regarding ordering of units.\n\nFlanked formations play a crucial part in competitive play, when countering the attack of onagers against dense infantry.\n\n### Marching formation\n\nThe marching formation cannot be select by the player but is used by the game when a movement order is placed more than 10 tiles away from the current location of the player's selection. It behaves like a Line Formation that is turned around by 90 degrees. Because this formation type is much narrower than all other types, units can fit through small gaps in tree lines and walls.\n\n## Limits in AoE2\n\n* Units will not sorted into the formation if they are too far away (10 tiles) from an already established formation. They will behave as if they were in `subformation[5]` and therefore don't follow Pottinger's rules. While this seems strange at first, it ensures that the point where the units join the group is not to far apart from the majority of units and that the formation gets moving towards its goal faster (instead of having to decrease its speed for a slow unit that is far away).\n* Formations will split on occasions where the obstruction is small (such as a single tree or a building). This was probably implemented to avoid formations to get stuck at a single choke point, e.g. a breach in the wall, when there are other optional paths nearby.\n* Units within the same formation don't collide with each other when the formation is established. It is assumed that the pathfinding algorithm of AoE2 was not capable enough to handle the complexity of the formation system.\n* Units do not know how far they have to travel and are following a rough path that is recalculated if they encounter an obstruction. Therefore units will sometimes transition from marching to combat formations far too early.\n\n## Algorithmic solutions\n\nThe proposed algorithms have a run time of `O(n)`, where `n` is the number of units. Therefore, implementing them shouldn't affect performance that much, even for large formations over 40 units. `player_selection` is a set of units that the player selected and that is passed to the algorithm.\n\n### Sort units into subformations (Pseudocode)\n\n```\nINPUT:\n- Set of units: player_selection\n- Formation type: formation_type\n```\n```\nInitilization:\n\n# subformations for military units\nsubformation[1] = new subformation();\nsubformation[2] = new subformation();\nsubformation[3] = new subformation();\nsubformation[4] = new subformation();\n\n# dummy subformation for every unit that will\n# not form formations\nsubformation[5] = new subformation();\n```\n```\nAlgorithm:\n\nfor unit u in player_selection do {\n    switch(u.GROUP_ID) {\n        case 912:\n        case 947: subformation[1].add(u);\n            break();\n        case 906: subformation[2].add(u);\n            break();\n        case 900:\n        case 923:\n        case 936:\n        case 944:\n        case 955: subformation[3].add(u);\n            break();\n        case 902:\n        case 913:\n        case 918:\n        case 919:\n        case 920:\n        case 935:\n        case 943:\n        case 951:\n        case 959: subformation[4].add(u);\n            break();\n        case 922:\n        switch(u.LINE_ID) {\n            case -294: subformation[1].add(u);\n                break();\n            case -293: subformation[2].add(u);\n                break();\n            case -283:\n            case -284:\n            case -292: subformation[3].add(u);\n                break();\n            case -285:\n            case 706: subformation[4].add(u);\n                break();\n            default: subformation[5].add(u);\n                break();\n        }\n        default: subformation[5].add(u);\n            break();\n    }\n}\n\nsubformation[1].order_units(formation_type)\nsubformation[2].order_units(formation_type)\nsubformation[3].order_units(formation_type)\nsubformation[4].order_units(formation_type)\nsubformation[5].order_units(formation_type)\n\n```\n### Add units into a subformation (Pseudocode)\n\n```\nINPUT:\n- A unit: unit\n```\n```\nInitilization:\n\n# Create a list of stacks. Each stack will\n# contain units of the same GroupID\nList unit_stacks = new List<Stack>();\n\n# Create a list of GroupIDs that contains\n# the IDs of units that are already in the\n# subformation\nList ids = new List();\n\nunit_count = 0;\n```\n```\nAlgorithm:\n\nif ids.contains(unit.GROUP_ID) then {\n    index = ids.indexOf(units.GROUP_ID);\n    unit_stacks.get(index).push(unit);\n    ++unit_count;\n}\nelse {\n    Stack unit_stack = new Stack();\n    unit_stack.push(unit);\n    ids.add(unit.GROUP_ID);\n    unit_stacks.add(unit_stack);\n    ++unit_count;\n}\n```\n### Order units inside subformation (Pseudocode)\n\n```\nINPUT:\n- List of Stacks: unit_stacks\n- Formation type: formation_type\n```\n```\nInitilization:\n\n# Contains the units in correct order beginning from\n# the top left (see section about Mixing Units).\nList unit_line = new List();\n```\n```\nAlgorithm:\n\n# Creates a single ordered line\nwhile !unit_stacks.isEmpty() do {\n    for Stack s in unit_stacks do {\n        unit_line.add(s.top());\n        if s.isEmpty() then {\n            unit_stacks.remove(s);\n        }\n    }\n}\n\n# Now calculate the position of each unit\n# depending on formation_type and their position\n# inside the unit_line list. This can represented\n# by a relative offset from the front of the formation,\n# which then has to be calculated to a global coordinate.\n```\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/garrison.md",
    "content": "# Garrison\n\nMechanics related to garrisoning units in buildings or siege units.\n\n## Number of projectiles\n\n[Source](https://www.youtube.com/watch?v=tCBlWAkanPY)\n\nHow many projectiles a building fires depends on two variables, the *damage per second* value of the building and the (pierce) *damage per second* of each garrisoned unit. The amount of arrows added by a unit can be derived from this formula:\n\n```\n    additional_arrows = sum(unit_dps_pierce) / building_dps\n```\n\n`unit_dps_pierce` is calculated by dividing a unit's pierce attack value by its reload speed. `building_dps` is obtained in the same way, by dividing the building's attack value by its reload speed. Units doing melee damage do not increase the number of arrows or projectiles. However, for villagers `unit_dps_pierce` is a fixed value of `2.5`. Researching the Teuton unique technology *Crenellations* has a similar effect by setting `unit_dps_pierce` to `2.5` for every unit in the infantry armor class.\n\nBombard towers have even stricter rules in that only gunpowder units can increase the projectile count. The calculation of `building_dps` is also done slightly different. For the first additional projectile `sum(unit_dps_pierce)` is divided by `(building_dps * 2)` which effectively doubles the amount of required units for an additional shot. For the second and third cannon ball the standard formula is used again.\n\n**Pseudocode**\n```\n    remaining_unit_dps = sum(unit_dps_pierce) - (building_dps * 2);\n\n    if(remaining_unit_dps >= 0) {\n        additional_arrows = 1 + (remaining_unit_dps / building_dps);\n    }\n    else {\n        additional_arrows = 0;\n    }\n```\n\nThe result `additional_arrows` is floored and added to the default arrow count of the building. A table with the default projectile counts is shown below.\n\nBuilding      | Default Arrow Count | Default Attack  | Maximum Arrow Count\n--------------|---------------------|-----------------|--------------------\nTown Center   | 0                   | 5               | 10\nCastle        | 5                   | 11              | 20\nTower         | 1                   | 5/7/8\\*         | 5\nBombard Tower | 1                   | 120\\*\\*         | 4\n\n\\* Damage of Watch Tower/Guard Tower/Keep\n\n\\*\\* Calculation is different; see pseudocode above\n\nIncreasing the default attack damage of buildings by researching technologies also affects `building_dps`.\n\n### Example\n\nThe Elite Janissary has a pierce attack value of `22` and a reload speed of `3.45` (The Conqueror's). With these values we can calculate `unit_dps_pierce`.\n\n```\nunit_dps_pierce = 22 / 3.45 = 6.37\n```\n\nNow that we know `unit_dps_pierce`, the amount of additional projectiles for each building can be obtained.\n\n**Town Center**\n```\n    building_dps = 5 / 2.03 = 2.46\n```\n```\n    additional_arrows = 6.37 / 2.46 = 2.58\n```\n\n**Castle**\n```\n    building_dps = 11 / 2.03 = 5.41\n```\n```\n    additional_arrows = 6.37 / 5.41 = 1.17\n```\n\n**Keep**\n```\n    building_dps = 8 / 2.03 = 3.94\n```\n```\n    additional_arrows = 6.37 / 3.94 = 1.61\n```\n\n**Bombard Tower**\n```\n(1st additional projectile)\n    building_dps = (120 / 6) * 2 = 40\n(Other additional projectile)\n    building_dps = (120 / 6)     = 20\n```\n```\n(1st additional projectile)\n    additional_arrows = 6.37 / 40 = 0.155\n(Other additional projectile)\n    additional_arrows = 6.37 / 20 = 0.31\n```\n\n7 Elite Janissaries are needed to make the bombard tower fire one additional projectile. Two additional cannon balls are only achievable with 10 Elite Janissaries garrisoned in a bombard tower.\n\n# Weird AoE2 Quirks\n\n* Only units that do pierce damage can increase the number of projectiles fired by a building. Units that fire projectiles dealing melee damage - like Gbetos, Throwing Axemen and Mamelukes - will have no effect on the arrow count.\n* For the Cho Ko Nu `unit_dps_pierce` does not use the pierce attack value of the first arrow fired. Instead it only takes the damage the last arrow does into consideration. Because this arrow only does 3 pierce damage, the number of projectiles added to a building's attack is fairly low.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/market.md",
    "content": "# Market\n\nSource: [here](http://ageofempires.wikia.com/wiki/Market_(Age_of_Empires_II)) for the first part - fixed through reverse engineering.\n\n[here](https://www.youtube.com/watch?v=NvsM9B4ac2g) for the rest.\n\nGold from trading calculation, tribute system, buying/selling resources and the destruction of markets.\n\n## Trade Cart gold calculation\n\nThe gold you receive from trading with a market depends on the *distance in tiles* (or *distance in tile width*) between the two markets. The further the markets are apart from each other, the more gold is generated by a trade cart.\n\nGenerated amount of gold is not linear to the distance in tiles. Note that the gold value shown when you select the market is rounded but the gold you receive is not. This means that in the case of two markets being placed next to each other, trade carts will still generate a tiny fraction of gold.\n\n### Math\n\n## Age of Kings\n\n    (gold value) = (d/size + 0.3) * d * 0.46 + 0.5\n\nwith\n`size`, the map size in tiles along the edge of the map and\n`d` is calculated as follows:\n\n    d = max(0.0, sqrt(max(0.0, deltaX-4)^2 + max(0.0, deltaY-4)^2))\n\nwith `deltaX` and `deltaY` the distances between the Markets in either axis-aligned direction, measured from the center of each Market.\n\n## The Conquerors\n\n    (gold value) = 2 * (d/size + 0.3) * d * A ) / B   + 0.5\n\n`A` and `B` may depend on civilization attributes and on the speed of the trade vehicle (trade cog or cart), or its radius.\n\n    d = max(0.1, sqrt(max(0.0, deltaX-5)^2 + max(0.0, deltaY-5)^2))\n\n## Tribute\n\nTribute can be sent to other players using the tribute menu in the upper right corner. The number of resources you are sending to a player is shown on top of the buttons of the receiving player. On top of that the tributing player has to pay a fee. The amount of resources left *after sending tribute* can be seen in the line with the players name in it:\n\n    (amount of resource) = (current stockpile) - ((tribute amount) + (tribute fee) * (tribute amount))\n\n## Prices\n\nAll prices for buying/selling resources are global. Thus every player in a game sees the same price and is affected by price changes. If the market is selected, prices are shown in two rows, with the upper one being the selling price and the lower one being the price for buying. The number that is shown, represents the amount of gold that the player has to spent to buy 100 units of the resource.\n\nBoth buying and selling price are calculated from the same *base price*. Buying a resource always costs 30 % *more* gold than the base price. Selling it costs 30 % *less*. The base price at the beginning of a game ist always 100 for wood and food, while being 130 for stone.\n\n    sell price = 70\n    base price = 100\n    buy  price = 130\n\nEvery time any player sells 100 resources of any kind, the base price of this resource decrements by 3. Buying a resource increments the base price by 3.\n\n    sell price = 70  ---> sell price = 68\n    base price = 100 ---> base price = 97\n    buy  price = 130 ---> buy  price = 127\n\nThe lowest the base price can go is 20, while the highest base price is 9999.\n\n    sell price = 70  ---> sell price = 68  ---> ... ---> sell price = 14\n    base price = 100 ---> base price = 97  ---> ... ---> base price = 20\n    buy  price = 130 ---> buy  price = 127 ---> ... ---> buy  price = 26\n\n## Destroying a market\n\nThere are two possible scenarios.\n\n### The trading partners' market is destroyed\n\nTrade Carts will not seek an alternative market to trade with. As soon as the market is destroyed, all of you trade carts that are trading with it return to your own market and stay there. Trade carts that are already on their way back from your ally will still deliver the gold that they are carrying and then stop at your market.\n\n### Your market is destroyed\n\nIf you only had one market, trade carts will stop and immediately lose all carried gold.\n\nIf you happen to have more than one market, trade carts will choose the market which is closest *from their current position* as their new trade destination.\n\n## Weird AoE2 Quirks\n\n* You can trade with enemy markets.\n* Full trade carts cannot be loaded onto a transport ship. As soon as you give the order to board the ship, they change into an empty cart, dropping all of their gold.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/monk_conversion.md",
    "content": "# Monk Conversion\n\nConversions of units and buildings.\n\n## Unit Conversion\n\n[Source](https://www.youtube.com/watch?v=UjOnv1YjHX0)\n\nConverted units are frozen in the tech level they had when they were converted. There's no way to upgrade or improve them, but they will get some abilities (e.g. an enemy villager that has been converted in the dark age will be able to build a stable once you've advanced to the feudal age; even if the enemy culture doesn't even have stables). It appears that build/research/train abilities are the only thing that is not affected by the \"conversion tech lock\".\n\nIf monks are converted, they do keep the techs that affect their attributes but not ones that affect their abilities. For example, monks that are converted from a player who can convert buildings can no longer do this after the conversion.\n\nAs a general rule, these stats carry over from a conversion:\n\n* Health Points\n* Damage\n* Speed\n* Attack rate\n\n## Building Conversion\n\n[Source](https://www.youtube.com/watch?v=mAXY31QVs2Q)\n\nBuildings are also frozen in tech level when converted. Besides that, they function just as normal as other buildings of the player and produce \"normal\" (not frozen in tech level) units.\n\nTeam bonuses are sometimes transferred over if they affect the stats of the building. For example, the faster building time from a Goth barrack is carried over but the cheaper infantry bonus is not. The higher population limit of Inca houses also seems to be a stat and stays if the house is converted.\n\nUnits inside a building are not converted and will ungarrison. The same is true for rams. Transport ships are a little bit complicated, since they do convert but the units inside don't. The player can choose to delete the transport ship, destroying all units inside or unload them at a shore so that the original owner regains control of them.\n\n## Weird AoE2 Quirks\n\n[Source](https://www.youtube.com/watch?v=_gjpDWfzaM0)\n\n* If a converted unit is \"replaced\" by the game, e.g. a villager changing to a farmer, the stats are not frozen anymore.\n* Converted siege units are only frozen in tech level until the new owner researches a technology that changes the units stats, e.g. chemistry. As soon as the research finishes, the units are replaced with ones that are on the same tech level as the player. Weirdly enough, Onagers are replaced if the player researches Heavy Scorpions but not if they research Siege/Capped Rams.\n* The flaming projectile caused by chemistry research is tied to the tech level of the owner but the +1 damage is kept, if the unit is converted.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/pathfinding.md",
    "content": "# Pathfinding\n\n[Source](http://www.gamasutra.com/view/feature/131844/postmortem_ensemble_studios_age_.php?page=2)\n\n[Source](https://web.archive.org/web/20081221121604/http://www.gamasutra.com/features/gdcarchive/2000/pottinger.doc)\n\nand *Ensemble Studios Official Strategies & Secrets to Microsoft's Age of Empires II: The Age of Kings*, Bruce Shelly, 1999, published by Sybex Inc.\n\n## Algorithms\n\nAge of Empires 2 uses three different algorithms for pathfinding plus two obstruction systems.\n\nThe first (high-level-)pathfinder is meant for long distances and uses mip-mapping to create a general route across the map. Criteria for the mip-map subdivision is *passability*. It ignores moving objects, e.g. units or wildlife, though it takes \"static\" object like buildings into account. Whenever a building is constructed the pathfinder gets notified and updates the corresponding mip-map chains.\n\nFurther reading: [3D Pathfinding](http://www.in.tum.de/fileadmin/user_upload/Lehrstuehle/Lehrstuhl_XV/Teaching/SS06/Seminar/3dpf-doc-final.pdf) (p. 11)\n\nThe other two pathfinders are for short and medium distances. One uses tile-based pathfinding while the other one is a polygonal pathfinder that dynamically generates convex hulls. These are used in combat situations or when units cross paths.\n\n## Examples\n\n*(Please add more examples if you know any)*\n\n```\n...............      ...............      ...............      ...............\n...............      ...............      ...............      ....XX.XXXXX...\n......T........ ---> ......T........ ---> ....XXTXXXXX... ---> ....XXTXXXXX...\n............... ---> ....XX.XXXXX... ---> ....XX.XXXXX... ---> ...............\n....XXXXXXX....      ....XX.XXXXX...      ...............      ...............\n....XXXXXXX....      ...............      ...............      ...............\n```\nUnits will split up their formation if they encounter an obstruction such as a tree.\n\n```\n..............      ..............      ..............      ..............\n..............      ..............      ..............      ....X....X....\n....YYYYYY.... ---> ....YYYYYY.... ---> ....YYYYYY.... ---> ...XYYYYYYX...\n.............. ---> ....XXXXXX.... ---> ...XXXXXXXX... ---> ...XXXXXXXX...\n....XXXXXX....      ....XXXXXX....      ....XX..XX....      ..............\n....XXXXXX....      ..............      ..............      ..............\n```\nWhen troops encounter an enemy formation they will try to go around to engage in combat. Though if units get killed and create a gap, the pathfinder has some difficulties to distribute units efficiently.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/ram_speed.md",
    "content": "# Ram Speed\n\n[Source](https://www.youtube.com/watch?v=aPK2RnpEgPA)\n\n## Garrison Capacity\n\nBasically all infantry and foot archer type units can garrison inside a ram. Exceptions are monks, petards and kings. In the *Forgotten Empires* Expansion villagers are also able to be garrisoned inside.\n\nBattering and Capped Rams have a total capacity of 4, while the Siege Ram increases the capacity to 6 units.\n\n## Speed\n\nThe ungarrisoned ram has a *base speed* which is shown below.\n\n    Battering/Capped Ram  0.5 tiles/s\n    Siege Ram             0.6 tiles/s\n\nEvery garrisoned infantry increases the speed of the ram by 0.05 tiles/s regardless of the speed of the unit. Foot archers, skirmishers and hand cannoneers **do not** increase the speed of the ram.\n\n| Ram Type         |0 Units|1 Unit|2 Units|3 Units|4 Units|5 Units|6 Units|\n| -----------------|:-----:|:----:|:-----:|:-----:|:-----:|:-----:|:-----:|\n| Battering/Capped | 0.5   | 0.55 | 0.6   | 0.65  | 0.7   | X     | X     |\n| Siege            | 0.6   | 0.65 | 0.7   | 0.75  | 0.8   | 0.85  | 0.9   |\n\nModifiers on the speed of siege units are only applied to the *base speed* of the ram and not to the speed contributed by garrisoned units.\n\n### Math\n\n    (total ram speed) = (base speed) * (speed modifiers) + 0.05 * (units garrisoned)\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/rates.md",
    "content": "# Rates\n\nThis file lists some rates used in AoE2\n\n# Animals\n\nSheep and deer rot at rate of `1 food / 2 seconds`.\nShepherds get food at `10 food / 12 seconds` (one every 0.8333 seconds).\n\n# Relics\n\nRelics give gold at rate of `1 every 2 / (number of relics) seconds`. Since one\ncan get a maximum of 10 relics, this means they can provide at most 5 gold per\nsecond.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/repair.md",
    "content": "# Repair\n\n[Source](https://www.youtube.com/watch?v=YyBFtDrrpg4)\n\nBuildings, siege weapons and ships can be repaired by villagers.\n\n## Speed\n\nBuildings are repaired at the base repairing speed of **750 hp/min** which is not affected by the total hp of the building/unit.\n\nSiege weapons and ships are repaired at **25%** of the base speed (188 hp/min).\n\nAfter the first villager, each villager repairs at **50%** of the base speed (375 hp/min for building and 188 hp/min for siege weapons and ships).\n\n## Cost\n\nFor each hp repaired, resources equal to the half of corresponding build cost are deducted.\n\n```\n(repair 1 hp cost) = 0.5 * (build cost) / (build max hp)\n```\n\n## Exceptions\n\n* Town center repair costs **twice** as much wood and not half. It also costs no stone except in Dark Age. [Source](https://youtu.be/7cpbi4V41ig?t=4m3s)\n\n## Math\n\n```\n750 hp/min = 750/60000 hp/msec = 0.0125 hp/msec => 80 msec/hp\n```\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/research.md",
    "content": "the following information stems from the internet (tm) and is thus utterly 100-% correct.\n\nbuildings have exactly one sprite\n\texception: e.g. town center, which has multiple sprites for different sections (walkable...)\n\nunits have 5 states: idle, walking, attacking, dying, rotting. for each state, there is a sprite for every direction the unit could be facing\n\texception: villagers, ofc, have a lot more states\n\tships have extra sprites for their sails\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/selection.md",
    "content": "# Selecting and Deselecting\n\nThis documents shows the methods of selecting and deselecting units.\n\n[Source](https://www.youtube.com/watch?v=mQf8GDttbDw)\n\n## Single units\n\nThe player doesn't have to click directly on the sprite of a unit because there is a tolerance factor involved. The tolerance factor is roughly `1 unit width` horizontally and `1 unit height` vertically measured from the center of the unit. This means that clicking in the general area of a unit will be accepted as a valid selection most of the time.\n\nIf two units' areas of tolerance overlap, the unit that is in the front seems to be preferred, but clicking directly on the sprite of one of the player's own units will select the unit that is pointed at. When the area of tolerance overlaps with an enemy (or ally) unit, the unit of the player will always be preferred, even if they click directly on the sprite of the enemy unit.\n\nRelics and animals also have an area of tolerance regarding selection, while resource spots like trees, bushes, gold/stone mines as well as buildings don't.\n\n## Multiple units\n\nAoE2 allows the player to select up to 40 units (60 in AoE2 HD) at once. This can be done by doing either of these actions:\n\n* Drawing a selection box around the units which should be selected\n* Double-clicking on a unit\n* Pressing the one of the number buttons `0-9` to select a previously establish group\n* Using `CTRL + MOUSE-BUTTON` to add or remove single units\n\nAll of these have a specific purpose and will now be explained further.\n\n### Selection Box\n\nThe selection box is the easiest way to select multiple units of any type in AoE2. When the player draws the box around the units they want to select, the units inside the box will be added to the selection queue, **going from the top to the bottom of the box** until the limit of 40 units is reached.\n\nThe tolerance factor is also used here, which can result in units which are slightly outside of the selection box to be selected.\n\n### Double-clicking\n\nDouble-clicking on one unit will add all units of the same type that are shown on screen to the selection queue. Units that are hidden under the HUD bars at the top and the bottom will be ignored. Furthermore, the tolerance factor is not used for this selection method. Only units which have their sprite shown (partially) in the rendering frame are qualified. Units are inserted into the selection queue by adding the units **sorted from oldest to youngest** until the queue has reached the limit of 40.\n\n### Grouping units\n\nAny selection can be made a group by pressing `CTRL + [0-9]`. A group can be quickly selected by pressing the corresponding number key. Units that are part of a group, show their group reference number to the left of their selection indicator.\n\nWhen the group is reestablished by pressing the number key, the units are added to the selection queue by **going from oldest to youngest** unit.\n\nGroups in AoE2 are fixed, which means there can be no units added or removed unless the group number is reassigned.\n\n## Selection queue actions\n\nThere are multiple commands that can be used on the icons of the units in the HUD bar:\n\n* `CTRL + MOUSE-BUTTON`: Remove the unit from the queue\n* `SHIFT + MOUSE-BUTTON`: Keep only units of the same type in the queue\n* `CTRL + SHIFT + MOUSE-BUTTON`: Remove all units of the same type from the queue\n\nThese commands also work for garrison queues.\n\n## Weird AoE2 Quirks\n\n* The tolerance factor when selecting single units can lead to two units being selected simultaneously with a single click, if their ares of tolerance overlap\n* Double-Clicking uses the `LineID` to determine whether units are of the same type. This can lead to a situation where different units of the same line can be part of the selection (e.g. militia and champions). A common occurrence of this behavior can be seen when a unit is converted and its tech level is frozen.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/swgb/power.md",
    "content": "# Power (energy) mechanics in SWGB\n\n## Intro\n\nPower is a unique feature of SWGB. It is necessary to produce units\nefficiently, and for shield generators. It thus makes power generators a\nstrategic target.\n\nPowered buildings have green lights on them, instead of flashing red lights. A\ngreen indicator also lights up [in the UI](user-interface.md#cap) when a\npowered unit is selected.\n\n[more details](https://swgb.fandom.com/wiki/Power_Core)\n\n## Mechanism\n\nPower **only** affects unit creation and research times.\n\nIt doesn't affect things like food production for animal nurseries, or other\nresource collection.\n\nUnpowered buildings work at 25% speed, including research and unit production.\nShield generator buildings need to be powered to provide a shield.\n\nThe clone campaigns extension contains [mobile power\ngenerators](https://swgb.fandom.com/wiki/Power_Droid), units that can be built\nat a power generator.\n\n## Always-powered buildings\n\nCommand centers and Fortresses are always powered. So are power generators.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/swgb/shields.md",
    "content": "# Shields mechanics in SWGB\n\n## Intro\n\nShields are another unique feature of the SWGB franchise. A shielded unit has\ndouble the number of hit points that of an unshielded one, and those extra hit\npoints regenerate.\n\n## Mechanism\n\nA shielded unit gradually fills a shield bar (originally yellow and placed\nunder the health bar), that doubles its health points. Aditionally, shielded\nunits have a yellow glow effect, and [the UI](user-interface.md#cap) lights the\nleftmost indicator in yellow when a shielded unit is selected, as visible\n[here](https://www.youtube.com/watch?v=pwAtMv_eiM4&t=1786).\n\nShields are depleted first when taking hits, before the regular health bar.\nRegenerating shields do not affect the health bar.\n\n### Modifiers\n\nWhen losing the shield ability (like when exiting a shield generator), the\nshield points will gradually deplete. They will gradually replenish when\nregaining the ability.\nThe depletion rate is reportedly set for the full depletion to happen in 40s.\nThis still needs confirmation.\n\nShielded units always spawn with an empty shield.\n\n## Applicable Units\n\n([Source](https://swgb.fandom.com/wiki/Shield))\n\n### Shield generators (always self-shielded when active)\n\n* Shield Generator (when powered)\n* Fambaa Shield Generator (Gungans' mobile shield generator)\n\n### Self-shielded units\n\n* Heavy Destroyer Droid (Trade Federation)\n* Royal Crusader (Royal Naboo)\n  * Requires the shielding technology in vanilla SWGB, which is unavailable in\n    SWGBCC\n  * Shielding automatically granted when upgraded to Elite Royal crusader in\n    SWGBCC\n* All aircraft except transport with the *Shield modifications* technology\n  (Rebel Alliance, Republic, Wookies, Royal Naboo)\n\n### Self-shielded buildings\n\n* Power Core (with *Power Core Shielding* technology)\n* Shield Wall (with necessary upgrade)\n\n## Future research\n\nConfirm depletion and regeneration rates.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/swgb/stealth.md",
    "content": "# Stealth and detection mechanics in SWGB\n\n## Intro\n\nA few units have a \"stealth\" ability.\nThis feature is unique to the SWGB franchise among games that run on the Genie\nEngine. Unfortunately, it is somewhat underutilized in the game.\n\nA lot more units have the \"detection\" ability, and can detect stealthed units\nwithin their line of sight.\n\n## Mechanism\n\nA stealthed unit cannot be seen by the enemy.\n\nA Detector enemy can reveal stealth units within its line of sight.\n\n## User interface\n\nThese capabilities are displayed in the [game UI](user-interface.md#cap) by\nlighting up the corresponding indicator when the unit is selected.\n\nA stealthed unit will lose the stealth indicator when it becomes unconcealed\n([reference video](https://youtu.be/S-SL-G7KMuE?t=2256)).\n\n### Modifiers\n\nA stealthed unit is unconcealed when it either:\n\n* Attacks\n* Enters the line of sight of a detector unit\n\n## Applicable Units\n\n([Source](https://swgb.fandom.com/wiki/Stealth/Detection))\n\n### Stealth\n\n#### Always active\n\n* Frigate (Gungans)\n* Underwater Prefab Shelter (Gungans)\n\n#### Available trough the *Jedi Mind Trick* technology\n\n* Jedi/Sith Master (Rebel Alliance, Royal Naboo and Republic)\n* Jedi Starfighter (Republic)\n\n### Detection\n\n#### Always active\n\n* Scout\n* Bounty Hunter\n* Sentry Post\n* Sensor Buoy\n* Dark Trooper (Galactic Empire)\n* Probot (Galactic Empire)\n* Airspeeder (Rebel Alliance)\n* Royal Crusader (Royal Naboo)\n\n#### Available trough the *Jedi/Sith Perception* technology\n\n* Jedi/Sith Knight\n* Jedi/Sith Master\n\n## Future research\n\nDo detector units unconceal an enemy unit for every player, or only for\nthemselves? How does this affect the indicator?\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/swgb/user-interface.md",
    "content": "# SWGB user interface\n\nThe Star Wars Galactic Battlegrounds user interface is very similar to the one\nof age of empires 2. The main difference is a row of indicators displaying unit\ncapabilities.\n\n## <a name=\"cap\"></a>Capabilities indicator\n\nIn the center of the user interface, above the unit information area, four\nindicators are lined up horizontally. They are turned off when no unit is\nselected, and the corresponding element turns on when a unit has or gains one\nof the listed capabilities.\n\nIn order, from left to right:\n\n* Shield, yellow\n* Energy, red when missing power, green when powered, green and turned off if\n  the unit is unpowered but doesn't need power\n* Stealth, blue\n* Detection, blue/white\n\nFor illustrative purposes, some screenshots follow:\n\n![](ui-gungan-all-off.png \"All indicators off, gungans (low quality\nscreenshot)\")\n\n![](ui-empire-shield-power.png \"Shield and power indicators on, empire\")\n\n![](ui-empire-nopower-detection.png \"Missing power, detection on, empire\")\n\n![](ui-gungan-stealth-detection-lq.png \"Stealth and detection on, gungans\")\n\nThe last screenshot is lower-quality, and the stealth indicator is quite faint.\nCompare it to the same screenshot, all indicators off:\n\n![](ui-gungan-all-off-lq.png \"All indicators off, gungans (low quality\nscreenshot)\")\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/switching_villager_tasks.md",
    "content": "# Switching tasks\n\n[Source](https://www.youtube.com/watch?v=FK__AnTL400)\n\n## Losing carried resources\n\nIn the original Age of Kings game, villagers would lose any resources they carried when they switched roles, e.g. from \"farmer\" to \"builder\". This was likely caused by AoE \"replacing\" a villager when they are sent to perform another task. In Age of Conquerors this was slightly changed so that builders could carry resources and would only lose them if they would not switch back to their original role.\n\nAoE 2 : HD Edition further removed some instances where a player would lose resources. Now resources are only lost if the villager gets assigned to a resource that is different from what they are carrying. Food that was gathered in the role of a \"shepard\" will therefore carry over if the new role is \"hunter\", \"gatherer\" or \"farmer\".\n\nLooking at the first entry from **Weird AoE2 Quirks** it can be assumed that the HD Edition probably uses a dirty workaround to prevent the player from losing resources. The food is most likely put into the newly assigned sheep/boar/deer and then immediately \"collected\" again.\n\n## Automatic task-switching for builders\n\nWhen builders finish a gathering site, they will automatically switch roles depending on the type of resources that the gathering site accepts and will start working on the nearest resource pile. For example, builders constructing a lumberyard will become lumberjacks and start to chop down nearby trees. If a gathering site accepts multiple resources, e.g. town centers, villagers will be assigned to the resource they are closest to and switch to a corresponding role.\n\n### Automatic Drop-off after building a gathering site\n\nBuilders will automatically drop off **any** resource they are carrying to the player's stockpile after they constructed a gathering site. The type of gathering site does not matter. For example, builders will drop off all food they are holding at a mining camp as soon as they finish the building. The same happens when they build a farm.\n\nNote: This only happens if the builder was actively working on the construction site at the time it was finished. It can be assumed that this is part of the automatic task-switching algorithm.\n\n## Weird AoE2 Quirks\n\n* If the cheat `aegis` (villagers gather instantly from a resource) is used in a game and the role of a villager changes from \"hunter\" to \"shepard\", they will dump all of the resources they are carrying \"into\" the sheep.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/town_bell.md",
    "content": "# Town Bell\n\n[Source](https://www.youtube.com/watch?v=pS4Xy3vVY6U)\n\n## Range\n\nThe town bell has a range of 25 tiles with the measuring point being the middle of a town center. Therefore the effective range is 23 tiles from the edges of the building. Villagers will always move to the *closest* town center or tower/castle they can find.\n\nIf the town bell is rang a second time, all villagers within the 25 tile range will get back to work.\n\n## Garrison limit\n\nVillagers respect the garrison limit of nearby buildings. The town center for example has a garrison limit of 15 units. As a consequence, only the 15 villagers that are closest to the town center will be affected by the town bell. Everyone else will continue on their tasks. The same principle applies to garrisoning in towers and castles.\n\n## Where will villagers garrison?\n\nIn the following figure the town bell range is represented by the dotted area. `C` is the town center, every `X` is a villager and `G` represents possible garrisons, e.g. towers or castles. The `O` shows a tile that is outside of the bell's range. The left and top row are coordinates for referencing, e.g. `coord(H,D)` is referencing the location of the town center.\n\n```\n__|ABCDEFGHIJKLMNO\n------------------\nA |OOOOOOOOOXOOOOO\nB |OO...G.......OO\nC |OO.......X...OO\nD |XO....XC.....OO\nE |OO...........OO\nF |OO.X........XOO\nG |OOOOOOOOOOOOGOO\n```\nVillagers within the 50x50 area will seek to garrison. The ones at `coord(A,D)` and `coord(J,A)` are outside of the range and will therefore not be affected.\n\nSince villagers will choose the nearest garrison they can find, we would expect the unit at `coord(M,F)` to move to the building at `coord(M,G)`. However, this doesn't happen. Because `coord(M,G)` is not in range of the town bell, villagers will not choose it as a destination and instead move to the town center. This makes sense because the tower would not be notified to ungarrison the villagers when the bell rings for the second time.\n\nNow let's pretend the the town center is already full, so that `coord(M,F)` can't garrison there either. Will the villager move to the tower at `coord(F,B)` if it still has not reached its garrison limit? No. The tower has to be within 25 tiles *from the villager's position*.\n\nVillagers will therefore choose a garrison that is\n\n* they are in the range of the town bell\n* the garrison is within the 25 tile *range of the townbell*\n* the garrison is within a 25 tile *range of the villager*\n* the chosen garrison has not reached its garrison limit\n\nBuildings will prefer villagers that are closest to them for garrisoning.\n\n## Possible implementation\n\nAs soon as the town bell is triggered, the algorithm searches for villagers that are inside the 50x50 tile range of the specific town center. It would probably be more efficient to search through the list of villagers belonging to the player and matching their coordinates than scanning all 2500 tiles in range of the bell. All contemplable villagers are then put into a list. In the same way, the algorithm searches for towers, castles and town centers (and other possible garrisons) in the area and puts them into a second list. This can be done in `O(n+m)` time (n is the number of villagers, m the number of garrisons).\n\nIn the next step, the algorithm tries to find the nearest garrison for a villager by calculating the distances between the villager and all buildings on the second list. The closest distance calculated and the corresponding garrison are then saved. Afterwards the villager is put into a third list, which is sorted by the *closest distance to any garrison*. This step is repeated for every villager.\n\nLast but not least, the villagers are assigned to a garrison. Since the third list is sorted by closest distance, villagers that are the nearest to a building are guaranteed to get a place, which satisfies the condition that towers prefer the villagers close to them. If the villager is assigned to a building which is already full, the algorithm recalculates the distances to other garrisons and sorts them back into the list.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/waypoints.md",
    "content": "# Waypoints\n\n[Source](https://www.youtube.com/watch?v=vaBoq9OHdWM)\n\n## Mechanics\n\nWaypoints are placed if the player holds down the `SHIFT`-Key while right-clicking. A flag appears at the position where the waypoint is set. If the player continues to use `SHIFT + Right-Mouse-Button`, more waypoints will be added to a queue. A final press on `Right-Mouse-Button` without `SHIFT` initiates movement. Units will now follow the waypoints from the queue.\n\nThis works for normal movement as well as for patrolling and \"Attack Move\", with the expected behavior from these commands. It also works when right-clicking on the minimap.\n\n## Limitation\n\nAlthough the flags on the ground start to disappear after 10 waypoints, there is no limit to how much waypoints can be added to the queue. This only applies to normal movement though. \"Patrol\" and \"Attack\"-Mode have a queue limit of 10 waypoints. If these modes are selected, the 10th `SHIFT + Right-Mouse-Button` automatically initiates movement.\n\nWaypoints are not as precise as normal movement commands because they only use the coordinates of the tile they are placed on as a reference point, instead of a floating point value.\n\n## Weird AoE2 Quirks\n\n* Normal movement waypoints cannot be canceled, even if the unit is deselected.\n* Buildings seem to support waypoints too because multiple flags can be placed down while holding `SHIFT`. However, the waypoints are not used by units produced in the building. Instead the last waypoint placed down becomes the rally point for the building.\n* The final waypoint cannot be on top of the selected unit.\n"
  },
  {
    "path": "doc/reverse_engineering/game_mechanics/wolves.md",
    "content": "# Wolves\n\nLine of sight and balancing of wolves.\n\n## Line of Sight\n\n[Source](https://www.youtube.com/watch?v=DBgK4e5w_uc)\n\nLine of sight depends on the selected difficulty of the game.\n\n    Easiest   4  tiles\n    Standard  6  tiles\n    Moderate  12 tiles\n    Hard      12 tiles\n    Hardest   12 tiles\n\nAs soon as a unit moves into the LOS of a wolf, they will chase and attack the unit. However some unit types are ignored, including:\n\n* King\n* Trade Cart\n* War Elephant\n* Teutonic Knight\n* War Waggon\n* Monk\n* All siege units\n* Scout (but light cavalry and hussars will be attacked)\n* Sheep (as comical as it sounds)\n\nThese units have to attack the wolf first, before they are chased and attacked.\n\n## Balancing\n\n[Source](https://www.youtube.com/watch?v=jgRyH7Ff5Wg)\n\nUnder normal circumstances a villager without \"Loom\" will always lose to a wolf. Even if the villager is the first to hit, the wolf should have at least 1 HP left.\n"
  },
  {
    "path": "doc/reverse_engineering/networking/01-general.md",
    "content": "# General Structure\n\nThis document defines the general structure of the protocol.\n\n## Test Setup\n\nThe test environment consists of 4 hosts to simulate a 4-player multiplayer game. Each host was equipped with:\n\n* Ubuntu 17.04\n* Age of Empires 2: The Conquerors 1.0c\n* Wine 1.8.7\n* Wireshark 2.2.6\n\nOne game was established by using the DirectPlay feature, while all other play sessions were established over LAN. There is no detectable difference for the communication protocol and DirectPlay is only used for the purpose of connecting players.\n\n## Transport Protocols\n\nThe game utilizes TCP to let players connect to a play session and UDP for all game configuration and ingame communications. This is not surprising, since UDP has the advantage of lightweight packets and therefore fast communication. This document will focus on the interesting part of the packets, i.e. the UDP data field.\n\n## Packet Types\n\nThere are 3 basic types of packets; sync packets, chat message packets and (player) action packets. Sync packets are used to synchronize communication turns, determine turn timers, calculate latency and validate the game state. Chat message packets transport everything the players type in the ingame or lobby chat over the network. The last type discussed in this documentation are the action packets which are utilized for commands from a player or an AI (e.g. movement, unit training).\n\nSync packets can be further categorized into periodic (sent in regular intervals) and non-periodic packets. Most of the network communication during a multiplayer game consists of synchronization data.\n\nPackets are recognized by a one byte long \"command byte\" in the header. So far 12 different network commands have been identified.\n\n| Command | Purpose          |\n| ------- | ---------------- |\n| 0x31    | Sync             |\n| 0x32    | Sync             |\n| 0x35    | Sync (Lobby)     |\n| 0x3e    | Player-issued    |\n| 0x41    | Sync             |\n| 0x43    | Chat Message     |\n| 0x44    | Sync             |\n| 0x4d    | Sync             |\n| 0x51    | De-Sync          |\n| 0x52    | Readying (Lobby) |\n| 0x53    | Sync             |\n| 0x5a    | Lobby            |\n\nAll packets with command `0x3e` have a second \"command byte\" after the header that represents the command a player has given ingame. To avoid confusion, we will call all player-issued commands \"actions\" and reserve the term \"commands\" for the actual network commands seen above. Furthermore, the identifier for a player's action will be called \"action byte\". 34 of these can be found in network packets.\n\n| Action | Purpose                                                                 |\n| ------ | ----------------------------------------------------------------------- |\n| 0x00   | Primary Action (Attacking, Resource gathering, Boarding Transport Ship) |\n| 0x01   | Stop                                                                    |\n| 0x02   | Primary Action (AI)                                                     |\n| 0x03   | Move                                                                    |\n| 0x0a   | Move (AI)                                                               |\n| 0x0b   | Resign                                                                  |\n| 0x10   | Set waypoint                                                            |\n| 0x12   | Stance                                                                  |\n| 0x13   | Guard                                                                   |\n| 0x14   | Follow                                                                  |\n| 0x15   | Patrol                                                                  |\n| 0x17   | Formation                                                               |\n| 0x1b   | Save & Exit                                                             |\n| 0x1f   | Coordinated Move (AI)                                                   |\n| 0x64   | Train unit (AI)                                                         |\n| 0x65   | Research                                                                |\n| 0x66   | Build                                                                   |\n| 0x67   | Diplomacy/Cheats/Change Speed                                           |\n| 0x69   | Build wall                                                              |\n| 0x6a   | Delete                                                                  |\n| 0x6b   | Attack ground                                                           |\n| 0x6c   | Tribute                                                                 |\n| 0x6e   | Repair                                                                  |\n| 0x6f   | Unboard/Ungarrison                                                      |\n| 0x72   | Toggle gate                                                             |\n| 0x73   | Flare                                                                   |\n| 0x75   | Garrison/Stop building unit                                             |\n| 0x77   | Train unit (Human)                                                      |\n| 0x78   | Rally point                                                             |\n| 0x7a   | Sell                                                                    |\n| 0x7b   | Buy                                                                     |\n| 0x7e   | Drop relic                                                              |\n| 0x7f   | Toggle townbell                                                         |\n| 0x80   | Back to work                                                            |\n\nWhen the game is recorded, the UDP data stream of a `0x3e` packet (without the header) is written straight into the .mgx files with few changes. Viewing the recording will therefore simulate the exact actions that were done by the players. For more information on this, check the **Further Reading** section below.\n\nMuch of the actions where already figured out by Stefan Kolb as part of his [.mgx Specification](https://github.com/stefan-kolb/aoc-mgx-format). This analysis will use his document style ([Example](https://github.com/stefan-kolb/aoc-mgx-format/blob/master/spec/body/actions/03-move.md)) as a template.\n\n## Values and Data Types\n\nValues in the network protocol can have a length of one, two or four byte. Little endian notation is used. Therefore, values with a length of two and four bytes have to be read starting with the rightmost byte.\n\nThe data is described with few data types, which are shown in the table below.\n\n| Length | Data Types            |\n| ------ | --------------------- |\n| 1 byte | int8, (ASCII) char    |\n| 2 byte | int16                 |\n| 4 byte | int32, float          |\n| other  | (1-dimensional) array |\n\nMost of the fields present in the network protocol have a fixed length. The use cases for variable length fields are usually lists of `unit_id`s or waypoints and will be handled as arrays in this documentation.\n\n## ID System\n\nAge of Empires II uses an ID system to reference every object in the game by a unique numerical identifier with a length of 4 bytes. IDs are assigned by using a simple counter, that assigns every new object a the next unassigned number. New objects are not necessarily created by players. For example, cutting down a tree replaces a standing with a fallen tree, whereby the latter is handled as a new object. As a rule of thumb, one can assume that objects have been replaced with new ones when their sprite has changed.\n\nThe players are referenced with not less than three IDs; their *network_source_id*/*network_dest_id* (4 bytes), the *player_id* and the *player_number* (both 1 byte). *network_ids* are used in the header to determine from which person a packet comes from or is sent to. The reason why *player_id* and *player_number* are handled differently is due to an undocumented cooperative multiplayer mode. In this mode, which can be activated by assigning to players the same *player_number*, two or more players share control of units, buildings and resources. In consequence, *player_id* is unique for every player, *player_number* not necessarily.\n\n## Coordinate System\n\nIn this document, we will assume that AoC uses a carthesian coordinate system with the left corner as an origin point. Keep in mind, that AoC uses a **Quadrant 4** representation for its coordinates, which means that the y-axis is represented by the edge in the bottom left and the x-axis by the top left edge.\n\n## Further Reading\n\nTo get a better understanding of the networking design and the underlying principles, it might be beneficial to read these sources.\n\n[1] Dave Pottering, *1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond*, 2001\nhttps://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond\n\n[2] Matthew Pritchard, *How to Hurt the Hackers: The Scoop on Internet Cheating and How You Can Combat It*, 2000\nhttps://www.gamedeveloper.com/design/how-to-hurt-the-hackers-the-scoop-on-internet-cheating-and-how-you-can-combat-it\n\n[3] Stefan Kolb, *Age of Empires 2: The Conquerors — Savegame File Format Specification*,\nhttps://github.com/stefan-kolb/aoc-mgx-format\n\n[4] Renée Kooi, *RecAnalyst*, https://github.com/goto-bus-stop/recanalyst\n"
  },
  {
    "path": "doc/reverse_engineering/networking/02-header.md",
    "content": "# Protocol Header\n\nThe header has a fixed length of 20 bytes and is present in almost all of the packets. Exceptions are the sync packets with 16 byte length, who have a smaller header.\n\n## Definition\n\n```ruby\ndef Header\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int32 :communication_turn\n  int32 :individual_counter\nend\n```\n\n## Description\n\n*:network_source_id*<br/>\nThe *:network_id* of the person who sent the packet. A *:network_id* is different for every game, but is not generated randomly for all players. When joining the lobby, every player gets assigned `last_network_id - 2` as their own *:network_id* where *last_network_id* is the ID of the person who joined before them.\n\n*:network_dest_id*<br/>\nThe *:network_id* of the person who should receive the packet. Is only used for sync packets and remains unused for most commands.\n\n*:command*<br/>\nThe command which tells the network engine how the data bytes should be interpreted.\n\n*:option{1-3}*<br/>\nThese are options with unknown effects.\n\n*:communication_turn*<br/>\nThe communication turn where the command is executed. Packets that contain a communication turn that has already passed are discarded.\n\n*:individual_counter*<br/>\nThe individual counter of the player who receives the packet. It increments by 1 with every 16BC41 sync command. For individual players, this counter starts with values that are 1200 units (two players) or 2000 units (three players, `0x7d0`) apart from each other. In the latter example player 1 starts with the value `0x7d0`, player 2 with `0xfa0` and player 3 with `0x1770`. If the differences between these values is not a multiple of 2000, this indicates a de-sync.\n\n## Examples\n\n`82 b2 45 00 00 00 00 00 53 02 8f 0b 30 00 00 00 01 08 00 00`\n\n>`82 b2 45 00` &mdash; network_source_id<br/>\n>`00 00 00 00` &mdash; network_dest_id<br/>\n>`53` &mdash; command<br/>\n>`02` &mdash; option1<br/>\n>`8f` &mdash; option2<br/>\n>`0b` &mdash; option3<br/>\n>`30 00 00 00` &mdash; communication_turn<br/>\n>`01 08 00 00` &mdash; individual_counter\n"
  },
  {
    "path": "doc/reverse_engineering/networking/03-sync.md",
    "content": "# Sync\n\nEverything about the packets that synchronize the game states. In this document, packets are distinguished by their length and command byte. For example a 16BC41 describes the packet with a length of 16 bytes and a command byte with value `0x41` in the header.\n\nWhen the game is paused, only 16BC31 and 16BC32 packets are sent.\n\n## Periodic\n\n### 16BC41\n\nA simple incremental counter that will increments every time a 16BC41 packet is sent. Packets are transmitted in intervals of 120ms.\n\n```ruby\ndef 16BC41\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int32 :individual_counter\nend\n```\n\n*:network_source_id*<br/>\nWorks the same way as outlined in [02-header.md](02-header.md).\n\n*:network_dest_id*<br/>\nThe destination of the packet. 16BC41 is one of the few packet types, where this value is not set to zero.\n\n*:command*<br/>\nAlways has the value `0x41`.\n\n*:option{1-3}*<br/>\nThese are options with unknown effects. They work the same way as outlined in [02-header.md](02-header.md).\n\n*:individual_counter*<br/>\nThe individual counter of the player who sent the packet. It increments by 1 with every 16BC41 sync command. For individual players, this counter starts with values that are 1200 units (two players) or 2000 units (three players, `0x7d0`) apart from each other. In the latter example player 1 starts with the value `0x7d0`, player 2 with `0xfa0` and player 3 with `0x1770`. If the differences between these values is not a multiple of 2000, this indicates a de-sync.\n\n### 16BC31\n\nMakes sure that the game time of players stays synced. It always receives an answer in form of 16BC32. Packets are transmitted in intervals of 8s.\n\n```ruby\ndef 16BC31\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int32 :time_passed\nend\n```\n\n*:network_source_id*<br/>\nWorks the same way as outlined in [02-header.md](02-header.md).\n\n*:network_dest_id*<br/>\nThe destination of the packet. 16BC31 is one of the few packet types, where this value is not set to zero.\n\n*:command*<br/>\nAlways has the value `0x31`.\n\n*:option{1-3}*<br/>\nAll 3 options are always zero for this command.\n\n*:time_passed*<br/>\nTime passed in milliseconds since the game started for the player. The difference between player 1 and player 2 was roughly 3,220 ms during tests. Note that for 16BC31 packets present in the lobby, this value represents milliseconds **since the application** started instead.\n\n### 16BC32\n\nThe answer for a 16BC31 packet. Packets are transmitted in intervals of 8s.\n\n```ruby\ndef 16BC32\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int32 :time_passed\nend\n```\n\n*:network_source_id*<br/>\nWorks the same way as outlined in [02-header.md](02-header.md).\n\n*:network_dest_id*<br/>\nThe destination of the packet. 16BC32 is one of the few packet types, where this value is not set to zero.\n\n*:command*<br/>\nAlways has the value `0x32`.\n\n*:option{1-3}*<br/>\nThey work the same way as outlined in [02-header.md](02-header.md).\n\n*:time_passed*<br/>\nHas the exact same value as the 16BC31 packet.\n\n### 24BC35\n\nSyncing the connection between players in lobby. Packets are transmitted in intervals of 2s.\n\n```ruby\ndef 24BC35\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int32 :connecting1\n  int32 :unknown\n  int32 :connecting2\nend\n```\n\n*:network_source_id*<br/>\nWorks the same way as outlined in [02-header.md](02-header.md).\n\n*:network_dest_id*<br/>\nThis value is always zero.\n\n*:command*<br/>\nAlways has the value `0x35`.\n\n*:option1*<br/>\n`0x35` for both host and regular players when initiating the connection. Otherwise it has the value `0x30`.\n\n*:option2*<br/>\n`0x32` for the host and `0xf8` for regular players when initiating the connection. Otherwise it has the value `0x5d`.\n\n*:option3*<br/>\n`0x00` for the host and `0x0a` for regular players when initiating the connection. Otherwise it has the value `0x00`.\n\n*:connecting1*<br/>\nThis field is only used to initiate a connection to a lobby. It always has the value `0x32d2a4` for the host and `0x503a87` for regular players when initiating a connection. Once the connecting player has joined the lobby, the value remains zero for all following packets.\n\n*:unknown*<br/>\nField with unknown purpose. The value in here is unique for every player and sent with every packet.\n\n*:connecting2*<br/>\nThis field is only used to initiate a connection to a lobby. It always has the value `0x5016b5` for the host and `0x32b5c4` for regular players when initiating a connection. Once the connecting player has joined the lobby, the value remains `0xFFFFFFFF` for all following packets.\n\n### 26BC53\n\nOnly sent by the host from the lobby and not by other players. Sent in intervals of 3s.\n\n```ruby\ndef 26BC53\n  byte20 :header\n  int16 :unknown1\n  int16 :unknown2\n  int16 :communication_turn\nend\n```\n\n*:header*<br/>\nThe standard header. Works the same way as outlined in [02-header.md](02-header.md).\n\n*:unknown1*<br/>\nA field with unknown purpose. Changes between values of `0x04` and `0x08`.\n\n*:unknown2*<br/>\nAnother field with unknown purpose. Possibly ping.\n\n*:communication_turn*<br/>\nThe current communication turn as a 16 bit value.\n\n### 32BC44\n\nSyncs up the communication turns in the game. Packets are transmitted in intervals of 120ms.\n\n```ruby\ndef 32BC44\n  byte20 :header\n  int8 :command2\n  int8 :unknown1\n  int8 :unknown2\n  int8 :unknown3\n  int32 :communication_turn_offset\n  int8 :ping1\n  int8 :ping2\n  int8 :unknown4\n  int8 :unknown5\nend\n```\n\n*:header*<br/>\nThe standard header. Works the same way as outlined in [02-header.md](02-header.md).\n\n*:command2*<br/>\nAlways has the value of the command byte from the header `0x44`. It is unknown whether this is supposed to be the same as *:command* or if it has other use cases.\n\n*:unknown1*<br/>\nPossibly used to communicate the connection status. The value is different for every player, but consistent for most packets throughout the game. An exception are packets sent at the start of a game. The first 32BC44 packet sent by the lobby host always has the value `0x96` in this field. All other players will have the value `0xce` in their first packet.\n\nSometimes the value of the fields *:unknown{1-3}* and *:unknown5* will be `0x00`, but no reason for this behavior could be found.\n\n*:unknown2*<br/>\nLike *:unknown1* it is probably used to communicate the connection status. Values for the very first 32BC44 packet are `0x98` for the lobby host and `0x32` for all other players.\n\nSometimes the value of the fields *:unknown{1-3}* and *:unknown5* will be `0x00`.\n\n*:unknown3*<br/>\nHas the value `0xf7` most of the time.\n\nSometimes the value of the fields *:unknown{1-3}* and *:unknown5* will be `0x00`.\n\n*:communication_turn_offset*<br/>\nIf the game desyncs, this field indicates the last communication turn where the game was in sync for the player sending this packet. While the game is synced, the value should match up with *:communication_turn* in the header.\n\n*:ping1*<br/>\nThe difference between the ping from the current turn and the ping from the previous turn.\n\n*:ping2*<br/>\nAverage time to receive an answer from the player the packet was sent to. Uses the ping times for the last 10-15 32BC44 packets for calculation.\n\n*:unknown4*<br/>\nByte with unknown purpose. It will have the value `0x4c` for the player who was the lobby host and a different value for all other players (one player: `0x48`; two players: both `0x47`). When *:unknown{1-3}* and *:unknown5* have the value `0x00`, the value for *:unknown4* will always be `0x32`.\n\n*:unknown5*<br/>\nHas the value `0xf7` most of the time.\n\nSometimes the value of the fields *:unknown{1-3}* and *:unknown5* will be `0x00`.\n\n### 56BC4D\n\nSyncs up the communication turns in the game. Seems to check if some values are the same for every player. In every interval, the values of 56BC4D stay the same for all players (except for the last 4 bytes). Packets are transmitted in intervals of 8s - 16s.\n\n```ruby\ndef 56BC4D\n  byte20 :header\n  int32 :unknown1\n  int32 :communication_turn_check\n  int32 :unknown2\n  int32 :unknown3\n  int32 :unknown3\n  int32 :unknown5\n  int32 :unknown6\n  int32 :unknown7\n  int32 :unknown8\nend\n```\n\n*:header*<br/>\nThe standard header. Works the same way as outlined in [02-header.md](02-header.md).\n\n*:communication_turn_check*<br/>\nThe communication turn which is validated. Always has the value (*:communication_turn* - 2).\n\n*:unknown{1-7}*<br/>\nThese are the same for all players during a given interval. Some of them seem to be counters, while others change values with an unknown pattern.\n\n*:unknown8*<br/>\nDifferent for every interval and every player.\n\n## Non-Periodic\n\n### 24BC51\n\nUtilized after dropping a player due to connection loss or de-sync.\n\n```ruby\ndef 24BC51\n  byte20 :header\n  int32 :unknown3\nend\n```\n\n*:header*<br/>\nThe standard header. Works the same way as outlined in [02-header.md](02-header.md).\n\n*:last_synced_communication_turn*<br/>\nThe number of the communication turn that the remaining players will continue the game from.\n\n### 24BC52\n\nUsed for readying in the lobby.\n\n```ruby\ndef 24BC52\n  int32 :network_source_id\n  int32 :network_dest_id\n  int8 :command\n  int8 :option1\n  int8 :option2\n  int8 :option3\n  int8 :unknown\n  int8 :player_id\n  int8 :unknown2\n  int32 :zero\n  int32 :unknown3\nend\n```\n\n*:network_source_id*<br/>\nWorks the same way as outlined in [02-header.md](02-header.md).\n\n*:network_dest_id*<br/>\nThis value is always zero.\n\n*:command*<br/>\nAlways has the value `0x52`.\n\n*:option1*<br/>\nIs `0x01` when readying and `0x00` when unreadying.\n\n*:option2*<br/>\nIs `0x1e` when readying and `0x00` when unreadying.\n\n*:option3*<br/>\nIs `0x00` when readying or unreadying.\n\n*:unknown*<br/>\nField with unknown purpose. Always has value `0x01`.\n\n*:player_id*<br/>\nThe ID of the player who is readying. Is `0x00` when player is unreadying.\n\n*:unknown2*<br/>\nField with unknown purpose. Always has value `0x01` when player is readying. Is `0x00` when player is unreadying.\n\n*:zero*<br/>\nThese 4 bytes are always zero.\n\n*:unknown3*<br/>\nField with unknown purpose. Always has value `0x04` when player is readying. Is `0x00` when player is unreadying.\n"
  },
  {
    "path": "doc/reverse_engineering/networking/04-lobby.md",
    "content": "# Lobby\n\nThe lobby uses a 196 byte data field to communicate game settings between the players. Lobbies always require a host to allow new players to connect. Only the hosting player can change the settings.\n\nPackets are transmitted in intervals of 4s.\n\n## Values and Length\n\n```ruby\ndef LobbySettings\n  byte20 :header\n  4 bytes\n  array :player_network_ids,\n        type => :int32,\n        initial_length => 8\n  int8 :ready\n  69 bytes\n  int8 :checkboxes\n  byte\n        6bit :reveal_map\n        2bit :game_speed\n  1 byte\n  byte\n        4bit :starting_age\n        4bit :starting_resources\n  byte\n        4bit :map_size\n        4bit :difficulty\n  int8 :map_id\n  int8 :victory\n  1 byte\n  int16 :victory_limit\n  int8 :max_population\n  8 bytes\n  array :player_civ_ids,\n        type => :int8,\n        initial_length => 8\n  16 bytes\n  array :teams,\n        type => :int8,\n        initial_length => 8\n  int16 :zero\n  int8 :map_description_length\n  array :map_description,\n        type => :char,\n        initial_length => :map_description_length + 6\nend\n```\n\n*:player_network_ids*<br/>\nThe network IDs of the players. If a slot is empty, the corresponding value in the array is `0x00`.\n\n*:ready*<br/>\nThis byte indicates readied slots. Every bit (read from right to left) is associated with a slot in the lobby. If the bit is set to one, the slot is closed. Slots are also considered ready when they are filled with an AI or are closed.\n\nFor example, the value `0x0c` translates to a binary notation of `00001100b`. Therefore, the third and forth slot are closed.\n\n*:checkboxes*<br/>\nA byte which is used to determine which of the checkbox options in the lower right are checked. Every bit is associated with checkbox. A bit is set to 1, when the checkbox is **not** set to its standard setting.\n\nCheckbox      | On|Off | Bit representation\n--------------|--------|-------------------\nTeam together |  0|1   | `01000000b`\nLock teams    |  1|0   | `00001000b`\nAll tech      |  1|0   | `10000000b`\nLock speed    |  1|0   | `00000100b`\nAllow cheats  |  1|0   | `00000001b`\n\n*:reveal_map*<br/>\nDetermines whether the fog of war is present. Uses the 5th and 6th bit of the byte.\n\nMode     | Value\n---------|------\nNormal   | 0x00\nExplored | `00001000b`\nVisible  | `00000100b`\n\n*:game_speed*<br/>\nSets the default game speed. Uses the 7th and 8th bit of the byte.\n\nSpeed  | Value\n-------|------\nSlow   | `00000001b`\nNormal | `00000010b`\nFast   | `00000011b`\n\n*:starting_age*<br/>\nSets the starting age.\n\nAge           | Value\n--------------|------\nStandard      | 0x0\nDark          | 0x2\nFeudal        | 0x3\nCastle        | 0x4\nImperial      | 0x5\nPost-Imperial | 0x6\n\n*:starting_resources*<br/>\nSets the amount of starting resources.\n\nResources| Value\n---------|------\nStandard | 0x0\nLow      | 0x1\nMedium   | 0x2\nHigh     | 0x3\n\n*:map_size*<br/>\nSets the map size.\n\nSize       | Value\n-----------|------\nVery Small | 0x0\nSmall      | 0x1\nMedium     | 0x2\nNormal     | 0x3\nLarge      | 0x4\nGiant      | 0x5\n\n*:difficulty*<br/>\nSets the difficulty of the AI.\n\nDifficulty | Value\n-----------|------\nVery Easy  | 0x4\nStandard   | 0x3\nMedium     | 0x2\nHard       | 0x1\nHardest    | 0x0\n\n*:map_id*<br/>\nThe ID of map that will be generated.\n\n*:victory*<br/>\nSets the victory condition.\n\nVictory           | Value\n------------------|------\nStandard          | 0x00\nConquest          | 0x01\nTime Limit        | 0x07\nScore             | 0x08\nLast Man Standing | 0x0b\n\n*:victory_limit*<br/>\nAmount of points to win in score mode or timer for the time limit game mode.\n\n*:max_population*<br/>\nThis field is used for setting the maximum population for the game.\n\n*:player_civ_ids*<br/>\nThe ID of the civilisations the players have chosen. Defaults to Random (`0x1e`).\n\n*:teams*<br/>\nThe teams of each individual player. For whatever reason, the value for no team is `0x01` which leaves the teams 1, 2, 3 and 4 with these values:\n\nTeam              | Value\n------------------|------\nNone              | 0x01\n1                 | 0x02\n2                 | 0x03\n3                 | 0x04\n4                 | 0x05\nRandom            | 0x06\n\n*:zero*<br/>\nThese 2 bytes are always zero.\n\n*:map_description_length*<br/>\nThe length of the name a custom maps. Only present when custom maps are selected.\n\n*:map_description*<br/>\nThe name of the custom map encoded in extended ASCII. It starts with 6 bytes that are used for formatting and not displayed by the game.\n"
  },
  {
    "path": "doc/reverse_engineering/networking/05-chat_protocol.md",
    "content": "# Chat messages\n\nChat messages can deliver an extended ASCII string to other players (Korean version?). The maximum length of a message can differ from 65 characters (in-game chat) to 247 characters (lobby chat). The data fields used for a chat message contain a lot of information that is not interpreted by the game, including the fields for message length and player number. Like all other packets, chat messages are sent to every player taking part in the game.\n\nIt should be noted that unlike all other packets, chat messages are not validated for errors or malicious behavior. This makes AoC vulnerable to [message spoofing](05-chat_protocol.md).\n\n## Definition\n\n```ruby\ndef ChatMessage\n  byte20 :header\n  int8 :player_id\n  int8 :send_button\n  array :intended_receivers,\n        type => :char,\n        initial_length => 8\n  int8 :separator\n  int8 :zero\n  int8 :message_length\n  int32 :zero2\n  array :message_string,\n        type => :char,\n        initial_length => :message_length + 1\n  int32 :unaligned_memory\nend\n```\n\n### Description\n\n*:header*<br/>\nThe default header of AoC. The command byte is always `0x43`.\n\n*:player_id*<br/>\nThe ID of the player who sends the message.\n\n*:send_button*<br/>\nDetermines whether the messages was send by pressing `Enter` (`0x00`) or the \"Send\" button (`0xd9`) in the Chat Menu.\n\n*:intended_receivers*<br/>\nThe intended receivers of the message. If the ASCII character at position X is \"Y\" (`0x59`), the message will be displayed to Player X. Vice versa, if the character at position X reads \"N\" (`0x4e`), Player X will not be able to see the message.\n\n*:separator*<br/>\nA separator value that is always `0x32`.\n\n*:zero*<br/>\nThis byte is always zero.\n\n*:message_length*<br/>\nThe length of the message in bytes.\n\n*:zero2*<br/>\nThese 4 bytes are always zero.\n\n*:message_string*<br/>\nThe raw C-String of the message. Always contains a `0x00` byte at the end to signal the end of the String.\n\n*:misaligned_memory*<br/>\n4 bytes (one word) of misaligned memory. It is possible that the Genie Engine operates directly on cached memory for optimization, which results in this behavior. The 4 bytes contain a part of an altered version of message, presumably after it was checked for cheat codes. Which part is read depends on the message length, starting with the byte after position 12. For example, at message length 0, the altered message bytes 13-16 are read. A message of length 9 will append the altered message bytes 22-25.\n\n# Example\n\n```\n0000 02 00 4e 59 59 4e 4e 4e 4e 4e 32 00 09 00 00 00\n0010 00 61 62 63 64 65 66 67 68 69 00 00 18 dc 32\n```\n\n>`02` &mdash; player_id<br/>\n>`00` &mdash; send_button<br/>\n>`4e 59 59 4e 4e 4e 4e 4e` &mdash; intended_receivers<br/>\n>`32` &mdash; separator<br/>\n>`00` &mdash; zero<br/>\n>`09` &mdash; message_length<br/>\n>`00 00 00 00` &mdash; zero<br/>\n>`61 62 63 64 65 66 67 68 69 00` &mdash; message_string<br/>\n>`00 18 dc 32` &mdash; misaligned_memory\n\n# Misaligned memory of the above message\n\n```\n0000 00 00 00 00 00 01 00 00 00 18 dc 32 00 00 00 00\n0010 00 00 00 00 00 01 00 00 00 XX 00 00 00 43 00 00\n0020 00 XX 00 XX 00 1c dd 32 00 20 dd 32 00 b0 00 00\n0030 00 6e 00 0a 00 XX 00 XX 00 00 58 c5 0a 20 dd 32\n0040 00 YY YY YY\n```\n"
  },
  {
    "path": "doc/reverse_engineering/networking/06-chat_message_spoofing.md",
    "content": "# Spoofing and forging AoC chat messages\n\n## Setup\n\nThe test environment consisted of 2 hosts and one router to simulate a 2-player multiplayer game. The hosts were equipped with:\n\n* Ubuntu 17.04\n* Age of Empires 2: The Conquerors 1.0c\n* Wine 1.8.7\n\nThe router was placed as a central point between the two hosts and used for capturing the network traffic. In this example, we also use the router to send the forged UDP packet to one of the host, though in practice, the packet could be spoofed by one of the hosts as well. The setup for our router looks like this:\n\n* Ubuntu 17.04\n* Wireshark 2.2.6\n* Packet Sender 5.3.1\n\n## Preparation\n\nTo construct a message the following parameters are needed:\n\n* Valid Network ID of the Player who is going to be spoofed\n* Current communication turn of the game in progress\n* Player number of the spoofed sender\n* Player number of the receiver(s)\n\nAn attacker that is in game with other players will have no problems getting to know the player numbers. As the game is constantly synced, they can easily get the value for *:communication_turn*.\n\nDeriving the valid Sender ID of the receiving player is more difficult and depends on the attacker's ability to capture network traffic. The easiest way to discover all Player IDs is by capturing a few packets of normal gameplay beforehand. The IDs of Players 1-8 have a fixed byte position in the data stream.\n\n## Forging the Packet\n\nWith the above knowledge and the protocol structure in mind, we are now able to handcraft a message ourselves. We will use the tool Packet Sender for this purpose.\n\nFew values need to be changes, so one can use a sample message packet as a starting point. The Network ID will be replaced with the one of our target. The command byte has to remain at value `0x43`, otherwise the packet will not be recognized as a message. The 3 control bytes after should contain values that fit the message command, but it's not necessary to set them to a specific value. `02 7b 00` and `02 00 00` will work.\n\nThe value of *:individual_counter* does not have to be changed, but the *:communication_turn* must have a greater value than the one of the latest 32 byte sync packet (command byte: `0x44`). If the value of *:communication_turn* is smaller, the message will not display. The value of *:communication_turn* determines when the message is displayed or - more precisely - the message will be displayed as soon as the values of *:communication_turn* in the sync packet and of *:communication_turn* in the message packet are equal.\n\n*:player_id* has to be set to spoofed player's ID. The 8 bytes of *intended_receivers* also must change accordingly. The value at position X should be `0x59` when player X is supposed to see the message.\n\nThe last thing to change are the message bytes. They will have to be converted to extended ASCII and then placed at the correct location within the data stream. It also has to be stressed that the value for *:message_length* does not have to be changed to the exact length of the message. It has no effect when the message is displayed. Forged messages can be longer than the ingame maximum of 65 characters.\n\n## Execution\n\nOnce the data bytes of the packet are ready, one can simply copy them into the HEX field of Packet Sender. We set the target IP address and port number to the receiver's address and port. The AoC standard port is `2350`, but can vary depending on which services run on the receivers computer.\n\nAfter sending the packet, the receiver will see the message on screen when *:communication_turn* of the sync packet reaches the value in the message packet. The player that we masquerade as will not see the message, unless it is specified in *:communication_turn*.\n\n## Example\n\nAs an example we want to send the german original first formulation of the categorical imperative by Immanuel Kant to Player 1. This is not achievable when in game, since the message is 110 characters long (AoC limit: 65 characters). Hence, if Player 1 sees this message, we know that spoofing is possible.\n\n1. Firstly, we create our message with the spoofed parameters:\n\n```\n0000 c0 13 0d 00 00 00 00 00 43 02 7b 00 dc 15 00 00\n0010 01 37 00 00 02 00 59 59 4e 4e 4e 4e 4e 4e 32 00\n0020 03 00 00 00 00 48 61 6e 64 6c 65 20 6e 75 72 20\n0030 6e 61 63 68 20 64 65 72 6a 65 6e 69 67 65 6e 20\n0040 4d 61 78 69 6d 65 2c 20 64 75 72 63 68 20 64 69\n0050 65 20 64 75 20 7a 75 67 6c 65 69 63 68 20 77 6f\n0060 6c 6c 65 6e 20 6b 61 6e 6e 73 74 2c 20 64 61 73\n0070 73 20 73 69 65 20 65 69 6e 20 61 6c 6c 67 65 6d\n0080 65 69 6e 65 73 20 47 65 73 65 74 7a 20 77 65 72\n0090 64 65 2e 00 00 00 00 01\n```\n\n2. Next up, the data bytes have to be pasted into Packet Sender. Target IP will be `192.168.160.2`, the IP address of Player 1. AoC runs on port `2350`.\n\n![Screenshot](images/aoe2-message-spoofing1.png)\n\n3. We will see the packet in Wireshark when it is sent. The value of *:communication_turn* is about 128 ticks behind the one in the latest sync packet. It will therefore take another 16 seconds to be displayed.\n\n![Screenshot](images/aoe2-message-spoofing2.png)\n\n4. A few seconds later and the message indeed has arrived for Player 1 and will be displayed.\n\n![Screenshot](images/aoe2-message-spoofing3.png)\n"
  },
  {
    "path": "doc/reverse_engineering/networking/07-primary_action.md",
    "content": "# Unit Interaction - Primary Action\n\nUsed for every action that is executed by selecting a unit and using `Right-Mouse-Botton` on an object in the game environment. It depends on the context (*:target_id*, *:selected_id*) which specific action is executed. Units with several right-click abilities (monks, villagers) will use their primary action for all of them.\n\nBecause primary actions are contextual, a primary action can have the exact same effect of another action (that was executed by using a button from the HUD or a hotkey). Notable examples are repairing and garrisoning units into rams/transport ships.\n\nAction that classify as a primary action include attacking, gathering, repairing, converting, healing, picking up relics and boarding transport ships.\n\n## Definition\n\n```ruby\ndef PrimaryAction\n    int8 :action_identifier\n    int8 :player_id\n    int16 :zero\n    int32 :target_id\n    int8 :selection_count\n    byte24 :zero2\n    float :x_coord\n    float :y_coord\n    array :selected_ids,\n      type => :int32,\n      length => :selection_count,\n      onlyif => :selection_count < 0xFF\nend\n```\n\n## Description\n\n*:action_identifier*<br/>\nAlways has the value `0x00`.\n\n*:player_id*<br/>\nThe ID of the player who executes the action (`0x01` - `0x08`).\n\n*:zero*<br/>\nThe two bytes following *:player_id* are unused.\n\n*:target_id*<br/>\nThe ID of the targeted object.\n\n*:selection_count*<br/>\nThe number of selected units. When the value of this field is `0xFF`, the action is executed for the units referenced in the previous command.\n\n*:zero2*<br/>\nThe three bytes following *:selection_count* are unused.\n\n*:x_coord*<br/>\nThe x-coordinate of the targeted position.\n\n*:y_coord*<br/>\nThe y-coordinate of the targeted position.\n\n*:selected_ids*<br/>\nThe IDs of the selected units.\n\n## Examples\n\nGathering stone with villager.\n\n`00 02 00 00 92 17 00 00 01 00 00 00 00 80 33 43 00 80 16 43 4d 17 00 00`\n\n>`00` &mdash; action_identifier<br/>\n>`02` &mdash; player_id<br/>\n>`00 00` &mdash; zero<br/>\n>`92 17 00 00` &mdash; target_id<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 80 33 43` &mdash; x_coord<br/>\n>`00 80 16 43` &mdash; y_coord<br/>\n>`4d 17 00 00` &mdash; selected_id<br/>\n\nAttacking a building.\n\n`00 02 00 00 38 1a 00 00 02 00 00 00 00 00 40 42 00 00 0b 43 5e 1a 00 00 62 1a 00 00`\n\n>`00` &mdash; action_identifier<br/>\n>`02` &mdash; player_id<br/>\n>`00 00` &mdash; zero<br/>\n>`38 1a 00 00` &mdash; target_id<br/>\n>`02` &mdash; selection_count<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 40 42` &mdash; x_coord<br/>\n>`00 00 0b 43` &mdash; y_coord<br/>\n>`5e 1a 00 00` &mdash; selected_id<br/>\n>`62 1a 00 00` &mdash; selected_id\n\nBoarding a transport ship with the same units\n\n`00 02 00 00 57 1a 00 00 ff 00 00 00 7a 7c 47 43 52 da 38 43`\n\n>`00` &mdash; action_identifier<br/>\n>`02` &mdash; player_id<br/>\n>`00 00` &mdash; zero<br/>\n>`57 1a 00 00` &mdash; target_id<br/>\n>`ff` &mdash; selection_count<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`7a 7c 47 43` &mdash; x-coord<br/>\n>`52 da 38 43` &mdash; y-coord\n"
  },
  {
    "path": "doc/reverse_engineering/networking/08-movement.md",
    "content": "# Unit Interaction - Movement Actions\n\n## Stop Action\n\nThis action stops the selected units from doing their currently active task (moving, gathering, attacking, etc.). It is also used to stop research in buildings.\n\n### Definition\n\n```ruby\ndef Stop\n  int8 :action_identifier\n  int8 :selection_count\n  array :selected_ids,\n        type => :int32,\n        initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x01`.\n\n*:selection_count*<br/>\nThe number of selected units. Is always `0x01` when stopping research in buildings, since AoC doesn't show the building queue when multiple buildings are selected.\n\n*selected_ids*<br/>\nThe IDs of the selected units. When stopping research, this is the ID of the building where the research is queued.\n\n### Examples\n\nStop a bunch of (moving) units.\n\n`01 03 99 06 00 00 98 06 00 00 97 06 00 00`\n\n>`01` &mdash; action_identifier<br/>\n>`03` &mdash; selection_count<br/>\n>`99 06 00 00` &mdash; selected_id<br/>\n>`98 06 00 00` &mdash; selected_id<br/>\n>`97 06 00 00` &mdash; selected_id<br/>\n\nStop a technology research from the building queue.\n\n`01 01 5b 07 00 00`\n\n>`01` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`5b 07 00 00` &mdash; building_id\n\n## Move Action\n\nUsed for moving units around the map.\n\n### Definition\n\n```ruby\ndef Move\n\tint8 :action_identifier\n\tint8 :player_id\n\tint16 :zero\n\tint32 :const\n\tint32 :selection_count\n\tfloat :x_coord\n\tfloat :y_coord\n\tarray :selected_ids,\n\t\t:length => :selection_count,\n \t\t:only_if => :selection_count < 0xFF\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x03`.\n\n*:player_id*<br/>\nThe ID of the player who moves their units (`0x01` - `0x08`).\n\n*:zero*<br/>\nThe two bytes following the *:player_id* are unused.\n\n*:const*<br/>\nAlways has the value `0xFFFFFFFF`.\n\n*:selection_count*<br/>\nThe number of selected units. When the value of this field is `0xFF`, the action is executed for the units referenced in the previous command.\n\n*:x_coord*<br/>\nThe x-coordinate of the targeted position.\n\n*:y_coord*<br/>\nThe y-coordinate of the targeted position.\n\n*:selected_ids*<br/>\nThe IDs of the selected units.\n\n### Examples\n\nMovement command with *:selected_ids* present.\n\n`03 02 00 00 ff ff ff ff 02 00 00 00 00 40 22 43 ab aa 51 43 62 1a 00 00 5e 1a 00 00`\n\n>`03` &mdash; action_identifier<br/>\n>`02` &mdash; player_id<br/>\n>`00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`02 00 00 00` &mdash; selection_count<br/>\n>`00 40 22 43` &mdash; x_coord<br/>\n>`ab aa 51 43` &mdash; y_coord<br/>\n>`62 1a 00 00` &mdash; selected_id<br/>\n>`5e 1a 00 00` &mdash; selected_id\n\nConsecutive movement command.\n\n`03 02 00 00 ff ff ff ff ff 00 00 00 00 78 27 43 55 cd 39 43`\n\n>`03` &mdash; action_identifier<br/>\n>`02` &mdash; player id<br/>\n>`00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`ff 00 00 00` &mdash; selection_count<br/>\n>`00 78 27 43` &mdash; x_coord<br/>\n>`55 cd 39 43` &mdash; y_coord\n\n## Setting Waypoints\n\nUsed for setting a waypoint.\n\n### Definition\n\n```ruby\ndef Waypoint\n  int8 :action_identifier\n  int8 :player_id\n  int8 :selection_count\n  int8 :x_coord\n  int8 :y_coord\n  array :selected_ids,\n        type :int32,\n        only_if => :selection_count < 0xFF\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x10`.\n\n*:player_id*<br/>\nThe ID of the player who the unit belongs to (`0x01` - `0x08`).\n\n*:selection_count*<br/>\nThe number of selected units. When the value of this field is `0xFF`, the action is executed for the units referenced in the previous command.\n\n*:x_coord*<br/>\nThe x-coordinate on the AoC grid.\n\n*:y_coord*<br/>\nThe y-coordinate on the AoC grid.\n\n*:selected_ids*<br/>\nThe IDs of the selected units. Can also be the ID of a building.\n\n### Examples\n\nSet a waypoint.\n\n`10 01 01 34 16 a0 06 00 00`\n\n>`10` &mdash; action_identifier<br/>\n>`01` &mdash; player_id<br/>\n>`01 `&mdash; selection_count<br/>\n>`34` &mdash; x_coord<br/>\n>`16` &mdash; y_coord<br/>\n>`a0 06 00 00` &mdash; selected_id\n\nSet consecutive waypoints.\n\n`10 01 ff 31 17`\n\n>`10` &mdash; action_identifier<br/>\n>`01` &mdash; player_id<br/>\n>`ff `&mdash; selection_count<br/>\n>`31` &mdash; x_coord<br/>\n>`17` &mdash; y_coord\n\n## Delete Action\n\nUsed for deleting units and buildings.\n\n### Definition\n\n```ruby\ndef Delete\n  int8 :action_identifier\n  byte24 :zero\n  int32 :object_id\n  int8 :player_number\n  byte24 :zero\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x6a`.\n\n*:zero*<br/>\nThe 3 bytes following *:action_identifier* and *player_id* are unused.\n\n*:object_id*<br/>\nID of the unit or building that will be deleted.\n\n*:player_number*<br/>\nThe number of the player who the unit belongs to (`0x01` - `0x08`).\n\n### Examples\n\n`6a 00 00 00 9f 06 00 00 02 00 00 00`\n\n>`6a` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`9f 06 00 00` &mdash; object_id<br/>\n>`02` &mdash; player_number<br/>\n>`00 00 00` &mdash; zero\n\n\n## Attack Ground\n\nUsed for the \"attack ground\" action of mangonels, onagers, trebuchets.\n\n### Definition\n\n```ruby\ndef AttackGround\n\tint8 :action_identifier\n\tint8 :selection_count\n\tint16 :zero\n\tfloat :x_coord\n\tfloat :y_coord\n\tarray :selected_ids,\n\t\t:type => int32,\n\t\t:length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x6b`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:zero*<br/>\nThe 2 bytes following *:selection_count* are unused.\n\n*:x_coord*<br/>\nThe x-coordinate represented as a 32-bit float. Values have to be read backwards, so `ab 0a d3 41` translates to `0x41d30aab` or `26.3802f`.\n\n*:y_coord*<br/>\nThe y-coordinate represented as a 32-bit float. Values have to be read backwards, so `ab 3e 22 43` translates to `0x43223eab` or `162.245f`.\n\n*selected_ids*<br/>\nThe IDs of the selected units.\n\n### Examples\n\n`6b 02 00 00 ab 0a d3 41 ab 3e 22 43 3d 1a 00 00 42 1a 00 00`\n\n>`6b` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`ab 0a d3 41` &mdash; x_coord<br/>\n>`ab 3e 22 43` &mdash; y_coord<br/>\n>`3d 1a 00 00` &mdash; selected_id<br/>\n>`42 1a 00 00` &mdash; selected_id\n\n## Ungarrison Action\n\nUsed for unloading units from transport ships or rams and ungarrisoning units.\n\n### Definition\n\n```ruby\ndef Ungarrison\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  float :x_coord\n  float :y_coord\n  int8 :ungarrison_type\n  byte24 :zero2\n  int32 :release_id\n  array :selected_ids,\n\t\t:type => int32,\n\t\t:length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x6f`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:zero*<br/>\nThe 2 bytes following *:selection_count* are unused.\n\n*:x_coord*<br/>\nThe x-coordinate represented as a 32-bit float. Values have to be read backwards, so `ab 0a d3 41` translates to `0x41d30aab` or `26.3802f`. When ungarrisoning from a building this value will always be `00 00 80 bf` = `-1.0f`.\n\n*:y_coord*<br/>\nThe y-coordinate represented as a 32-bit float. Values have to be read backwards, so `ab 3e 22 43` translates to `0x43223eab` or `162.245f`. When ungarrisoning from a building this value will always be `00 00 80 bf` = `-1.0f`.\n\n*:ungarrison_type*<br/>\nIs used to reflect the use of hotkeys to release only a certain type of units.\n\nHotkey                   | Hex Value | Action\n-------------------------|-----------|--------\nG                        | 0x00      | Release all\nMouse-BTN                | 0x03      | Release unit that was clicked on\nSHIFT + Mouse-BTN        | 0x04      | Release units of the same type\nCTRL + Mouse-BTN         | 0x05      | Release all except unit that was clicked on\nCTRL + SHIFT + Mouse-BTN | 0x06      | Release all not of the same type\n\n*:release_id*<br/>\nThe unit that was clicked on in the garrison queue. Value is `0xFFFFFFFF` if *:ungarrison_type* is `0x00`.\n\n*:selected_ids*<br/>\nThe IDs of the rams, transport ships or buildings the units are released from.\n\n### Examples\n\nUnloading a transport ship.\n\n`6f 01 00 00 00 c0 c7 41 55 4d 3f 43 00 00 00 00 ff ff ff ff 4c 1a 00 00`\n\n>`6f` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`00 c0 c7 41` &mdash; x_coord<br/>\n>`55 4d 3f 43` &mdash; y_coord<br/>\n>`00` &mdash; ungarrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`ff ff ff ff` &mdash; release_id<br/>\n>`4c 1a 00 00` &mdash; selected_id\n\nUngarrisoning all units from a building.\n\n`6f 01 00 00 00 00 80 bf 00 00 80 bf 00 00 00 00 ff ff ff ff 4b 17 00 00`\n\n>`6F` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`00 00 80 bf` &mdash; x_coord<br/>\n>`00 00 80 bf` &mdash; y_coord<br/>\n>`00` &mdash; ungarrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`ff ff ff ff` &mdash; release_id<br/>\n>`4b 17 00 00` &mdash; selected_id<br/>\n\nUngarrisoning with `CTRL + SHIFT`.\n\n`6f 01 00 00 00 00 80 bf 00 00 80 bf 06 00 00 00 5a 1a 00 00 4b 17 00 00`\n\n>`6F` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`00 00 80 bf` &mdash; x_coord<br/>\n>`00 00 80 bf` &mdash; y_coord<br/>\n>`06` &mdash; ungarrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`5a 1a 00 00` &mdash; release_id<br/>\n>`4b 17 00 00` &mdash; selected_id<br/>\n\n## Garrison Action\n\nUsed for garrisoning units into buildings as well as packing and unpacking of trebuchets.\n\n### Definition\n\n```ruby\ndef Garrison\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  int32 :building_id\n  int8 :garrison_type\n  byte24 :zero\n  float :x_coord\n  float :y_coord\n  int32 :const\n  array :selected_ids,\n\t\t:type => int32,\n\t\t:length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x75`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:zero*<br/>\nThe 2 bytes following *:selection_count* are unused.\n\n*:building_id*<br/>\nThe ID of a building, ram or transport ship which the units will be garrisoned in. When packing/unpacking a trebuchet, this value is always `0xFFFFFFFF`.\n\n*:garrison_type*\n\nHex Value | Action\n----------|--------\n0x01      | Pack trebuchet\n0x02      | Unpack trebuchet\n0x05      | Garrison units into building, ram or transport ship\n\n*:zero2*<br/>\nThe 3 bytes following *:garrison_type* are unused.\n\n*:x_coord*<br/>\nThe x-coordinate represented as a 32-bit float. Value is always `00 00 00 00` when *:garrison_type* is `0x01` and `00 00 80 bf` = `-1.0f` when it is `0x02` and `0x05`.\n\n*:y_coord*<br/>\nThe x-coordinate represented as a 32-bit float. Value is always `00 00 00 00` when *:garrison_type* is `0x01` and `00 00 80 bf` = `-1.0f` when it is `0x02` and `0x05`.\n\n*:const*<br/>\nThis value was always `0xFFFFFFFF` in testing.\n\n*:selected_ids*<br/>\nThe IDs of the rams, transport ships or buildings the units are released from.\n\n### Examples\n\nPacking a trebuchet.\n\n`75 01 00 00 ff ff ff ff 01 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff 65 1a 00 00`\n\n>`75` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; building_id<br/>\n>`01` &mdash; garrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 00 00` &mdash; x_coord<br/>\n>`00 00 00 00` &mdash; y_coord<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`65 1a 00 00` &mdash; selected_id\n\nUnpacking a trebuchet.\n\n`75 01 00 00 ff ff ff ff 02 00 00 00 00 00 80 bf 00 00 80 bf ff ff ff ff 65 1a 00 00`\n\n>`75` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; building_id<br/>\n>`02` &mdash; garrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 80 bf` &mdash; x_coord<br/>\n>`00 00 80 bf` &mdash; y_coord<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`65 1a 00 00` &mdash; selected_id\n\nGarrisoning units into a building.\n\n`75 02 00 00 46 17 00 00 05 00 00 00 00 00 80 bf 00 00 80 bf ff ff ff ff 6f 1a 00 00 6d 1a 00 00`\n\n>`75` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`46 17 00 00` &mdash; building_id<br/>\n>`05` &mdash; garrison_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 80 bf` &mdash; x_coord<br/>\n>`00 00 80 bf` &mdash; y_coord<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`6f 1a 00 00` &mdash; selected_id<br/>\n>`6d 1a 00 00` &mdash; selected_id\n\n## Drop Relic\n\nDropping a relic on the ground.\n\n### Definition\n\n```ruby\ndef DropRelic\n  int8 :action_identifier\n  byte24 :zero\n  int32 :monk_id\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x7e`.\n\n*:zero*<br/>\nThe 3 bytes following *:action_identifier* are unused.\n\n*:monk_id*<br/>\nThe ID of the monk who will drop the relic.\n\n### Examples\n\n`7e 00 00 00 17 1b 00 00`\n\n>`7e` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`17 1b 00 00` &mdash; monk_id\n"
  },
  {
    "path": "doc/reverse_engineering/networking/09-formation.md",
    "content": "# Unit Interaction - Formation Actions\n\n## Stance Action\n\nUsed for changing the stance of units.\n\n### Definition\n\n```ruby\ndef Stance\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :stance\n  array :selected_ids,\n    type => :int32,\n    initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x12`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:stance*<br/>\nRepresents which stance the units are going to take. AoC supports 4 stances.\n\nHex Value | Stance\n----------|-------\n0x00      | Aggressive\n0x01      | Defensive\n0x02      | Stand Ground\n0x03      | Passive\n\n*:selected_ids*<br/>\nThe IDs of the selected units.\n\n### Examples\n\n`12 01 02 4b 15 00 00`\n\n>`12` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`02` &mdash; stance<br/>\n>`4b 15 00 00` &mdash; selected_id\n\n## Guard Action\n\nUsed for guarding a unit with others.\n\n### Definition\n\n```ruby\ndef Guard\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  int32 :guarded_id\n  array :selected_ids,\n    type => :int32,\n    initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x13`.\n\n*:selection_count*<br/>\nThe number of selected units that are put on guard.\n\n*:zero*<br/>\nThe two bytes after *:selection_count* are always zero.\n\n*:guarded_id*<br/>\nThe ID of the unit that will be guarded.\n\n*:selected_ids*<br/>\nThe IDs of the guarding units.\n\n### Examples\n\n`13 03 00 00 41 15 00 00 44 18 00 00 4b 15 00 00 42 18 00 00`\n\n>`13` &mdash; action_identifier<br/>\n>`03` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`41 15 00 00` &mdash; guarded_id<br/>\n>`44 18 00 00` &mdash; selected_id<br/>\n>`4b 15 00 00` &mdash; selected_id<br/>\n>`42 18 00 00` &mdash; selected_id\n\n## Follow Action\n\nUsed for following a unit.\n\n### Definition\n\n```ruby\ndef Follow\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  int32 :followed_id\n  array :selection_ids,\n    type => :int32,\n    initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x14`.\n\n*:selection_count*<br/>\nNumber of selected units.\n\n*:zero*<br/>\nThe two bytes after *:selection_count* are always zero.\n\n*:followed_id*<br/>\nThe ID of the unit that will be followed.\n\n*:selection_ids*<br/>\nThe IDs of the units that are following the unit with *:followed_id*.\n\n### Examples\n\n`14 02 00 00 40 18 00 00 44 18 00 00 42 18 00 00`\n\n>`14` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`40 18 00 00` &mdash; followed_id<br/>\n>`44 18 00 00` &mdash; selected_id<br/>\n>`42 18 00 00` &mdash; selected_id\n\n## Patrol Action\n\nUsed for patrolling units.\n\n### Definition\n\n```ruby\ndef Patrol\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :patrol_waypoint_count\n  int8 :zero\n  array :x_coords,\n    type => :float,\n    length => 10\n  array :y_coords,\n    type => :float,\n    length => 10\n  array :selection_ids,\n    type => :int32,\n    initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x15`.\n\n*:selection_count*<br/>\nNumber of selected units.\n\n*:patrol_waypoint_count*<br/>\nNumber of waypoints set (with `SHIFT + Mouse-Button`).\n\n*:zero*<br/>\nThe byte after *:patrol_waypoint_count* is always zero.\n\n*:x_coords*<br/>\nThe x-coordinates of the patrol waypoints. Always has a length of 10 which is also the maximum number of patrol waypoints. For every waypoint that is not set, a placeholder value of `0x00000000` is used.\n\n*:y_coords*<br/>\nThe y-coordinates of the patrol waypoints. Always has a length of 10 which is also the maximum number of patrol waypoints. For every waypoint that is not set, a placeholder value of `0x00000000` is used.\n\n*:selection_ids*<br/>\nThe IDs of the units that are patrolling.\n\n### Examples\n\n```\n0000   15 02 03 00 00 f8 2c 43 55 65 2c 43 00 88 2b 43\n0010   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n0020   00 00 00 00 00 00 00 00 00 00 00 00 55 0d 46 43\n0030   00 70 48 43 00 48 45 43 00 00 00 00 00 00 00 00\n0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n0050   00 00 00 00 43 18 00 00 40 18 00 00\n```\n\n>`15` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`03` &mdash; patrol_waypoint_count<br/>\n>`00 00` &mdash; zero<br/>\n>`00 f8 2c 43` &mdash; x_coord1<br/>\n>`55 65 2c 43` &mdash; x_coord2<br/>\n>`00 88 2b 43` &mdash; x_coord3<br/>\n>`00 00 00 00` &mdash; x_coord4<br/>\n>`00 00 00 00` &mdash; x_coord5<br/>\n>`00 00 00 00` &mdash; x_coord6<br/>\n>`00 00 00 00` &mdash; x_coord7<br/>\n>`00 00 00 00` &mdash; x_coord8<br/>\n>`00 00 00 00` &mdash; x_coord9<br/>\n>`00 00 00 00` &mdash; x_coord10<br/>\n>`55 0d 46 43` &mdash; y_coord1<br/>\n>`00 70 48 43` &mdash; y_coord2<br/>\n>`00 48 45 43` &mdash; y_coord3<br/>\n>`00 00 00 00` &mdash; y_coord4<br/>\n>`00 00 00 00` &mdash; y_coord5<br/>\n>`00 00 00 00` &mdash; y_coord6<br/>\n>`00 00 00 00` &mdash; y_coord7<br/>\n>`00 00 00 00` &mdash; y_coord8<br/>\n>`00 00 00 00` &mdash; y_coord9<br/>\n>`00 00 00 00` &mdash; y_coord10<br/>\n>`43 18 00 00` &mdash; selected_id<br/>\n>`40 18 00 00` &mdash; selected_id\n\n## Formation Action\n\nUsed for changing the formation type.\n\n### Definition\n\n```ruby\ndef Formation\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :player_number\n  int8 :zero\n  int8 :formation_type\n  byte24 :zero2\n  array :selected_ids,\n    type => :int32,\n    initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x17`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:player_number*<br/>\nThe number of the player for whom the formation is changed (`0x01` - `0x08`).\n\n*:zero*<br/>\nThe byte after *:player_number* is always zero.\n\n*:formation_type*<br/>\nThe formation type that is selected. AoC supports 4 formation types.\n\nHex Value | Formation\n----------|-------\n0x02      | Line\n0x04      | Staggered\n0x07      | Box\n0x08      | Split\n\n*:zero2*<br/>\nThe 3 bytes after *:formation_type* are always zero.\n\n*:selected_ids*<br/>\nThe IDs of the units whose formation type are changed.\n\n### Examples\n\n`17 02 02 00 07 00 00 00 40 18 00 00 43 18 00 00`\n\n>`17` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`02` &mdash; player_number<br/>\n>`00` &mdash; zero<br/>\n>`07` &mdash; formation_type<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`40 18 00 00` &mdash; selected_id<br/>\n>`43 18 00 00` &mdash; selected_id\n"
  },
  {
    "path": "doc/reverse_engineering/networking/10-unit_creation.md",
    "content": "# Unit Creation\n\n## Training Unit (Human Player)\n\nUsed for training units with human player.\n\n### Definition\n\n```ruby\ndef TrainHM\n  int8 :action_identifier\n  byte24 :zero\n  int32 :building_id\n  int16 :unit_id\n  int16 :train_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x77`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:building_id*<br/>\nThe ID of the building where the units are trained.\n\n*:unit_id*<br/>\nThe `UnitID` (`LineID` for villagers) of the unit which is trained.\n\n*:train_count*<br/>\nThe number of trained units.\n\n### Examples\n\n`77 00 00 00 4d 1a 00 00 5d 00 05 00`\n\n>`77` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`4d 1a 00 00` &mdash; building_id<br/>\n>`5d 00` &mdash; unit_id<br/>\n>`05 00` &mdash; train_count\n\n## Rally Point Action\n\nUsed for setting rally points.\n\n### Definition\n\n```ruby\ndef RallyPoint\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  int32 :target_id\n  int32 :target_unit_id\n  float :x_coord\n  float :y_coord\n  array :selected_ids,\n    type => :int32,\n    length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x78`.\n\n*:selection_count*<br/>\nThe number of selected buildings.\n\n*:zero*<br/>\nThe two bytes following *:selection_count* are always zero.\n\n*:target_id*<br/>\nIf the rally point is set on another object, this has the ID of this object. Otherwise has the value `0xFFFFFFFF`.\n\n*:target_unit_id*<br/>\nThe `UnitID` of the target. Will be `ff ff 00 00` if no target is selected.\n\n*:x_coord*<br/>\nThe x-coordinate of the rally point.\n\n*:y_coord*<br/>\nThe y-coordinate of the rally point.\n\n*:selected_ids*<br/>\nThe IDs of the buildings for which the rally point will be set.\n\n### Examples\n\nSet rally point in the open.\n\n`78 01 00 00 ff ff ff ff ff ff 00 00 ab b2 17 43 00 20 49 42 43 17 00 00`\n\n>`78` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; target_id<br/>\n>`ff ff 00 00` &mdash; target_unit_id<br/>\n>`ab b2 17 43` &mdash; x_coord<br/>\n>`00 20 49 42` &mdash; y_coord<br/>\n>`43 17 00 00` &mdash; selected_id\n\nSet rally point on object.\n\n`78 01 00 00 83 17 00 00 66 00 00 00 00 80 29 43 00 00 1a 42 43 17 00 00`\n\n>`78` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`83 17 00 00` &mdash; target_id<br/>\n>`66 00 00 00` &mdash; target_unit_id<br/>\n>`00 80 29 43` &mdash; x_coord<br/>\n>`00 00 1a 42` &mdash; y_coord<br/>\n>`43 17 00 00` &mdash; selected_id\n"
  },
  {
    "path": "doc/reverse_engineering/networking/11-buildings.md",
    "content": "# Buildings\n\n## Research\n\nUsed for researching technologies.\n\n### Definition\n\n```ruby\ndef Research\n  int8 :action_identifier\n  int24 :zero\n  int32 :building_id\n  int8 :player_number\n  int8 :zero2\n  int16 :technology_id\n  int32 :const\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x65`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:building_id*<br/>\nThe ID of the building where the technology is researched.\n\n*:player_number*<br/>\nThe number of the player for whom the technology is researched (`0x01` - `0x08`).\n\n*:zero2*<br/>\nThe byte after *:player_number* is always zero.\n\n*:technology_id*<br/>\nThe [ID of the technology](technology_ids.md) that is researched.\n\n*:const*<br/>\nThis value is always `0xFFFFFFFF`.\n\n### Examples\n\n`65 00 00 00 63 1a 00 00 01 00 d9 00 ff ff ff ff`\n\n>`65` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`63 1a 00 00` &mdash; building_id<br/>\n>`01` &mdash; player_number<br/>\n>`00` &mdash; zero2<br/>\n>`d9 00` &mdash; technology_id<br/>\n>`ff ff ff ff` &mdash; const\n\n## Build Action\n\nUsed for building everything except walls.\n\n### Definition\n\n```ruby\ndef Build\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :player_number\n  int8 :zero\n  float :x_coord\n  float :y_coord\n  int16 :building_id\n  int16 :zero2\n  int32 :const\n  int8 :sprite_id\n  array :selected_ids,\n        type => :int32,\n        initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x66`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:player_number*<br/>\nThe number of the player who the building belongs to (`0x01` - `0x08`).\n\n*:zero*<br/>\nThe byte after *:player_number* is always zero.\n\n*:x_coord*<br/>\nThe x-coordinate of the construction site.\n\n*:y_coord*<br/>\nThe y-coordinate of the construction site.\n\n*:building_id*<br/>\nThe `UnitID` of the building.\n\n*:zero2*<br/>\nThe two bytes after *:building_id* are always zero.\n\n*:const*<br/>\nThis value is always `0xFFFFFFFF`.\n\n*:sprite_id*<br/>\nRepresents which sprite is used for the building (only used for houses, which have several variations per civ).\n\n*:selected_ids*<br/>\nThe IDs of the selected builders.\n\n### Examples\n\n`66 02 01 00 00 00 10 43 00 00 88 42 46 00 00 00 ff ff ff ff 02 00 00 00 6e 17 00 00 6c 17 00 00`\n\n>`65` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`01` &mdash; player_number<br/>\n>`00` &mdash; zero<br/>\n>`00 00 10 43` &mdash; x_coord<br/>\n>`00 00 88 42` &mdash; y_coord<br/>\n>`46 00` &mdash; building_id<br/>\n>`00 00` &mdash; zero2<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`02 00 00 00` &mdash; sprite_id<br/>\n>`6e 17 00 00` &mdash; selected_id<br/>\n>`6c 17 00 00` &mdash; selected_id\n\n## Build Wall Action\n\nUsed for building walls.\n\n### Definition\n\n```ruby\ndef BuildWall\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :player_number\n  int8 :start_x_coord\n  int8 :start_y_coord\n  int8 :end_x_coord\n  int8 :end_y_coord\n  int8 :zero\n  int16 :building_id\n  int16 :zero2\n  int32 :const\n  array :selected_ids,\n        type => :int32,\n        initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x69`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:player_number*<br/>\nThe number of the player who the wall belongs to (`0x01` - `0x08`).\n\n*:zero*<br/>\nThe byte after *:player_number* is always zero.\n\n*:start_x_coord*<br/>\nThe x-coordinate of the starting tile of the wall.\n\n*:start_y_coord*<br/>\nThe y-coordinate of the starting tile of the wall.\n\n*:end_x_coord*<br/>\nThe x-coordinate of the end tile of the wall.\n\n*:end_y_coord*<br/>\nThe y-coordinate of the end tile of the wall.\n\n*:building_id*<br/>\nThe `UnitID` of the wall.\n\n*:zero2*<br/>\nThe two bytes after *:building_id* are always zero.\n\n*:const*<br/>\nThis value is always `0xFFFFFFFF`.\n\n*:selected_ids*<br/>\nThe IDs of the selected builders.\n\n### Examples\n\n`69 02 01 a0 4a ac 47 00 48 00 00 00 ff ff ff ff 6c 17 00 00 6e 17 00 00`\n\n>`69` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`01` &mdash; player_number<br/>\n>`a0` &mdash; start_x_coord<br/>\n>`4a` &mdash; start_y_coord<br/>\n>`ac` &mdash; end_x_coord<br/>\n>`47` &mdash; end_y_coord<br/>\n>`00` &mdash; zero<br/>\n>`48 00` &mdash; building_id<br/>\n>`00 00` &mdash; zero2<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`6c 17 00 00` &mdash; selected_id<br/>\n>`6e 17 00 00` &mdash; selected_id\n\n## Repair\n\nUsed for repairing buildings and siege.\n\n### Definition\n\n```ruby\ndef Repair\n  int8 :action_identifier\n  int8 :selection_count\n  int16 :zero\n  int32 :repaired_id\n  array :selected_ids,\n        type => :int32,\n        initial_length => :selection_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x6e`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:zero*<br/>\nThe 2 bytes after *:selection_count* are always zero.\n\n*:repaired_id*<br/>\nThe ID of the building or siege that is repaired.\n\n*:selected_ids*<br/>\nThe IDs of the selected repairmen.\n\n### Examples\n\n`6e 02 00 00 70 1a 00 00 a0 1a 00 00 9f 1a 00 00`\n\n>`6e` &mdash; action_identifier<br/>\n>`02` &mdash; selection_count<br/>\n>`00 00` &mdash; zero<br/>\n>`70 1a 00 00` &mdash; repaired_id<br/>\n>`a0 1a 00 00` &mdash; selected_id<br/>\n>`9f 1a 00 00` &mdash; selected_id\n\n## Toggle Gate\n\nUsed for setting a gate to closed/open.\n\n### Definition\n\n```ruby\ndef ToggleGate\n  int8 :action_identifier\n  byte24 :zero\n  int32 :gate_id\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x72`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:gate_id*<br/>\nThe ID of the gate.\n\n### Examples\n\n`72 00 00 00 bf 1a 00 00`\n\n>`72` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`bf 1a 00 00` &mdash; gate_id\n\n## Townbell\n\nUsed for ringing the town bell.\n\n### Definition\n\n```ruby\ndef Townbell\n  int8 :action_identifier\n  byte24 :zero\n  int32 :building_id\n  int8 :active\n  byte24 :zero2\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x7f`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:building_id*<br/>\nThe ID of the town center where the bell is rung.\n\n*:active*<br/>\nRepresents whether the townbell is active (`0x01`) or not active (`0x00`).\n\n*:zero2*<br/>\nThe 3 bytes after *:active* are always zero.\n\n### Examples\n\n`7f 00 00 00 68 17 00 00 01 00 00 00`\n\n>`7f` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`68 17 00 00` &mdash; building_id<br/>\n>`01` &mdash; active<br/>\n>`00 00 00` &mdash; zero2\n\n## Back to Work\n\nUsed for sending villagers back to work.\n\n### Definition\n\n```ruby\ndef BackToWork\n  int8 :action_identifier\n  byte24 :zero\n  int32 :building_id\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x80`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:building_id*<br/>\nThe ID of the building from which the villagers are released.\n\n### Examples\n\n`80 00 00 00 68 17 00 00`\n\n>`80` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`68 17 00 00` &mdash; building_id\n"
  },
  {
    "path": "doc/reverse_engineering/networking/12-market.md",
    "content": "# Market actions\n\n## Sell\n\nUsed for selling resources at the market.\n\n### Definition\n\n```ruby\ndef Sell\n  int8 :action_identifier\n  int8 :player_number\n  int8 :resource_type\n  int8 :amount\n  int32 :market_id\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x7a`.\n\n*:player_number*<br/>\nThe ID of the player who sells the resources (`0x01` - `0x08`).\n\n*:resource_type*<br/>\nThe resource type that is sold.\n\nHex Value | Resource\n----------|---------\n0x00      | Food\n0x01      | Wood\n0x02      | Stone\n\n*:amount*<br/>\nThe amount being sold in hundreds. Can be either `0x01` or `0x05`.\n\n*:market_id*<br/>\nThe ID of the market.\n\n### Examples\n\n`7a 01 02 01 46 1a 00 00`\n\n>`7b` &mdash; action_identifier<br/>\n>`01` &mdash; player_number<br/>\n>`02` &mdash; resource_type<br/>\n>`01` &mdash; amount<br/>\n>`46 1a 00 00` &mdash; market_id\n\n## Buy\n\nUsed for buying resources at the market.\n\n### Definition\n\n```ruby\ndef Buy\n  int8 :action_identifier\n  int8 :player_number\n  int8 :resource_type\n  int8 :amount\n  int32 :market_id\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x7b`.\n\n*:player_number*<br/>\nThe ID of the player who buys the resources (`0x01` - `0x08`).\n\n*:resource_type*<br/>\nThe resource type that is sold.\n\nHex Value | Resource\n----------|---------\n0x00      | Food\n0x01      | Wood\n0x02      | Stone\n\n*:amount*<br/>\nThe amount being bought in hundreds. Can be either `0x01` or `0x05`.\n\n*:market_id*<br/>\nThe ID of the market.\n\n### Examples\n\n`7b 01 01 05 46 1a 00 00`\n\n>`7b` &mdash; action_identifier<br/>\n>`01` &mdash; player_id<br/>\n>`01` &mdash; resource_type<br/>\n>`05` &mdash; amount<br/>\n>`46 1a 00 00` &mdash; market_id\n"
  },
  {
    "path": "doc/reverse_engineering/networking/13-other.md",
    "content": "# Others\n\n## Resign/Disconnect\n\nUsed for resigning and disconnecting.\n\n### Definition\n\n```ruby\ndef Resign\n  int8 :action_identifier\n  int8 :player_number\n  int8 :player_id\n  int32 :disconnect\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x0b`.\n\n*:player_number*<br/>\nThe player's number which can be changed in the lobby. Is only different from *:player_id* if playing in coop mode.\n\n*:player_id*<br/>\nThe ID of the player.\n\n*:disconnect*<br/>\nDetermines whether the player gave up himself (`0x00`) or was disconnected (`0x01`).\n\n### Examples\n\n`0b 02 03 00`\n\n>`0b` &mdash; action_identifier<br/>\n>`02` &mdash; player_number<br/>\n>`03` &mdash; player_id<br/>\n>`00` &mdash; disconnect\n\n## Save Game\n\nUsed for saving the game.\n\n### Definition\n\n```ruby\ndef Save\n  int8 :action_identifier\n  int8 :exit\n  int8 :player_id\n  array :filename,\n        type => :int8\n  unknown :memory\n  int32 :checksum\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x1b`.\n\n*:exit*<br/>\nRepresents whether this action is a normal Save (`0x00`) or a Save & Exit (`0x01`).\n\n*:player_id*<br/>\nThe ID of the player saving the game.\n\n*:filename*<br/>\nThe filename of the savegame in ASCII characters.\n\n*:memory*<br/>\nThe space between filename and checksum is filled with uninitialized memory.\n\n*:checksum*<br/>\nThe last 4 bytes are a checksum.\n\n### Examples\n\n```\n0000   1b 00 01 78 79 7a 2e 6d 73 78 00 7b 78 79 7a 00\n0010   00 00 00 00 7e cd 83 7b 5b f3 83 7b 00 8c ff 3f\n0020   00 00 00 80 03 00 00 00 b0 d7 32 00 03 00 00 00\n0030   80 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00\n0040   b0 d7 32 00 03 00 00 00 00 00 00 80 00 00 00 00\n0050   06 f3 83 7b 88 d7 32 00 09 00 00 00 80 00 00 00\n0060   ff ff ff ff cc d7 32 00 63 51 61 00 00 00 00 00\n0070   63 51 61 00 3c d8 32 00 00 00 00 80 03 00 00 00\n0080   b0 d7 32 00 03 00 00 00 87 51 61 00 02 00 00 00\n0090   d2 ed 66 00 01 00 00 00 00 00 00 00 0c 00 00 00\n00a0   00 00 00 00 01 00 00 00 03 00 00 00 00 00 00 80\n00b0   03 00 00 00 16 00 00 00 f8 d7 32 00 9d 94 61 00\n00c0   3c d8 32 00 00 80 00 00 40 00 00 00 a4 01 00 00\n00d0   54 6d 14 0b 50 65 14 0b 58 6e 14 0b 00 00 00 00\n00e0   00 00 00 00 b0 d7 79 0c 9d 39 61 00 3c d8 32 00\n00f0   d0 ed 66 00 40 00 00 00 08 1b 68 00 b0 39 61 00\n0100   3c d8 32 00 d0 ed 66 00 2c 00 00 00\n```\n\n>`1b` &mdash; action_identifier<br/>\n>`00` &mdash; exit<br/>\n>`01` &mdash; player_id<br/>\n> `78 79 7a 2e 6d 73 78` &mdash; filename<br/>\n> memory<br/>\n> `2c 00 00 00` &mdash; checksum<br/>\n\n## Diplomacy, Cheats and Game Speed\n\nUsed for diplomacy, cheating and changing the game's speed.\n\n### Definition\n\n```ruby\ndef Diplomacy\n  int8 :action_identifier\n  int8 :action_type\n  int8 :source_player_number\n  int8 :zero\n  int8 :option\n  byte24 :zero2\n  float :option2\n  int8 :diplomatic_stance\n  byte24 :zero3\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x67`.\n\n*:action_type*<br/>\nRepresents the specific action that was taken (diplomatic, changing speed or cheating).\n\nHex Value | Action\n----------|-------\n0x00      | Diplomacy\n0x01      | Change Game Speed\n0x04      | Cheat Response (if cheat is activated for all players; only \"aegis\")\n0x05      | Allied Victory\n0x06      | Cheat\n0x0a      | Research Treason (only Regicide)\n0x0b      | AI policy\n\n*:source_player_number*<br/>\nThe player number of the player who has executed the action. In Cheat Responses, the value is `0x01` when \"aegis\" was turned on and `0x00` when it is turned off.\n\n*:zero*<br/>\nThe byte after *:source_player_number* is always zero.\n\n*:option*<br/>\nOnly used for diplomacy and cheats. Stays at `0x00` for game speed. The purpose of the option depends on the action. For diplomacy, this value represents the player number of the player who is targeted by the diplomatic action (which can be the same player number as *:source_player_number* when selecting \"Allied Victory\"). When changing the game speed, this value is always `0x00`. If the action is a cheat (`0x06`), then this field has the value of the `CheatID`, while for a cheat response the value is always `0x00`.\n\n*:zero2*<br/>\nThe 3 bytes after *:option* are always zero.\n\n*:option2*<br/>\nOnly used for diplomacy and changing game speed. Stays at `0x00000000` for cheats.\n\nHex Value  | Float  | Diplomatic Stance\n-----------|--------|------------------\n0x00000000 | 0.0f   | Allied\n0x3f800000 | 1.0f   | Neutral\n0x40400000 | 3.0f   | Enemy\n\nHex Value  | Float  | Game Speed\n-----------|--------|------------------\n0x3f800000 | 1.0f   | Slow\n0x3fc00000 | 1.5f   | Normal\n0x40000000 | 2.0f   | Fast\n\n*:diplomatic_stance*<br/>\nOnly used for diplomacy. Changes depending on the diplomatic stance that was selected.\n\nHex Value | Diplomatic Stance\n----------|------------------\n0x00      | Allied\n0x01      | Neutral\n0x03      | Enemy\n\n*:zero3*<br/>\nThe 3 bytes after *:diplomatic_stance* are always zero.\n\n### Examples\n\nDiplomacy.\n\n`67 00 01 00 02 00 00 00 00 00 80 3f 01 00 00 00`\n\n>`67` &mdash; action_identifier<br/>\n>`00` &mdash; action_type<br/>\n>`01` &mdash; source_player_number<br/>\n>`00` &mdash; zero<br/>\n>`02` &mdash; option<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 80 3f` &mdash; option2<br/>\n>`01` &mdash; diplomatic_stance<br/>\n>`00 00 00` &mdash; zero3\n\nChanging the game speed.\n\n`67 01 01 00 00 00 00 00 00 00 c0 3f 00 00 00 00`\n\n>`67` &mdash; action_identifier<br/>\n>`01` &mdash; action_type<br/>\n>`01` &mdash; source_player_number<br/>\n>`00` &mdash; zero<br/>\n>`00` &mdash; option<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 c0 3f` &mdash; option2<br/>\n>`00` &mdash; diplomatic_stance<br/>\n>`00 00 00` &mdash; zero3\n\nCheating.\n\n`67 06 01 00 68 00 00 00 00 00 00 00 00 00 00 00`\n\n>`67` &mdash; action_identifier<br/>\n>`06` &mdash; action_type<br/>\n>`01` &mdash; source_player_number<br/>\n>`00` &mdash; zero<br/>\n>`68` &mdash; option<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 00 00 00` &mdash; option2<br/>\n>`00` &mdash; diplomatic_stance<br/>\n>`00 00 00` &mdash; zero3\n\n## Tribute\n\nUsed for sending resources to other players.\n\n### Definition\n\n```ruby\ndef Tribute\n  int8 :action_identifier\n  int8 :source_player_number\n  int8 :target_player_number\n  int8 :resource_type\n  float :amount\n  float :transaction_fee\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x6c`.\n\n*:source_player_number*<br/>\nThe number of the player who sends the resources.\n\n*:target_player_number*<br/>\nThe number of the player who receives the resources.\n\n*:resource_type*<br/>\nThe resource type that is send.\n\nHex Value | Resource\n----------|---------\n0x00      | Food\n0x01      | Wood\n0x02      | Stone\n\n*:amount*<br/>\nThe amount of resources being transferred.\n\n*:transaction_fee*<br/>\nThe transaction fee.\n\n### Examples\n\n`6c 03 01 03 00 00 c8 42 cd cc 4c 3e`\n\n>`6c` &mdash; action_identifier<br/>\n>`03` &mdash; source_player_number<br/>\n>`01` &mdash; target_player_number<br/>\n>`03` &mdash; resource_type<br/>\n>`00 00 c8 42` &mdash; amount<br/>\n>`cd cc 4c 3e` &mdash; transaction_fee\n\n## Flare\n\nUsed for launching a flare.\n\n### Definition\n\n```ruby\ndef Flare\n  int8 :action_identifier\n  byte24 :zero\n  int32 :const\n  int8 :zero2\n  array :receiving,\n        type => :int8,\n        initial_length => 8\n  byte24 :zero3\n  float :x_coord\n  float :y_coord\n  int8 :player_number\n  int8 :player_id\n  int16 :zero4\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x0b`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:const*<br/>\nThese 4 bytes are always `0xFFFFFFFF`.\n\n*:zero2*<br/>\nThe one byte after *:player_id* is always zero. Could also represent \"Gaia\" in *:receiving* or be a counter of some sorts that is never used.\n\n*:receiving*<br/>\nEvery byte in this array represents a player number (first byte: player 1, second byte: player 2, ...). If the value of byte X is `0x01`, player X will see the flare. If the value of byte X is `0x00`, player X won't see the flare.\n\n*:zero3*<br/>\nThe 3 bytes after *:receiving* are always zero.\n\n*:x_coord*<br/>\nThe x-coordinate of the targeted position.\n\n*:y_coord*<br/>\nThe y-coordinate of the targeted position.\n\n*:player_number*<br/>\nThe player's number which can be changed in the lobby. Is only different from *:player_id* if playing in coop mode.\n\n*:player_id*<br/>\nThe ID of the player.\n\n*:zero4*<br/>\nThe 3 bytes after *:player_id* are always zero.\n\n### Examples\n\n`73 00 00 00 ff ff ff ff 00 01 01 01 00 00 00 00 00 00 00 00 00 00 c7 42 00 80 33 43 03 04 00 00`\n\n>`73` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`ff ff ff ff` &mdash; const<br/>\n>`00` &mdash; zero2<br/>\n>`01` &mdash; receiving_p1<br/>\n>`01` &mdash; receiving_p2<br/>\n>`01` &mdash; receiving_p3<br/>\n>`00` &mdash; receiving_p4<br/>\n>`00` &mdash; receiving_p5<br/>\n>`00` &mdash; receiving_p6<br/>\n>`00` &mdash; receiving_p7<br/>\n>`00` &mdash; receiving_p8<br/>\n>`00 00 00` &mdash; zero3<br/>\n>`00 00 c7 42` &mdash; x_coord<br/>\n>`00 80 33 43` &mdash; y_coord<br/>\n>`03` &mdash; player_number<br/>\n>`04` &mdash; player_id<br/>\n>`00` &mdash; disconnect\n"
  },
  {
    "path": "doc/reverse_engineering/networking/14-ai.md",
    "content": "# AI Actions\n\n## Primary Action (AI Player)\n\nUsed by the AI to interact with objects in the game world.\n\n### Definition\n\n```ruby\ndef PrimaryActionAI\n    int8 :action_identifier\n    byte24 :zero\n    int32 :target_id\n    int8 :selection_count\n    byte24 :zero2\n    float :x_coord\n    float :y_coord\n    array :selected_ids,\n      type => :int32,\n      length => :selection_count,\n      onlyif => :selection_count < 0xFF\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x02`.\n\n*:zero*<br/>\nThe 3 bytes following *:action_identifier* are unused.\n\n*:target_id*<br/>\nThe ID of the targeted object.\n\n*:selection_count*<br/>\nThe number of selected units. Always `0x01`.\n\n*:zero2*<br/>\nThe three bytes following *:selection_count* are unused.\n\n*:x_coord*<br/>\nThe x-coordinate of the targeted position.\n\n*:y_coord*<br/>\nThe y-coordinate of the targeted position.\n\n*:selected_ids*<br/>\nThe IDs of the selected units.\n\n### Examples\n\n`02 00 00 00 21 1f 00 00 01 00 00 00 00 80 36 43 00 80 0f 43 72 1e 00 00`\n\n>`02` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`21 1f 00 00` &mdash; target_id<br/>\n>`01` &mdash; selection_count<br/>\n>`00 00 00` &mdash; zero2<br/>\n>`00 80 36 43` &mdash; x_coord<br/>\n>`00 80 0f 43` &mdash; y_coord<br/>\n>`72 1e 00 00` &mdash; selected_id\n\n## Move (AI Player)\n\nUsed for moving and attacking. If that is the only purpose is unknown.\n\n### Definition\n\n```ruby\ndef PrimaryActionAI\n    int8 :action_identifier\n    int8 :selection_count\n    int8 :player_number\n    int8 :player_id\n    int32 :unknown_id\n    int32 :unknown_bytes\n    int32 :target_id\n    int8 :unknown_count\n    byte24 :zero\n    float :unknown_x_coord\n    float :unknown_y_coord\n    float :unknown_float\n    float :unknown_float2\n    int32 :unknown_bytes2\n    array :selected_ids,\n      type => :int32,\n      length => :selection_count,\n      onlyif => :selection_count > 0x01\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x0a`.\n\n*:selection_count*<br/>\nThe number of selected units.\n\n*:player_number*<br/>\nThe player's number which can be changed in the lobby. Is only different from *:player_id* if playing in coop mode.\n\n*:player_id*<br/>\nThe ID of the player.\n\n*:unknown_id*<br/>\nThe ID of a unit that is able to move.\n\n*:unknown_bytes*<br/>\nPurpose unknown.\n\n*:target_id*<br/>\nThe ID of a target. Is `0xFFFFFFFF` if no target is selected.\n\n*:unknown_count*<br/>\nCould be the number of targets, but is never greater than `0x01`. Is `0xFF` if no target is selected.\n\n*:zero*<br/>\nThe 3 bytes after *:unknown_count* are always zero.\n\n*:unknown_x_coord*<br/>\nPresumably an x-coordinate, that could describe the position of a target.\n\n*:unknown_y_coord*<br/>\nPresumably an y-coordinate, that could describe the position of a target.\n\n*:unknown_float*<br/>\nA float value that can be `1.0f` or `-1.0f`.\n\n*:unknown_float2*<br/>\nA float value that can be `1.0f` or `-1.0f`.\n\n*:unknown_bytes2*<br/>\nPurpose unknown.\n\n*:selected_ids*<br/>\nThe IDs of the selected units. Only present if *:selection_count* is greater than `0x01`. Always contains *:unknown_id*.\n\n### Examples\n\n`0a 01 03 03 1b 17 00 00 c1 02 01 00 ff ff ff ff ff 00 00 00 00 00 09 43 00 00 4a 43 00 00 80 bf 00 00 80 3f 01 00 00 00`\n\n>`0a` &mdash; action_identifier<br/>\n>`01` &mdash; selection_count<br/>\n>`03` &mdash; player_number<br/>\n>`03` &mdash; player_id<br/>\n>`1b 17 00 00` &mdash; unknown_id<br/>\n>`c1 02 01 00` &mdash; unknown_bytes<br/>\n>`ff ff ff ff` &mdash; target_id<br/>\n>`ff` &mdash; unknown_count<br/>\n>`00 00 00` &mdash; zero<br/>\n>`00 00 09 43` &mdash; unknown_x_coord<br/>\n>`00 00 4a 43` &mdash; unknown_y_coord<br/>\n>`00 00 80 bf` &mdash; unknown_float<br/>\n>`00 00 80 3f` &mdash; unknown_float2<br/>\n>`01 00 00 00` &mdash; unknown_bytes2\n\n## Waypoints (AI Player)\n\nUsed for setting multiple waypoints for AI units.\n\n### Definition\n\n```ruby\ndef WaypointsAI\n  int8 :action_identifier\n  int8 :selection_count\n  int8 :waypoint_count\n  array :selected_ids,\n        type => :int32,\n        initial_length => :selection_count\n  array :x_coords,\n        type => :int8,\n        initial_length => :waypoint_count\n  array :y_coords,\n        type => :int8,\n        initial_length => :waypoint_count\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x1f`.\n\n*:selection_count*<br/>\nThe number of units selected by the AI.\n\n*:waypoint_count*<br/>\nThe number of waypoints.\n\n*:selected_ids*<br/>\nThe IDs of the selected units.\n\n*:x_coords*<br/>\nThe x-coordinates on the AoC grid.\n\n*:y_coords*<br/>\nThe y-coordinates on the AoC grid.\n\n### Examples\n\n`1f 04 09 84 21 00 00 bd 20 00 00 a4 21 00 00 bb 21 00 00 6e 6a 68 67 67 64 64 64 5f 99 a0 a4 a7 a8 b0 b4 b8 be`\n\n>`1f` &mdash; action_identifier<br/>\n>`04` &mdash; selection_count<br/>\n>`09` &mdash; waypoint_count<br/>\n>`84 21 00 00` &mdash; selected_id<br/>\n>`bd 20 00 00` &mdash; selected_id<br/>\n>`a4 21 00 00` &mdash; selected_id<br/>\n>`bb 21 00 00` &mdash; selected_id<br/>\n>`6e` &mdash; x_coord1<br/>\n>`6a` &mdash; x_coord2<br/>\n>`68` &mdash; x_coord3<br/>\n>`67` &mdash; x_coord4<br/>\n>`67` &mdash; x_coord5<br/>\n>`64` &mdash; x_coord6<br/>\n>`64` &mdash; x_coord7<br/>\n>`64` &mdash; x_coord8<br/>\n>`5f` &mdash; x_coord9<br/>\n>`99` &mdash; y_coord1<br/>\n>`a0` &mdash; y_coord2<br/>\n>`a4` &mdash; y_coord3<br/>\n>`a7` &mdash; y_coord4<br/>\n>`a8` &mdash; y_coord5<br/>\n>`b0` &mdash; y_coord6<br/>\n>`b4` &mdash; y_coord7<br/>\n>`b8` &mdash; y_coord8<br/>\n>`be` &mdash; y_coord9\n\n## Training Unit (AI Player)\n\nUsed for training unit with AI player.\n\n### Definition\n\n```ruby\ndef TrainAI\n  int8 :action_identifier\n  byte24 :zero\n  int32 :building_id\n  int16 :player_id\n  int16 :unit_id\n  int32 :const\nend\n```\n\n### Description\n\n*:action_identifier*<br/>\nAlways has the value `0x64`.\n\n*:zero*<br/>\nThe 3 bytes after *:action_identifier* are always zero.\n\n*:building_id*<br/>\nThe ID of the building where the units are trained.\n\n*:player_number*<br/>\nThe number of the AI player who trains the unit (`0x01` - `0x08`).\n\n*:unit_id*<br/>\nThe `UnitID` (`LineID` for villagers) of the unit which is trained.\n\n*:const*<br/>\nThe value is always `0xFFFFFFFF`.\n\n### Examples\n\n`64 00 00 00 ae 1a 00 00 03 00 27 00 ff ff ff ff`\n\n>`64` &mdash; action_identifier<br/>\n>`00 00 00` &mdash; zero<br/>\n>`ae 1a 00 00` &mdash; building_id<br/>\n>`03 00` &mdash; player_id<br/>\n>`27 00` &mdash; unit_id<br/>\n>`ff ff ff ff` &mdash; const\n"
  },
  {
    "path": "doc/reverse_engineering/networking/15-c-structs.md",
    "content": "# C Structs\n\nA collection of C structs for commands and actions.\n\n## Sync\n\n### 16BC41\n\n```c\nstruct 16bc41 {\n  uint32_t network_source_id;\n  uint32_t network_dest_id;\n  uint8_t command;\n  uint8_t option1;\n  uint8_t option2;\n  uint8_t option3;\n  uint32_t individual_counter;\n};\n```\n\n### 16BC31\n\n```c\nstruct 16bc31 {\n  uint32_t network_source_id;\n  uint32_t network_dest_id;\n  uint8_t command;\n  uint8_t option1; // = 0x00\n  uint8_t option2; // = 0x00\n  uint8_t option3; // = 0x00\n  uint32_t time_passed;\n};\n```\n\n### 16BC32\n\n```c\nstruct 16bc32 {\n  uint32_t network_source_id;\n  uint32_t network_dest_id;\n  uint8_t command;\n  uint8_t option1;\n  uint8_t option2;\n  uint8_t option3;\n  uint32_t time_passed;\n};\n```\n\n### 24BC35\n\nToo many unknown bytes.\n\n### 26BC53\n\nToo many unknown bytes.\n\n### 32BC44\n\nToo many unknown bytes.\n\n### 56BC4D\n\nToo many unknown bytes.\n\n### 24BC51\n\nToo many unknown bytes.\n\n### 24BC52\n\nToo many unknown bytes.\n\n## Actions\n\n### 00 - Primary Action\n\n```c\nstruct primary_action {\n    uint8_t action_identifier;\n    uint8_t player_id;\n    uint32_t target_id;\n    uint8_t selection_count;\n    float x_coord;\n    float y_coord;\n    uint32_t selected_ids[selection_count];\n};\n```\n\n### 01 - Stop\n\n```c\nstruct stop {\n    uint8_t action_identifier;\n    uint8_t selection_count;\n    uint32_t selected_ids[selection_count];\n};\n```\n\n### 02 - AI Primary Action\n\n```c\nstruct ai_primary_action {\n    uint8_t action_identifier;\n    uint32_t target_id;\n    uint8_t selection_count;\n    float x_coord;\n    float y_coord;\n    uint32_t selected_ids[selection_count];\n};\n```\n\n### 03 - Move\n\n```c\nstruct move {\n  \tuint8_t action_identifier;\n  \tuint8_t player_id;\n  \tuint32_t constant; // = 0xFFFFFFFF\n  \tuint32_t selection_count;\n  \tfloat x_coord;\n  \tfloat y_coord;\n  \tuint32_t selected_ids[selection_count];\n};\n```\n\n### 0a - AI Move\n\nToo many unknown bytes.\n\n### 0b - Resign\n\n```c\nstruct resign {\n    uint8_t action_identifier;\n    uint8_t player_number;\n    uint8_t player_id;\n    uint32_t disconnect;\n};\n```\n\n### 10 - Waypoint\n\n```c\nstruct waypoint {\n  uint8_t action_identifier;\n  uint8_t player_id;\n  uint8_t selection_count;\n  uint8_t x_coord;\n  uint8_t y_coord;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 12 - Stance\n\n```c\nstruct stance {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t stance;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 13 - Guard\n\n```c\nstruct guard {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint32_t guarded_id;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 14 - Follow\n\n```c\nstruct follow {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint32_t followed_id;\n  uint32_t selection_ids[selection_count];\n};\n```\n\n### 15 - Patrol\n\n```c\nstruct patrol {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t patrol_waypoint_count;\n  float x_coords[10];\n  float y_coords[10];\n  uint32_t selection_ids[selection_count];\n};\n```\n\n### 17 - Formation\n\n```c\nstruct formation {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t player_number;\n  uint8_t unknown; // = 0x00\n  uint8_t formation_type;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 1b - Save\n\nToo many unknown bytes.\n\n### 1f - AI Waypoints\n\n```c\nstruct ai_waypoints {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t waypoint_count;\n  uint32_t selected_ids[selection_count];\n  uint8_t x_coords[waypoint_count];\n  uint8_t y_coords[waypoint_count];\n};\n```\n\n### 64 - AI Training\n\n```c\nstruct ai_train {\n  uint8_t action_identifier;\n  uint32_t building_id;\n  uint16_t player_id;\n  uint16_t unit_id;\n  uint32_t constant; // = 0xFFFFFFFF\n};\n```\n\n### 65 - Research\n\n```c\nstruct research {\n  uint8_t action_identifier;\n  uint32_t building_id;\n  uint8_t player_number;\n  uint16_t technology_id;\n  uint32_t constant; // = 0xFFFFFFFF\n};\n```\n\n### 66 - Build\n\n```c\nstruct build {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t player_number;\n  float x_coord;\n  float y_coord;\n  uint16_t building_id;\n  uint32_t constant; // = 0xFFFFFFFF\n  uint8_t sprite_id;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 67 - Multipurpose\n\nToo many unknown bytes.\n\n### 69 - Build Wall\n\n```c\nstruct build_wall {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint8_t player_number;\n  uint8_t start_x_coord;\n  uint8_t start_y_coord;\n  uint8_t end_x_coord;\n  uint8_t end_y_coord;\n  uint16_t building_id;\n  uint32_t constant; // = 0xFFFFFFFF\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 6a - Delete\n\n```c\nstruct delete {\n  uint8_t action_identifier;\n  uint32_t object_id;\n  uint8_t player_number;\n};\n```\n\n### 6b - Attack Ground\n\n```c\nstruct attack_ground {\n\tuint8_t action_identifier;\n\tuint8_t selection_count;\n\tfloat x_coord;\n\tfloat y_coord;\n\tuint32_t selected_ids[selection_count];\n};\n```\n\n### 6c - Attack Ground\n\n```c\nstruct attack_ground {\n  uint8_t action_identifier;\n  uint8_t source_player_number;\n  uint8_t target_player_number;\n  uint8_t resource_type;\n  float amount;\n  float transaction_fee;\n};\n```\n\n### 6e - Repair\n\n```c\nstruct repair {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint32_t repaired_id;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 6f - Ungarrison\n\n```c\nstruct ungarrison {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  float x_coord;\n  float y_coord;\n  uint8_t ungarrison_type;\n  uint32_t release_id;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 72 - Toggle Gate\n\n```c\nstruct toggle_gate {\n  uint8_t action_identifier;\n  uint32_t gate_id;\n};\n```\n\n### 73 - Flare\n\n```c\nstruct flare {\n  uint8_t action_identifier;\n  uint32_t constant; // = 0xFFFFFFFF\n  uint8_t receiving[8];\n  float x_coord;\n  float y_coord;\n  uint8_t player_number;\n  uint8_t player_id;\n};\n```\n\n### 75 - Garrison\n\n```c\nstruct garrison {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint32_t building_id;\n  uint8_t garrison_type;\n  float x_coord;\n  float y_coord;\n  uint32_t constant; // = 0xFFFFFFFF\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 77 - Training\n\n```c\nstruct train {\n  uint8_t :action_identifier;\n  uint32_t :building_id;\n  uint16_t :unit_id;\n  uint16_t :train_count;\n};\n```\n\n### 78 - Rally Point\n\n```c\nstruct rally_point {\n  uint8_t action_identifier;\n  uint8_t selection_count;\n  uint32_t target_id;\n  uint32_t target_unit_id;\n  float x_coord;\n  float y_coord;\n  uint32_t selected_ids[selection_count];\n};\n```\n\n### 7a - Sell\n\n```c\nstruct sell {\n  uint8_t action_identifier;\n  uint8_t player_number;\n  uint8_t resource_type;\n  uint8_t amount;\n  uint32_t market_id;\n};\n```\n\n### 7b - Buy\n\n```c\nstruct buy {\n  uint8_t action_identifier;\n  uint8_t player_number;\n  uint8_t resource_type;\n  uint8_t amount;\n  uint32_t market_id;\n};\n```\n\n### 7e - Drop Relic\n\n```c\nstruct drop_relic {\n  uint8_t action_identifier;\n  uint32_t monk_id;\n};\n```\n\n### 7f - Townbell\n\n```c\nstruct townbell {\n  uint8_t action_identifier;\n  uint32_t building_id;\n  uint8_t active;\n};\n```\n\n### 80 - Back to Work\n\n```c\nstruct back_to_work {\n  uint8_t action_identifier;\n  uint32_t building_id;\n};\n```\n"
  },
  {
    "path": "doc/reverse_engineering/networking/technology_ids.md",
    "content": "# Technology IDs\n\nResearchID | Decimal | Name\n-----------|---------|-----------\n0x02       | 2       | Elite Tarkan\n0x08       | 8       | Town Watch\n0x0c       | 12      | Crop Rotation\n0x0d       | 13      | Heavy Plow\n0x0e       | 14      | Horse Collar\n0x0f       | 15      | Guilds\n0x11       | 17      | Banking\n0x13       | 19      | Cartography\n0x16       | 22      | Loom\n0x17       | 23      | Coinage\n0x1b       | 27      | Elite Kalmyuk\n0x22       | 34      | War Galley\n0x23       | 35      | Galleon\n0x25       | 37      | Cannon Galeon\n0x27       | 39      | Husbandry\n0x2d       | 45      | Faith\n0x2f       | 47      | Chemistry\n0x30       | 48      | Caravan\n0x32       | 50      | Masonry\n0x33       | 51      | Architecture\n0x36       | 54      | Treadmill Crane\n0x37       | 55      | Gold Mining\n0x3c       | 60      | Elite Conquistador\n0x3f       | 63      | Keep\n0x40       | 64      | Bombard Tower\n0x43       | 67      | Forging\n0x44       | 68      | Iron Casting\n0x4a       | 74      | Scale Mail Armor\n0x4b       | 75      | Blast Furnace\n0x4c       | 76      | Chain Mail Armor\n0x4d       | 77      | Plate Mail Armor\n0x50       | 80      | Plate Barding Armor\n0x51       | 81      | Scale Barding Armor\n0x52       | 82      | Chain Barding Armor\n0x5a       | 90      | Tracking\n0x5d       | 93      | Ballistics\n0x60       | 96      | Capped Ram\n0x62       | 98      | Elite Skirmisher\n0x64       | 100     | Crossbowman\n0x65       | 101     | Feudal Age\n0x66       | 102     | Castle Age\n0x67       | 103     | Imperial Age\n0x8c       | 140     | Guard Tower\n0xb6       | 182     | Gold Shaft Mining\n0xc2       | 194     | Fortified Wall\n0xc5       | 197     | Pikeman\n0xc7       | 199     | Fletching\n0xc8       | 200     | Bodkin Arrow\n0xc9       | 201     | Bracer\n0xca       | 202     | Double-Bit Axe\n0xcb       | 203     | Bow Saw\n0xcf       | 207     | Longswordsman\n0xd1       | 209     | Chevalier\n0xd3       | 211     | Padded Archer Armor\n0xd4       | 212     | Leather Archer Armor\n0xd5       | 213     | Wheelbarrow\n0xd7       | 215     | Squires\n0xd9       | 217     | Two-Handed Swordsman\n0xda       | 218     | Heavy Cavalry Archer\n0xdb       | 219     | Ring Archer Armor\n0xdd       | 221     | Two-Man Saw\n0xde       | 222     | Man-At-Arms\n0xe6       | 230     | Block Printing\n0xe7       | 231     | Sanctity\n0xe9       | 233     | Illumination\n0xec       | 236     | Heavy Camel\n0xed       | 237     | Arbelest\n0xef       | 238     | Heavy Scorpion\n0xf4       | 244     | Heavy Demolition Ship\n0xf6       | 246     | Heavy Fire Ship\n0xf9       | 249     | Hand Cart\n0xfc       | 252     | Fervor\n0xfe       | 254     | Light Cavalry\n0xff       | 255     | Siege Ram\n0x101      | 257     | Onager\n0x108      | 264     | Champion\n0x109      | 265     | Paladin\n0x116      | 278     | Stone Mining\n0x117      | 279     | Stone Shaft Mining\n0x118      | 280     | Town Patrol\n0x13b      | 315     | Conscription\n0x13c      | 316     | Redemption\n0x13f      | 319     | Atonement\n0x140      | 320     | Siege Onager\n0x141      | 321     | Sappers\n0x142      | 322     | Murder Holes\n0x168      | 360     | Elite Longbowman\n0x169      | 361     | Elite Cataphract\n0x16a      | 362     | Elite Cho Ko Nu\n0x16b      | 363     | Elite Throwing Axeman\n0x16d      | 364     | Elite Huscarl\n0x16c      | 365     | Elite Teutonic Knight\n0x16e      | 366     | Elite Samurai\n0x16f      | 367     | Elite War Elefant\n0x170      | 368     | Elite Mameluke\n0x171      | 369     | Elite Janissary\n0x172      | 370     | Elite Wood Raider\n0x173      | 371     | Elite Mangudai\n0x175      | 372     | Shipwright\n0x176      | 373     | Careening\n0x177      | 374     | Dry Dock\n0x178      | 375     | Elite Cannon Galleon\n0x179      | 376     | Siege Engineers\n0x17b      | 378     | Hoardings\n0x17c      | 379     | Heated Shot\n0x18e      | 398     | Elite Berserk\n0x198      | 408     | Spies\n0x1ac      | 428     | Husar\n0x1ad      | 429     | Helbardier\n0x1b0      | 432     | Elite Jaguar Warrior\n0x1b2      | 434     | Elite Eagle Warrior\n0x1b3      | 435     | Bloodlines\n0x1b4      | 436     | Parthian Tactics\n0x1b5      | 437     | Thumb Ring\n0x1b6      | 438     | Theocracy\n0x1b7      | 439     | Heresy\n0x1b9      | 441     | Herbal Medicine\n0x1c2      | 450     | Elite War Waggon\n"
  },
  {
    "path": "doc/reverse_engineering/scoring.md",
    "content": "# Score system\n\n## Age of Empires I\n\nThe score is based on 5 categories: military, economy, religion, technology and\nsurvival/wonders.\n\nIn case of equality there should not be any player that gets a bonus for the\nlargest number. However, the game engine does pick one player that gets the\nbonus. The player that gets the bonus is defined by a couple of factors:\n\n* If player X gets the largest number before player Y does, X gets the bonus\n  before Y does and keeps the bonus.\n* If both players get the largest number at the same gametick the player with\n  the lowest index (i.e. the game engine's internal assigned identifier) gets\n  the bonus.\n\n### Military\n\nTask                           | Score\n-------------------------------|-----------------------------------------------------------------\nKills                          | 0.5 points per unit\nBuildings destroyed            | 1 point per building\nGeneralship                    | number of kills minus number of losses (value must be positive)\nMost military units and towers | 25 point bonus\n\n### Economy\n\nTask                        | Score           | Notes\n----------------------------|-----------------|---------\nGold from mining and trade  | 1/100 of value  |\nNet resources tributed      | 1/60 of value   |\nVillager population         | 1 per villager  | includes trade, transport, and fishing vessels\nLargest villager population | 25 point bonus  | includes trade, transport, and fishing vessels\nExploration                 | 1 per 3% of map |\nLargest area explored       | 25 point bonus  |\n\n### Religion\n\nTask                              | Score\n----------------------------------|-------------------------\nConversion                        | 2 points per conversion\nMost conversions                  | 25 point bonus\nTemples built                     | 3 points per temple\nRuins controlled                  | 10 points per ruin\nArtifacts controlled              | 10 points per artifact\nControl of all Ruins or Artifacts | 50 point bonus\n\n### Technology\n\nTask                             | Score\n---------------------------------|-------------------------\nTechnologies researched          | 2 points per technology\nMost technologies researched     | 50 point bonus\nFirst civilization to Bronze Age | 25 point bonus\nFirst civilization to Iron Age   | 25 point bonus\n\n### Survival/Wonders\n\nTask                         | Score\n-----------------------------|-----------------------\nStill alive before game ends | 100 points\nWonders held                 | 100 points per wonder\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats.md",
    "content": "# Unit statistics\n\n Taken from a post of *Leif Ericson* from [AoK Heaven](http://aok.heavengames.com/university/game-info/stat-tables/units-table/).\n\n## Age of Kings\n\n| Name                      | Age | HP  |Att.|Att. Type|Reload Time|M. Arm.|P. Arm.| LOS |  R  | Acc.|Speed |Tr. Time|   Cost   | Attack Bonuses   |\n|---------------------------|-----|-----|----|---------|-----------|-------|-------|-----|-----|-----|------|--------|----------|------------------|\n| Militia                   | I   |  40 |  4 |    M    |    2.0    |    0  |    0  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | -                |\n| Man-at-Arms               | II  |  45 |  6 |    M    |    2.0    |    0  |    0  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Bldg             |\n| Long Swordsman            | III |  55 |  9 |    M    |    2.0    |    0  |    0  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Bldg             |\n| Two-Handed Swordsman      | IV  |  60 | 11 |    M    |    2.0    |    0  |    0  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Bldg             |\n| Champion                  | IV  |  70 | 13 |    M    |    2.0    |    1  |    0  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Bldg             |\n| Spearman                  | II  |  45 |  3 |    M    |    3.0    |    0  |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav, Eleph, Bldg |\n| Pikeman                   | III |  55 |  4 |    M    |    3.0    |    0  |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav, Eleph, Bldg |\n| Wood Raider               | III |  65 |  8 |    M    |    2.0    |    0  |    0  |  3  |  0  |   - | 1.03 |   10   | 65F/25G  | Bldg             |\n| Elite Wood Raider         | IV  |  80 | 13 |    M    |    2.0    |    0  |    0  |  5  |  0  |   - | 1.03 |   10   | 65F/25G  | Bldg             |\n| Throwing Axeman           | III |  50 |  7 |    M    |    2.0    |    0  |    0  |  5  |  3  | 100 | 0.9  |   17   | 55F/25G  | Bldg             |\n| Elite Throwing Axeman     | IV  |  60 |  8 |    M    |    2.0    |    1  |    0  |  6  |  4  | 100 | 0.9  |   17   | 55F/25G  | Bldg             |\n| Huskarl                   | III |  60 | 10 |    M    |    2.0    |    0  |    4  |  3  |  0  |   - | 0.9  |   16   | 80F/40G  | Arch,Bldg        |\n| Elite Huskarl             | IV  |  70 | 12 |    M    |    2.0    |    0  |    6  |  5  |  0  |   - | 0.9  |   16   | 80F/40G  | Arch,Bldg        |\n| Samurai                   | III |  60 |  8 |    M    |    1.9    |    1  |    0  |  4  |  0  |   - | 0.9  |   16   | 60F/30G  | Unique,Bldg      |\n| Elite Samurai             | IV  |  80 | 12 |    M    |    1.9    |    1  |    0  |  5  |  0  |   - | 0.9  |   16   | 60F/30G  | Unique,Bldg      |\n| Teutonic Knight           | III |  70 | 12 |    M    |    2.0    |    5  |    2  |  3  |  0  |   - | 0.65 |   19   | 85F/40G  | Bldg             |\n| Elite Teutonic Knight     | IV  | 100 | 17 |    M    |    2.0    |   10  |    2  |  5  |  0  |   - | 0.65 |   19   | 85F/40G  | Bldg             |\n| Berserk                   | III |  48 |  9 |    M    |    2.0    |    0  |    0  |  3  |  0  |   - | 0.9  |   16   | 65F/25G  | Bldg             |\n| Elite Berserk             | IV  |  60 | 14 |    M    |    2.0    |    2  |    0  |  5  |  0  |   - | 0.9  |   16   | 65F/25G  | Bldg             |\n| Archer                    | II  |  30 |  4 |    P    |    2.0    |    0  |    0  |  6  |  4  |  80 | 0.96 |   35   | 25W/45G  | -                |\n| Crossbowman               | III |  35 |  5 |    P    |    2.0    |    0  |    0  |  7  |  5  |  85 | 0.96 |   27   | 25W/45G  | -                |\n| Arbalest                  | IV  |  40 |  6 |    P    |    2.0    |    0  |    0  |  7  |  5  |  90 | 0.96 |   27   | 25W/45G  | -                |\n| Skirmisher                | II  |  30 |  2 |    P    |    3.0    |    0  |    3  |  6  |  4¹ |  90 | 0.96 |   22   | 25F/35W  | Arch             |\n| Elite Skirmisher          | III |  35 |  3 |    P    |    3.0    |    0  |    4  |  7  |  5¹ |  90 | 0.96 |   22   | 25F/35W  | Arch             |\n| Cavalry Archer            | III |  50 |  6 |    P    |    2.0    |    0  |    0  |  5  |  3  |  50 | 1.4  |   34   | 40W/70G  | -                |\n| Heavy Cavalry Archer      | IV  |  60 |  7 |    P    |    2.0    |    1  |    0  |  6  |  4  |  50 | 1.4  |   27   | 40W/70G  | -                |\n| Hand Cannoneer            | IV  |  35 | 17 |    P    |    3.45   |    1  |    0  |  9  |  7  |  65 | 0.96 |   34   | 45F/50G  | Inf,Ram          |\n| Longbowman                | III |  35 |  6 |    P    |    2.0    |    0  |    0  |  7  |  5  |  70 | 0.96 |   19   | 35W/40G  | -                |\n| Elite Longbowman          | IV  |  40 |  7 |    P    |    2.0    |    0  |    1  |  8  |  6  |  80 | 0.96 |   19   | 35W/40G  | -                |\n| Chu Ko Nu                 | III |  45 |  8²|    P    |    3.0    |    0  |    0  |  6  |  4  |  85 | 0.96 |   19   | 40W/35G  | -                |\n| Elite Chu Ko Nu           | IV  |  50 |  8²|    P    |    3.0    |    0  |    0  |  6  |  4  |  85 | 0.96 |   13   | 40W/35G  | -                |\n| Mangudai                  | III |  60 |  6 |    P    |    2.1    |    0  |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Siege            |\n| Elite Mangudai            | IV  |  60 |  8 |    P    |    2.1    |    1  |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Siege            |\n| Jannissary                | III |  35 | 15 |    P    |    3.45   |    1  |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Inf, Ram         |\n| Elite Jannissary          | IV  |  40 | 18 |    P    |    3.45   |    2  |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Inf, Ram         |\n| Scout Cavalry             | II  |  45 |  3³|    M    |    2.0    |    0  |    2  |  4³ |  0  |   - | 1.2³ |   30   | 80F      | -                |\n| Light Cavalry             | III |  60 |  7 |    M    |    2.0    |    0  |    2  |  4  |  0  |   - | 1.5  |   30   | 80F      | -                |\n| Knight                    | III | 100 | 10 |    M    |    1.8    |    2  |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                |\n| Cavalier                  | IV  | 120 | 12 |    M    |    1.8    |    2  |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                |\n| Paladin                   | IV  | 160 | 14 |    M    |    1.9    |    2  |    3  |  5  |  0  |   - | 1.35 |   30   | 60F/75G  | -                |\n| Camel                     | III | 100 |  5 |    M    |    2.0    |    0  |    0  |  4  |  0  |   - | 1.45 |   29   | 55F/60G  | Cav              |\n| Heavy Camel               | IV  | 120 |  7 |    M    |    2.0    |    0  |    0  |  5  |  0  |   - | 1.45 |   29   | 55F/60G  | Cav              |\n| Cataphract                | III | 110 |  9 |    M    |    1.8    |    2  |    1  |  4  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf              |\n| Elite Cataphract          | IV  | 150 | 12 |    M    |    1.7    |    2  |    1  |  5  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf              |\n| War Elephant              | III | 450 | 15 |    M    |    2.0    |    1  |    2  |  4  |  0  |   - | 0.6  |   31   | 200F/75G | Bldg             |\n| Elite War Elephant        | IV  | 600 | 20 |    M    |    2.0    |    1  |    3  |  5  |  0¤ |   - | 0.6  |   31   | 200F/75G | Bldg             |\n| Mameluke                  | III |  65 |  7 |    M    |    2.0    |    0  |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav              |\n| Elite Mameluke            | IV  |  80 | 10 |    M    |    2.0    |    1  |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav              |\n| Battering Ram             | III | 175 |  2 |    M    |    5.0    |   -3  |  180  |  3  |  0  |   - | 0.5  |   36   | 160W/75G | Bldg, Siege      |\n| Capped Ram                | IV  | 200 |  3 |    M    |    5.0    |   -3  |  190  |  3  |  0¤ |   - | 0.5  |   36   | 160W/75G | Bldg, Siege      |\n| Siege Ram                 | IV  | 270 |  4 |    M    |    5.0    |   -3  |  195  |  3  |  0¤ |   - | 0.6  |   36   | 160W/75G | Bldg, Siege      |\n| Mangonel                  | III |  50 | 40 |    M    |    6.0    |    0  |    6  |  9  |  7¹ | 100 | 0.6  |   46   | 160W/135G| Bldg             |\n| Onager                    | IV  |  60 | 50 |    M    |    6.0    |    0  |    7  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg             |\n| Siege Onager              | IV  |  70 | 75 |    M    |    6.0    |    0  |    8  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg             |\n| Scorpion                  | III |  40 | 12 |    P    |    3.6    |    0  |    6  |  9  |  5¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph, Bldg, Ram |\n| Heavy Scorpion            | IV  |  50 | 16 |    P    |    3.6    |    0  |    6  |  9  |  5¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph, Bldg, Ram |\n| Bombard Cannon            | IV  |  50 | 40 |    M    |    6.5    |    2  |    5  | 14  | 12¹ |  92 | 0.7  |   56   | 225W/225G| Bldg, Ship       |\n| Trebuchet (Packed)        | IV  | 150 |  - |    -    |     -     |    2  |    8  | 18  |  -  |   - | 0.8  |   50   | 200W/200G| -                |\n| Trebuchet                 | IV  | 150 |200 |    P    |   10.0    |    1  |  150  | 18  | 16¹ |  15 |  -   |   50   | 200W/200G| Bldg             |\n| Fishing Ship              | I   |  60 |  - |    -    |     -     |    0  |    4  |  5  |  -  |   - | 1.26 |   40   | 75W      | -                |\n| Transport Ship            | II  | 100 |  - |    -    |     -     |    4  |    8  |  5  |  -  |   - | 1.45 |   46   | 125W     | -                |\n| Trade Cog                 | II  |  80 |  - |    -    |     -     |    0  |    6  |  6  |  -  |   - | 1.32 |   36   | 100W/50G | -                |\n| Galley                    | II  | 120 |  6 |    P    |    3.0    |    0  |    6  |  7  |  5  | 100 | 1.43 |   36   | 90W/30G  | Ship, Bldg, Ram  |\n| War Galley                | III | 135 |  7 |    P    |    3.0    |    0  |    6  |  8  |  6  | 100 | 1.43 |   36   | 90W/30G  | Ship, Bldg, Ram  |\n| Galleon                   | IV  | 165 |  8 |    P    |    3.0    |    0  |    8  |  9  |  7  | 100 | 1.43 |   36   | 90W/30G  | Ship, Bldg, Ram  |\n| Fire Ship                 | III | 100 |  2²|    P    |    0.25   |    0  |    6  |  5  | 2.5 |   0 | 1.35 |   36   | 75W/45G  | Ship, Bldg       |\n| Fast Fire Ship            | IV  | 120 |  3²|    P    |    0.25   |    0  |    8  |  6  | 2.5 |   0 | 1.43 |   36   | 75W/45G  | Ship, Bldg       |\n| Demolition Ship           | III |  50 |110¤|    M    |     -     |    0  |    3  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg             |\n| Heavy Demolition Ship     | IV  |  60 |140¤|    M    |     -     |    0  |    5  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg             |\n| Cannon Galleon            | IV  | 120 | 35 |    M    |   10.0    |    0  |    6  | 15  | 13¹ |  50 | 1.1  |   46   | 200W/150G| Bldg             |\n| Elite Cannon Galleon      | IV  | 150 | 45 |    M    |   10.0    |    0  |    6  | 17  | 15¹ |  50 | 1.1  |   46   | 200W/150G| Bldg             |\n| Longboat                  | III | 130 |  7²|    P    |    3.0    |    0  |    6  |  8  |  6  | 100 | 1.54 |   41   | 100W/50G | -                |\n| Elite Longboat            | IV  | 160 |  8¹|    P    |    3.0    |    0  |    8  |  9  |  7  | 100 | 1.54 |   41   | 100W/50G | Ship, Bldg, Ram  |\n| Villager                  | I   |  25 |  3 |    M    |    2.0    |    0  |    0  |  4  |  0  |   - | 0.8  |   25   | 50F      | Bldg             |\n| Trade Cart                | II  |  70 |  - |    -    |     -     |    0  |    0  |  7  |  -  |   - | 1.0  |   51   | 100W/50G | -                |\n| Monk                      | III |  30 |  - |    C    |    1.0    |    0  |    0  | 11  |  9  |  25 | 0.7  |   51   | 100G     | -                |\n| King                      | I   |  75 |  - |    -    |     -     |    0  |    0  |  6  |  -  |   - | 1.32 |    -   | -        | -                |\n\n## Age of Conquerors\n\n| Name                      | Age | HP  |Att.|Att. Type|Reload Time|M. Arm.|P. Arm.| LOS |  R  | Acc.|Speed |Tr. Time|   Cost   | Attack Bonuses                |\n|---------------------------|-----|-----|----|---------|-----------|-------|-------|-----|-----|-----|------|--------|----------|-------------------------------|\n| Militia                   | I   |  40 |  4 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | -                             |\n| Man-at-Arms               | II  |  45 |  6 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Long Swordsman            | III |  55 |  9 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Two-Handed Swordsman      | IV  |  60 | 11 |    M    |    2.0    |   0   |    1  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Champion                  | IV  |  70 | 13 |    M    |    2.0    |   1   |    1  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Spearman                  | II  |  45 |  3 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Pikeman                   | III |  55 |  4 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Halberdier                | IV  |  60 |  6 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Eagle Warrior             | III |  50 |  4³|    M    |    2.0    |   0   |    2  |  6  |  0  |   - | 1.1³ |   35   | 20F/50G  | Monk,Siege,Cav³,Cam³,Ship³    |\n| Elite Eagle Warrior       | IV  |  60 |  9 |    M    |    2.0    |   0   |    4  |  6  |  0  |   - | 1.3  |   20   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Jaguar Warrior            | III |  50 | 10 |    M    |    2.0    |   1   |    0  |  3  |  0  |   - | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Elite Jaguar Warrior      | IV  |  75 | 12 |    M    |    2.0    |   2   |    0  |  5  |  0  |   - | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Wood Raider               | III |  65 |  8 |    M    |    2.0    |   0   |    1  |  3  |  0  |   - | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Elite Wood Raider         | IV  |  80 | 13 |    M    |    2.0    |   0   |    1  |  5  |  0  |   - | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Throwing Axeman           | III |  50 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 0.9  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Elite Throwing Axeman     | IV  |  60 |  8 |    M    |    2.0    |   1   |    0  |  6  |  4  | 100 | 0.9  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Huskarl                   | III |  60 | 10 |    M    |    2.0    |   0   |    6  |  3  |  0  |   - | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Elite Huskarl             | IV  |  70 | 12 |    M    |    2.0    |   0   |    8  |  5  |  0  |   - | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Samurai                   | III |  60 |  8 |    M    |    1.9    |   1   |    1  |  4  |  0  |   - | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Elite Samurai             | IV  |  80 | 12 |    M    |    1.9    |   1   |    1  |  5  |  0  |   - | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Teutonic Knight           | III |  70 | 12 |    M    |    2.0    |   5   |    2  |  3  |  0  |   - | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Elite Teutonic Knight     | IV  | 100 | 17 |    M    |    2.0    |  10   |    2  |  5  |  0  |   - | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Berserk                   | III |  48 |  9 |    M    |    2.0    |   0   |    1  |  3  |  0  |   - | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Elite Berserk             | IV  |  60 | 14 |    M    |    2.0    |   2   |    1  |  5  |  0  |   - | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Archer                    | II  |  30 |  4 |    P    |    2.0    |   0   |    0  |  6  |  4  |  80 | 0.96 |   35   | 25W/45G  | Spear                         |\n| Crossbowman               | III |  35 |  5 |    P    |    2.0    |   0   |    0  |  7  |  5  |  85 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Arbalest                  | IV  |  40 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  90 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Skirmisher                | II  |  30 |  2 |    P    |    3.0    |   0   |    3  |  6  |  4¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear                    |\n| Elite Skirmisher          | III |  35 |  3 |    P    |    3.0    |   0   |    4  |  7  |  5¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear,CavArch            |\n| Cavalry Archer            | III |  50 |  6 |    P    |    2.0    |   0   |    0  |  5  |  4  |  50 | 1.4  |   34   | 40W/70G  | Spear                         |\n| Heavy Cavalry Archer      | IV  |  60 |  7 |    P    |    2.0    |   1   |    0  |  6  |  4  |  50 | 1.4  |   27   | 40W/70G  | Spear                         |\n| Hand Cannoneer            | IV  |  35 | 17 |    P    |    3.45   |   1   |    0  |  9  |  7  |  65 | 0.96 |   34   | 45F/50G  | Inf,Ram,Spear                 |\n| Longbowman                | III |  35 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  70 | 0.96 |   19   | 35W/40G  | Spear                         |\n| Elite Longbowman          | IV  |  40 |  7 |    P    |    2.0    |   0   |    1  |  8  |  6  |  80 | 0.96 |   19   | 35W/40G  | Spear                         |\n| Chu Ko Nu                 | III |  45 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   19   | 40W/35G  | Spear                         |\n| Elite Chu Ko Nu           | IV  |  50 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   13   | 40W/35G  | Spear                         |\n| War Wagon                 | III | 150 |  9 |    P    |    2.5    |   0   |    3  |  7  |  4  | 100 | 1.2  |   25   | 120W/60G | Bldg                          |\n| Elite War Wagon           | IV  | 200 |  9 |    P    |    2.5    |   0   |    4  |  8  |  5  | 100 | 1.2  |   25   | 120W/60G | Bldg                          |\n| Plumed Archer             | III |  50 |  5 |    P    |    1.9    |   0   |    1  |  6  |  4  |  80 | 1.2  |   16   | 46W/46G  | Spear,Inf                     |\n| Elite Plumed Archer       | IV  |  65 |  5 |    P    |    1.9    |   0   |    2  |  7  |  5  |  90 | 1.2  |   16   | 46W/46G  | Spear,Inf                     |\n| Mangudai                  | III |  60 |  6 |    P    |    2.1    |   0   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Siege,Spear                   |\n| Elite Mangudai            | IV  |  60 |  8 |    P    |    2.1    |   1   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Siege,Spear                   |\n| Jannissary                | III |  35 | 17 |    P    |    3.45   |   1   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Elite Jannissary          | IV  |  40 | 22 |    P    |    3.45   |   2   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Scout Cavalry             | II  |  45 |  3³|    M    |    2.0    |   0   |    2  |  4³ |  0  |   - | 1.2³ |   30   | 80F      | Monk                          |\n| Light Cavalry             | III |  60 |  7 |    M    |    2.0    |   0   |    2  |  4  |  0  |   - | 1.5  |   30   | 80F      | Monk                          |\n| Hussar                    | IV  |  75 |  7 |    M    |    1.9    |   0   |    2  |  4  |  0  |   - | 1.5  |   30   | 80F      | Monk                          |\n| Knight                    | III | 100 | 10 |    M    |    1.8    |   2   |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Cavalier                  | IV  | 120 | 12 |    M    |    1.8    |   2   |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Paladin                   | IV  | 160 | 14 |    M    |    1.9    |   2   |    3  |  5  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Camel                     | III | 100 |  5 |    M    |    2.0    |   0   |    0  |  4  |  0  |   - | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Heavy Camel               | IV  | 120 |  7 |    M    |    2.0    |   0   |    0  |  5  |  0  |   - | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Cataphract                | III | 110 |  9 |    M    |    1.8    |   2   |    1  |  4  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf                           |\n| Elite Cataphract          | IV  | 150 | 12 |    M    |    1.7    |   2   |    1  |  5  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf                           |\n| Tarkan                    | III |  90 |  7 |    M    |    2.1    |   1   |    2  |  5  |  0  |   - | 1.35 |   14   | 60F/60G  | Bldg                          |\n| Elite Tarkan              | IV  | 150 | 11 |    M    |    2.1    |   1   |    3  |  7  |  0  |   - | 1.35 |   14   | 60F/60G  | Bldg                          |\n| War Elephant              | III | 450 | 15 |    M    |    2.0    |   1   |    2  |  4  |  0  |   - | 0.6  |   31   | 200F/75G | Bldg                          |\n| Elite War Elephant        | IV  | 600 | 20 |    M    |    2.0    |   1   |    3  |  5  |  0¤ |   - | 0.6  |   31   | 200F/75G | Bldg                          |\n| Mameluke                  | III |  65 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Elite Mameluke            | IV  |  80 | 10 |    M    |    2.0    |   1   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Conquistador              | III |  55 | 16 |    P    |    2.9    |   2   |    2  |  8  |  6  |  65 | 1.3  |   24   | 60F/70G  | Ram                           |\n| Elite Conquistador        | IV  |  70 | 18 |    P    |    2.9    |   2   |    2  |  9  |  6  |  70 | 1.3  |   24   | 60F/70G  | Ram,Bldg                      |\n| Battering Ram             | III | 175 |  2 |    M    |    5.0    |  -3   |  180  |  3  |  0  |   - | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Capped Ram                | IV  | 200 |  3 |    M    |    5.0    |  -3   |  190  |  3  |  0¤ |   - | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Siege Ram                 | IV  | 270 |  4 |    M    |    5.0    |  -3   |  195  |  3  |  0¤ |   - | 0.6  |   36   | 160W/75G | Bldg, Siege                   |\n| Mangonel                  | III |  50 | 40 |    M    |    6.0    |   0   |    6  |  9  |  7¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Onager                    | IV  |  60 | 50 |    M    |    6.0    |   0   |    7  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Siege Onager              | IV  |  70 | 75 |    M    |    6.0    |   0   |    8  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Scorpion                  | III |  40 | 12 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Heavy Scorpion            | IV  |  50 | 16 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Bombard Cannon            | IV  |  80 | 40 |    M    |    6.5    |   2   |    5  | 14  | 12¹ |  92 | 0.7  |   56   | 225W/225G| Bldg,Ship,Cam,Siege           |\n| Petard                    | III |  50 | 25 |    M    |    5.0    |   0   |    2  |  4  |  0¤ |   - | 0.8  |   25   | 80F/20G  | Bldg,Siege                    |\n| Trebuchet (Packed)        | IV  | 150 |  - |    -    |     -     |   2   |    8  | 18  |  -  |   - | 0.8  |   50   | 200W/200G| -                             |\n| Trebuchet                 | IV  | 150 |200 |    P    |   10.0    |   1   |  150  | 18  | 16¹ |  15 |  -   |   50   | 200W/200G| Bldg                          |\n| Fishing Ship              | I   |  60 |  - |    -    |     -     |   0   |    4  |  5  |  -  |   - | 1.26 |   40   | 75W      | -                             |\n| Transport Ship            | II  | 100 |  - |    -    |     -     |   4   |    8  |  5  |  -  |   - | 1.45 |   46   | 125W     | -                             |\n| Trade Cog                 | II  |  80 |  - |    -    |     -     |   0   |    6  |  6  |  -  |   - | 1.32 |   36   | 100W/50  | -                             |\n| Galley                    | II  | 120 |  6 |    P    |    3.0    |   0   |    6  |  7  |  5  | 100 | 1.43 |   60   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| War Galley                | III | 135 |  7 |    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Galleon                   | IV  | 165 |  8 |    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Fire Ship                 | III | 100 |  2 |    P    |    0.25   |   0   |    6  |  5  | 2.5 |   0 | 1.35 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Fast Fire Ship            | IV  | 120 |  3 |    P    |    0.25   |   0   |    8  |  6  | 2.5 |   0 | 1.43 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Demolition Ship           | III |  50 |110¤|    M    |     -     |   0   |    3  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Heavy Demolition Ship     | IV  |  60 |140¤|    M    |     -     |   0   |    5  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Cannon Galleon            | IV  | 120 | 35 |    M    |   10.0    |   0   |    6  | 15  | 13¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Elite Cannon Galleon      | IV  | 150 | 45 |    M    |   10.0    |   0   |    6  | 17  | 15¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Turtle Ship               | III | 200 | 50 |    M    |    6.0    |   6   |    5  |  8  |  6¹ | 100 | 0.9  |   50   | 200W/200G| -                             |\n| Elite Turtle Ship         | IV  | 300 | 50 |    M    |    6.0    |   8   |    6  |  8  |  6¹ | 100 | 0.9  |   50   | 200W/200G| -                             |\n| Longboat                  | III | 130 | 7² |    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.54 |   25   | 100W/50G | -                             |\n| Elite Longboat            | IV  | 160 | 8² |    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.54 |   25   | 100W/50G | Ship,Cam,Bldg,Ram             |\n| Villager                  | I   |  25 | 3  |    M    |    2.0    |   0   |    0  |  4  |  0  |   - | 0.8  |   25   | 50F      | Bldg                          |\n| Trade Cart                | II  |  70 | -  |    -    |     -     |   0   |    0  |  7  |  -  |   - | 1.0  |   51   | 100W/50G | -                             |\n| Monk                      | III |  30 | -  |    C    |    1.0    |   0   |    0  | 11  |  9  |  25 | 0.7  |   51   | 100G     | -                             |\n| Missionary                | III |  30 | -  |    C    |    1.0    |   0   |    0  |  9  |  7  |  25 | 1.1  |   51   | 100G     | -                             |\n| King                      | I   |  75 | -  |    -    |     -     |   0   |    0  |  6  |  -  |   - | 1.32 |    -   | -        | -                             |\n\n## Forgotten Empires\n\n| Name                      | Age |  HP |Att.|Att. Type|Reload Time|M. Arm.|P. Arm.| LOS |  R  | Acc.|Speed |Tr. Time|   Cost   | Attack Bonuses                |\n|---------------------------|-----|-----|----|---------|-----------|-------|-------|-----|-----|-----|------|--------|----------|-------------------------------|\n| Militia                   | I   |  40 |  4 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | -                             |\n| Man-at-Arms               | II  |  45 |  6 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Long Swordsman            | III |  60 |  9 |    M    |    2.0    |   0   |    1  |  4  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Two-Handed Swordsman      | IV  |  60 | 11 |    M    |    2.0    |   0   |    1  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Champion                  | IV  |  70 | 13 |    M    |    2.0    |   1   |    1  |  5  |  0  |   - | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Spearman                  | II  |  45 |  3 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Pikeman                   | III |  55 |  4 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Halberdier                | IV  |  60 |  6 |    M    |    3.0    |   0   |    0  |  4  |  0  |   - | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Eagle Scout               | III |  50 |  4³|    M    |    2.0    |   0   |    2  |  6  |  0  |   - | 1.1³ |   35   | 20F/50G  | Monk,Siege,Cav³,Cam³,Ship³    |\n| Eagle Warrior             | III |  55 |  7 |    M    |    2.0    |   0   |    3  |  6  |  0  |   - | 1.1  |   35   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Elite Eagle Warrior       | IV  |  60 |  9 |    M    |    2.0    |   0   |    4  |  6  |  0  |   - | 1.3  |   20   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Condottiero               | IV  |  80 |  9 |    M    |    1.9    |   1   |    3  |  6  |  0  |   - | 1.0  |   11   | 50F/35G  | Gun,Bldg                      |\n| Jaguar Warrior            | III |  50 | 10 |    M    |    2.0    |   1   |    1  |  3  |  0  |   - | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Elite Jaguar Warrior      | IV  |  75 | 12 |    M    |    2.0    |   2   |    1  |  5  |  0  |   - | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Wood Raider               | III |  65 |  8 |    M    |    2.0    |   0   |    1  |  3  |  0  |   - | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Elite Wood Raider         | IV  |  80 | 13 |    M    |    2.0    |   0   |    1  |  5  |  0  |   - | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Throwing Axeman           | III |  50 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 0.9  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Elite Throwing Axeman     | IV  |  60 |  8 |    M    |    2.0    |   1   |    0  |  6  |  4  | 100 | 0.9  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Huskarl                   | III |  60 | 10 |    M    |    2.0    |   0   |    6  |  3  |  0  |   - | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Elite Huskarl             | IV  |  70 | 12 |    M    |    2.0    |   0   |    8  |  5  |  0  |   - | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Samurai                   | III |  60 |  8 |    M    |    1.9    |   1   |    1  |  4  |  0  |   - | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Elite Samurai             | IV  |  80 | 12 |    M    |    1.9    |   1   |    1  |  5  |  0  |   - | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Teutonic Knight           | III |  80 | 12 |    M    |    2.0    |   5   |    2  |  3  |  0  |   - | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Elite Teutonic Knight     | IV  | 100 | 17 |    M    |    2.0    |  10   |    2  |  5  |  0  |   - | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Berserk                   | III |  54 |  9 |    M    |    2.0    |   0   |    1  |  3  |  0  |   - | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Elite Berserk             | IV  |  62 | 14 |    M    |    2.0    |   2   |    1  |  5  |  0  |   - | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Kamayuk                   | III |  60 |  7 |    M    |    2.0    |   0   |    0  |  4  |  1  | 100 | 0.95 |   10   | 60F/30G  | Cav,Cam                       |\n| Elite Kamayuk             | IV  |  80 |  8 |    M    |    2.0    |   1   |    0  |  5  |  1  | 100 | 0.95 |   10   | 60F/30G  | Cav,Cam                       |\n| Archer                    | II  |  30 |  4 |    P    |    2.0    |   0   |    0  |  6  |  4  |  80 | 0.96 |   35   | 25W/45G  | Spear                         |\n| Crossbowman               | III |  35 |  5 |    P    |    2.0    |   0   |    0  |  7  |  5  |  85 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Arbalest                  | IV  |  40 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  90 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Skirmisher                | II  |  30 |  2 |    P    |    3.0    |   0   |    3  |  6  |  4¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear                    |\n| Elite Skirmisher          | III |  35 |  3 |    P    |    3.0    |   0   |    4  |  7  |  5¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear,CavArch            |\n| Cavalry Archer            | III |  50 |  6 |    P    |    2.0    |   0   |    0  |  5  |  4  |  50 | 1.4  |   34   | 40W/65G  | Spear                         |\n| Heavy Cavalry Archer      | IV  |  60 |  7 |    P    |    2.0    |   1   |    0  |  6  |  4  |  50 | 1.4  |   27   | 40W/65G  | Spear                         |\n| Slinger                   | III |  40 |  5 |    P    |    2.0    |   0   |    0  |  7  |  5  |  90 | 0.96 |   25   | 30F/40G  | Inf,Ram,Spear                 |\n| Hand Cannoneer            | IV  |  35 | 17 |    P    |    3.45   |   1   |    0  |  9  |  7  |  65 | 0.96 |   34   | 45F/50G  | Inf,Ram,Spear                 |\n| Longbowman                | III |  35 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  70 | 0.96 |   18   | 35W/40G  | Spear                         |\n| Elite Longbowman          | IV  |  40 |  7 |    P    |    2.0    |   0   |    1  |  8  |  6  |  80 | 0.96 |   18   | 35W/40G  | Spear                         |\n| Chu Ko Nu                 | III |  45 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   19   | 40W/35G  | Spear                         |\n| Elite Chu Ko Nu           | IV  |  50 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   13   | 40W/35G  | Spear                         |\n| War Wagon                 | III | 150 |  9 |    P    |    2.5    |   0   |    3  |  7  |  4  | 100 | 1.2  |   21   | 120W/60G | Bldg                          |\n| Elite War Wagon           | IV  | 200 |  9 |    P    |    2.5    |   0   |    4  |  8  |  5  | 100 | 1.2  |   21   | 120W/60G | Bldg                          |\n| Plumed Archer             | III |  50 |  5 |    P    |    1.9    |   0   |    1  |  6  |  4  |  80 | 1.2  |   16   | 50W/50G  | Spear,Inf                     |\n| Elite Plumed Archer       | IV  |  65 |  5 |    P    |    1.9    |   0   |    2  |  7  |  5  |  90 | 1.2  |   16   | 50W/50G  | Spear,Inf                     |\n| Mangudai                  | III |  60 |  6 |    P    |    2.1    |   0   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Ram,Spear                     |\n| Elite Mangudai            | IV  |  60 |  8 |    P    |    2.1    |   1   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Ram,Spear                     |\n| Jannissary                | III |  35 | 17 |    P    |    3.45   |   1   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Elite Jannissary          | IV  |  40 | 22 |    P    |    3.45   |   2   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Genoese Crossbowman       | III |  45 |  6 |    P    |    3.0    |   1   |    0  |  8  |  4  | 100 | 0.96 |   22   | 50W/50G  | Cav,Eleph,Ship,Cam,Class 30   |\n| Elite Genoese Crossbowman | IV  |  50 |  6 |    P    |    2.0    |   1   |    0  |  8  |  4  | 100 | 0.96 |   22   | 50W/50G  | Cav,Eleph,Ship,Cam,Class 30   |\n| Elephant Archer           | III | 250 |  6 |    P    |    2.5    |   0   |    4  |  7  |  4  | 100 | 0.8  |   25   | 110F/80G | Bldg                          |\n| Elite Elephant Archer     | IV  | 350 |  7 |    P    |    2.5    |   0   |    4  |  7  |  4  | 100 | 0.8  |   25   | 110F/80G | Bldg                          |\n| Scout Cavalry             | II  |  45 |  3³|    M    |    2.0    |   0   |    2  |  4³ |  0  |   - | 1.2³ |   30   | 80F      | Monk                          |\n| Light Cavalry             | III |  60 |  7 |    M    |    2.0    |   0   |    2  |  4  |  0  |   - | 1.5  |   30   | 80F      | Monk                          |\n| Hussar                    | IV  |  75 |  7 |    M    |    1.9    |   0   |    2  |  4  |  0  |   - | 1.5  |   30   | 80F      | Monk                          |\n| Knight                    | III | 100 | 10 |    M    |    1.8    |   2   |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Cavalier                  | IV  | 120 | 12 |    M    |    1.8    |   2   |    2  |  4  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Paladin                   | IV  | 160 | 14 |    M    |    1.9    |   2   |    3  |  5  |  0  |   - | 1.35 |   30   | 60F/75G  | -                             |\n| Camel                     | III | 100 |  5 |    M    |    2.0    |   0   |    0  |  4  |  0  |   - | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Heavy Camel               | IV  | 120 |  7 |    M    |    2.0    |   0   |    0  |  5  |  0  |   - | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Imperial Camel            | IV  | 140 |  9 |    M    |    2.0    |   0   |    0  |  5  |  0  |   - | 1.45 |   20   | 55F/60G  | Cav,Cam,Ship                  |\n| Cataphract                | III | 110 |  9 |    M    |    1.8    |   2   |    1  |  4  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf                           |\n| Elite Cataphract          | IV  | 150 | 12 |    M    |    1.7    |   2   |    1  |  5  |  0  |   - | 1.35 |   20   | 70F/75G  | Inf                           |\n| Tarkan                    | III | 100 |  7 |    M    |    2.1    |   1   |    2  |  5  |  0  |   - | 1.35 |   14   | 60F/60G  | Bldg                          |\n| Elite Tarkan              | IV  | 150 | 11 |    M    |    2.1    |   1   |    3  |  7  |  0  |   - | 1.35 |   14   | 60F/60G  | Bldg                          |\n| War Elephant              | III | 450 | 15 |    M    |    2.0    |   1   |    2  |  4  |  0  |   - | 0.6  |   31   | 200F/75G | Bldg                          |\n| Elite War Elephant        | IV  | 600 | 20 |    M    |    2.0    |   1   |    3  |  5  |  0¤ |   - | 0.6  |   31   | 200F/75G | Bldg                          |\n| Mameluke                  | III |  65 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Elite Mameluke            | IV  |  80 | 10 |    M    |    2.0    |   1   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Conquistador              | III |  55 | 16 |    P    |    2.9    |   2   |    2  |  8  |  6  |  65 | 1.3  |   24   | 60F/70G  | Ram                           |\n| Elite Conquistador        | IV  |  70 | 18 |    P    |    2.9    |   2   |    2  |  9  |  6  |  70 | 1.3  |   24   | 60F/70G  | Ram,Bldg                      |\n| Magyar Hussar             | III |  70 |  9 |    M    |    1.8    |   0   |    2  |  5  |  0  |   - | 1.5  |   16   | 80F/10G  | Siege                         |\n| Elite Magyar Hussar       | IV  |  85 | 10 |    M    |    1.8    |   0   |    2  |  6  |  0  |   - | 1.5  |   16   | 80F/10G  | Siege                         |\n| Boyar                     | III | 100 | 12 |    M    |    1.9    |   4   |    1  |  5  |  0  |   - | 1.35 |   23   | 50F/80G  | -                             |\n| Elite Boyar               | IV  | 130 | 14 |    M    |    1.9    |   6   |    2  |  5  |  0  |   - | 1.35 |   20   | 50F/80G  | -                             |\n| Battering Ram             | III | 175 |  2 |    M    |    5.0    |  -3   |  180  |  3  |  0  |   - | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Capped Ram                | IV  | 200 |  3 |    M    |    5.0    |  -3   |  190  |  3  |  0¤ |   - | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Siege Ram                 | IV  | 270 |  4 |    M    |    5.0    |  -3   |  195  |  3  |  0¤ |   - | 0.6  |   36   | 160W/75G | Bldg, Siege                   |\n| Mangonel                  | III |  50 | 40 |    M    |    6.0    |   0   |    6  |  9  |  7¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Onager                    | IV  |  60 | 50 |    M    |    6.0    |   0   |    7  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Siege Onager              | IV  |  70 | 75 |    M    |    6.0    |   0   |    8  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Scorpion                  | III |  40 | 12 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Heavy Scorpion            | IV  |  50 | 16 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Bombard Cannon            | IV  |  80 | 40 |    M    |    6.5    |   2   |    5  | 14  | 12¹ |  92 | 0.7  |   56   | 225W/225G| Bldg,Ship,Cam,Siege           |\n| Petard                    | III |  50 | 25 |    M    |    5.0    |   0   |    2  |  4  |  0¤ |   - | 0.8  |   25   | 65F/20G  | Bldg,Siege                    |\n| Trebuchet (Packed)        | IV  | 150 | -  |    -    |    -      |   2   |    8  | 18  |  -  |   - | 0.8  |   50   | 200W/200G| -                             |\n| Trebuchet                 | IV  | 150 |200 |    P    |   10.0    |   1   |  150  | 18  | 16¹ |  15 |  -   |   50   | 200W/200G| Bldg                          |\n| Fishing Ship              | I   |  60 |  - |    -    |    -      |   0   |    4  |  5  |  -  |   - | 1.26 |   40   | 75W      | -                             |\n| Transport Ship            | II  | 100 |  - |    -    |    -      |   4   |    8  |  5  |  -  |   - | 1.45 |   46   | 125W     | -                             |\n| Trade Cog                 | II  |  80 |  - |    -    |    -      |   0   |    6  |  6  |  -  |   - | 1.32 |   36   | 100W/50G | -                             |\n| Galley                    | II  | 120 |  6 |    P    |    3.0    |   0   |    6  |  7  |  5  | 100 | 1.43 |   60   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| War Galley                | III | 135 |  7 |    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Galleon                   | IV  | 165 |  8 |    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Fire Ship                 | III | 120 |  2 |    P    |    0.25   |   0   |    6  |  5  |2.49 |   0 | 1.35 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Fast Fire Ship            | IV  | 140 |  3 |    P    |    0.25   |   0   |    8  |  6  |2.49 |   0 | 1.43 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Demolition Ship           | III |  60 |110¤|    M    |    -      |   0   |    3  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Heavy Demolition Ship     | IV  |  70 |140¤|    M    |    -      |   0   |    5  |  6  |  0  |   - | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Cannon Galleon            | IV  | 120 | 35 |    M    |   10.0    |   0   |    6  | 15  | 13¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Elite Cannon Galleon      | IV  | 150 | 45 |    M    |   10.0    |   0   |    6  | 17  | 15¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Turtle Ship               | III | 200 | 50 |    M    |    6.0    |   6   |    5  |  8  |  6¹ | 100 | 0.9  |   50   | 180W/180G| -                             |\n| Elite Turtle Ship         | IV  | 300 | 50 |    M    |    6.0    |   8   |    6  |  8  |  6¹ | 100 | 0.9  |   50   | 180W/180G| -                             |\n| Longboat                  | III | 130 |  7²|    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.54 |   25   | 90W/50G  | -                             |\n| Elite Longboat            | IV  | 160 |  8²|    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.54 |   25   | 90W/50G  | Ship,Cam,Bldg,Ram             |\n| Villager                  | I   |  25 |  3 |    M    |    2.0    |   0   |    0  |  4  |  0  |   - | 0.8  |   25   | 50F      | B ldg                         |\n| Trade Cart                | II  |  70 |  - |    -    |    -      |   0   |    0  |  7  |  -  |   - | 1.0  |   51   | 100W/50G | -                             |\n| Monk                      | III |  30 |  - |    C    |    1.0    |   0   |    0  | 11  |  9  |  25 | 0.7  |   51   | 100G     | -                             |\n| Missionary                | III |  30 |  - |    C    |    1.0    |   0   |    0  |  9  |  7  |  25 | 1.1  |   51   | 100G     | -                             |\n| King                      | I   |  75 |  - |    -    |    -      |   0   |    0  |  6  |  -  |   - | 1.32 |    -   | -        | -                             |\n\n## African Kingsdoms\n\n| Name                      | Age |  HP |Att.|Att. Type|Reload Time|M. Arm.|P. Arm.| LOS |  R  | Acc.|Speed |Tr. Time|   Cost   | Attack Bonuses                |\n|---------------------------|-----|-----|----|---------|-----------|-------|-------|-----|-----|-----|------|--------|----------|-------------------------------|\n| Militia                   | I   |  40 |  4 |    M    |    2.0    |   0   |    1  |  4  |  0  |  -  | 0.9  |   21   | 60F/20G  | -                             |\n| Man-at-Arms               | II  |  45 |  6 |    M    |    2.0    |   0   |    1  |  4  |  0  |  -  | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Long Swordsman            | III |  60 |  9 |    M    |    2.0    |   0   |    1  |  4  |  0  |  -  | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Two-Handed Swordsman      | IV  |  60 | 11 |    M    |    2.0    |   0   |    1  |  5  |  0  |  -  | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Champion                  | IV  |  70 | 13 |    M    |    2.0    |   1   |    1  |  5  |  0  |  -  | 0.9  |   21   | 60F/20G  | Eagle,Bldg                    |\n| Spearman                  | II  |  45 |  3 |    M    |    3.0    |   0   |    0  |  4  |  0  |  -  | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Pikeman                   | III |  55 |  4 |    M    |    3.0    |   0   |    0  |  4  |  0  |  -  | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Halberdier                | IV  |  60 |  6 |    M    |    3.0    |   0   |    0  |  4  |  0  |  -  | 1.0  |   22   | 35F/25W  | Cav,Eleph,Cam,Ship,Bldg,Eagle |\n| Eagle Scout               | II  |  50 |  4³|    M    |    2.0    |   0   |    2  |  6  |  0  |  -  | 1.1  |   35   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Eagle Warrior             | III |  55 |  7 |    M    |    2.0    |   0   |    3  |  6  |  0  |  -  | 1.2  |   35   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Elite Eagle Warrior       | IV  |  60 |  9 |    M    |    2.0    |   0   |    4  |  6  |  0  |  -  | 1.3  |   20   | 20F/50G  | Monk,Siege,Cav,Cam,Ship       |\n| Condottiero               | IV  |  80 | 10 |    M    |    1.9    |   1   |    3  |  6  |  0  |  -  | 1.2  |   11   | 50F/35G  | Gun,Bldg                      |\n| Jaguar Warrior            | III |  50 | 10 |    M    |    2.0    |   1   |    1  |  3  |  0  |  -  | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Elite Jaguar Warrior      | IV  |  75 | 12 |    M    |    2.0    |   2   |    1  |  5  |  0  |  -  | 1.0  |   20   | 60F/30G  | Inf,Eagle,Bldg                |\n| Wood Raider               | III |  65 |  8 |    M    |    2.0    |   0   |    1  |  3  |  0  |  -  | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Elite Wood Raider         | IV  |  80 | 13 |    M    |    2.0    |   0   |    1  |  5  |  0  |  -  | 1.2  |   10   | 65F/25G  | Eagle,Bldg                    |\n| Throwing Axeman           | III |  50 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 1.0  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Elite Throwing Axeman     | IV  |  60 |  8 |    M    |    2.0    |   1   |    0  |  6  |  4  | 100 | 1.0  |   17   | 55F/25G  | Eagle,Bldg                    |\n| Huskarl                   | III |  60 | 10 |    M    |    2.0    |   0   |    6  |  3  |  0  |  -  | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Elite Huskarl             | IV  |  70 | 12 |    M    |    2.0    |   0   |    8  |  5  |  0  |  -  | 1.05 |   16   | 80F/40G  | Arch,Eagle,Bldg               |\n| Samurai                   | III |  60 |  8 |    M    |    1.9    |   1   |    1  |  4  |  0  |  -  | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Elite Samurai             | IV  |  80 | 12 |    M    |    1.9    |   1   |    1  |  5  |  0  |  -  | 1.0  |    9   | 60F/30G  | Unique,Eagle,Bldg             |\n| Teutonic Knight           | III |  80 | 12 |    M    |    2.0    |   5   |    2  |  3  |  0  |  -  | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Elite Teutonic Knight     | IV  | 100 | 17 |    M    |    2.0    |  10   |    2  |  5  |  0  |  -  | 0.65 |   12   | 85F/40G  | Eagle,Bldg                    |\n| Berserk                   | III |  54 |  9 |    M    |    2.0    |   0   |    1  |  3  |  0  |  -  | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Elite Berserk             | IV  |  62 | 14 |    M    |    2.0    |   2   |    1  |  5  |  0  |  -  | 1.05 |   16   | 65F/25G  | Eagle,Bldg                    |\n| Kamayuk                   | III |  60 |  7 |    M    |    2.0    |   0   |    0  |  4  |  1  | 100 | 0.95 |   10   | 60F/30G  | Cav,Cam                       |\n| Elite Kamayuk             | IV  |  80 |  8 |    M    |    2.0    |   1   |    0  |  5  |  1  | 100 | 0.95 |   10   | 60F/30G  | Cav,Cam                       |\n| Gbeto                     | III |  30 | 10 |    M    |    2.0    |   0   |    0  |  6  |  5  | 100 | 1.25 |   17   | 50F/40G  | Eagle                         |\n| Elite Gbeto               | IV  |  45 | 14 |    M    |    2.0    |   0   |    0  |  7  |  6  | 100 | 1.25 |   17   | 50F/40G  | Eagle                         |\n| Shotel Warrior            | III |  40 | 16 |    M    |    2.0    |   0   |    0  |  3  |  0  |  -  | 1.2  |    8   | 50F/35G  | Eagle,Bldg                    |\n| Elite Shotel Warrior      | IV  |  50 | 18 |    M    |    2.0    |   0   |    0  |  3  |  0  |  -  | 1.2  |    8   | 50F/35G  | Eagle,Bldg                    |\n| Archer                    | II  |  30 |  4 |    P    |    2.0    |   0   |    0  |  6  |  4  |  80 | 0.96 |   35   | 25W/45G  | Spear                         |\n| Crossbowman               | III |  35 |  5 |    P    |    2.0    |   0   |    0  |  7  |  5  |  85 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Arbalest                  | IV  |  40 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  90 | 0.96 |   27   | 25W/45G  | Spear                         |\n| Skirmisher                | II  |  30 |  2 |    P    |    3.0    |   0   |    3  |  6  |  4¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear                    |\n| Elite Skirmisher          | III |  35 |  3 |    P    |    3.0    |   0   |    4  |  7  |  5¹ |  90 | 0.96 |   22   | 25F/35W  | Arch,Spear,CavArch            |\n| Cavalry Archer            | III |  50 |  6 |    P    |    2.0    |   0   |    0  |  5  |  4  |  50 | 1.4  |   34   | 40W/65G  | Spear                         |\n| Heavy Cavalry Archer      | IV  |  60 |  7 |    P    |    2.0    |   1   |    0  |  6  |  4  |  50 | 1.4  |   27   | 40W/65G  | Spear                         |\n| Slinger                   | II  |  40 |  5 |    P    |    2.0    |   0   |    0  |  7  |  5  |  90 | 0.96 |   25   | 30F/40G  | Inf,Ram,Spear                 |\n| Genitour                  | III |  50 |  4 |    P    |    3.0    |   0   |    3  |  5  |  3  |  90 | 1.35 |   25   | 50F/35W  | Arch                          |\n| Elite Genitour            | IV  |  55 |  4 |    P    |    3.0    |   0   |    4  |  6  |  4  |  90 | 1.35 |   23   | 50F/35W  | Arch,CavArch                  |\n| Hand Cannoneer            | IV  |  35 | 17 |    P    |    3.45   |   1   |    0  |  9  |  7  |  65 | 0.96 |   34   | 45F/50G  | Inf,Ram,Spear                 |\n| Longbowman                | III |  35 |  6 |    P    |    2.0    |   0   |    0  |  7  |  5  |  70 | 0.96 |   18   | 35W/40G  | Spear                         |\n| Elite Longbowman          | IV  |  40 |  7 |    P    |    2.0    |   0   |    1  |  8  |  6  |  80 | 0.96 |   18   | 35W/40G  | Spear                         |\n| Chu Ko Nu                 | III |  45 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   19   | 40W/35G  | Spear                         |\n| Elite Chu Ko Nu           | IV  |  50 |  8²|    P    |    3.0    |   0   |    0  |  6  |  4  |  85 | 0.96 |   13   | 40W/35G  | Spear                         |\n| War Wagon                 | III | 150 |  9 |    P    |    2.5    |   0   |    3  |  7  |  4  | 100 | 1.2  |   21   | 120W/60G | Bldg                          |\n| Elite War Wagon           | IV  | 200 |  9 |    P    |    2.5    |   0   |    4  |  8  |  5  | 100 | 1.2  |   21   | 120W/60G | Bldg                          |\n| Plumed Archer             | III |  50 |  5 |    P    |    1.9    |   0   |    1  |  6  |  4  |  80 | 1.2  |   16   | 50W/50G  | Spear,Inf                     |\n| Elite Plumed Archer       | IV  |  65 |  5 |    P    |    1.9    |   0   |    2  |  7  |  5  |  90 | 1.2  |   16   | 50W/50G  | Spear,Inf                     |\n| Mangudai                  | III |  60 |  6 |    P    |    2.1    |   0   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Ram,Spear                     |\n| Elite Mangudai            | IV  |  60 |  8 |    P    |    2.1    |   1   |    0  |  6  |  4  |  95 | 1.45 |   26   | 55W/65G  | Ram,Spear                     |\n| Jannissary                | III |  35 | 17 |    P    |    3.45   |   1   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Elite Jannissary          | IV  |  40 | 22 |    P    |    3.45   |   2   |    0  | 10  |  8  |  50 | 0.96 |   21   | 60F/55G  | Ram                           |\n| Genoese Crossbowman       | III |  45 |  6 |    P    |    3.0    |   1   |    0  |  8  |  4  | 100 | 0.96 |   22   | 50W/50G  | Cav,Eleph,Ship,Cam,Class 30   |\n| Elite Genoese Crossbowman | IV  |  50 |  6 |    P    |    2.0    |   1   |    0  |  8  |  4  | 100 | 0.96 |   22   | 50W/50G  | Cav,Eleph,Ship,Cam,Class 30   |\n| Elephant Archer           | III | 280 |  6 |    P    |    2.5    |   0   |    3  |  7  |  4  | 100 | 0.8  |   25   | 100F/80G | Bldg                          |\n| Elite Elephant Archer     | IV  | 330 |  7 |    P    |    2.5    |   0   |    3  |  7  |  4  | 100 | 0.8  |   25   | 100F/80G | Bldg                          |\n| Camel Archer              | III |  60 |  7 |    P    |    2.0    |   0   |    1  |  5  |  4  |  95 | 1.4  |   21   | 50W/60G  | CavArch,Inf,Ram               |\n| Elite Camel Archer        | IV  |  65 |  8 |    P    |    2.0    |   1   |    1  |  5  |  4  |  95 | 1.4  |   21   | 50W/60G  | CavArch,Inf,Ram               |\n| Scout Cavalry             | II  |  45 |  3³|    M    |    2.0    |   0   |    2  |  4³ |  0  |  -  | 1.2³ |   30   | 80F      | Monk                          |\n| Light Cavalry             | III |  60 |  7 |    M    |    2.0    |   0   |    2  |  4  |  0  |  -  | 1.5  |   30   | 80F      | Monk                          |\n| Hussar                    | IV  |  75 |  7 |    M    |    1.9    |   0   |    2  |  4  |  0  |  -  | 1.5  |   30   | 80F      | Monk                          |\n| Knight                    | III | 100 | 10 |    M    |    1.8    |   2   |    2  |  4  |  0  |  -  | 1.35 |   30   | 60F/75G  | -                             |\n| Cavalier                  | IV  | 120 | 12 |    M    |    1.8    |   2   |    2  |  4  |  0  |  -  | 1.35 |   30   | 60F/75G  | -                             |\n| Paladin                   | IV  | 160 | 14 |    M    |    1.9    |   2   |    3  |  5  |  0  |  -  | 1.35 |   30   | 60F/75G  | -                             |\n| Camel                     | III | 100 |  5 |    M    |    2.0    |   0   |    0  |  4  |  0  |  -  | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Heavy Camel               | IV  | 120 |  7 |    M    |    2.0    |   0   |    0  |  5  |  0  |  -  | 1.45 |   22   | 55F/60G  | Cav,Cam,Ship                  |\n| Imperial Camel            | IV  | 140 |  9 |    M    |    2.0    |   0   |    0  |  5  |  0  |  -  | 1.45 |   20   | 55F/60G  | Cav,Cam,Ship                  |\n| Cataphract                | III | 110 |  9 |    M    |    1.8    |   2   |    1  |  4  |  0  |  -  | 1.35 |   20   | 70F/75G  | Inf                           |\n| Elite Cataphract          | IV  | 150 | 12 |    M    |    1.7    |   2   |    1  |  5  |  0  |  -  | 1.35 |   20   | 70F/75G  | Inf                           |\n| Tarkan                    | III | 100 |  7 |    M    |    2.1    |   1   |    2  |  5  |  0  |  -  | 1.35 |   14   | 60F/60G  | Bldg                          |\n| Elite Tarkan              | IV  | 150 | 11 |    M    |    2.1    |   1   |    3  |  7  |  0  |  -  | 1.35 |   14   | 60F/60G  | Bldg                          |\n| War Elephant              | III | 450 | 15 |    M    |    2.0    |   1   |    2  |  4  |  0  |  -  | 0.6  |   31   | 200F/75G | Bldg                          |\n| Elite War Elephant        | IV  | 600 | 20 |    M    |    2.0    |   1   |    3  |  5  |  0¤ |  -  | 0.6  |   31   | 200F/75G | Bldg                          |\n| Mameluke                  | III |  65 |  7 |    M    |    2.0    |   0   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Elite Mameluke            | IV  |  80 | 10 |    M    |    2.0    |   1   |    0  |  5  |  3  | 100 | 1.4  |   23   | 55F/85G  | Cav                           |\n| Conquistador              | III |  55 | 16 |    P    |    2.9    |   2   |    2  |  8  |  6  |  65 | 1.3  |   24   | 60F/70G  | Ram                           |\n| Elite Conquistador        | IV  |  70 | 18 |    P    |    2.9    |   2   |    2  |  9  |  6  |  70 | 1.3  |   24   | 60F/70G  | Ram,Bldg                      |\n| Magyar Hussar             | III |  70 |  9 |    M    |    1.8    |   0   |    2  |  5  |  0  |  -  | 1.5  |   16   | 80F/10G  | Siege                         |\n| Elite Magyar Hussar       | IV  |  85 | 10 |    M    |    1.8    |   0   |    2  |  6  |  0  |  -  | 1.5  |   16   | 80F/10G  | Siege                         |\n| Boyar                     | III | 100 | 12 |    M    |    1.9    |   4   |    1  |  5  |  0  |  -  | 1.35 |   23   | 50F/80G  | -                             |\n| Elite Boyar               | IV  | 130 | 14 |    M    |    1.9    |   6   |    2  |  5  |  0  |  -  | 1.35 |   20   | 50F/80G  | -                             |\n| Siege Tower               | III | 220 |  - |    -    |     -     |   0   |  100  |  8  |  -  |  -  | 0.8  |   36   | 300W/160G| -                             |\n| Organ Gun                 | III |  60 | 16²|    P    |    3.45   |   2   |    4  |  9  |  7  |  50 | 0.85 |   21   | 80W/70G  | Inf,Bldg                      |\n| Elite Organ Gun           | IV  |  70 | 20²|    P    |    3.45   |   2   |    6  |  9  |  7  |  50 | 0.85 |   21   | 80W/70G  | Inf,Bldg                      |\n| Battering Ram             | III | 175 |  2 |    M    |    5.0    |  -3   |  180  |  3  |  0  |  -  | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Capped Ram                | IV  | 200 |  3 |    M    |    5.0    |  -3   |  190  |  3  |  0¤ |  -  | 0.5  |   36   | 160W/75G | Bldg, Siege                   |\n| Siege Ram                 | IV  | 270 |  4 |    M    |    5.0    |  -3   |  195  |  3  |  0¤ |  -  | 0.6  |   36   | 160W/75G | Bldg, Siege                   |\n| Mangonel                  | III |  50 | 40 |    M    |    6.0    |   0   |    6  |  9  |  7¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Onager                    | IV  |  60 | 50 |    M    |    6.0    |   0   |    7  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Siege Onager              | IV  |  70 | 75 |    M    |    6.0    |   0   |    8  | 10  |  8¹ | 100 | 0.6  |   46   | 160W/135G| Bldg,Siege                    |\n| Scorpion                  | III |  40 | 12 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Heavy Scorpion            | IV  |  50 | 16 |    P    |    3.6    |   0   |    6  |  9  |  7¹ | 100 | 0.65 |   30   | 75W/75G  | Eleph,Bldg,Ram                |\n| Bombard Cannon            | IV  |  80 | 40 |    M    |    6.5    |   2   |    5  | 14  | 12¹ |  92 | 0.7  |   56   | 225W/225G| Bldg,Ship,Cam,Siege           |\n| Petard                    | III |  50 | 25 |    M    |    5.0    |   0   |    2  |  4  |  0¤ |  -  | 0.8  |   25   | 65F/20G  | Bldg,Siege                    |\n| Trebuchet (Packed)        | IV  | 150 |  - |    -    |     -     |   2   |    8  | 18  |  -  |  -  | 0.8  |   50   | 200W/200G| -                             |\n| Trebuchet                 | IV  | 150 |200 |    P    |   10.0    |   1   |  150  | 18  | 16¹ |  15 |  -   |   50   | 200W/200G| Bldg                          |\n| Fishing Ship              | I   |  60 |  - |    -    |     -     |   0   |    4  |  5  |  -  |  -  | 1.26 |   40   | 75W      | -                             |\n| Transport Ship            | II  | 100 |  - |    -    |     -     |   4   |    8  |  5  |  -  |  -  | 1.45 |   46   | 125W     | -                             |\n| Trade Cog                 | II  |  80 |  - |    -    |     -     |   0   |    6  |  6  |  -  |  -  | 1.32 |   36   | 100W/50G | -                             |\n| Galley                    | II  | 120 |  6 |    P    |    3.0    |   0   |    6  |  7  |  5  | 100 | 1.43 |   60   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| War Galley                | III | 135 |  7 |    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Galleon                   | IV  | 165 |  8 |    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.43 |   36   | 90W/30G  | Ship,Cam,Bldg,Ram             |\n| Fire Galley               | II  | 100 |  1 |    P    |    0.25   |   0   |    5  |  5  |2.49 |   0 | 1.3  |   60   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Fire Ship                 | III | 120 |  2 |    P    |    0.25   |   0   |    7  |  5  |2.49 |   0 | 1.35 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Fast Fire Ship            | IV  | 140 |  3 |    P    |    0.25   |   0   |    9  |  6  |2.49 |   0 | 1.43 |   36   | 75W/45G  | Ship,Cam,Turtle,Bldg          |\n| Demolition Raft           | II  |  50 | 90¤|    M    |     -     |   0   |    3  |  6  |  0  |  -  | 1.6  |   45   | 70W/50G  | Bldg                          |\n| Demolition Ship           | III |  60 |110¤|    M    |     -     |   0   |    3  |  6  |  0  |  -  | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Heavy Demolition Ship     | IV  |  70 |140¤|    M    |     -     |   0   |    5  |  6  |  0  |  -  | 1.6  |   31   | 70W/50G  | Bldg                          |\n| Cannon Galleon            | IV  | 120 | 35 |    M    |   10.0    |   0   |    6  | 15  | 13¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Elite Cannon Galleon      | IV  | 150 | 45 |    M    |   10.0    |   0   |    6  | 17  | 15¹ |  50 | 1.1  |   46   | 200W/150G| Bldg,Siege,Inf,Arch,Cav       |\n| Turtle Ship               | III | 200 | 50 |    M    |    6.0    |   6   |    5  |  8  |  6¹ | 100 | 0.9  |   50   | 180W/180G| -                             |\n| Elite Turtle Ship         | IV  | 300 | 50 |    M    |    6.0    |   8   |    6  |  8  |  6¹ | 100 | 0.9  |   50   | 180W/180G| -                             |\n| Longboat                  | III | 130 |  7²|    P    |    3.0    |   0   |    6  |  8  |  6  | 100 | 1.54 |   25   | 90W/50G  | -                             |\n| Elite Longboat            | IV  | 160 |  8²|    P    |    3.0    |   0   |    8  |  9  |  7  | 100 | 1.54 |   25   | 90W/50G  | Ship,Cam,Bldg,Ram             |\n| Villager                  | I   |  25 |  3 |    M    |    2.0    |   0   |    0  |  4  |  0  |  -  | 0.8  |   25   | 50F      | Bldg                          |\n| Trade Cart                | II  |  70 |  - |    -    |     -     |   0   |    0  |  7  |  -  |  -  | 1.0  |   51   | 100W/50G | -                             |\n| Monk                      | III |  30 |  - |    C    |    1.0    |   0   |    0  | 11  |  9  |  25 | 0.7  |   51   | 100G     | -                             |\n| Missionary                | III |  30 |  - |    C    |    1.0    |   0   |    0  |  9  |  7  |  25 | 1.1  |   51   | 100G     | -                             |\n| King                      | I   |  75 |  - |    -    |     -     |   0   |    0  |  6  |  -  |  -  | 1.32 |    -   | -        | -                             |\n\n¹   minimum range existing\n\n² per arrow\n\n³ stats change in feudal age\n\n¤ also damage to adjacent tiles\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_afr.csv",
    "content": "Icon,Name,Age,HP,Att.,Att. Type,Reload Time,M. Arm.,P. Arm.,LOS,R,Acc.,Sp.,Tr. Time,Cost,Attack Bonuses\r\n,Militia,I,40,4,M,2.0,0,1,4,0,-,0.9,21,60F 20G,-\r\n,Man-at-Arms,II,45,6,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Long Swordsman,III,60,9,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Two-Handed Swordsman,IV,60,11,M,2.0,0,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Champion,IV,70,13,M,2.0,1,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Spearman,II,45,3,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Pikeman,III,55,4,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Eleph, Cav, Cam, Ship, Bldg, Eagle\"\r\n,Halberdier,IV,60,6,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Eagle Scout,II,50,4,M,2.0,0,2,6,0,-,1.1,35,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Eagle Warrior,III,55,7,M,2.0,0,3,6,0,-,1.2,35,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Elite Eagle Warrior,IV,60,9,M,2.0,0,4,6,0,-,1.3,20,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Condottiero,IV,80,10,M,1.9,1,3,6,0,-,1.2,11,50F 35G,\"Gun, Bldg\"\r\n,Jaguar Warrior,III,50,10,M,2.0,1,1,3,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Elite Jaguar Warrior,IV,75,12,M,2.0,2,1,5,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Woad Raider,III,65,8,M,2.0,0,1,3,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Elite Woad Raider,IV,80,13,M,2.0,0,1,5,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Throwing Axeman,III,50,7,M,2.0,0,0,5,3,100,1.0,17,55F 25G,\"Bldg, Eagle\"\r\n,Elite Throwing Axeman,IV,60,8,M,2.0,1,0,6,4,100,1.0,17,55F 25G,\"Bldg, Eagle\"\r\n,Huskarl,III,60,10,M,2.0,0,6,3,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Elite Huskarl,IV,70,12,M,2.0,0,8,5,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Samurai,III,60,8,M,1.9,1,1,4,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Elite Samurai,IV,80,12,M,1.9,1,1,5,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Teutonic Knight,III,80,12,M,2.0,5,2,3,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Elite Teutonic Knight,IV,100,17,M,2.0,10,2,5,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Berserk,III,54,9,M,2.0,0,1,3,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Elite Berserk,IV,62,14,M,2.0,2,1,5,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Kamayuk,III,60,7,M,2.0,0,0,4,1,100,0.95,10,60F 30G,\"Cav, Cam\"\r\n,Elite Kamayuk,IV,80,8,M,2.0,1,0,5,1,100,0.95,10,60F 30G,\"Cav, Cam\"\r\n,Gbeto,III,30,10,M,2.0,0,0,6,5,100,1.25,17,50F 40G,Eagle\r\n,Elite Gbeto,IV,45,14,M,2.0,0,0,7,6,100,1.25,17,50F 40G,Eagle\r\n,Shotel Warrior,III,40,16,M,2.0,0,0,3,0,-,1.2,8,50F 35G,\"Bldg, Eagle\"\r\n,Elite Shotel Warrior,IV,50,18,M,2.0,0,0,3,0,-,1.2,8,50F 35G,\"Bldg, Eagle\"\r\n,Archer,II,30,4,P,2.0,0,0,6,4,80,0.96,35,25W 45G,Spear\r\n,Crossbowman,III,35,5,P,2.0,0,0,7,5,85,0.96,27,25W 45G,Spear\r\n,Arbalest,IV,40,6,P,2.0,0,0,7,5,90,0.96,27,25W 45G,Spear\r\n,Skirmisher,II,30,2,P,3.0,0,3,6,4,90,0.96,22,25F 35W,\"Arch, Spear\"\r\n,Elite Skirmisher,III,35,3,P,3.0,0,4,7,5,90,0.96,22,25F 35W,\"Arch, Spear, Cav Arch\"\r\n,Cavalry Archer,III,50,6,P,2.0,0,0,5,4,50,1.4,34,40W 60G,Spear\r\n,Heavy Cavalry Archer,IV,60,7,P,2.0,1,0,6,4,50,1.4,27,40W 65G,Spear\r\n,Slinger,III,40,5,P,2.0,0,0,7,5,90,0.96,25,30F 40G,\"Inf, Ram, Spear\"\r\n,Genitour,III,50,4,P,3.0,0,3,5,3,90,1.35,25,50F 35W,Arch\r\n,Elite Genitour,IV,55,4,P,3.0,0,4,6,4,90,1.35,23,50F 35W,\"Arch, Cav Arch\"\r\n,Hand Cannoneer,IV,35,17,P,3.45,1,0,9,7,65,0.96,34,45F 50G,\"Inf, Ram, Spear\"\r\n,Longbowman,III,35,6,P,2.0,0,0,7,5,70,0.96,18,35W 40G,Spear\r\n,Elite Longbowman,IV,40,7,P,2.0,0,1,8,6,80,0.96,18,35W 40G,Spear\r\n,Chu Ko Nu,III,45,8,P,3.0,0,0,6,4,85,0.96,19,40W 35G,Spear\r\n,Elite Chu Ko Nu,IV,50,8,P,3.0,0,0,6,4,85,0.96,13,40W 35G,Spear\r\n,War Wagon,III,150,9,P,2.5,0,3,7,4,100,1.2,21,120W 60G,Bldg\r\n,Elite War Wagon,IV,200,9,P,2.5,0,4,8,5,100,1.2,21,120W 60G,Bldg\r\n,Plumed Archer,III,50,5,P,1.9,0,1,6,4,80,1.2,16,50W 50G,\"Spear, Inf\"\r\n,Elite Plumed Archer,IV,65,5,P,1.9,0,2,7,5,90,1.2,16,50W 50G,\"Spear, Inf\"\r\n,Mangudai,III,60,6,P,2.1,0,0,6,4,95,1.45,26,55W 65G,\"Ram, Spear\"\r\n,Elite Mangudai,IV,60,8,P,2.1,1,0,6,4,95,1.45,26,55W 65G,\"Ram, Spear\"\r\n,Janissary,III,35,17,P,3.45,1,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Elite Janissary,IV,40,22,P,3.45,2,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Genoese Crossbowman,III,45,6,P,3.0,1,0,8,4,100,0.96,22,50W 50G,\"Cav, Eleph, Ship, Cam, Class 30\"\r\n,Elite Genoese Crossbowman,IV,50,6,P,2.0,1,0,8,4,100,0.96,22,50W 50G,\"Cav, Eleph, Ship, Cam, Class 30\"\r\n,Elephant Archer,III,280,6,P,2.5,0,3,7,4,100,0.8,25,100F 80G,Bldg\r\n,Elite Elephant Archer,IV,330,7,P,2.5,0,3,7,4,100,0.8,25,100F 80G,Bldg\r\n,Camel Archer,III,60,7,P,2.0,0,1,5,4,95,1.4,21,50W 60G,\"Cav Arch, Ram, Inf\"\r\n,Elite Camel Archer,IV,65,8,P,2.0,1,1,5,4,95,1.4,21,50W 60G,\"Cav Arch, Ram, Inf\"\r\n,Scout Cavalry,II,45,3,M,2.0,0,2,4,0,-,1.2,30,80F,Monk\r\n,Light Cavalry,III,60,7,M,2.0,0,2,4,0,-,1.5,30,80F,Monk\r\n,Hussar,IV,75,7,M,1.9,0,2,4,0,-,1.5,30,80F,Monk\r\n,Knight,III,100,10,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Cavalier,IV,120,12,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Paladin,IV,160,14,M,1.9,2,3,5,0,-,1.35,30,60F 75G,-\r\n,Camel,III,100,5,M,2.0,0,0,4,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Heavy Camel,IV,120,7,M,2.0,0,0,5,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Imperial Camel,IV,140,9,M,2.0,0,0,5,0,-,1.45,20,55F 60G,\"Cav, Cam, Ship\"\r\n,Cataphract,III,110,9,M,1.8,2,1,4,0,-,1.35,20,70F 75G,Inf\r\n,Elite Cataphract,IV,150,12,M,1.7,2,1,5,0,-,1.35,20,70F 75G,Inf\r\n,Tarkan,III,100,7,M,2.1,1,2,5,0,-,1.35,14,60F 60G,Bldg\r\n,Elite Tarkan,IV,150,11,M,2.1,1,3,7,0,-,1.35,14,60F 60G,Bldg\r\n,War Elephant,III,450,15,M,2.0,1,2,4,0,-,0.6,31,200F 75G,Bldg\r\n,Elite War Elephant,IV,600,20,M,2.0,1,3,5,0,-,0.6,31,200F 75G,Bldg\r\n,Mameluke,III,65,7,M,2.0,0,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Elite Mameluke,IV,80,10,M,2.0,1,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Conquistador,III,55,16,P,2.9,2,2,8,6,65,1.3,24,60F 70G,Ram\r\n,Elite Conquistador,IV,70,18,P,2.9,2,2,9,6,70,1.3,24,60F 70G,\"Ram, Bldg\"\r\n,Magyar Huszar,III,70,9,M,1.8,0,2,5,0,-,1.5,16,80F 10G,\"Siege, Ram\"\r\n,Elite Magyar Huszar,IV,85,10,M,1.8,0,2,6,0,-,1.5,16,80F 10G,\"Siege, Ram\"\r\n,Boyar,III,100,12,M,1.9,4,1,5,0,-,1.35,23,50F 80G,-\r\n,Elite Boyar,IV,130,14,M,1.9,6,2,5,0,-,1.35,20,50F 80G,-\r\n,Siege Tower,III,220,-,-,-,0,100,8,-,-,0.8,36,300W 160G,-\r\n,Organ Gun,III,60,16,P,3.45,2,4,9,7,50,0.85,21,80W 70G,\"Bldg, Inf\"\r\n,Elite Organ Gun,IV,70,20,P,3.45,2,6,9,7,50,0.85,21,80W 70G,\"Bldg, Inf\"\r\n,Battering Ram,III,175,2,M,5.0,-3,180,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Capped Ram,IV,200,3,M,5.0,-3,190,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Siege Ram,IV,270,4,M,5.0,-3,195,3,0,-,0.6,36,160W 75G,\"Bldg, Siege\"\r\n,Mangonel,III,50,40,M,6.0,0,6,9,7,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Onager,IV,60,50,M,6.0,0,7,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Siege Onager,IV,70,75,M,6.0,0,8,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Scorpion,III,40,12,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Heavy Scorpion,IV,50,16,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Bombard Cannon,IV,80,40,M,6.5,2,5,14,12,92,0.7,56,225W 225G,\"Bldg, Ship, Cam, Siege\"\r\n,Petard,III,50,25,M,5,0,2,4,0,-,0.8,25,65F 20G,\"Bldg, Siege\"\r\n,Trebuchet (Packed),IV,150,-,-,-,2,8,18,-,-,0.8,50,200W 200G,-\r\n,Trebuchet,IV,150,200,P,10.0,1,150,18,16,15,-,50,200W 200G,Bldg\r\n,Fishing Ship,I,60,-,-,-,0,4,5,-,-,1.26,40,75W,-\r\n,Transport Ship,II,100,-,-,-,4,8,5,-,-,1.45,46,125W,-\r\n,Trade Cog,II,80,-,-,-,0,6,6,-,-,1.32,36,100W 50G,-\r\n,Galley,II,120,6,P,3.0,0,6,7,5,100,1.43,60,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,War Galley,III,135,7,P,3.0,0,6,8,6,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Galleon,IV,165,8,P,3.0,0,8,9,7,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Fire Galley,II,100,1,P,0.25,0,5,5,2.49,0,1.3,60,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Fire Ship,III,120,2,P,0.25,0,7,5,2.49,0,1.35,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Fast Fire Ship,IV,140,3,P,0.25,0,9,6,2.49,0,1.43,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Demolition Raft,II,50,90,M,-,0,3,6,0,-,1.6,45,70W 50G,Bldg\r\n,Demolition Ship,III,60,110,M,-,0,3,6,0,-,1.6,31,70W 50G,Bldg\r\n,Heavy Demolition Ship,IV,70,140,M,-,0,5,6,0,-,1.6,31,70W 50G,Bldg\r\n,Cannon Galleon,IV,120,35,M,10.0,0,6,15,13,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Elite Cannon Galleon,IV,150,45,M,10.0,0,6,17,15,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Turtle Ship,III,200,50,M,6.0,6,5,8,6,100,0.9,50,180W 180G,-\r\n,Elite Turtle Ship,IV,300,50,M,6.0,8,6,8,6,100,0.9,50,180W 180G,-\r\n,Longboat,III,130,7,P,3.0,0,6,8,6,100,1.54,25,90W 50G,\r\n,Elite Longboat,IV,160,8,P,3.0,0,8,9,7,100,1.54,25,90W 50G,\"Ship, Cam, Bldg, Ram\"\r\n,Villager,I,25,3,M,2.0,0,0,4,0,-,0.8,25,50F,Bldg\r\n,Trade Cart,II,70,-,-,-,0,0,7,-,-,1.0,51,100W 50G,-\r\n,Monk,III,30,-,C,1.0,0,0,11,9,25,0.7,51,100G,-\r\n,Missionary,III,30,-,C,1.0,0,0,9,7,25,1.1,51,100G,-\r\n,King,I,75,-,-,-,0,0,6,-,-,1.32,-,-,-\r\n,Aethelfirth,-,140,8,M,2.0,0,0,3,0,-,1.03,-,-,Bldg\r\n,Charlemagne,-,200,7,M,2.0,0,0,5,3,100,0.9,-,-,Bldg\r\n,Charles Martel,-,180,13,M,1.0,1,0,8,6,100,0.9,-,-,Bldg\r\n,El Cid,-,330,13,M,2.0,2,1,5,0,-,0.9,-,-,Bldg\r\n,Erik the Red,-,440,9,M,2.0,0,1,3,0,-,0.9,-,-,Bldg\r\n,Harald Hardraade,-,150,13,M,1.0,3,3,7,5,30,1.32,-,-,-\r\n,Hrolf the Ganger,-,140,20,M,1.0,4,4,6,0,-,0.7,-,-,Bldg\r\n,Joan the Maid,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,King Arthur,-,270,13,M,2.0,2,2,5,0,-,0.9,-,-,Bldg\r\n,Kitabatake,-,350,8,M,2.0,1,0,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,La Hire,-,200,18,M,2.0,2,1,4,0,-,0.9,-,-,Bldg\r\n,Minamoto,-,200,8,M,2.0,2,1,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,Scythian Wild Woman,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,Sheriff of Nottingham,-,90,9,M,2.0,0,0,4,0,-,0.9,-,-,Bldg\r\n,Siegfried,-,240,13,M,2.0,1,0,5,0,-,0.9,-,-,Bldg\r\n,Theodoric the Goth,-,450,10,M,2.0,2,4,3,0,-,0.9,-,-,\"Arch, Bldg\"\r\n,William Wallace,-,400,20,M,2.0,5,5,6,0,-,1.0,-,-,-\r\n,Cuauhtemoc,-,100,18,M,2.0,2,6,6,0,-,1.3,-,-,\"Monk, Siege, Cav, Ship, Cam\"\r\n,Francesco Sforza,-,200,12,M,1.9,1,3,6,0,-,1.0,-,-,\"Gun, Bldg\"\r\n,Frederick Barbarossa,-,170,19,M,2.0,10,2,5,0,-,0.75,-,-,\"Eagle, Bldg\"\r\n,Gidajan,-,300,18,M,2.0,4,2,3,0,-,1.1,-,-,\"Bldg, Eagle\"\r\n,Henry II,-,120,16,M,2.0,2,2,5,0,-,1.0,-,-,\"Eagle, Bldg\"\r\n,Itzcoatl,-,300,18,M,2.0,2,1,5,0,-,1.0,-,-,\"Inf, Eagle, Bldg\"\r\n,Little John,-,120,7,M,3.0,0,0,4,0,-,1.0,-,-,\"Cav, Eleph, Ship, Cam, Eagle, Bldg\"\r\n,Pachacuti,-,230,10,M,2.0,2,2,5,1,-,0.95,-,-,Cavalry\r\n,Yekuno Amlak,-,120,20,M,2.0,1,1,3,0,-,1.4,-,-,\"Bldg, Eagle\"\r\n,Yodit,-,170,20,M,2.0,0,0,6,6,100,1.3,-,-,Eagle\r\n,Archers of the Eyes,-,55,7,P,2.0,0,0,6,5,100,1.0,-,-,Ram\r\n,Lord de Graville,-,60,6,P,3.0,0,0,6,5,60,1.00,-,-,Ram\r\n,Robin Hood,-,110,1,P,2.0,0,1,7,5,70,0.96,-,-,-\r\n,Prithvi,-,90,7,P,2.0,1,0,7,4,85,0.96,-,-,Spear\r\n,Guglielmo Embriaco,-,170,16,P,2.0,1,1,8,4,100,0.96,-,-,\"Cav, Eleph, Ship, Cam, Class 30, Bldg\"\r\n,Huayna Capac,-,190,10,P,2.0,0,0,7,5,100,0.96,-,-,\"Inf, Spear\"\r\n,Mustafa Pasha,-,180,25,P,3.45,2,0,10,8,70,0.96,-,-,Ram\r\n,Pacal II,-,190,8,P,1.9,0,2,6,5,90,1.2,-,-,\"Inf, Spear\"\r\n,Su Dingfang,-,200,14,P,3.0,0,0,6,5,85,0.96,-,-,Spear\r\n,Genghis Khan,-,300,25,P,2.0,2,2,4,5,100,1.45,-,-,Siege\r\n,Subotai,-,250,12,P,2.0,2,2,6,5,50,1.65,-,-,\"Bldg, Ram\"\r\n,Tamerlane,-,350,6,P,2.0,2,2,6,4,100,1.41,-,-,Siege\r\n,Khosrau,-,370,14,P,2.5,0,4,7,4,100,0.9,-,-,Bldg\r\n,Osman,-,180,10,P,2.0,2,2,6,5,70,1.4,-,-,\"Bldg, Spear\"\r\n,Prithviraj,-,180,10,P,2.0,2,0,6,4,70,1.4,-,-,\"Bldg, Spear\"\r\n,Dagnajan,-,930,12,P,2.5,1,7,7,4,100,0.9,-,-,Bldg\r\n,Musa ibn Nusayr,-,230,18,P,2.0,1,1,5,4,100,1.41,-,-,\"Cav Arch, Ram, Inf\"\r\n,Tariq ibn Ziyad,-,170,15,P,3.0,0,1,6,4,100,1.35,-,-,Arch\r\n,Alexander Nevski,-,230,12,M,2.0,8,6,4,0,-,1.3,-,-,-\r\n,Attila the Hun,-,350,13,M,2.0,2,2,5,0,-,1.2,-,-,\"Boars, Bldg\"\r\n,Belisarius,-,210,9,M,2.0,2,1,4,0,-,1.35,-,-,Inf\r\n,Bleda the Hun,-,210,13,M,2.0,2,2,5,0,-,1.2,-,-,Bldg\r\n,Constable Richemont,-,250,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Duke d'Alencon,-,150,16,M,2.0,2,3,5,0,-,1.45,-,-,-\r\n,El Cid Campeador,-,430,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Frankish Paladin,-,175,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Gawain,-,180,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Guy Josselyne,-,190,14,M,2.0,2,3,4,0,-,1.45,-,-,-\r\n,Henry V,-,240,16,M,2.0,5,4,5,0,-,1.3,-,-,-\r\n,Joan of Arc,-,200,17,M,2.0,2,2,6,0,-,1.45,-,-,-\r\n,Kushluk,-,275,17,M,2.0,4,2,4,0,-,1.45,-,-,-\r\n,Lancelot,-,240,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Master of the Templar,-,250,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Reynald de Chatillon,-,250,16,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Richard the Lionhearted,-,220,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Roland,-,150,10,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Scythian Scout,-,90,10,M,2.0,2,8,4,0,-,1.5,-,-,-\r\n,Sieur Bertrand,-,120,12,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sieur de Metz,-,120,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sir John Fastolf,-,200,17,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,The Black Prince,-,370,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,William the Conqueror,-,250,15,M,2.0,4,4,6,0,-,1.32,-,-,-\r\n,Francisco de Orellana,-,170,22,P,2.9,2,2,9,7,70,1.4,-,-,\"Ram, Bldg\"\r\n,Gonzalo Pizarro,-,150,20,P,2.9,2,3,9,6,70,1.4,-,-,\"Ram, Bldg\"\r\n,Jarl,-,140,18,M,2.1,1,1,7,4,100,1.3,-,-,Bldg\r\n,Saladin,-,150,12,P,2.0,1,0,5,3,100,1.4,-,-,Cav\r\n,Savaran,-,170,15,M,1.7,2,1,5,0,-,1.35,-,-,Inf\r\n,Vlad Dracula,-,210,16,M,1.9,6,2,5,0,-,1.35,-,-,-\r\n,Abraha Elephant,-,750,25,M,2,1,3,5,0,-,0.7,-,-,Bldg\r\n,Babur,-,300,15,M,2,0,0,5,0,-,1.45,-,-,\"Cav, Ship, Cam\"\r\n,Miklos Toldi,-,285,14,M,1.8,2,4,6,0,-,1.35,-,-,\"Siege, Ram\"\r\n,Richard de Clare,-,200,10,M,2,0,0,4,0,-,1.5,-,-,-\r\n,Sumanguru,-,340,15,M,2,2,2,5,0,-,1.2,-,-,Bldg\r\n,Sundjata,-,290,14,M,2,2,3,4,0,-,1.5,-,-,Monk\r\n,Tristan,-,140,14,M,1.8,0,2,6,0,-,1.5,-,-,\"Siege, Ram\"\r\n,Zawisza the Black,-,240,17,M,1.9,1,5,4,0,-,1.5,-,-,Monk\r\n,Hunting Wolf,-,100,8,M,2.0,2,2,3,0,-,1.0,-,-,-\r\n,Ornlu the Wolf,-,400,20,M,2.0,0,2,3,0,-,0.75,-,-,-\r\n,Bad Neighbor,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,Bad Neighbor (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,God's Own Sling,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,God's Own Sling (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,Jean Bureau,-,60,40,M,6.0,0,2,12,11,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Jean de Lorrain,-,75,100,M,6.0,5,10,12,14,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Saboteur,-,45,100,M,-,1,3,6,0,-,1.35,-,-,Bldg\r\n,Admiral Yi Sun-shin,-,600,75,M,3.0,6,6,12,10,100,0.9,-,-,-\r\n,Archbishop,-,70,-,C,1.0,0,2,11,9,25,0.7,-,-,-\r\n,Friar Tuck,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Imam,-,45,-,C,1.0,4,4,11,9,25,0.7,-,-,-\r\n,Pope Leo I,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Chand Bhai,-,70,-,C,1.0,4,4,8,4,25,1.0,-,-,-\r\n,Emperor in a Barrel,-,100,-,-,-,0,0,7,-,-,1.0,-,-,-\r\n,King Alfonso,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,King Sancho,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Shah,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Queen,-,75,-,-,-,0,0,6,0,-,1.2,-,-,-\r\n,Sanyogita,-,75,-,-,-,0,0,6,0,-,1.2,-,-,-\r\n,Princess Yodit,-,75,-,-,-,0,0,6,0,-,1.2,-,-,-\r\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_aoc.csv",
    "content": "Icon,Name,Age,HP,Att.,Att. Type,Reload Time,M. Arm.,P. Arm.,LOS,R,Acc.,Sp.,Tr. Time,Cost,Attack Bonuses\r\n,Militia,I,40,4,M,2.0,0,1,4,0,-,0.9,21,60F 20G,-\r\n,Man-at-Arms,II,45,6,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Long Swordsman,III,55,9,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Two-Handed Swordsman,IV,60,11,M,2.0,0,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Champion,IV,70,13,M,2.0,1,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Spearman,II,45,3,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Pikeman,III,55,4,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Eleph, Cav, Cam, Ship, Bldg, Eagle\"\r\n,Halberdier,IV,60,6,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Eagle Warrior,III,50,4,M,2.0,0,2,6,0,-,1.1,35,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Elite Eagle Warrior,IV,60,9,M,2.0,0,4,6,0,-,1.3,20,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Jaguar Warrior,III,50,10,M,2.0,1,0,3,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Elite Jaguar Warrior,IV,75,12,M,2.0,2,0,5,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Woad Raider,III,65,8,M,2.0,0,1,3,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Elite Woad Raider,IV,80,13,M,2.0,0,1,5,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Throwing Axeman,III,50,7,M,2.0,0,0,5,3,100,0.9,17,55F 25G,\"Bldg, Eagle\"\r\n,Elite Throwing Axeman,IV,60,8,M,2.0,1,0,6,4,100,0.9,17,55F 25G,\"Bldg, Eagle\"\r\n,Huskarl,III,60,10,M,2.0,0,6,3,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Elite Huskarl,IV,70,12,M,2.0,0,8,5,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Samurai,III,60,8,M,1.9,1,1,4,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Elite Samurai,IV,80,12,M,1.9,1,1,5,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Teutonic Knight,III,70,12,M,2.0,5,2,3,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Elite Teutonic Knight,IV,100,17,M,2.0,10,2,5,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Berserk,III,48,9,M,2.0,0,1,3,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Elite Berserk,IV,60,14,M,2.0,2,1,5,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Archer,II,30,4,P,2.0,0,0,6,4,80,0.96,35,25W 45G,Spear\r\n,Crossbowman,III,35,5,P,2.0,0,0,7,5,85,0.96,27,25W 45G,Spear\r\n,Arbalest,IV,40,6,P,2.0,0,0,7,5,90,0.96,27,25W 45G,Spear\r\n,Skirmisher,II,30,2,P,3.0,0,3,6,4,90,0.96,22,25F 35W,\"Arch, Spear\"\r\n,Elite Skirmisher,III,35,3,P,3.0,0,4,7,5,90,0.96,22,25F 35W,\"Arch, Spear, Cav Arch\"\r\n,Cavalry Archer,III,50,6,P,2.0,0,0,5,4,50,1.4,34,40W 70G,Spear\r\n,Heavy Cavalry Archer,IV,60,7,P,2.0,1,0,6,4,50,1.4,27,40W 70G,Spear\r\n,Hand Cannoneer,IV,35,17,P,3.45,1,0,9,7,65,0.96,34,45F 50G,\"Infantry, Ram, Spear\"\r\n,Longbowman,III,35,6,P,2.0,0,0,7,5,70,0.96,19,35W 40G,Spear\r\n,Elite Longbowman,IV,40,7,P,2.0,0,1,8,6,80,0.96,19,35W 40G,Spear\r\n,Chu Ko Nu,III,45,8,P,3.0,0,0,6,4,85,0.96,19,40W 35G,Spear\r\n,Elite Chu Ko Nu,IV,50,8,P,3.0,0,0,6,4,85,0.96,13,40W 35G,Spear\r\n,War Wagon,III,150,9,P,2.5,0,3,7,4,100,1.2,25,120W 60G,Bldg\r\n,Elite War Wagon,IV,200,9,P,2.5,0,4,8,5,100,1.2,25,120W 60G,Bldg\r\n,Plumed Archer,III,50,5,P,1.9,0,1,6,4,80,1.2,16,46W 46G,\"Spear, Inf\"\r\n,Elite Plumed Archer,IV,65,5,P,1.9,0,2,7,5,90,1.2,16,46W 46G,\"Spear, Inf\"\r\n,Mangudai,III,60,6,P,2.1,0,0,6,4,95,1.45,26,55W 65G,\"Siege, Spear\"\r\n,Elite Mangudai,IV,60,8,P,2.1,1,0,6,4,95,1.45,26,55W 65G,\"Siege, Spear\"\r\n,Janissary,III,35,17,P,3.45,1,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Elite Janissary,IV,40,22,P,3.45,2,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Scout Cavalry,II,45,3,M,2.0,0,2,4,0,-,1.2,30,80F,Monk\r\n,Light Cavalry,III,60,7,M,2.0,0,2,4,0,-,1.5,30,80F,Monk\r\n,Hussar,IV,75,7,M,1.9,0,2,4,0,-,1.5,30,80F,Monk\r\n,Knight,III,100,10,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Cavalier,IV,120,12,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Paladin,IV,160,14,M,1.9,2,3,5,0,-,1.35,30,60F 75G,-\r\n,Camel,III,100,5,M,2.0,0,0,4,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Heavy Camel,IV,120,7,M,2.0,0,0,5,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Cataphract,III,110,9,M,1.8,2,1,4,0,-,1.35,20,70F 75G,Inf\r\n,Elite Cataphract,IV,150,12,M,1.7,2,1,5,0,-,1.35,20,70F 75G,Inf\r\n,Tarkan,III,90,7,M,2.1,1,2,5,0,-,1.35,14,60F 60G,Bldg\r\n,Elite Tarkan,IV,150,11,M,2.1,1,3,7,0,-,1.35,14,60F 60G,Bldg\r\n,War Elephant,III,450,15,M,2.0,1,2,4,0,-,0.6,31,200F 75G,Bldg\r\n,Elite War Elephant,IV,600,20,M,2.0,1,3,5,0,-,0.6,31,200F 75G,Bldg\r\n,Mameluke,III,65,7,M,2.0,0,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Elite Mameluke,IV,80,10,M,2.0,1,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Conquistador,III,55,16,P,2.9,2,2,8,6,65,1.3,24,60F 70G,Ram\r\n,Elite Conquistador,IV,70,18,P,2.9,2,2,9,6,70,1.3,24,60F 70G,\"Ram, Bldg\"\r\n,Battering Ram,III,175,2,M,5.0,-3,180,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Capped Ram,IV,200,3,M,5.0,-3,190,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Siege Ram,IV,270,4,M,5.0,-3,195,3,0,-,0.6,36,160W 75G,\"Bldg, Siege\"\r\n,Mangonel,III,50,40,M,6.0,0,6,9,7,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Onager,IV,60,50,M,6.0,0,7,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Siege Onager,IV,70,75,M,6.0,0,8,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Scorpion,III,40,12,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Heavy Scorpion,IV,50,16,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Bombard Cannon,IV,80,40,M,6.5,2,5,14,12,92,0.7,56,225W 225G,\"Bldg, Ship, Cam, Siege\"\r\n,Petard,III,50,25,M,5,0,2,4,0,-,0.8,25,80F 20G,\"Bldg, Siege\"\r\n,Trebuchet (Packed),IV,150,-,-,-,2,8,18,-,-,0.8,50,200W 200G,-\r\n,Trebuchet,IV,150,200,P,10.0,1,150,18,16,15,-,50,200W 200G,Bldg\r\n,Fishing Ship,I,60,-,-,-,0,4,5,-,-,1.26,40,75W,-\r\n,Transport Ship,II,100,-,-,-,4,8,5,-,-,1.45,46,125W,-\r\n,Trade Cog,II,80,-,-,-,0,6,6,-,-,1.32,36,100W 50G,-\r\n,Galley,II,120,6,P,3.0,0,6,7,5,100,1.43,60,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,War Galley,III,135,7,P,3.0,0,6,8,6,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Galleon,IV,165,8,P,3.0,0,8,9,7,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Fire Ship,III,100,2,P,0.25,0,6,5,2.5,0,1.35,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Fast Fire Ship,IV,120,3,P,0.25,0,8,6,2.5,0,1.43,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Demolition Ship,III,50,110,M,-,0,3,6,0,-,1.6,31,70W 50G,Bldg\r\n,Heavy Demolition Ship,IV,60,140,M,-,0,5,6,0,-,1.6,31,70W 50G,Bldg\r\n,Cannon Galleon,IV,120,35,M,10.0,0,6,15,13,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Elite Cannon Galleon,IV,150,45,M,10.0,0,6,17,15,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Turtle Ship,III,200,50,M,6.0,6,5,8,6,100,0.9,50,200W 200G,-\r\n,Elite Turtle Ship,IV,300,50,M,6.0,8,6,8,6,100,0.9,50,200W 200G,-\r\n,Longboat,III,130,7,P,3.0,0,6,8,6,100,1.54,25,100W 50G,\r\n,Elite Longboat,IV,160,8,P,3.0,0,8,9,7,100,1.54,25,100W 50G,\"Ship, Cam, Bldg, Ram\"\r\n,Villager,I,25,3,M,2.0,0,0,4,0,-,0.8,25,50F,Bldg\r\n,Trade Cart,II,70,-,-,-,0,0,7,-,-,1.0,51,100W 50G,-\r\n,Monk,III,30,-,C,1.0,0,0,11,9,25,0.7,51,100G,-\r\n,Missionary,III,30,-,C,1.0,0,0,9,7,25,1.1,51,100G,-\r\n,King,I,75,-,-,-,0,0,6,-,-,1.32,-,-,-\r\n,Aethelfirth,-,140,8,M,2.0,0,0,3,0,-,1.03,-,-,Bldg\r\n,Charlemagne,-,200,7,M,2.0,0,0,5,3,100,0.9,-,-,Bldg\r\n,Charles Martel,-,180,13,M,1.0,1,0,8,6,100,0.9,-,-,Bldg\r\n,El Cid,-,330,13,M,2.0,2,1,5,0,-,0.9,-,-,Bldg\r\n,Erik the Red,-,440,9,M,2.0,0,1,3,0,-,0.9,-,-,Bldg\r\n,Harald Hardraade,-,150,13,M,1.0,3,3,7,5,30,1.32,-,-,-\r\n,Hrolf the Ganger,-,140,20,M,1.0,4,4,6,0,-,0.7,-,-,Bldg\r\n,Joan the Maid,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,King Arthur,-,270,13,M,2.0,2,2,5,0,-,0.9,-,-,Bldg\r\n,Kitabatake,-,350,8,M,2.0,1,0,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,La Hire,-,200,18,M,2.0,2,1,4,0,-,0.9,-,-,Bldg\r\n,Minamoto,-,200,8,M,2.0,2,1,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,Scythian Wild Woman,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,Sheriff of Nottingham,-,90,9,M,2.0,0,0,4,0,-,0.9,-,-,Bldg\r\n,Siegfried,-,240,13,M,2.0,1,0,5,0,-,0.9,-,-,Bldg\r\n,Theodoric the Goth,-,450,10,M,2.0,2,4,3,0,-,0.9,-,-,\"Arch, Bldg\"\r\n,William Wallace,-,400,20,M,2.0,5,5,6,0,-,1.0,-,-,-\r\n,Archers of the Eyes,-,55,7,P,2.0,0,0,6,5,100,1.0,-,-,Ram\r\n,Lord de Graville,-,60,6,P,3.0,0,0,6,5,60,1.00,-,-,Ram\r\n,Robin Hood,-,110,1,P,2.0,0,1,7,5,70,0.96,-,-,-\r\n,Genghis Khan,-,300,25,P,2.0,2,2,4,5,100,1.45,-,-,Siege\r\n,Subotai,-,250,12,P,2.0,2,2,6,5,50,1.65,-,-,\"Bldg, Ram\"\r\n,Tamerlane,-,350,6,P,2.0,2,2,6,4,100,1.41,-,-,Siege\r\n,Alexander Nevski,-,230,12,M,2.0,8,6,4,0,-,1.3,-,-,-\r\n,Attila the Hun,-,350,13,M,2.0,2,2,5,0,-,1.2,-,-,\"Boars, Bldg\"\r\n,Belisarius,-,210,9,M,2.0,2,1,4,0,-,1.35,-,-,Inf\r\n,Bleda the Hun,-,210,13,M,2.0,2,2,5,0,-,1.2,-,-,Bldg\r\n,Constable Richemont,-,250,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Duke d'Alencon,-,150,16,M,2.0,2,3,5,0,-,1.45,-,-,-\r\n,El Cid Campeador,-,430,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Frankish Paladin,-,175,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Gawain,-,180,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Guy Josselyne,-,190,14,M,2.0,2,3,4,0,-,1.45,-,-,-\r\n,Henry V,-,240,16,M,2.0,5,4,5,0,-,1.3,-,-,-\r\n,Joan of Arc,-,200,17,M,2.0,2,2,6,0,-,1.45,-,-,-\r\n,Kushluk,-,275,17,M,2.0,4,2,4,0,-,1.45,-,-,-\r\n,Lancelot,-,240,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Master of the Templar,-,250,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Reynald de Chatillon,-,250,16,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Richard the Lionhearted,-,220,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Roland,-,150,10,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Scythian Scout,-,90,10,M,2.0,2,8,4,0,-,1.5,-,-,-\r\n,Sieur Bertrand,-,120,12,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sieur de Metz,-,120,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sir John Fastolf,-,200,17,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,The Black Prince,-,370,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,William the Conqueror,-,250,15,M,2.0,4,4,6,0,-,1.32,-,-,-\r\n,Hunting Wolf,-,100,8,M,2.0,2,2,3,0,-,1.0,-,-,-\r\n,Ornlu the Wolf,-,400,20,M,2.0,0,2,3,0,-,0.75,-,-,-\r\n,Bad Neighbor,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,Bad Neighbor (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,God's Own Sling,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,God's Own Sling (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,Jean Bureau,-,60,40,M,6.0,0,2,12,11,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Jean de Lorrain,-,75,100,M,6.0,5,10,12,14,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Saboteur,-,45,100,M,-,1,3,6,0,-,1.35,-,-,Bldg\r\n,Admiral Yi Sun-shin,-,600,75,M,3.0,6,6,12,10,100,0.9,-,-,-\r\n,Archbishop,-,70,-,C,1.0,0,2,11,9,25,0.7,-,-,-\r\n,Friar Tuck,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Imam,-,45,-,C,1.0,4,4,11,9,25,0.7,-,-,-\r\n,Pope Leo I,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Emperor in a Barrel,-,100,-,-,-,0,0,7,-,-,1.0,-,-,-\r\n,King Alfonso,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,King Sancho,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Shah,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_aoe.csv",
    "content": "Name              ,Age, HP,Att.,Att. Type,Reload Time,M. Arm,P. Arm,LOS, R,Acc.,Speed,Tr. Time,Food,Wood,Stone,Gold,Special\nVillager          ,I  , 25,   3,        M,        1.5,     0,     0,  4, 0,   0,    M,      20,  50,   0,    0,   0,\nPriest            ,III, 25,   0,        P,          0,     0,     0, 15,10,  10,    S,      50,   0,   0,    0, 125,\nClubman           ,I  , 40,   3,        M,        1.5,     0,     0,  4, 0,   0,    M,      27,  50,   0,    0,   0,\nAxeman            ,II , 50,   5,        M,        1.5,     0,     0,  4, 0,   0,    M,      27,  50,   0,    0,   0,\nShort Swordsman   ,III, 60,   7,        M,        1.5,     1,     0,  4, 0,   0,    M,      27,  35,   0,    0,  15,\nBroad Swordsman   ,III, 70,   9,        M,        1.5,     1,     0,  4, 0,   0,    M,      27,  35,   0,    0,  15,\nLong Swordsman    ,IV , 80,  11,        M,        1.5,     2,     0,  4, 0,   0,    M,      27,  35,   0,    0,  15,\nLegion            ,IV ,160,  13,        M,        1.5,     2,     0,  4, 0,   0,    M,      27,  35,   0,    0,  15,\nHoplite           ,III,120,  17,        M,        1.5,     5,     0,  4, 0,   0,    S,      36,  60,   0,    0,  40,\nPhalanx           ,IV ,120,  20,        M,        1.5,     7,     0,  4, 0,   0,    S,      36,  60,   0,    0,  40,\nCenturion         ,IV ,160,  30,        M,        1.5,     8,     0,  4, 0,   0,    S,      36,  60,   0,    0,  40,\nBowman            ,II , 35,   3,        P,        1.4,     0,     0,  7, 5,   5,    M,      30,  40,  20,    0,   0,\nImproved Bowman   ,III, 40,   4,        P,        1.4,     0,     0,  8, 6,   6,    M,      30,  40,   0,    0,  20,\nComposite Bowman  ,III, 45,   5,        P,        1.4,     0,     0,  9, 7,   7,    M,      30,  40,   0,    0,  20,\nChariot Archer    ,III, 70,   4,        P,        1.5,     0,     0,  9, 7,   7,    F,      40,  40,   0,    0,  70,High resistance to conversion; triple attack vs. Priest.\nElephant Archer   ,IV ,600,   5,        P,        1.5,     0,     0,  8, 7,   7,    S,      50, 180,   0,    0,  60,\nHorse Archer      ,IV , 60,   7,        P,        1.5,     0,     2,  9, 7,   7,    F,      40,  50,   0,    0,  70,\"+2 armor vs. missile weapons*, Ballista, Helepolis\"\nHeavy Horse Archer,IV , 90,   8,        P,        1.5,     0,     2,  9, 7,   7,    F,      40,  50,   0,    0,  70,\"+2 armor vs. missile weapons*, Ballista, Helepolis\"\nScout             ,II , 60,   3,        M,        0.9,     0,     0,  8, 0,   0,    F,      30, 100,   0,    0,   0,\nChariot           ,III,100,   7,        M,        1.4,     0,     0,  4, 0,   0,    F,      40,  40,  60,    0,   0,High resistance to conversion; double attack vs. Priest.\nCavalry           ,III,150,   8,        M,        1.3,     0,     0,  4, 0,   0,    F,      40,  70,  80,    0,   0,+5 attack vs. infantry.\nHeavy Cavalry     ,IV ,150,  10,        M,        1.3,     1,     1,  4, 0,   0,    F,      40,  70,  80,    0,   0,\"+5 attack vs. infantry; +1 armor vs. missile weapons*, Ballista, Helepolis.\"\nCataphract        ,IV ,180,  12,        M,        1.3,     3,     1,  4, 0,   0,    F,      40,  70,  80,    0,   0,\"+5 attack vs. infantry; +1 armor vs. missile weapons*, Ballista, Helepolis.\"\nWar Elephant      ,IV ,600,  15,        M,          1,     0,     0,  4, 0,   0,    S,      50, 170,   0,    0,  40,Trample damage to adjacent units; attack strength not upgradable.\nStone Thrower     ,III, 75,  50,        M,          5,     0,     0, 13,10,  10,    S,      60,   0, 180,    0,  80,Fire rate once/5 sec; small damage area; minimum range 2.\nCatapult          ,IV , 75,  60,        M,          5,     0,     0, 15,12,  12,    S,      60,   0, 180,    0,  80,Fire rate once/5 sec; medium damage area; minimum range 2.\nHeavy Catapult    ,IV ,150,  60,        M,          5,     0,     0, 16,13,  13,    S,      60,   0, 180,    0,  80,Fire rate once/5 sec; large damage area; minimum range 2.\nBallista          ,IV , 55,  40,        M,          3,     0,     0, 11, 9,   9,    S,      50,   0, 100,    0,  80,Fire rate once/3 sec; minimum range 3.\nHelepolis         ,IV , 55,  40,        M,        1.5,     0,     0, 12,10,  10,    S,      50,   0, 100,    0,  80,Fire rate once/1.5 sec; minimum range 3.\nFishing Boat      ,I  , 45,   0,         ,          0,     0,     0,  6, 0,   0,    M,      20,   0,  50,    0,   0,\nFishing Ship      ,III, 75,   0,         ,          0,     0,     0,  6, 0,   0,    F,      20,   0,  50,    0,   0,\nTrade Boat        ,I  ,200,   0,         ,          0,     0,     0,  4, 0,   0,    F,      25,   0, 100,    0,   0,\nMerchant Ship     ,III,250,   0,         ,          0,     0,     0,  5, 0,   0,    F,      25,   0, 100,    0,   0,\nLight Transport   ,II ,150,   0,         ,          0,     0,     0,  5, 0,   0,    M,      38,   0, 150,    0,   0,\nHeavy Transport   ,IV ,200,   0,         ,          0,     0,     0,  5, 0,   0,    F,      38,   0, 150,    0,   0,\nScout Ship        ,II ,120,   5,        P,        1.4,     0,     0,  7, 5,   5,    F,      30,   0, 135,    0,   0,\nWar Galley        ,III,160,   8,        P,        1.5,     0,     0,  9, 6,   6,    F,      30,   0, 135,    0,   0,\nTrireme           ,IV ,200,  12,        P,          2,     0,     0, 10, 7,   7,    F,      30,   0, 135,    0,   0,Fire rate once/2 sec.\nCatapult Trireme  ,IV ,120,  35,        P,          5,     0,     0, 12, 9,   9,    F,      45,   0, 135,    0,  75,Fire rate once/5 sec; small damage area.\nJuggernaught      ,IV ,200,  35,        P,          5,     0,     0, 13,10,  10,    F,      45,   0, 135,    0,  75,Fire rate once/5 sec; medium damage area.\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_aoe.txt",
    "content": "Notes:\nThis unit attributes table is copied from the original game manual of Age of Empires I for MacOS 9\nMissile weapons: Archery Range units, towers, Scout Ship, War Galley, Trireme\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_aok.csv",
    "content": "Icon,Name,Age,HP,Att.,Att. Type,Reload Time,M. Arm.,P. Arm.,LOS,R,Acc.,Sp.,Tr. Time,Cost,Attack Bonuses\r\n,Militia,I,40,4,M,2.0,0,0,4,0,-,0.9,21,60F 20G,-\r\n,Man-at-Arms,II,45,6,M,2.0,0,0,4,0,-,0.9,21,60F 20G,Bldg\r\n,Long Swordsman,III,55,9,M,2.0,0,0,4,0,-,0.9,21,60F 20G,Bldg\r\n,Two-Handed Swordsman,IV,60,11,M,2.0,0,0,5,0,-,0.9,21,60F 20G,Bldg\r\n,Champion,IV,70,13,M,2.0,1,0,5,0,-,0.9,21,60F 20G,Bldg\r\n,Spearman,II,45,3,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Bldg\"\r\n,Pikeman,III,55,4,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Eleph, Cav, Bldg\"\r\n,Woad Raider,III,65,8,M,2.0,0,0,3,0,-,1.03,10,65F 25G,Bldg\r\n,Elite Woad Raider,IV,80,13,M,2.0,0,0,5,0,-,1.03,10,65F 25G,Bldg\r\n,Throwing Axeman,III,50,7,M,2.0,0,0,5,3,100,0.9,17,55F 25G,Bldg\r\n,Elite Throwing Axeman,IV,60,8,M,2.0,1,0,6,4,100,0.9,17,55F 25G,Bldg\r\n,Huskarl,III,60,10,M,2.0,0,4,3,0,-,0.9,16,80F 40G,\"Arch, Bldg\"\r\n,Elite Huskarl,IV,70,12,M,2.0,0,6,5,0,-,0.9,16,80F 40G,\"Arch, Bldg\"\r\n,Samurai,III,60,8,M,1.9,1,0,4,0,-,0.9,16,60F 30G,\"Unique, Bldg\"\r\n,Elite Samurai,IV,80,12,M,1.9,1,0,5,0,-,0.9,16,60F 30G,\"Unique, Bldg\"\r\n,Teutonic Knight,III,70,12,M,2.0,5,2,3,0,-,0.65,19,85F 40G,Bldg\r\n,Elite Teutonic Knight,IV,100,17,M,2.0,10,2,5,0,-,0.65,19,85F 40G,Bldg\r\n,Berserk,III,48,9,M,2.0,0,0,3,0,-,0.9,16,65F 25G,Bldg\r\n,Elite Berserk,IV,60,14,M,2.0,2,0,5,0,-,0.9,16,65F 25G,Bldg\r\n,Archer,II,30,4,P,2.0,0,0,6,4,80,0.96,35,25W 45G,-\r\n,Crossbowman,III,35,5,P,2.0,0,0,7,5,85,0.96,27,25W 45G,-\r\n,Arbalest,IV,40,6,P,2.0,0,0,7,5,90,0.96,27,25W 45G,-\r\n,Skirmisher,II,30,2,P,3.0,0,3,6,4,90,0.96,22,25F 35W,Arch\r\n,Elite Skirmisher,III,35,3,P,3.0,0,4,7,5,90,0.96,22,25F 35W,Arch\r\n,Cavalry Archer,III,50,6,P,2.0,0,0,5,3,50,1.4,34,40W 70G,-\r\n,Heavy Cavalry Archer,IV,60,7,P,2.0,1,0,6,4,50,1.4,27,40W 70G,-\r\n,Hand Cannoneer,IV,35,17,P,3.45,1,0,9,7,65,0.96,34,45F 50G,\"Infantry, Ram\"\r\n,Longbowman,III,35,6,P,2.0,0,0,7,5,70,0.96,19,35W 40G,-\r\n,Elite Longbowman,IV,40,7,P,2.0,0,1,8,6,80,0.96,19,35W 40G,-\r\n,Chu Ko Nu,III,45,8,P,3.0,0,0,6,4,85,0.96,19,40W 35G,-\r\n,Elite Chu Ko Nu,IV,50,8,P,3.0,0,0,6,4,85,0.96,13,40W 35G,-\r\n,Mangudai,III,60,6,P,2.1,0,0,6,4,95,1.45,26,55W 65G,Siege\r\n,Elite Mangudai,IV,60,8,P,2.1,1,0,6,4,95,1.45,26,55W 65G,Siege\r\n,Janissary,III,35,15,P,3.45,1,0,10,8,50,0.96,21,60F 55G,\"Inf, Ram\"\r\n,Elite Janissary,IV,40,18,P,3.45,2,0,10,8,50,0.96,21,60F 55G,\"Inf, Ram\"\r\n,Scout Cavalry,II,45,3,M,2.0,0,2,4,0,-,1.2,30,80F,-\r\n,Light Cavalry,III,60,7,M,2.0,0,2,4,0,-,1.5,30,80F,-\r\n,Knight,III,100,10,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Cavalier,IV,120,12,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Paladin,IV,160,14,M,1.9,2,3,5,0,-,1.35,30,60F 75G,-\r\n,Camel,III,100,5,M,2.0,0,0,4,0,-,1.45,29,55F 60G,Cav\r\n,Heavy Camel,IV,120,7,M,2.0,0,0,5,0,-,1.45,29,55F 60G,Cav\r\n,Cataphract,III,110,9,M,1.8,2,1,4,0,-,1.35,20,70F 75G,Inf\r\n,Elite Cataphract,IV,150,12,M,1.7,2,1,5,0,-,1.35,20,70F 75G,Inf\r\n,War Elephant,III,450,15,M,2.0,1,2,4,0,-,0.6,31,200F 75G,Bldg\r\n,Elite War Elephant,IV,600,20,M,2.0,1,3,5,0,-,0.6,31,200F 75G,Bldg\r\n,Mameluke,III,65,7,M,2.0,0,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Elite Mameluke,IV,80,10,M,2.0,1,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Battering Ram,III,175,2,M,5.0,-3,180,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Capped Ram,IV,200,3,M,5.0,-3,190,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Siege Ram,IV,270,4,M,5.0,-3,195,3,0,-,0.6,36,160W 75G,\"Bldg, Siege\"\r\n,Mangonel,III,50,40,M,6.0,0,6,9,7,100,0.6,46,160W 135G,Bldg\r\n,Onager,IV,60,50,M,6.0,0,7,10,8,100,0.6,46,160W 135G,Bldg\r\n,Siege Onager,IV,70,75,M,6.0,0,8,10,8,100,0.6,46,160W 135G,Bldg\r\n,Scorpion,III,40,12,P,3.6,0,6,9,5,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Heavy Scorpion,IV,50,16,P,3.6,0,6,9,5,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Bombard Cannon,IV,50,40,M,6.5,2,5,14,12,92,0.7,56,225W 225G,\"Bldg, Ship\"\r\n,Trebuchet (Packed),IV,150,-,-,-,2,8,18,-,-,0.8,50,200W 200G,-\r\n,Trebuchet,IV,150,200,P,10.0,1,150,18,16,15,-,50,200W 200G,Bldg\r\n,Fishing Ship,I,60,-,-,-,0,4,5,-,-,1.26,40,75W,-\r\n,Transport Ship,II,100,-,-,-,4,8,5,-,-,1.45,46,125W,-\r\n,Trade Cog,II,80,-,-,-,0,6,6,-,-,1.32,36,100W 50G,-\r\n,Galley,II,120,6,P,3.0,0,6,7,5,100,1.43,36,90W 30G,\"Ship, Bldg, Ram\"\r\n,War Galley,III,135,7,P,3.0,0,6,8,6,100,1.43,36,90W 30G,\"Ship, Bldg, Ram\"\r\n,Galleon,IV,165,8,P,3.0,0,8,9,7,100,1.43,36,90W 30G,\"Ship, Bldg, Ram\"\r\n,Fire Ship,III,100,2,P,0.25,0,6,5,2.5,0,1.35,36,75W 45G,\"Ship, Bldg\"\r\n,Fast Fire Ship,IV,120,3,P,0.25,0,8,6,2.5,0,1.43,36,75W 45G,\"Ship, Bldg\"\r\n,Demolition Ship,III,50,110,M,-,0,3,6,0,-,1.6,31,70W 50G,Bldg\r\n,Heavy Demolition Ship,IV,60,140,M,-,0,5,6,0,-,1.6,31,70W 50G,Bldg\r\n,Cannon Galleon,IV,120,35,M,10.0,0,6,15,13,50,1.1,46,200W 150G,Bldg\r\n,Elite Cannon Galleon,IV,150,45,M,10.0,0,6,17,15,50,1.1,46,200W 150G,Bldg\r\n,Longboat,III,130,7,P,3.0,0,6,8,6,100,1.54,41,100W 50G,\r\n,Elite Longboat,IV,160,8,P,3.0,0,8,9,7,100,1.54,41,100W 50G,\"Ship, Bldg, Ram\"\r\n,Villager,I,25,3,M,2.0,0,0,4,0,-,0.8,25,50F,Bldg\r\n,Trade Cart,II,70,-,-,-,0,0,7,-,-,1.0,51,100W 50G,-\r\n,Monk,III,30,-,C,1.0,0,0,11,9,25,0.7,51,100G,-\r\n,King,I,75,-,-,-,0,0,6,-,-,1.32,-,-,-\r\n,Aethelfirth,-,140,8,M,2.0,0,0,3,0,-,1.03,-,-,Bldg\r\n,Charlemagne,-,200,7,M,2.0,0,0,5,3,100,0.9,-,-,Bldg\r\n,Charles Martel,-,180,13,M,1.0,1,0,8,6,100,0.9,-,-,Bldg\r\n,El Cid,-,330,13,M,2.0,2,1,5,0,-,0.9,-,-,Bldg\r\n,Erik the Red,-,440,9,M,2.0,0,1,3,0,-,0.9,-,-,Bldg\r\n,Harald Hardraade,-,150,13,M,1.0,3,3,7,5,30,1.32,-,-,-\r\n,Hrolf the Ganger,-,140,20,M,1.0,4,4,6,0,-,0.7,-,-,Bldg\r\n,Joan the Maid,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,King Arthur,-,270,13,M,2.0,2,2,5,0,-,0.9,-,-,Bldg\r\n,Kitabatake,-,350,8,M,2.0,1,0,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,La Hire,-,200,18,M,2.0,2,1,4,0,-,0.9,-,-,Bldg\r\n,Minamoto,-,200,8,M,2.0,2,1,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,Scythian Wild Woman,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,Sheriff of Nottingham,-,90,9,M,2.0,0,0,4,0,-,0.9,-,-,Bldg\r\n,Siegfried,-,240,13,M,2.0,1,0,5,0,-,0.9,-,-,Bldg\r\n,Theodoric the Goth,-,450,10,M,2.0,2,4,3,0,-,0.9,-,-,\"Arch, Bldg\"\r\n,William Wallace,-,400,20,M,2.0,5,5,6,0,-,1.0,-,-,-\r\n,Archers of the Eyes,-,55,7,P,2.0,0,0,6,5,100,1.0,-,-,Ram\r\n,Lord de Graville,-,60,6,P,3.0,0,0,6,5,60,1.00,-,-,Ram\r\n,Robin Hood,-,110,1,P,2.0,0,1,7,5,70,0.96,-,-,-\r\n,Genghis Khan,-,300,25,P,2.0,2,2,4,5,100,1.45,-,-,Siege\r\n,Subotai,-,250,12,P,2.0,2,2,6,5,50,1.65,-,-,\"Bldg, Ram\"\r\n,Tamerlane,-,350,6,P,2.0,2,2,6,4,100,1.41,-,-,Siege\r\n,Alexander Nevski,-,230,12,M,2.0,8,6,4,0,-,1.3,-,-,-\r\n,Attila the Hun,-,350,13,M,2.0,2,2,5,0,-,1.2,-,-,\"Boars, Bldg\"\r\n,Belisarius,-,210,9,M,2.0,2,1,4,0,-,1.35,-,-,Inf\r\n,Bleda the Hun,-,210,13,M,2.0,2,2,5,0,-,1.2,-,-,Bldg\r\n,Constable Richemont,-,250,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Duke d'Alencon,-,150,16,M,2.0,2,3,5,0,-,1.45,-,-,-\r\n,El Cid Campeador,-,430,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Frankish Paladin,-,175,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Gawain,-,180,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Guy Josselyne,-,190,14,M,2.0,2,3,4,0,-,1.45,-,-,-\r\n,Henry V,-,240,16,M,2.0,5,4,5,0,-,1.3,-,-,-\r\n,Joan of Arc,-,200,17,M,2.0,2,2,6,0,-,1.45,-,-,-\r\n,Kushluk,-,275,17,M,2.0,4,2,4,0,-,1.45,-,-,-\r\n,Lancelot,-,240,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Master of the Templar,-,250,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Reynald de Chatillon,-,250,16,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Richard the Lionhearted,-,220,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Roland,-,150,10,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Scythian Scout,-,90,10,M,2.0,2,8,4,0,-,1.5,-,-,-\r\n,Sieur Bertrand,-,120,12,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sieur de Metz,-,120,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sir John Fastolf,-,200,17,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,The Black Prince,-,370,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,William the Conqueror,-,250,15,M,2.0,4,4,6,0,-,1.32,-,-,-\r\n,Hunting Wolf,-,100,8,M,2.0,2,2,3,0,-,1.0,-,-,-\r\n,Ornlu the Wolf,-,400,20,M,2.0,0,2,3,0,-,0.75,-,-,-\r\n,Bad Neighbor,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,Bad Neighbor (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,God's Own Sling,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,God's Own Sling (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,Jean Bureau,-,60,40,M,6.0,0,2,12,11,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Jean de Lorrain,-,75,100,M,6.0,5,10,12,14,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Saboteur,-,45,100,M,-,1,3,6,0,-,1.35,-,-,Bldg\r\n,Admiral Yi Sun-shin,-,600,75,M,3.0,6,6,12,10,100,0.9,-,-,-\r\n,Archbishop,-,70,-,C,1.0,0,2,11,9,25,0.7,-,-,-\r\n,Friar Tuck,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Imam,-,45,-,C,1.0,4,4,11,9,25,0.7,-,-,-\r\n,Pope Leo I,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Emperor in a Barrel,-,100,-,-,-,0,0,7,-,-,1.0,-,-,-\r\n,King Alfonso,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,King Sancho,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Shah,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n"
  },
  {
    "path": "doc/reverse_engineering/unit_stats/unit_stats_fe.csv",
    "content": "Icon,Name,Age,HP,Att.,Att. Type,Reload Time,M. Arm.,P. Arm.,LOS,R,Acc.,Sp.,Tr. Time,Cost,Attack Bonuses\r\n,Militia,I,40,4,M,2.0,0,1,4,0,-,0.9,21,60F 20G,-\r\n,Man-at-Arms,II,45,6,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Long Swordsman,III,60,9,M,2.0,0,1,4,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Two-Handed Swordsman,IV,60,11,M,2.0,0,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Champion,IV,70,13,M,2.0,1,1,5,0,-,0.9,21,60F 20G,\"Eagle, Bldg\"\r\n,Spearman,II,45,3,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Pikeman,III,55,4,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Eleph, Cav, Cam, Ship, Bldg, Eagle\"\r\n,Halberdier,IV,60,6,M,3.0,0,0,4,0,-,1.0,22,35F 25W,\"Cav, Eleph, Cam, Ship, Bldg, Eagle\"\r\n,Eagle Scout,III,50,4,M,2.0,0,2,6,0,-,1.1,35,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Eagle Warrior,III,55,7,M,2.0,0,3,6,0,-,1.1,35,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Elite Eagle Warrior,IV,60,9,M,2.0,0,4,6,0,-,1.3,20,20F 50G,\"Monk, Siege, Cav, Cam, Ship\"\r\n,Condottiero,IV,80,9,M,1.9,1,3,6,0,-,1.0,11,50F 35G,\"Gun, Bldg\"\r\n,Jaguar Warrior,III,50,10,M,2.0,1,1,3,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Elite Jaguar Warrior,IV,75,12,M,2.0,2,1,5,0,-,1.0,20,60F 30G,\"Inf, Bldg, Eagle\"\r\n,Woad Raider,III,65,8,M,2.0,0,1,3,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Elite Woad Raider,IV,80,13,M,2.0,0,1,5,0,-,1.2,10,65F 25G,\"Bldg, Eagle\"\r\n,Throwing Axeman,III,50,7,M,2.0,0,0,5,3,100,0.9,17,55F 25G,\"Bldg, Eagle\"\r\n,Elite Throwing Axeman,IV,60,8,M,2.0,1,0,6,4,100,0.9,17,55F 25G,\"Bldg, Eagle\"\r\n,Huskarl,III,60,10,M,2.0,0,6,3,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Elite Huskarl,IV,70,12,M,2.0,0,8,5,0,-,1.05,16,80F 40G,\"Arch, Bldg, Eagle\"\r\n,Samurai,III,60,8,M,1.9,1,1,4,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Elite Samurai,IV,80,12,M,1.9,1,1,5,0,-,1.0,9,60F 30G,\"Unique, Bldg, Eagle\"\r\n,Teutonic Knight,III,80,12,M,2.0,5,2,3,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Elite Teutonic Knight,IV,100,17,M,2.0,10,2,5,0,-,0.65,12,85F 40G,\"Bldg, Eagle\"\r\n,Berserk,III,54,9,M,2.0,0,1,3,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Elite Berserk,IV,62,14,M,2.0,2,1,5,0,-,1.05,16,65F 25G,\"Bldg, Eagle\"\r\n,Kamayuk,III,60,7,M,2.0,0,0,4,1,100,0.95,10,60F 30G,\"Cav, Cam\"\r\n,Elite Kamayuk,IV,80,8,M,2.0,1,0,5,1,100,0.95,10,60F 30G,\"Cav, Cam\"\r\n,Archer,II,30,4,P,2.0,0,0,6,4,80,0.96,35,25W 45G,Spear\r\n,Crossbowman,III,35,5,P,2.0,0,0,7,5,85,0.96,27,25W 45G,Spear\r\n,Arbalest,IV,40,6,P,2.0,0,0,7,5,90,0.96,27,25W 45G,Spear\r\n,Skirmisher,II,30,2,P,3.0,0,3,6,4,90,0.96,22,25F 35W,\"Arch, Spear\"\r\n,Elite Skirmisher,III,35,3,P,3.0,0,4,7,5,90,0.96,22,25F 35W,\"Arch, Spear, Cav Arch\"\r\n,Cavalry Archer,III,50,6,P,2.0,0,0,5,4,50,1.4,34,40W 65G,Spear\r\n,Heavy Cavalry Archer,IV,60,7,P,2.0,1,0,6,4,50,1.4,27,40W 65G,Spear\r\n,Slinger,III,40,5,P,2.0,0,0,7,5,90,0.96,25,30F 40G,\"Inf, Ram, Spear\"\r\n,Hand Cannoneer,IV,35,17,P,3.45,1,0,9,7,65,0.96,34,45F 50G,\"Inf, Ram, Spear\"\r\n,Longbowman,III,35,6,P,2.0,0,0,7,5,70,0.96,18,35W 40G,Spear\r\n,Elite Longbowman,IV,40,7,P,2.0,0,1,8,6,80,0.96,18,35W 40G,Spear\r\n,Chu Ko Nu,III,45,8,P,3.0,0,0,6,4,85,0.96,19,40W 35G,Spear\r\n,Elite Chu Ko Nu,IV,50,8,P,3.0,0,0,6,4,85,0.96,13,40W 35G,Spear\r\n,War Wagon,III,150,9,P,2.5,0,3,7,4,100,1.2,21,120W 60G,Bldg\r\n,Elite War Wagon,IV,200,9,P,2.5,0,4,8,5,100,1.2,21,120W 60G,Bldg\r\n,Plumed Archer,III,50,5,P,1.9,0,1,6,4,80,1.2,16,50W 50G,\"Spear, Inf\"\r\n,Elite Plumed Archer,IV,65,5,P,1.9,0,2,7,5,90,1.2,16,50W 50G,\"Spear, Inf\"\r\n,Mangudai,III,60,6,P,2.1,0,0,6,4,95,1.45,26,55W 65G,\"Ram, Spear\"\r\n,Elite Mangudai,IV,60,8,P,2.1,1,0,6,4,95,1.45,26,55W 65G,\"Ram, Spear\"\r\n,Janissary,III,35,17,P,3.45,1,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Elite Janissary,IV,40,22,P,3.45,2,0,10,8,50,0.96,21,60F 55G,Ram\r\n,Genoese Crossbowman,III,45,6,P,3.0,1,0,8,4,100,0.96,22,50W 50G,\"Cav, Eleph, Ship, Cam, Class 30\"\r\n,Elite Genoese Crossbowman,IV,50,6,P,2.0,1,0,8,4,100,0.96,22,50W 50G,\"Cav, Eleph, Ship, Cam, Class 30\"\r\n,Elephant Archer,III,250,6,P,2.5,0,4,7,4,100,0.8,25,110F 80G,Bldg\r\n,Elite Elephant Archer,IV,350,7,P,2.5,0,4,7,4,100,0.8,25,110F 80G,Bldg\r\n,Scout Cavalry,II,45,3,M,2.0,0,2,4,0,-,1.2,30,80F,Monk\r\n,Light Cavalry,III,60,7,M,2.0,0,2,4,0,-,1.5,30,80F,Monk\r\n,Hussar,IV,75,7,M,1.9,0,2,4,0,-,1.5,30,80F,Monk\r\n,Knight,III,100,10,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Cavalier,IV,120,12,M,1.8,2,2,4,0,-,1.35,30,60F 75G,-\r\n,Paladin,IV,160,14,M,1.9,2,3,5,0,-,1.35,30,60F 75G,-\r\n,Camel,III,100,5,M,2.0,0,0,4,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Heavy Camel,IV,120,7,M,2.0,0,0,5,0,-,1.45,22,55F 60G,\"Cav, Cam, Ship\"\r\n,Imperial Camel,IV,140,9,M,2.0,0,0,5,0,-,1.45,20,55F 60G,\"Cav, Cam, Ship\"\r\n,Cataphract,III,110,9,M,1.8,2,1,4,0,-,1.35,20,70F 75G,Inf\r\n,Elite Cataphract,IV,150,12,M,1.7,2,1,5,0,-,1.35,20,70F 75G,Inf\r\n,Tarkan,III,100,7,M,2.1,1,2,5,0,-,1.35,14,60F 60G,Bldg\r\n,Elite Tarkan,IV,150,11,M,2.1,1,3,7,0,-,1.35,14,60F 60G,Bldg\r\n,War Elephant,III,450,15,M,2.0,1,2,4,0,-,0.6,31,200F 75G,Bldg\r\n,Elite War Elephant,IV,600,20,M,2.0,1,3,5,0,-,0.6,31,200F 75G,Bldg\r\n,Mameluke,III,65,7,M,2.0,0,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Elite Mameluke,IV,80,10,M,2.0,1,0,5,3,100,1.4,23,55F 85G,Cav\r\n,Conquistador,III,55,16,P,2.9,2,2,8,6,65,1.3,24,60F 70G,Ram\r\n,Elite Conquistador,IV,70,18,P,2.9,2,2,9,6,70,1.3,24,60F 70G,\"Ram, Bldg\"\r\n,Magyar Huszar,III,70,9,M,1.8,0,2,5,0,-,1.5,16,80F 10G,\"Siege, Ram\"\r\n,Elite Magyar Huszar,IV,85,10,M,1.8,0,2,6,0,-,1.5,16,80F 10G,\"Siege, Ram\"\r\n,Boyar,III,100,12,M,1.9,4,1,5,0,-,1.35,23,50F 80G,-\r\n,Elite Boyar,IV,130,14,M,1.9,6,2,5,0,-,1.35,20,50F 80G,-\r\n,Battering Ram,III,175,2,M,5.0,-3,180,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Capped Ram,IV,200,3,M,5.0,-3,190,3,0,-,0.5,36,160W 75G,\"Bldg, Siege\"\r\n,Siege Ram,IV,270,4,M,5.0,-3,195,3,0,-,0.6,36,160W 75G,\"Bldg, Siege\"\r\n,Mangonel,III,50,40,M,6.0,0,6,9,7,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Onager,IV,60,50,M,6.0,0,7,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Siege Onager,IV,70,75,M,6.0,0,8,10,8,100,0.6,46,160W 135G,\"Bldg, Siege\"\r\n,Scorpion,III,40,12,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Heavy Scorpion,IV,50,16,P,3.6,0,6,9,7,100,0.65,30,75W 75G,\"Eleph, Bldg, Ram\"\r\n,Bombard Cannon,IV,80,40,M,6.5,2,5,14,12,92,0.7,56,225W 225G,\"Bldg, Ship, Cam, Siege\"\r\n,Petard,III,50,25,M,5,0,2,4,0,-,0.8,25,65F 20G,\"Bldg, Siege\"\r\n,Trebuchet (Packed),IV,150,-,-,-,2,8,18,-,-,0.8,50,200W 200G,-\r\n,Trebuchet,IV,150,200,P,10.0,1,150,18,16,15,-,50,200W 200G,Bldg\r\n,Fishing Ship,I,60,-,-,-,0,4,5,-,-,1.26,40,75W,-\r\n,Transport Ship,II,100,-,-,-,4,8,5,-,-,1.45,46,125W,-\r\n,Trade Cog,II,80,-,-,-,0,6,6,-,-,1.32,36,100W 50G,-\r\n,Galley,II,120,6,P,3.0,0,6,7,5,100,1.43,60,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,War Galley,III,135,7,P,3.0,0,6,8,6,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Galleon,IV,165,8,P,3.0,0,8,9,7,100,1.43,36,90W 30G,\"Ship, Cam, Bldg, Ram\"\r\n,Fire Ship,III,120,2,P,0.25,0,6,5,2.49,0,1.35,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Fast Fire Ship,IV,140,3,P,0.25,0,8,6,2.49,0,1.43,36,75W 45G,\"Ship, Cam, Turtle, Bldg\"\r\n,Demolition Ship,III,60,110,M,-,0,3,6,0,-,1.6,31,70W 50G,Bldg\r\n,Heavy Demolition Ship,IV,70,140,M,-,0,5,6,0,-,1.6,31,70W 50G,Bldg\r\n,Cannon Galleon,IV,120,35,M,10.0,0,6,15,13,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Elite Cannon Galleon,IV,150,45,M,10.0,0,6,17,15,50,1.1,46,200W 150G,\"Bldg, Siege, Inf, Arch, Cav\"\r\n,Turtle Ship,III,200,50,M,6.0,6,5,8,6,100,0.9,50,180W 180G,-\r\n,Elite Turtle Ship,IV,300,50,M,6.0,8,6,8,6,100,0.9,50,180W 180G,-\r\n,Longboat,III,130,7,P,3.0,0,6,8,6,100,1.54,25,90W 50G,\r\n,Elite Longboat,IV,160,8,P,3.0,0,8,9,7,100,1.54,25,90W 50G,\"Ship, Cam, Bldg, Ram\"\r\n,Villager,I,25,3,M,2.0,0,0,4,0,-,0.8,25,50F,Bldg\r\n,Trade Cart,II,70,-,-,-,0,0,7,-,-,1.0,51,100W 50G,-\r\n,Monk,III,30,-,C,1.0,0,0,11,9,25,0.7,51,100G,-\r\n,Missionary,III,30,-,C,1.0,0,0,9,7,25,1.1,51,100G,-\r\n,King,I,75,-,-,-,0,0,6,-,-,1.32,-,-,-\r\n,Aethelfirth,-,140,8,M,2.0,0,0,3,0,-,1.03,-,-,Bldg\r\n,Charlemagne,-,200,7,M,2.0,0,0,5,3,100,0.9,-,-,Bldg\r\n,Charles Martel,-,180,13,M,1.0,1,0,8,6,100,0.9,-,-,Bldg\r\n,El Cid,-,330,13,M,2.0,2,1,5,0,-,0.9,-,-,Bldg\r\n,Erik the Red,-,440,9,M,2.0,0,1,3,0,-,0.9,-,-,Bldg\r\n,Harald Hardraade,-,150,13,M,1.0,3,3,7,5,30,1.32,-,-,-\r\n,Hrolf the Ganger,-,140,20,M,1.0,4,4,6,0,-,0.7,-,-,Bldg\r\n,Joan the Maid,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,King Arthur,-,270,13,M,2.0,2,2,5,0,-,0.9,-,-,Bldg\r\n,Kitabatake,-,350,8,M,2.0,1,0,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,La Hire,-,200,18,M,2.0,2,1,4,0,-,0.9,-,-,Bldg\r\n,Minamoto,-,200,8,M,2.0,2,1,4,0,-,0.9,-,-,\"Unique, Bldg\"\r\n,Scythian Wild Woman,-,100,4,M,2.0,1,1,6,0,-,0.9,-,-,-\r\n,Sheriff of Nottingham,-,90,9,M,2.0,0,0,4,0,-,0.9,-,-,Bldg\r\n,Siegfried,-,240,13,M,2.0,1,0,5,0,-,0.9,-,-,Bldg\r\n,Theodoric the Goth,-,450,10,M,2.0,2,4,3,0,-,0.9,-,-,\"Arch, Bldg\"\r\n,William Wallace,-,400,20,M,2.0,5,5,6,0,-,1.0,-,-,-\r\n,Cuauhtemoc,-,100,18,M,2.0,2,6,6,0,-,1.3,-,-,\"Monk, Siege, Cav, Ship, Cam\"\r\n,Francesco Sforza,-,200,12,M,1.9,1,3,6,0,-,1.0,-,-,\"Gun, Bldg\"\r\n,Frederick Barbarossa,-,170,19,M,2.0,10,2,5,0,-,0.75,-,-,\"Eagle, Bldg\"\r\n,Archers of the Eyes,-,55,7,P,2.0,0,0,6,5,100,1.0,-,-,Ram\r\n,Lord de Graville,-,60,6,P,3.0,0,0,6,5,60,1.00,-,-,Ram\r\n,Robin Hood,-,110,1,P,2.0,0,1,7,5,70,0.96,-,-,-\r\n,Prithvi,-,90,7,P,2.0,1,0,7,4,85,0.96,-,-,Spear\r\n,Genghis Khan,-,300,25,P,2.0,2,2,4,5,100,1.45,-,-,Siege\r\n,Subotai,-,250,12,P,2.0,2,2,6,5,50,1.65,-,-,\"Bldg, Ram\"\r\n,Tamerlane,-,350,6,P,2.0,2,2,6,4,100,1.41,-,-,Siege\r\n,Khosrau,-,370,14,P,2.5,0,4,7,4,100,0.9,-,-,Bldg\r\n,Osman,-,180,10,P,2.0,2,2,6,5,70,1.4,-,-,\"Bldg, Spear\"\r\n,Prithviraj,-,180,10,P,2.0,2,0,6,4,70,1.4,-,-,\"Bldg, Spear\"\r\n,Alexander Nevski,-,230,12,M,2.0,8,6,4,0,-,1.3,-,-,-\r\n,Attila the Hun,-,350,13,M,2.0,2,2,5,0,-,1.2,-,-,\"Boars, Bldg\"\r\n,Belisarius,-,210,9,M,2.0,2,1,4,0,-,1.35,-,-,Inf\r\n,Bleda the Hun,-,210,13,M,2.0,2,2,5,0,-,1.2,-,-,Bldg\r\n,Constable Richemont,-,250,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Duke d'Alencon,-,150,16,M,2.0,2,3,5,0,-,1.45,-,-,-\r\n,El Cid Campeador,-,430,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Frankish Paladin,-,175,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Gawain,-,180,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Guy Josselyne,-,190,14,M,2.0,2,3,4,0,-,1.45,-,-,-\r\n,Henry V,-,240,16,M,2.0,5,4,5,0,-,1.3,-,-,-\r\n,Joan of Arc,-,200,17,M,2.0,2,2,6,0,-,1.45,-,-,-\r\n,Kushluk,-,275,17,M,2.0,4,2,4,0,-,1.45,-,-,-\r\n,Lancelot,-,240,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Master of the Templar,-,250,15,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Reynald de Chatillon,-,250,16,M,2.0,2,3,6,0,-,1.32,-,-,-\r\n,Richard the Lionhearted,-,220,14,M,2.0,2,3,5,0,-,1.3,-,-,-\r\n,Roland,-,150,10,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,Scythian Scout,-,90,10,M,2.0,2,8,4,0,-,1.5,-,-,-\r\n,Sieur Bertrand,-,120,12,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sieur de Metz,-,120,15,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,Sir John Fastolf,-,200,17,M,2.0,2,2,4,0,-,1.45,-,-,-\r\n,The Black Prince,-,370,12,M,2.0,2,2,4,0,-,1.3,-,-,-\r\n,William the Conqueror,-,250,15,M,2.0,4,4,6,0,-,1.32,-,-,-\r\n,Francisco de Orellana,-,170,22,P,2.9,2,2,9,7,70,1.4,-,-,\"Ram, Bldg\"\r\n,Gonzalo Pizarro,-,150,20,P,2.9,2,3,9,6,70,1.4,-,-,\"Ram, Bldg\"\r\n,Jarl,-,140,18,M,2.1,1,1,7,4,100,1.3,-,-,Bldg\r\n,Saladin,-,150,12,P,2.0,1,0,5,3,100,1.4,-,-,Cav\r\n,Savaran,-,170,15,M,1.7,2,1,5,0,-,1.35,-,-,Inf\r\n,Vlad Dracula,-,210,16,M,1.9,6,2,5,0,-,1.35,-,-,-\r\n,Hunting Wolf,-,100,8,M,2.0,2,2,3,0,-,1.0,-,-,-\r\n,Ornlu the Wolf,-,400,20,M,2.0,0,2,3,0,-,0.75,-,-,-\r\n,Bad Neighbor,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,Bad Neighbor (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,God's Own Sling,-,300,200,P,10.0,2,8,21,20,92,-,-,-,Bldg\r\n,God's Own Sling (Packed),-,300,-,-,-,2,8,18,-,-,0.8,-,-,-\r\n,Jean Bureau,-,60,40,M,6.0,0,2,12,11,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Jean de Lorrain,-,75,100,M,6.0,5,10,12,14,100,0.75,-,-,\"Bldg, Ship, Cam\"\r\n,Saboteur,-,45,100,M,-,1,3,6,0,-,1.35,-,-,Bldg\r\n,Admiral Yi Sun-shin,-,600,75,M,3.0,6,6,12,10,100,0.9,-,-,-\r\n,Archbishop,-,70,-,C,1.0,0,2,11,9,25,0.7,-,-,-\r\n,Friar Tuck,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Imam,-,45,-,C,1.0,4,4,11,9,25,0.7,-,-,-\r\n,Pope Leo I,-,60,-,C,1.0,2,4,11,9,25,0.7,-,-,-\r\n,Chand Bhai,-,70,-,C,1.0,4,4,8,4,25,1.0,-,-,-\r\n,Emperor in a Barrel,-,100,-,-,-,0,0,7,-,-,1.0,-,-,-\r\n,King Alfonso,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,King Sancho,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Shah,-,75,-,-,-,0,0,6,0,-,1.32,-,-,-\r\n,Queen,-,75,-,-,-,0,0,6,0,-,1.2,-,-,-\r\n,Sanyogita,-,75,-,-,-,0,0,6,0,-,1.2,-,-,-\r\n"
  },
  {
    "path": "doc/running.md",
    "content": "# How to run openage?\n\nThis document explains the different run modes in openage.\n\n1. [Quickstart](#quickstart)\n2. [Modes](#modes)\n   1. [`game`](#game)\n   2. [`main`](#main)\n   3. [`test`](#test)\n   4. [`convert`](#convert)\n   5. [`convert-file`](#convert-file)\n   6. [`convert-export-api`](#convert-export-api)\n   7. [`codegen`](#codegen)\n\n\n## Quickstart\n\nAfter building the project with the commands\n\n```\n./configure\nmake\n```\n\nyou can execute\n\n```\nbin/run\n```\n\nfrom the same subfolder. This automatically selects the [`game` mode](#game) as default\nto start a new game instance and also creates all necessary configs.\n\nIf prompts appear, follow the instructions and choose what you think is best. It's\nalmost idiot-proof!\n\n\n## Modes\n\nModes can be selected manually by appending the `bin/run` prompt with the mode name.\n\n### `game`\n\n```\nbin/run game\n```\n\nStart the engine and immediately create a new game instance. This run mode is supposed\nto jump straight into a game (or replay recording).\n\nIf no converted modpacks can be found, this mode will start with a prompt asking if\nthe user wants to convert any before initializing the game.\n\nIt's the default run mode.\n\n\n### `main`\n\n```\nbin/run main\n```\n\nThis run mode is supposed to start a main menu or launcher which allows configuring a\ngame. Neither of these are implemented at the moment, so `main` just does the same\nthing as `game`.\n\n\n### `test`\n\n```\nbin/run test\n```\n\nUsed for running [tests and engine demos](code/testing.md). These show off selected\nsubsystems of the engine.\n\n\n### `convert`\n\n```\nbin/run convert\n```\n\nRuns the [asset conversion](media_convert.md) subsystem which creates openage modpacks\nfrom original game installations.\n\n\n### `convert-file`\n\n```\nbin/run convert-file\n```\n\nAllows converting single media files using the original game formats to open formats\n(PNG for graphics, OPUS for sounds).\n\n\n### `convert-export-api`\n\n```\nbin/run convert-export-api\n```\n\nExports the [openage modding API](nyan/README.md) nyan objects to a modpack that can\nbe loaded by the engine.\n\nThis is supposed to be temporary solution until the modding API is finalized. Currently,\nthe modding API nyan objects are hardcoded into the converter. In the future, the\nmodding API modpack should be part of the engine config which is loaded by both the engine\nthe converter subsystem as a single source of truth.\n\n\n### `codegen`\n\n```\nbin/run codegen\n```\n\nRuns the code generation logic for the build process. In particular, this generates code\nfor [mako](https://www.makotemplates.org/) templates and creates the test lists for the\n[testing](code/testing.md) subsystem.\n"
  },
  {
    "path": "doc/troubleshooting.md",
    "content": "# Troubleshooting\n\n## Windows Installer\n\n### More than one python installation\n\nIf you have two (or more) different python installations on your computer edit the `openage.bat` in the install directory:\nReplace the line `python.exe -m openage` with `call \"%INST_DIR%\\python\\python.exe\" -m openage` to start `python.exe` explicitly.\n\n## Asset Conversion\n\n### Error: *No valid game version(s) could not be detected in <folder>*\n\nCheck if you have passed the **root folder** of the game to the converter and not a subfolder.\n\nIf that doesn't help, you could have a mod installed that messes with the detection algorithm.\nIn that case, you should reinstall a clean unmodded version of the game and retry the conversion.\n\n### Conversion raises exception when converting *The Conquerors* 1.0c\n\nMake sure you don't have *UserPatch*, compatibility patches or modifications installed that make\nchanges to the original asset files.\n\nIf you have Wololo Kingdoms and various mods installed that change the base assets the converter will not work.\nA workaround would be to make a backup of your AGE2 directory and let the converter run on that backup. In that\nbackup at subfolder `AGE2/resources` delete all files ***except*** folders. Another workaround would be to\nbackup your AGE2 folder and redownload it to have a clean install. After conversion you can replace\nit with the backup.\n"
  },
  {
    "path": "etc/gdb_pretty/__init__.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nGDB pretty printers for openage.\n\"\"\"\n"
  },
  {
    "path": "etc/gdb_pretty/printers.py",
    "content": "# Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPretty printers for GDB.\n\"\"\"\n\nimport re\nimport gdb  # type: ignore\nimport gdb.printing  # type: ignore\n\n# TODO: Printers should inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.\n\n\nclass PrinterControl(gdb.printing.PrettyPrinter):\n    \"\"\"\n    Exposes a pretty printer for a specific type.\n\n    Printer are searched in the following order:\n        1. Exact type name _with_ typedefs\n        2. Regex of type name _without_ typedefs\n    \"\"\"\n\n    def __init__(self, name: str):\n        super().__init__(name)\n\n        self.name_printers = {}\n        self.regex_printers = {}\n\n    def add_printer(self, type_name: str, printer):\n        \"\"\"\n        Adds a printer for a specific type name.\n        \"\"\"\n        self.name_printers[type_name] = printer\n\n    def add_printer_regex(self, regex: str, printer):\n        \"\"\"\n        Adds a printer for a specific type name.\n\n        :param regex: The regex to match the type name.\n        :type regex: str\n        \"\"\"\n        self.regex_printers[re.compile(regex)] = printer\n\n    def __call__(self, val: gdb.Value):\n        # Check the exact type name with typedefa\n        type_name = val.type.name\n        if type_name in self.name_printers:\n            return self.name_printers[val.type.name](val)\n\n        # Check the type name without typedefs and regex\n        type_name = val.type.unqualified().strip_typedefs().tag\n        if type_name is None:\n            return None\n\n        for regex, printer in self.regex_printers.items():\n            if regex.match(type_name):\n                return printer(val)\n\n        return None\n\n\npp = PrinterControl('openage')\ngdb.printing.register_pretty_printer(None, pp)\n\n\ndef printer_typedef(type_name: str):\n    \"\"\"\n    Decorator for pretty printers.\n\n    :param type_name: The name of the type to register the printer for.\n    :type type_name: str\n    \"\"\"\n    def _register_printer(printer):\n        \"\"\"\n        Registers the printer with GDB.\n        \"\"\"\n        pp.add_printer(type_name, printer)\n\n    return _register_printer\n\n\ndef printer_regex(regex: str):\n    \"\"\"\n    Decorator for pretty printers.\n\n    :param regex: The regex to match the type name.\n    :type regex: str\n    \"\"\"\n    def _register_printer(printer):\n        \"\"\"\n        Registers the printer with GDB.\n        \"\"\"\n        pp.add_printer_regex(regex, printer)\n\n    return _register_printer\n\n\ndef format_fixed_point(value: int, fractional_bits: int) -> float:\n    \"\"\"\n    Formats a fixed point value to a double.\n\n    :param value: The fixed point value.\n    :type value: int\n    :param fractional_bits: The number of fractional bits.\n    :type fractional_bits: int\n    \"\"\"\n    to_double_factor = 1 / pow(2, fractional_bits)\n    return float(value) * to_double_factor\n\n\n@printer_regex('^openage::coord::(camhud|chunk|input|phys|scene|term|tile|viewport)(2|3)?(_delta)?')\nclass CoordPrinter:\n    \"\"\"\n    Pretty printer for openage::coord types (CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ).\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n        # Each coord type has one parent which is either\n        # of CoordNeSe, CoordNeSeUp, CoordXY, CoordXYZ\n        # From this parent we can get the fields\n        self._parent_type = self.__val.type.fields()[0].type\n\n    def to_string(self):\n        \"\"\"\n        Get the coord as a string.\n        \"\"\"\n        field_vals = []\n        for child in self._parent_type.fields():\n            # Include the fixed point coordinates in the summary\n            val = self.__val[child.name]\n            num = format_fixed_point(\n                int(val['raw_value']),\n                int(val.type.template_argument(1))\n            )\n            field_vals.append(f\"{num:.5f}\")\n\n        # Example: phys3[1.00000, 2.00000, 3.00000]\n        return f\"{self.__val.type.tag.split('::')[-1]}[{', '.join(field_vals)}]\"\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the coord.\n        \"\"\"\n        for child in self._parent_type.fields():\n            yield (child.name, self.__val[child.name])\n\n\n@printer_typedef('openage::time::time_t')\nclass TimePrinter:\n    \"\"\"\n    Pretty printer for openage::time::time_t.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the time as a string.\n\n        Format: SS.sss (e.g. 12.345s)\n        \"\"\"\n        seconds = format_fixed_point(\n            int(self.__val['raw_value']),\n            int(self.__val.type.template_argument(1))\n        )\n\n        # show as seconds with millisecond precision\n        return f'{seconds:.3f}s'\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the time value.\n        \"\"\"\n        yield ('raw_value', self.__val['raw_value'])\n\n\n@printer_regex('^openage::util::FixedPoint<.*>')\nclass FixedPointPrinter:\n    \"\"\"\n    Pretty printer for openage::util::FixedPoint.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the fixed point value as a string.\n\n        Format: 0.12345\n        \"\"\"\n        num = format_fixed_point(\n            int(self.__val['raw_value']),\n            int(self.__val.type.template_argument(1))\n        )\n        return f'{num:.5f}'\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the fixed point value.\n        \"\"\"\n        yield ('raw_value', self.__val['raw_value'])\n\n        # calculate the precision of the fixed point value\n        # 16 * log10(2) = 16 * 0.30103 = 4.81648\n        # do this manualy because it's usually optimized out by the compiler\n        fractional_bits = int(self.__val.type.template_argument(1))\n\n        precision = int(fractional_bits * 0.30103 + 1)\n        yield ('approx_precision', precision)\n\n\n@printer_regex('^openage::util::Vector<.*>')\nclass VectorPrinter:\n    \"\"\"\n    Pretty printer for openage::util::Vector.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the vector as a string.\n        \"\"\"\n        size = self.__val.type.template_argument(0)\n        int_type = self.__val.type.template_argument(1)\n        return f'openage::util::Vector<{size}, {int_type}>'\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the vector.\n        \"\"\"\n        size = self.__val.type.template_argument(0)\n        for i in range(size):\n            yield (str(i), self.__val['_M_elems'][i])\n\n    def child(self, index):\n        \"\"\"\n        Get the child at the given index.\n        \"\"\"\n        return self.__val['_M_elems'][index]\n\n    def num_children(self):\n        \"\"\"\n        Get the number of children of the vector.\n        \"\"\"\n        return self.__val.type.template_argument(0)\n\n    @staticmethod\n    def display_hint():\n        \"\"\"\n        Get the display hint for the vector.\n        \"\"\"\n        return 'array'\n\n\n@printer_regex('^openage::curve::Keyframe<.*>')\nclass KeyframePrinter:\n    \"\"\"\n    Pretty printer for openage::curve::Keyframe.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the keyframe as a string.\n        \"\"\"\n        return f'openage::curve::Keyframe<{self.__val.type.template_argument(0)}>'\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the keyframe.\n        \"\"\"\n        yield ('time', self.__val['timestamp'])\n        yield ('value', self.__val['value'])\n\n\n@printer_typedef('openage::path::flow_t')\nclass PathFlowTypePrinter:\n    \"\"\"\n    Pretty printer for openage::path::flow_t.\n\n    TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.\n    \"\"\"\n\n    FLOW_FLAGS: dict = {\n        0x10: 'PATHABLE',\n        0x20: 'LOS',\n        0x40: 'TARGET',\n        0x80: 'UNUSED',\n    }\n\n    FLOW_DIRECTION: dict = {\n        0x00: 'NORTH',\n        0x01: 'NORTHEAST',\n        0x02: 'EAST',\n        0x03: 'SOUTHEAST',\n        0x04: 'SOUTH',\n        0x05: 'SOUTHWEST',\n        0x06: 'WEST',\n        0x07: 'NORTHWEST',\n    }\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the flow type as a string.\n        \"\"\"\n        flow = int(self.__val)\n        flags = flow & 0xF0\n        direction = flow & 0x0F\n        return (f\"{self.FLOW_DIRECTION.get(direction, 'INVALID')} (\"\n                f\"{', '.join([flag for mask, flag in self.FLOW_FLAGS.items() if mask & flags])})\")\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the flow type.\n        \"\"\"\n        flow = int(self.__val)\n        flags = flow & 0xF0\n        direction = flow & 0x0F\n        yield ('direction', self.FLOW_DIRECTION[direction])\n        for mask, flag in self.FLOW_FLAGS.items():\n            yield (flag, bool(flags & mask))\n\n\n# Integrated flags\nINTEGRATED_FLAGS: dict = {\n    0x01: 'UNUSED',\n    0x02: 'FOUND',\n    0x04: 'WAVEFRONT_BLOCKED',\n    0x08: 'UNUSED',\n    0x10: 'UNUSED',\n    0x20: 'LOS',\n    0x40: 'TARGET',\n    0x80: 'UNUSED',\n}\n\n\ndef get_integrated_flags_list(value: int) -> str:\n    \"\"\"\n    Get the list of flags as a string.\n\n    :param value: The value to get the flags for.\n    :type value: int\n    \"\"\"\n    flags = []\n    for mask, flag in INTEGRATED_FLAGS.items():\n        if value & mask:\n            flags.append(flag)\n\n    return ' | '.join(flags)\n\n\n@printer_typedef('openage::path::integrated_flags_t')\nclass PathIntegratedFlagsTypePrinter:\n    \"\"\"\n    Pretty printer for openage::path::integrated_flags_t.\n\n    TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the integrate type as a string.\n        \"\"\"\n        integrate = int(self.__val)\n        return get_integrated_flags_list(integrate)\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the integrate type.\n        \"\"\"\n        integrate = int(self.__val)\n        for mask, flag in INTEGRATED_FLAGS.items():\n            yield (flag, bool(integrate & mask))\n\n\n@printer_typedef('openage::path::integrated_t')\nclass PathIntegratedTypePrinter:\n    \"\"\"\n    Pretty printer for openage::path::integrated_t.\n\n    TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros.\n    \"\"\"\n\n    def __init__(self, val: gdb.Value):\n        self.__val = val\n\n    def to_string(self):\n        \"\"\"\n        Get the integrate type as a string.\n        \"\"\"\n        output_str = f'cost = {self.__val[\"cost\"]}'\n        flags = get_integrated_flags_list(int(self.__val['flags']))\n        if len(flags) > 0:\n            output_str += f' ({flags})'\n        return output_str\n\n    def children(self):\n        \"\"\"\n        Get the displayed children of the integrate type.\n        \"\"\"\n        yield ('cost', self.__val['cost'])\n        yield ('flags', self.__val['flags'])\n\n\n# TODO: curve types\n# TODO: input event codes\n# TODO: eigen types https://github.com/dmillard/eigengdb\n"
  },
  {
    "path": "etc/lsan.supp",
    "content": "# Curse you, Python! (this list is probably incomplete)\nleak:_PyObject_Malloc\nleak:_PyObject_GC_Resize\nleak:PyThread_allocate_lock\nleak:PyCode_New\nleak:PyList_New\nleak:new_keys_object\nleak:make_keys_shared\nleak:new_dict_with_shared_keys\nleak:list_resize\nleak:set_table_resize\nleak:resize_compact\nleak:atexit_register\nleak:PyInit_atexit\nleak:PyModule_Create2\nleak:newblock\nleak:_buffered_init\n\n# stdlib\nleak:__strdup\n"
  },
  {
    "path": "etc/openage.gdbinit",
    "content": "python\nimport sys, os\n\nprint(\"Loading openage.gdbinit\")\nprint(f\"Adding custom pretty-printers directory to the GDB path: {os.getcwd() + '../../etc'}\")\n\nsys.path.insert(0, \"../../etc\")\n\nimport gdb_pretty.printers\nend\n"
  },
  {
    "path": "etc/pylintrc",
    "content": "[MASTER]\n\n# Specify a configuration file.\n#rcfile=\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n#init-hook=\n\n# Add files or directories to the blacklist. They should be base names, not\n# paths.\nignore=CVS\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=pylint.extensions.no_self_use,pylint.extensions.bad_builtin\n\n# Use multiple processes to speed up Pylint.\njobs=1\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code\nextension-pkg-whitelist=openage.convert.opus.opusenc\n\n\n[REPORTS]\n\n# Set the output format. Available formats are text, parseable, colorized, msvs\n# (visual studio) and html. You can also give a reporter class, eg\n# mypackage.mymodule.MyReporterClass.\noutput-format=text\n\n# Tells whether to display a full report or only the messages\nreports=yes\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details\n#msg-template=\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED\nconfidence=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once).You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use\"--disable=all --enable=classes\n# --disable=W\"\n#\n# The import-error and no-name-in-module messages are disabled because\n# if cython modules are not yet build, they can't be imported.\n#\n# TODO: enable cyclic-import, once those issues have been fixed.\ndisable=locally-disabled, cyclic-import, file-ignored, import-error, no-name-in-module, import-outside-toplevel, duplicate-code\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time. See also the \"--disable\" option for examples.\nenable=\n\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,XXX,ASDF\n\n\n[BASIC]\n\n# List of builtins function names that should not be used, separated by a comma\nbad-functions=map,filter\n\n# Good variable names which should always be accepted, separated by a comma\ngood-names=i,j,k,ex,Run,_\n\n# Bad variable names which should always be refused, separated by a comma\nbad-names=foo,bar,baz,toto,tutu,tata\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Include a hint for the correct naming format with invalid-name\ninclude-naming-hint=no\n\n# Regular expression matching correct function names\nfunction-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression matching correct method names\nmethod-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression matching correct constant names\nconst-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$\n\n# Regular expression matching correct attribute names\nattr-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression matching correct argument names\nargument-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression matching correct inline iteration names\ninlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$\n\n# Regular expression matching correct variable names\nvariable-rgx=[a-z_][a-z0-9_]{2,30}$\n\n# Regular expression matching correct class names\nclass-rgx=[A-Z_][a-zA-Z0-9]+$\n\n# Regular expression matching correct class attribute names\nclass-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$\n\n# Regular expression matching correct module names\nmodule-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=__.*__\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=-1\n\n\n[TYPECHECK]\n\n# Tells whether missing members accessed in mixin class should be ignored. A\n# mixin class is detected if its name ends with \"mixin\" (case insensitive).\nignore-mixin-members=yes\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis\nignored-modules=\n\n# List of classes names for which member attributes should not be checked\n# (useful for classes with attributes dynamically set).\nignored-classes=SQLObject\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E0201 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=REQUEST,acl_users,aq_parent\n\n\n[VARIABLES]\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# A regular expression matching the name of dummy variables (i.e. expectedly\n# not used).\ndummy-variables-rgx=_$|dummy\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid to define new builtins when possible.\nadditional-builtins=\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,_cb\n\n\n[SPELLING]\n\n# Spelling dictionary name. Available dictionaries: none. To make it working\n# install python-enchant package.\nspelling-dict=\n\n# List of comma separated words that should not be checked.\nspelling-ignore-words=\n\n# A path to a file that contains private dictionary; one word per line.\nspelling-private-dict-file=\n\n# Tells whether to store unknown words to indicated private dictionary in\n# --spelling-private-dict-file option instead of raising a message.\nspelling-store-unknown-words=no\n\n\n[SIMILARITIES]\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n# Ignore imports when computing similarities.\nignore-imports=no\n\n\n[LOGGING]\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format\nlogging-modules=logging\n\n\n[FORMAT]\n\n# Maximum number of characters on a single line.\nmax-line-length=100\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=^\\s*(# )?<?https?://\\S+>?$\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=no\n\n# Maximum number of lines in a module\nmax-module-lines=1000\n\n# String used as indentation unit. This is usually \" \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n# Number of spaces of indent required inside a hanging or continued line.\nindent-after-paren=4\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n\n[IMPORTS]\n\n# Deprecated modules which should not be used, separated by a comma\ndeprecated-modules=stringprep,optparse\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled)\nimport-graph=\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled)\next-import-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled)\nint-import-graph=\n\n\n[DESIGN]\n\n# Maximum number of arguments for function / method\nmax-args=5\n\n# Argument names that match this expression will be ignored. Default to name\n# with leading underscore\nignored-argument-names=_.*\n\n# Maximum number of locals for function / method body\nmax-locals=15\n\n# Maximum number of return / yield for function / method body\nmax-returns=6\n\n# Maximum number of branch for function / method body\nmax-branches=12\n\n# Maximum number of statements in function / method body\nmax-statements=50\n\n# Maximum number of parents for a class (see R0901).\nmax-parents=7\n\n# Maximum number of attributes for a class (see R0902).\nmax-attributes=7\n\n# Minimum number of public methods for a class (see R0903).\nmin-public-methods=2\n\n# Maximum number of public methods for a class (see R0904).\nmax-public-methods=20\n\n\n[CLASSES]\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,__new__,setUp\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=mcs\n\n# List of member names, which should be excluded from the protected access\n# warning.\nexclude-protected=_asdict,_fields,_replace,_source,_make\n\n\n[EXCEPTIONS]\n\n# Exceptions that will emit a warning when being caught. Defaults to\n# \"Exception\"\novergeneral-exceptions=builtins.Exception\n"
  },
  {
    "path": "etc/valgrind-python.supp",
    "content": "#\n# This is a valgrind suppression file that should be used when using valgrind.\n#\n# It was taken from the cpython project and adapted for openage.\n# Upstream URL is: https://raw.githubusercontent.com/python/cpython/master/Misc/valgrind-python.supp\n#\n#\n#  Here's an example of running valgrind:\n#\n#\tcd openage/bin/\n#\tPYTHONMALLOC=malloc valgrind --tool=memcheck --suppressions=../etc/valgrind-python.supp \\\n#\t\tpython3 -m openage test -a\n#\n# Python can be compiled with --with-valgrind to enhance the valgrind experience:\n# valgrind presence is detected and python memory is allocated more valgrind-friendly then.\n#\n# Python 3.6 supports PYTHONMALLOC=malloc environment var to force malloc()\n#\n# Disable the suppressions for _PyObject_Free and _PyObject_Realloc below by comment\n# if valgrind shall complain about python memory leaks.\n#\n# See cpython/Misc/README.valgrind for more information\n#\n# all tool names: Addrcheck,Memcheck,cachegrind,helgrind,massif\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Addr4\n   fun:address_in_range\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Value4\n   fun:address_in_range\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 8 (x86_64 aka amd64)\n   Memcheck:Value8\n   fun:address_in_range\n}\n\n{\n   ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value\n   Memcheck:Cond\n   fun:address_in_range\n}\n\n#\n# Leaks (including possible leaks)\n#    Hmmm, I wonder if this masks some real leaks.  I think it does.\n#    Will need to fix that.\n#\n\n{\n   Suppress leaking the GIL.  Happens once per process, see comment in ceval.c.\n   Memcheck:Leak\n   fun:malloc\n   fun:PyThread_allocate_lock\n   fun:PyEval_InitThreads\n}\n\n{\n   Suppress leaking the GIL after a fork.\n   Memcheck:Leak\n   fun:malloc\n   fun:PyThread_allocate_lock\n   fun:PyEval_ReInitThreads\n}\n\n{\n   Suppress leaking the autoTLSkey.  This looks like it shouldn't leak though.\n   Memcheck:Leak\n   fun:malloc\n   fun:PyThread_create_key\n   fun:_PyGILState_Init\n   fun:Py_InitializeEx\n   fun:Py_Main\n}\n\n{\n   Hmmm, is this a real leak or like the GIL?\n   Memcheck:Leak\n   fun:malloc\n   fun:PyThread_ReInitTLS\n}\n\n{\n   Handle PyMalloc confusing valgrind (possibly leaked)\n   Memcheck:Leak\n   fun:realloc\n   fun:_PyObject_GC_Resize\n   fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING\n}\n\n{\n   Handle PyMalloc confusing valgrind (possibly leaked)\n   Memcheck:Leak\n   fun:malloc\n   fun:_PyObject_GC_New\n   fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING\n}\n\n{\n   Handle PyMalloc confusing valgrind (possibly leaked)\n   Memcheck:Leak\n   fun:malloc\n   fun:_PyObject_GC_NewVar\n   fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING\n}\n\n#\n# Non-python specific leaks\n#\n\n{\n   Handle pthread issue (possibly leaked)\n   Memcheck:Leak\n   fun:calloc\n   fun:allocate_dtv\n   fun:_dl_allocate_tls_storage\n   fun:_dl_allocate_tls\n}\n\n{\n   Handle pthread issue (possibly leaked)\n   Memcheck:Leak\n   fun:memalign\n   fun:_dl_allocate_tls_storage\n   fun:_dl_allocate_tls\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Addr4\n   fun:_PyObject_Free\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Value4\n   fun:_PyObject_Free\n}\n\n{\n   ADDRESS_IN_RANGE/Use of uninitialised value of size 8\n   Memcheck:Addr8\n   fun:_PyObject_Free\n}\n\n{\n   ADDRESS_IN_RANGE/Use of uninitialised value of size 8\n   Memcheck:Value8\n   fun:_PyObject_Free\n}\n\n{\n   ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value\n   Memcheck:Cond\n   fun:_PyObject_Free\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Addr4\n   fun:_PyObject_Realloc\n}\n\n{\n   ADDRESS_IN_RANGE/Invalid read of size 4\n   Memcheck:Value4\n   fun:_PyObject_Realloc\n}\n\n{\n   ADDRESS_IN_RANGE/Use of uninitialised value of size 8\n   Memcheck:Addr8\n   fun:_PyObject_Realloc\n}\n\n{\n   ADDRESS_IN_RANGE/Use of uninitialised value of size 8\n   Memcheck:Value8\n   fun:_PyObject_Realloc\n}\n\n{\n   ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value\n   Memcheck:Cond\n   fun:_PyObject_Realloc\n}\n\n###\n### All the suppressions below are for errors that occur within libraries\n### that Python uses.  The problems to not appear to be related to Python's\n### use of the libraries.\n###\n\n{\n   Generic ubuntu ld problems\n   Memcheck:Addr8\n   obj:/lib/ld-2.4.so\n   obj:/lib/ld-2.4.so\n   obj:/lib/ld-2.4.so\n   obj:/lib/ld-2.4.so\n}\n\n{\n   Generic gentoo ld problems\n   Memcheck:Cond\n   obj:/lib/ld-2.3.4.so\n   obj:/lib/ld-2.3.4.so\n   obj:/lib/ld-2.3.4.so\n   obj:/lib/ld-2.3.4.so\n}\n\n{\n   DBM problems, see test_dbm\n   Memcheck:Param\n   write(buf)\n   fun:write\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   fun:dbm_close\n}\n\n{\n   DBM problems, see test_dbm\n   Memcheck:Value8\n   fun:memmove\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   fun:dbm_store\n   fun:dbm_ass_sub\n}\n\n{\n   DBM problems, see test_dbm\n   Memcheck:Cond\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   fun:dbm_store\n   fun:dbm_ass_sub\n}\n\n{\n   DBM problems, see test_dbm\n   Memcheck:Cond\n   fun:memmove\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   obj:/usr/lib/libdb1.so.2\n   fun:dbm_store\n   fun:dbm_ass_sub\n}\n\n{\n   GDBM problems, see test_gdbm\n   Memcheck:Param\n   write(buf)\n   fun:write\n   fun:gdbm_open\n\n}\n\n{\n   Uninitialised byte(s) false alarm, see bpo-35561\n   Memcheck:Param\n   epoll_ctl(event)\n   fun:epoll_ctl\n   fun:pyepoll_internal_ctl\n}\n\n{\n   ZLIB problems, see test_gzip\n   Memcheck:Cond\n   obj:/lib/libz.so.1.2.3\n   obj:/lib/libz.so.1.2.3\n   fun:deflate\n}\n\n{\n   Avoid problems w/readline doing a putenv and leaking on exit\n   Memcheck:Leak\n   fun:malloc\n   fun:xmalloc\n   fun:sh_set_lines_and_columns\n   fun:_rl_get_screen_size\n   fun:_rl_init_terminal_io\n   obj:/lib/libreadline.so.4.3\n   fun:rl_initialize\n}\n\n# Valgrind emits \"Conditional jump or move depends on uninitialised value(s)\"\n# false alarms on GCC builtin strcmp() function. The GCC code is correct.\n#\n# Valgrind bug: https://bugs.kde.org/show_bug.cgi?id=264936\n{\n   bpo-38118: Valgrind emits false alarm on GCC builtin strcmp()\n   Memcheck:Cond\n   fun:PyUnicode_Decode\n}\n\n\n###\n### These occur from somewhere within the SSL, when running\n###  test_socket_sll.  They are too general to leave on by default.\n###\n###{\n###   somewhere in SSL stuff\n###   Memcheck:Cond\n###   fun:memset\n###}\n###{\n###   somewhere in SSL stuff\n###   Memcheck:Value4\n###   fun:memset\n###}\n###\n###{\n###   somewhere in SSL stuff\n###   Memcheck:Cond\n###   fun:MD5_Update\n###}\n###\n###{\n###   somewhere in SSL stuff\n###   Memcheck:Value4\n###   fun:MD5_Update\n###}\n\n# Fedora's package \"openssl-1.0.1-0.1.beta2.fc17.x86_64\" on x86_64\n# See http://bugs.python.org/issue14171\n{\n   openssl 1.0.1 prng 1\n   Memcheck:Cond\n   fun:bcmp\n   fun:fips_get_entropy\n   fun:FIPS_drbg_instantiate\n   fun:RAND_init_fips\n   fun:OPENSSL_init_library\n   fun:SSL_library_init\n   fun:init_hashlib\n}\n\n{\n   openssl 1.0.1 prng 2\n   Memcheck:Cond\n   fun:fips_get_entropy\n   fun:FIPS_drbg_instantiate\n   fun:RAND_init_fips\n   fun:OPENSSL_init_library\n   fun:SSL_library_init\n   fun:init_hashlib\n}\n\n{\n   openssl 1.0.1 prng 3\n   Memcheck:Value8\n   fun:_x86_64_AES_encrypt_compact\n   fun:AES_encrypt\n}\n\n#\n# All of these problems come from using test_socket_ssl\n#\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:BN_bin2bn\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:BN_num_bits_word\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   fun:BN_num_bits_word\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:BN_mod_exp_mont_word\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:BN_mod_exp_mont\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Param\n   write(buf)\n   fun:write\n   obj:/usr/lib/libcrypto.so.0.9.7\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:RSA_verify\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   fun:RSA_verify\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   fun:DES_set_key_unchecked\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   fun:DES_encrypt2\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   obj:/usr/lib/libssl.so.0.9.7\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   obj:/usr/lib/libssl.so.0.9.7\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:BUF_MEM_grow_clean\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:memcpy\n   fun:ssl3_read_bytes\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Cond\n   fun:SHA1_Update\n}\n\n{\n   from test_socket_ssl\n   Memcheck:Value4\n   fun:SHA1_Update\n}\n\n{\n   test_buffer_non_debug\n   Memcheck:Addr4\n   fun:PyUnicodeUCS2_FSConverter\n}\n\n{\n   test_buffer_non_debug\n   Memcheck:Addr4\n   fun:PyUnicode_FSConverter\n}\n\n{\n   wcscmp_false_positive\n   Memcheck:Addr8\n   fun:wcscmp\n   fun:_PyOS_GetOpt\n   fun:Py_Main\n   fun:main\n}\n\n# Additional suppressions for the unified decimal tests:\n{\n   test_decimal\n   Memcheck:Addr4\n   fun:PyUnicodeUCS2_FSConverter\n}\n\n{\n   test_decimal2\n   Memcheck:Addr4\n   fun:PyUnicode_FSConverter\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  # This is a nix flake that contains a declarative definition of the openage\n  # and nyan packages, providing convenient and reproducible builds and\n  # development shells.\n\n  description = \"Free (as in freedom) open source clone of the Age of Empires II engine\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-23.11\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils, ... }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let pkgs = import nixpkgs { inherit system; }; in\n      {\n        # This output is to build the derivation with `nix build` as well as to\n        # get development shells using `nix develop`.\n        # These are the packages provided by this flake: nyan and openage.\n        packages = rec {\n          # `nix build .#nyan` to build this\n          nyan = pkgs.callPackage ./nix/nyan.nix { };\n          # `nix build .#openage` to build this\n          openage = pkgs.callPackage ./nix/openage.nix {\n            # Nyan is not provided by nixpkgs, but it comes from this flake\n            inherit (self.packages.${system}) nyan;\n          };\n          # If no path is specified, openage is the default\n          default = openage;\n        };\n\n        # This output is to run the application directly with `nix run`\n        # (or `nix run .#openage` if you want to be explicit)\n        apps = rec {\n          openage = flake-utils.lib.mkApp { drv = self.packages.${system}.openage; };\n          default = openage;\n        };\n      });\n}\n"
  },
  {
    "path": "kevinfile",
    "content": "# kevin ci config script for openage\n#\n# see here for the CI source code:\n# https://github.com/SFTtech/kevin\n\n\nsanity_check:\n\t- skip              (? if job != \"debian\" ?)\n\tmake checkmerge\n\n# Various optimisation options can affect warnings compiler generates.\n# Make sure both release and debug are tested. Arch job has more checks,\n# so it should run debug, while Debian can test build in release.\nconfigure:\n\t- env: mode=debug   compiler=gcc            (? if job == \"debian\" ?)\n\t- env: mode=debug   compiler=clang          (? if job == \"debian-clang\" ?)\n\t./configure --mode=${mode} --compiler=${compiler} --ccache --download-nyan\n\t# TODO: once all warnings are gone again, set --flags=\"-Werror\"\n\nbuild: configure\n\tmake -j$(nproc) build\n\ntest: build\n\tmake tests\n\ninstall: build\n\tmake install DESTDIR=/tmp/openage\n"
  },
  {
    "path": "legal/BSD-3-clause",
    "content": "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "legal/GPLv3",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "legal/LGPLv2.0",
    "content": "                  GNU LIBRARY GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1991 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the library GPL.  It is\n numbered 2 because it goes with version 2 of the ordinary GPL.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Library General Public License, applies to some\nspecially designated Free Software Foundation software, and to any\nother libraries whose authors decide to use it.  You can use it for\nyour libraries, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if\nyou distribute copies of the library, or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link a program with the library, you must provide\ncomplete object files to the recipients so that they can relink them\nwith the library, after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  Our method of protecting your rights has two steps: (1) copyright\nthe library, and (2) offer you this license which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  Also, for each distributor's protection, we want to make certain\nthat everyone understands that there is no warranty for this free\nlibrary.  If the library is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original\nversion, so that any problems introduced by others will not reflect on\nthe original authors' reputations.\n\f\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that companies distributing free\nsoftware will individually obtain patent licenses, thus in effect\ntransforming the program into proprietary software.  To prevent this,\nwe have made it clear that any patent must be licensed for everyone's\nfree use or not licensed at all.\n\n  Most GNU software, including some libraries, is covered by the ordinary\nGNU General Public License, which was designed for utility programs.  This\nlicense, the GNU Library General Public License, applies to certain\ndesignated libraries.  This license is quite different from the ordinary\none; be sure to read it in full, and don't assume that anything in it is\nthe same as in the ordinary license.\n\n  The reason we have a separate public license for some libraries is that\nthey blur the distinction we usually make between modifying or adding to a\nprogram and simply using it.  Linking a program with a library, without\nchanging the library, is in some sense simply using the library, and is\nanalogous to running a utility program or application program.  However, in\na textual and legal sense, the linked executable is a combined work, a\nderivative of the original library, and the ordinary General Public License\ntreats it as such.\n\n  Because of this blurred distinction, using the ordinary General\nPublic License for libraries did not effectively promote software\nsharing, because most developers did not use the libraries.  We\nconcluded that weaker conditions might promote sharing better.\n\n  However, unrestricted linking of non-free programs would deprive the\nusers of those programs of all benefit from the free status of the\nlibraries themselves.  This Library General Public License is intended to\npermit developers of non-free programs to use free libraries, while\npreserving your freedom as a user of such programs to change the free\nlibraries that are incorporated in them.  (We have not seen how to achieve\nthis as regards changes in header files, but we have achieved it as regards\nchanges in the actual functions of the Library.)  The hope is that this\nwill lead to faster development of free libraries.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, while the latter only\nworks together with the library.\n\n  Note that it is possible for a library to be covered by the ordinary\nGeneral Public License rather than by this special one.\n\f\n                  GNU LIBRARY GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library which\ncontains a notice placed by the copyright holder or other authorized\nparty saying it may be distributed under the terms of this Library\nGeneral Public License (also called \"this License\").  Each licensee is\naddressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also compile or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    c) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    d) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe source code distributed need not include anything that is normally\ndistributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Library General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\f\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Library General Public\n    License as published by the Free Software Foundation; either\n    version 2 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Library General Public License for more details.\n\n    You should have received a copy of the GNU Library General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "libopenage/.gitignore",
    "content": "# generated by codegen\n*.gen.*\n# generated by pxdgen\n*.pxd\n__init__.py\n# generated by cmake\n/config.h\n/config.cpp\n"
  },
  {
    "path": "libopenage/CMakeLists.txt",
    "content": "# Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n# main C++ library definitions.\n# dependency and source file setup for the resulting library.\ndeclare_binary(libopenage openage library allow_no_undefined)\n\n\n#################################################################\n# source files and folders are added at the bottom of this file #\n#################################################################\n\n# set basic library settings\nset_target_properties(libopenage PROPERTIES\n\tVERSION 0\n\tAUTOMOC ON\n\tAUTOGEN_TARGET_DEPENDS \"cppgen\"\n)\n\n##################################################\n# library dependency specification\n\n# freetype includedir hint for ubuntu...\nfind_path(FREETYPE_INCLUDE_DIRS freetype/freetype.h HINTS /usr/include/freetype2)\n\n# provide apple qt location\nif(APPLE)\n\texecute_process(\n\t\tCOMMAND brew --prefix\n\t\tOUTPUT_VARIABLE HOMEBREW_PREFIX\n\t\tOUTPUT_STRIP_TRAILING_WHITESPACE\n\t\t)\n\tlist(APPEND CMAKE_PREFIX_PATH ${HOMEBREW_PREFIX}/opt/qt)\nendif()\n\n# windows does not have libm\nif(NOT WIN32)\n\tfind_library(MATH_LIB m)\n\tfind_library(UTIL_LIB util)\nendif()\nif(WIN32)\n\tfind_library(OGG_LIB ogg)\n\ttarget_link_libraries(libopenage PRIVATE DbgHelp)\nendif()\nif(NOT APPLE AND NOT WIN32)\n\tfind_library(RT_LIB rt)\n\tif (${CMAKE_SYSTEM_NAME} STREQUAL \"FreeBSD\")\n\t\tfind_library(EXECINFO_LIB execinfo)\n\tendif()\nendif()\n\nfind_library(FONTCONFIG_LIB fontconfig)\n\nfind_package(toml11 REQUIRED)\nfind_package(Freetype REQUIRED)\nfind_package(PNG REQUIRED)\nfind_package(Opusfile REQUIRED)\nfind_package(Epoxy REQUIRED)\nfind_package(HarfBuzz 1.0.0 REQUIRED)\nfind_package(Eigen3 3.3 REQUIRED NO_MODULE)\n\nset(CMAKE_THREAD_PREFER_PTHREAD TRUE)\nfind_package(Threads REQUIRED)\n\nset(QT_VERSION_REQ \"6.2\")\nfind_package(Qt6 ${QT_VERSION_REQ} REQUIRED COMPONENTS Core Quick Multimedia)\n\nif(WANT_BACKTRACE)\n\tfind_package(GCCBacktrace)\nendif()\n\nif(WANT_IWYU)\n\t# include-what-you-use warnings during C++ compilation\n\tfind_program(IWYU_PATH NAMES include-what-you-use iwyu REQUIRED)\n\tset_property(TARGET libopenage PROPERTY CXX_INCLUDE_WHAT_YOU_USE \"${IWYU_PATH}\")\nendif()\n\n##################################################\n# nyan integration\n\n# first, try to locate nyan directly\n# this discovers the system package or the user-registry package\nfind_package(nyan CONFIG)\n\n# if this didn't work, we can download nyan like a git submodule.\n# this is the treeish to be checked out.\nif(NOT DEFINED NYAN_CLONE_VERSION)\n\tset(NYAN_CLONE_VERSION origin/master)\nendif()\n\noption(\n\tDOWNLOAD_NYAN\n\t\"whether to clone the nyan project in case it is not found\"\n\tOFF\n)\n\noption(\n\tFORCE_DOWNLOAD_NYAN\n\t\"Force the download and usage of the nyan project\"\n\tOFF\n)\n\noption(\n\tDISABLE_SUBPROJECT_UPDATES\n\t\"Disable the automatic update of subprojects over the internet\"\n\tOFF\n)\n\n# if nyan was not found, consider downloading it as subproject\n# only use the subproject mode if it was requested\n# or if it was used before.\nif((NOT nyan_FOUND AND DOWNLOAD_NYAN) OR FORCE_DOWNLOAD_NYAN)\n\tmessage(STATUS \"Downloading nyan as submodule project...\")\n\n\tif(DISABLE_SUBPROJECT_UPDATES)\n\t\tset(DISABLE_NYAN_UPDATES \"DISABLE_UPDATES\")\n\tendif()\n\n\tfetch_project(\n\t\tNAME nyan\n\t\t${DISABLE_NYAN_UPDATES}\n\t\tGIT_REPOSITORY https://github.com/SFTtech/nyan\n\t\tGIT_TAG ${NYAN_CLONE_VERSION}\n\t)\n\n\t# don't register nyan to the userpackage-repo!\n\tset(REGISTER_USERPACKAGE OFF)\n\t# don't generate the `doc` target again (name conflict!)\n\tset(DOXYGEN_ENABLE OFF)\n\n\t# register the targets\n\tadd_subdirectory(${nyan_SOURCE_DIR} ${nyan_BINARY_DIR})\n\n\tmessage(STATUS \"nyan processed successfully!\")\n\nelseif(NOT nyan_FOUND)\n\tmessage(FATAL_ERROR \"\n  Could not find the cmake package configuration file \\\"nyanConfig.cmake\\\".\n  To find it, you have several options:\n  * If your distribution provides it, install \\\"nyan\\\" through the package manager.\n  * If you want openage to automatically download \\\"nyan\\\", append `-DDOWNLOAD_NYAN=YES` to the cmake invocation or use `./configure --download-nyan`.\n  * If you want to build nyan manually, follow the build instructions:\n      [[  doc/building.md#nyan-installation  ]]\n  * If you already built nyan but it still can't be found (cmake package repo fails):\n    * Try to set \\\"nyan_DIR\\\" to the nyan build directory (it contains nyanConfig.cmake)\n      either through:  \\\"./configure $youroptions -- -Dnyan_DIR=/home/dev/nyan/build\\\"\n      or:              \\\"cmake $yourotheroptions -Dnyan_DIR=/home/dev/nyan/build ..\\\"\n\n  In case of other problems, please try to figure them out (and tell us what you did).\n  Contact information is in README.md.\n\")\nendif()\n\n\n##################################################\n# optional dependencies\n\n# advanced stacktraces with libbacktrace from gcc\nif(GCCBacktrace_FOUND)\n\ttarget_include_directories(libopenage PRIVATE ${GCCBacktrace_INCLUDE_DIRS})\n\ttarget_link_libraries(libopenage PRIVATE \"${GCCBacktrace_LIBRARIES}\")\n\thave_config_option(backtrace BACKTRACE true)\nelse()\n\thave_config_option(backtrace BACKTRACE false)\nendif()\n\n# google performance tools\nif(WANT_GPERFTOOLS_PROFILER OR WANT_GPERFTOOLS_TCMALLOC)\n\tfind_package(GPerfTools)\nendif()\n\nif(WANT_GPERFTOOLS_PROFILER AND GPERFTOOLS_FOUND)\n\thave_config_option(gperftools-profiler GPERFTOOLS_PROFILER true)\n\ttarget_include_directories(libopenage PRIVATE ${GPERFTOOLS_INCLUDE_DIR})\n\ttarget_link_libraries(libopenage PRIVATE ${GPERFTOOLS_PROFILER})\nelse()\n\thave_config_option(gperftools-profiler GPERFTOOLS_PROFILER false)\nendif()\n\nif(WITH_GPERFTOOLS_TCMALLOC AND GPERFTOOLS_FOUND)\n\thave_config_option(gperftools-tcmalloc GPERFTOOLS_TCMALLOC true)\n\ttarget_include_directories(libopenage PRIVATE ${GPERFTOOLS_INCLUDE_DIR})\n\ttarget_link_libraries(libopenage PRIVATE ${GPERFTOOLS_TCMALLOC})\nelse()\n\thave_config_option(gperftools-tcmalloc GPERFTOOLS_TCMALLOC false)\nendif()\n\n# inotify support\nif(WANT_INOTIFY)\n\tfind_package(Inotify)\nendif()\n\nif(WANT_INOTIFY AND INOTIFY_FOUND)\n\thave_config_option(inotify INOTIFY true)\n\ttarget_include_directories(libopenage PRIVATE ${INOTIFY_INCLUDE_DIRS})\nelse()\n\thave_config_option(inotify INOTIFY false)\nendif()\n\n# ncurses support\nif(WANT_NCURSES)\n\tset(CURSES_NEED_NCURSES TRUE)\n\tset(CURSES_NEED_WIDE TRUE)\n\tfind_package(Curses)\nendif()\n\nif(WANT_NCURSES AND CURSES_FOUND)\n\thave_config_option(ncurses NCURSES true)\n\ttarget_include_directories(libopenage PRIVATE ${CURSES_INCLUDE_DIRS})\n\ttarget_link_libraries(libopenage PRIVATE ${CURSES_LIBRARIES})\nelse()\n\thave_config_option(ncurses NCURSES false)\nendif()\n\n# opengl support\nif(WANT_OPENGL)\n\tfind_package(OpenGL)\nendif()\n\n# vulkan support\nif(WANT_VULKAN)\n\tfind_package(Vulkan)\nendif()\n\nif(WANT_OPENGL AND OPENGL_FOUND)\n\thave_config_option(opengl OPENGL true)\n\ttarget_link_libraries(libopenage PRIVATE OpenGL::GL)\nelse()\n\thave_config_option(opengl OPENGL false)\nendif()\n\nif(WANT_VULKAN AND VULKAN_FOUND)\n\thave_config_option(vulkan VULKAN true)\n\ttarget_link_libraries(libopenage PRIVATE Vulkan::Vulkan)\nelse()\n\thave_config_option(vulkan VULKAN false)\nendif()\n\nif(NOT (OPENGL_FOUND OR VULKAN_FOUND))\n\tmessage(FATAL_ERROR \"One of OpenGL or Vulkan is required!\")\nendif()\n\n##################################################\n# build configuration generation\nget_config_option_string()\n\nconfigure_file(config.h.in config.h)\nconfigure_file(config.cpp.in config.cpp)\nconfigure_file(version.h.in version.h)\nconfigure_file(version.cpp.in version.cpp)\n\nconfigure_file(\n\t\"${CMAKE_SOURCE_DIR}/openage/config.py.in\"\n\t\"${CMAKE_BINARY_DIR}/openage/config.py\"\n)\n\n##################################################\n# directories for header inclusion\ntarget_include_directories(libopenage\n\tPUBLIC\n\t${CMAKE_CURRENT_BINARY_DIR}\n\tPRIVATE\n\t${CMAKE_CURRENT_SOURCE_DIR}\n\t${FREETYPE_INCLUDE_DIRS}\n\t${EPOXY_INCLUDE_DIRS}\n\t${OPUS_INCLUDE_DIRS}\n\t${PNG_INCLUDE_DIRS}\n\t${HarfBuzz_INCLUDE_DIRS}\n\t${QTPLATFORM_INCLUDE_DIRS}\n)\n\n##################################################\n# dependency linking\n\n# all the libraries are not exposed\n# to the public api of libopenage\ntarget_link_libraries(libopenage\n\tPRIVATE\n\t\tThreads::Threads\n\t\tnyan::nyan\n\t\tEigen3::Eigen\n\t\t${PNG_LIBRARIES}\n\t\t${OPUS_LIBRARIES}\n\t\t${OGG_LIB}\n\n\t\t${CMAKE_DL_LIBS}\n\t\t${FONTCONFIG_LIB}\n\t\t${FREETYPE_LIBRARIES}\n\t\t${EPOXY_LIBRARIES}\n\t\t${MATH_LIB}\n\t\t${UTIL_LIB}\n\t\t${HarfBuzz_LIBRARIES}\n\t\t${RT_LIB}\n\t\t${EXECINFO_LIB}\n\t\tQt6::Core\n\t\tQt6::Quick\n\t\tQt6::Multimedia\n)\n\n##################################################\n# installation of the library\ninstall(TARGETS libopenage\n\tRUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}\n\tLIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})\n\nif(WIN32)\n\tinstall(FILES $<TARGET_FILE:nyan::nyan>\n\t\tDESTINATION ${CMAKE_INSTALL_BINDIR})\nendif()\n\n\n##################################################\n# source file definitions\nget_codegen_scu_file()\n\n# add new sources here, dependencies for linking and including\n# are specified above the source file list.\n\nadd_sources(libopenage\n\tmain.cpp\n\toptions.cpp\n\t${CMAKE_CURRENT_BINARY_DIR}/config.cpp\n\t${CMAKE_CURRENT_BINARY_DIR}/version.cpp\n\t${CODEGEN_SCU_FILE}\n)\n\npxdgen(\n\tmain.h\n)\n\n# add subsystem folders\nadd_subdirectory(\"assets\")\nadd_subdirectory(\"audio\")\nadd_subdirectory(\"console\")\nadd_subdirectory(\"coord\")\nadd_subdirectory(\"curve\")\nadd_subdirectory(\"cvar\")\nadd_subdirectory(\"datastructure\")\nadd_subdirectory(\"engine\")\nadd_subdirectory(\"error\")\nadd_subdirectory(\"event\")\nadd_subdirectory(\"gamestate\")\nadd_subdirectory(\"input\")\nadd_subdirectory(\"job\")\nadd_subdirectory(\"log\")\nadd_subdirectory(\"main\")\nadd_subdirectory(\"pathfinding\")\nadd_subdirectory(\"presenter\")\nadd_subdirectory(\"pyinterface\")\nadd_subdirectory(\"renderer\")\nadd_subdirectory(\"rng\")\nadd_subdirectory(\"testing\")\nadd_subdirectory(\"time\")\nadd_subdirectory(\"util\")\nadd_subdirectory(\"versions\")\n"
  },
  {
    "path": "libopenage/assets/CMakeLists.txt",
    "content": "add_sources(libopenage\n    mod_manager.cpp\n    modpack.cpp\n)\n"
  },
  {
    "path": "libopenage/assets/mod_manager.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"mod_manager.h\"\n\n#include <unordered_set>\n\nnamespace openage::assets {\n\nModManager::ModManager(const util::Path &asset_base_dir) :\n\tasset_base_dir{asset_base_dir} {\n}\n\nvoid ModManager::register_modpack(const util::Path &info_file) {\n\tauto info = parse_modepack_def(info_file);\n\tthis->available.emplace(info.id, info);\n}\n\nvoid ModManager::register_modpack(const ModpackInfo &info) {\n\tthis->available.emplace(info.id, info);\n}\n\nvoid ModManager::activate_modpacks(const std::vector<std::string> &load_order) {\n\tthis->set_load_order(load_order);\n\n\t// Clear in case there are already active modpacks\n\tthis->active.clear();\n\n\tfor (const auto &modpack_id : load_order) {\n\t\tauto &modpack = this->available.at(modpack_id);\n\t\tthis->active.emplace(modpack_id, std::make_shared<Modpack>(modpack));\n\t\tlog::log(MSG(info) << \"Activated modpack: \" << modpack_id);\n\t}\n}\n\nvoid ModManager::set_load_order(const std::vector<std::string> &load_order) {\n\tstd::unordered_set<std::string> requested{load_order.begin(), load_order.end()};\n\n\t// sanity check: availability, dependencies, conflicts\n\tfor (const auto &modpack_id : load_order) {\n\t\tif (not this->available.contains(modpack_id)) {\n\t\t\tthrow Error{MSG(err) << \"Requested modpack '\" << modpack_id << \"' not available.\"};\n\t\t}\n\n\t\tauto &info = this->available.at(modpack_id);\n\t\tfor (const auto &dependency : info.dependencies) {\n\t\t\tif (not requested.contains(dependency)) {\n\t\t\t\tthrow Error{MSG(err) << \"Dependency '\" << dependency << \"' of modpack '\"\n\t\t\t\t                     << modpack_id << \"' not satisfied\"};\n\t\t\t}\n\t\t}\n\n\t\tfor (const auto &conflict : info.conflicts) {\n\t\t\tif (requested.contains(conflict)) {\n\t\t\t\tthrow Error{MSG(err) << \"Modpack '\" << modpack_id << \"' conflicts with modpack '\"\n\t\t\t\t                     << conflict << \"'\"};\n\t\t\t}\n\t\t}\n\t}\n\n\tthis->load_order = load_order;\n}\n\nstd::shared_ptr<Modpack> ModManager::get_modpack(const std::string &modpack_id) const {\n\tif (not this->active.contains(modpack_id)) {\n\t\tthrow Error{MSG(err) << \"Modpack not loaded: \" << modpack_id};\n\t}\n\n\treturn this->active.at(modpack_id);\n}\n\nconst std::vector<std::string> &ModManager::get_load_order() const {\n\treturn this->load_order;\n}\n\n} // namespace openage::assets\n"
  },
  {
    "path": "libopenage/assets/mod_manager.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"assets/modpack.h\"\n#include \"util/path.h\"\n\nnamespace openage::assets {\n\nclass ModManager {\npublic:\n\tModManager(const util::Path &asset_base_dir);\n\t~ModManager() = default;\n\n\t/**\n\t * Adds a modpack to the list of available modpacks.\n\t *\n\t * @param info_file Path to the modpack definition file.\n\t */\n\tvoid register_modpack(const util::Path &info_file);\n\n\t/**\n\t * Adds a modpack to the list of available modpacks.\n\t *\n\t * @param info Modpack definition.\n\t */\n\tvoid register_modpack(const ModpackInfo &info);\n\n\t/**\n\t * Ready a set of modpacks with the given IDs.\n\t *\n\t * This prepares the modpacks for loading by valiating the integrity of\n\t * the contained data, checking if the load order and mounting the\n\t * assets into the virtual filesystem.\n\t *\n\t * TODO:\n\t *     - Mount modpacks into virtual filesystem.\n\t *     - Validate manifest.toml\n\t *     - Verify signature of manifest.toml (if signed)\n\t *\n\t * Note that data inside the modpack is not loaded yet. Data\n\t * loading is done inside the simulation before the game starts or\n\t * in case of media files, when they are requested during the game.\n\t *\n\t * Modpacks must have been registered with \\p register_modpack() before\n\t * activating.\n\t *\n\t * @param load_order Load order of modpacks.\n\t */\n\tvoid activate_modpacks(const std::vector<std::string> &load_order);\n\n\t/**\n\t * Get a loaded modpack by its ID.\n\t *\n\t * @param modpack_id ID of the modpack to get.\n\t *\n\t * @return Modpack with the given ID.\n\t */\n\tstd::shared_ptr<Modpack> get_modpack(const std::string &modpack_id) const;\n\n\t/**\n\t * Get the load order of modpacks.\n\t *\n\t * @return Modpack IDs in the order that they should be loaded.\n\t */\n\tconst std::vector<std::string> &get_load_order() const;\n\n\t/**\n\t * Enumerates all modpack ids in a given directory.\n\t *\n\t * This also loads available modpack definition files.\n\t *\n\t * @param directory Path to the directory to enumerate.\n\t *\n\t * @return Infos of the identified modpacks.\n\t */\n\tstatic std::vector<ModpackInfo> enumerate_modpacks(const util::Path &directory) {\n\t\tstd::vector<ModpackInfo> result;\n\n\t\tif (not(directory.exists() and directory.is_dir())) {\n\t\t\tthrow Error{MSG(err) << \"Modpack directory '\" << directory << \"' does not exist.\"};\n\t\t}\n\n\t\tauto dir = const_cast<util::Path &>(directory);\n\t\tfor (auto entry : dir.iterdir()) {\n\t\t\tif (entry.is_dir()) {\n\t\t\t\tauto info_file = entry / \"modpack.toml\";\n\t\t\t\tif (info_file.exists()) {\n\t\t\t\t\tresult.push_back(parse_modepack_def(info_file));\n\t\t\t\t\tlog::log(INFO << \"Found modpack: \" << result.back().id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\nprivate:\n\t/**\n\t * Set the order in which modpack data should be loaded.\n\t *\n\t * This also checks whether the given load order is valid, i.e.\n\t * by checking if all dependencies/conflicts are resolved.\n\t *\n\t * TODO: Dynamically resolve load order?\n\t *\n\t * @param load_order Load order of modpacks.\n\t */\n\tvoid set_load_order(const std::vector<std::string> &load_order);\n\n\t/**\n\t * TODO: Mount point for modpacks.\n\t */\n\tutil::Path asset_base_dir;\n\n\t/**\n\t * Active modpacks. Maps their ID ('name' in the modpack definition file)\n\t * to the modpack.\n\t */\n\tstd::unordered_map<std::string, std::shared_ptr<Modpack>> active;\n\n\t/**\n\t * Available modpacks that can be activated. Maps their ID ('name' in the modpack\n\t * definition file) to the modpack info.\n\t */\n\tstd::unordered_map<std::string, ModpackInfo> available;\n\n\t/**\n\t * Load order of modpacks.\n\t */\n\tstd::vector<std::string> load_order;\n};\n\n} // namespace openage::assets\n"
  },
  {
    "path": "libopenage/assets/modpack.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"modpack.h\"\n\n#include <toml.hpp>\n\n#include \"error/error.h\"\n\nnamespace openage::assets {\n\nModpackInfo parse_modepack_def(const util::Path &info_file) {\n\tModpackInfo def;\n\tdef.path = info_file.get_parent();\n\n\tconst auto modpack_def = toml::parse(info_file.resolve_native_path());\n\n\t// info table\n\tconst toml::table info = toml::find<toml::table>(modpack_def, \"info\");\n\tif (info.contains(\"name\")) {\n\t\tdef.id = info.at(\"name\").as_string();\n\t}\n\telse {\n\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t                     << \" 'info' table misses 'name' parameter.\"};\n\t}\n\n\tif (info.contains(\"version\")) {\n\t\tdef.version = info.at(\"version\").as_string();\n\t}\n\telse {\n\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t                     << \" 'info' table misses 'version' parameter.\"};\n\t}\n\n\t// optionals\n\tif (info.contains(\"versionstr\")) {\n\t\tdef.versionstr = info.at(\"versionstr\").as_string();\n\t}\n\tif (info.contains(\"repo\")) {\n\t\tdef.repo = info.at(\"repo\").as_string();\n\t}\n\tif (info.contains(\"alias\")) {\n\t\tdef.alias = info.at(\"alias\").as_string();\n\t}\n\tif (info.contains(\"title\")) {\n\t\tdef.title = info.at(\"title\").as_string();\n\t}\n\tif (info.contains(\"description\")) {\n\t\tdef.description = info.at(\"description\").as_string();\n\t}\n\tif (info.contains(\"long_description\")) {\n\t\tdef.long_description = info.at(\"long_description\").as_string();\n\t}\n\tif (info.contains(\"url\")) {\n\t\tdef.url = info.at(\"url\").as_string();\n\t}\n\tif (info.contains(\"licenses\")) {\n\t\tstd::vector<std::string> licenses{};\n\t\tfor (const auto &license : info.at(\"licenses\").as_array()) {\n\t\t\tlicenses.push_back(license.as_string());\n\t\t}\n\t\tdef.licenses = licenses;\n\t}\n\n\t// assets table\n\tconst toml::table assets = toml::find<toml::table>(modpack_def, \"assets\");\n\tstd::vector<std::string> includes{};\n\tfor (const auto &include : assets.at(\"include\").as_array()) {\n\t\tincludes.push_back(include.as_string());\n\t}\n\tdef.includes = includes;\n\n\t// optionals\n\tif (assets.contains(\"exclude\")) {\n\t\tstd::vector<std::string> excludes{};\n\t\tfor (const auto &exclude : assets.at(\"exclude\").as_array()) {\n\t\t\texcludes.push_back(exclude.as_string());\n\t\t}\n\t\tdef.excludes = excludes;\n\t}\n\n\t// dependency table\n\tif (modpack_def.contains(\"dependency\")) {\n\t\tconst toml::table dependency = toml::find<toml::table>(modpack_def, \"dependency\");\n\t\tstd::vector<std::string> deps{};\n\n\t\tif (not dependency.contains(\"modpacks\")) {\n\t\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t\t                     << \" 'dependency' table misses 'modpacks' parameter.\"};\n\t\t}\n\n\t\tfor (const auto &dep : dependency.at(\"modpacks\").as_array()) {\n\t\t\tdeps.push_back(dep.as_string());\n\t\t}\n\t\tdef.dependencies = deps;\n\t}\n\n\t// conflicts table\n\tif (modpack_def.contains(\"conflict\")) {\n\t\tconst toml::table conflict = toml::find<toml::table>(modpack_def, \"conflict\");\n\t\tstd::vector<std::string> conflicts{};\n\n\t\tif (not conflict.contains(\"modpacks\")) {\n\t\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t\t                     << \" 'conflict' table misses 'modpacks' parameter.\"};\n\t\t}\n\n\t\tfor (const auto &cf : conflict.at(\"modpacks\").as_array()) {\n\t\t\tconflicts.push_back(cf.as_string());\n\t\t}\n\t\tdef.conflicts = conflicts;\n\t}\n\n\t// authors table\n\tif (modpack_def.contains(\"authors\")) {\n\t\tconst toml::table authors = toml::find<toml::table>(modpack_def, \"authors\");\n\t\tstd::vector<AuthorInfo> author_infos{};\n\t\tfor (const auto &author : authors) {\n\t\t\tAuthorInfo author_info{};\n\t\t\tif (author.second.contains(\"name\")) {\n\t\t\t\tauthor_info.name = author.second.at(\"name\").as_string();\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t\t\t                     << \" 'author' table misses 'name' parameter.\"};\n\t\t\t}\n\n\t\t\t// optionals\n\t\t\tif (author.second.contains(\"fullname\")) {\n\t\t\t\tauthor_info.fullname = author.second.at(\"fullname\").as_string();\n\t\t\t}\n\t\t\tif (author.second.contains(\"since\")) {\n\t\t\t\tauthor_info.since = author.second.at(\"since\").as_string();\n\t\t\t}\n\t\t\tif (author.second.contains(\"until\")) {\n\t\t\t\tauthor_info.until = author.second.at(\"until\").as_string();\n\t\t\t}\n\t\t\tif (author.second.contains(\"roles\")) {\n\t\t\t\tstd::vector<std::string> roles{};\n\t\t\t\tfor (const auto &role : author.second.at(\"roles\").as_array()) {\n\t\t\t\t\troles.push_back(role.as_string());\n\t\t\t\t}\n\t\t\t\tauthor_info.roles = roles;\n\t\t\t}\n\t\t\tif (author.second.contains(\"contact\")) {\n\t\t\t\tconst toml::table contact = toml::find<toml::table>(author.second, \"contact\");\n\t\t\t\tstd::unordered_map<std::string, std::string> contacts{};\n\t\t\t\tfor (const auto &contact_info : contact) {\n\t\t\t\t\tcontacts[contact_info.first] = contact_info.second.as_string();\n\t\t\t\t}\n\t\t\t\tauthor_info.contact = contacts;\n\t\t\t}\n\n\t\t\tauthor_infos.push_back(author_info);\n\t\t}\n\t\tdef.authors = author_infos;\n\t}\n\n\t// authorgroups table\n\tif (modpack_def.contains(\"authorgroups\")) {\n\t\tconst toml::table authorgroups = toml::find<toml::table>(modpack_def, \"authorgroups\");\n\t\tstd::vector<AuthorGroupInfo> author_group_infos{};\n\t\tfor (const auto &authorgroup : authorgroups) {\n\t\t\tAuthorGroupInfo author_group_info{};\n\t\t\tif (authorgroup.second.contains(\"name\")) {\n\t\t\t\tauthor_group_info.name = authorgroup.second.at(\"name\").as_string();\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t\t\t                     << \" 'authorgroups' table misses 'name' parameter.\"};\n\t\t\t}\n\n\t\t\tif (authorgroup.second.contains(\"authors\")) {\n\t\t\t\tstd::vector<std::string> authors{};\n\t\t\t\tfor (const auto &author : authorgroup.second.at(\"authors\").as_array()) {\n\t\t\t\t\tauthors.push_back(author.as_string());\n\t\t\t\t}\n\t\t\t\tauthor_group_info.authors = authors;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow Error{MSG(err) << \"Modpack definition file at \" << info_file\n\t\t\t\t                     << \" 'authorgroups' table misses 'authors' parameter.\"};\n\t\t\t}\n\n\t\t\t// optionals\n\t\t\tif (authorgroup.second.contains(\"description\")) {\n\t\t\t\tauthor_group_info.description = authorgroup.second.at(\"description\").as_string();\n\t\t\t}\n\n\t\t\tauthor_group_infos.push_back(author_group_info);\n\t\t}\n\t\tdef.author_groups = author_group_infos;\n\t}\n\n\treturn def;\n}\n\n\nModpack::Modpack(const util::Path &info_file) :\n\tinfo{parse_modepack_def(info_file)} {\n}\n\nModpack::Modpack(const ModpackInfo &info) :\n\tinfo{info} {}\n\nModpack::Modpack(const ModpackInfo &&info) :\n\tinfo{info} {\n}\n\nconst ModpackInfo &Modpack::get_info() const {\n\treturn this->info;\n}\n\nbool Modpack::check_integrity() const {\n\t// TODO: Check manifest file\n\tthrow Error{MSG(err) << \"Integrity check not implemented yet.\"};\n}\n\n} // namespace openage::assets\n"
  },
  {
    "path": "libopenage/assets/modpack.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"util/path.h\"\n\n\nnamespace openage::assets {\n\n/**\n * Author information.\n */\nstruct AuthorInfo {\n\tstd::string name;\n\tstd::string fullname;\n\tstd::string since;\n\tstd::string until;\n\tstd::vector<std::string> roles;\n\tstd::unordered_map<std::string, std::string> contact;\n};\n\n/**\n * Author group information.\n */\nstruct AuthorGroupInfo {\n\tstd::string name;\n\tstd::string description;\n\tstd::vector<std::string> authors;\n};\n\n/**\n * Modpack metadata information.\n */\nstruct ModpackInfo {\n\t// modpack location (directory)\n\tutil::Path path;\n\n\t// modpack information\n\t// see /doc/media/openage/modpack_definition_file.md\n\tstd::string id;\n\tstd::string version;\n\tstd::string versionstr;\n\tstd::string repo;\n\tstd::string alias;\n\tstd::string title;\n\tstd::string description;\n\tstd::string long_description;\n\tstd::string url;\n\tstd::vector<std::string> licenses;\n\n\tstd::vector<std::string> dependencies;\n\tstd::vector<std::string> conflicts;\n\n\tstd::vector<std::string> includes;\n\tstd::vector<std::string> excludes;\n\n\tstd::vector<AuthorInfo> authors;\n\tstd::vector<AuthorGroupInfo> author_groups;\n};\n\n/**\n * Parse a modpack definition file.\n *\n * @param info_file openage modpack definition file.\n * @return Modpack metadata information.\n */\nModpackInfo parse_modepack_def(const util::Path &info_file);\n\n/**\n * Modpack definition.\n *\n * Modpacks are a collection of assets (data files, graphics, sounds, etc.)\n * that can be loaded into the engine.\n */\nclass Modpack {\npublic:\n\t/**\n\t * Create a new modpack.\n\t *\n\t * Loads the modpack using the information in the definition file.\n\t *\n\t * @param info_file Path to the modpack definition file.\n\t */\n\tModpack(const util::Path &info_file);\n\n\t/**\n\t * Create a new modpack from an existing modpack info.\n\t *\n\t * @param info Modpack metadata information.\n\t */\n\tModpack(const ModpackInfo &info);\n\n\t/**\n\t * Create a new modpack from an existing modpack info.\n\t *\n\t * @param info Modpack metadata information.\n\t */\n\tModpack(const ModpackInfo &&info);\n\n\tModpack(const Modpack &) = delete;\n\tModpack(Modpack &&) = default;\n\t~Modpack() = default;\n\n\t/**\n\t * Get the metadata information of the modpack.\n\t *\n\t * @return Modpack metadata information.\n\t */\n\tconst ModpackInfo &get_info() const;\n\n\t/**\n\t * Check if the modpack is valid.\n\t *\n\t * @return true if the modpack is valid, false otherwise.\n\t */\n\tbool check_integrity() const;\n\nprivate:\n\t/**\n\t * Modpack metadata information.\n\t */\n\tModpackInfo info;\n};\n\n} // namespace openage::assets\n"
  },
  {
    "path": "libopenage/audio/CMakeLists.txt",
    "content": "add_sources(libopenage\n\taudio_manager.cpp\n\tcategory.cpp\n\tdynamic_loader.cpp\n\tdynamic_resource.cpp\n\terror.cpp\n\tformat.cpp\n\tin_memory_loader.cpp\n\tin_memory_resource.cpp\n\topus_dynamic_loader.cpp\n\topus_in_memory_loader.cpp\n\topus_loading.cpp\n\tloader_policy.cpp\n\tresource.cpp\n\tresource_def.cpp\n\tsound.cpp\n)\n"
  },
  {
    "path": "libopenage/audio/audio_manager.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include \"audio_manager.h\"\n\n#include <cstring>\n#include <sstream>\n\n#include <QAudioDevice>\n#include <QAudioFormat>\n#include <QAudioSink>\n#include <QMediaDevices>\n\n#include \"../log/log.h\"\n#include \"../util/misc.h\"\n#include \"error.h\"\n#include \"hash_functions.h\"\n#include \"resource.h\"\n\n\nnamespace openage {\nnamespace audio {\n\nAudioManager::AudioManager(const std::shared_ptr<job::JobManager> &job_manager,\n                           const std::string &device_name) :\n\tavailable{false},\n\tjob_manager{job_manager},\n\tdevice_name{device_name},\n\tdevice_format{std::make_shared<QAudioFormat>()},\n\tdevice{nullptr},\n\taudio_sink{nullptr} {\n\t// set desired audio output format\n\tthis->device_format->setSampleRate(48000);\n\tthis->device_format->setSampleFormat(QAudioFormat::SampleFormat::Int16);\n\tthis->device_format->setChannelCount(2);\n\n\t// find the device with the given name\n\tfor (auto &device : QMediaDevices::audioOutputs()) {\n\t\tif (device.description().toStdString() == device_name) {\n\t\t\tthis->device = std::make_shared<QAudioDevice>(device);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// select the default device if the name was not found\n\tif (not this->device) {\n\t\tthis->device = std::make_shared<QAudioDevice>(QMediaDevices::defaultAudioOutput());\n\t\treturn;\n\t}\n\n\tif (not this->device->isFormatSupported(*this->device_format)) {\n\t\tlog::log(MSG(err) << \"Audio device does not support the desired format!\");\n\t\treturn;\n\t}\n\n\t// create audio sink\n\tthis->audio_sink = std::make_unique<QAudioSink>(*this->device.get(), *this->device_format.get());\n\t// TODO: connect callback to get audio data to device\n\n\t// initialize playing sounds vectors\n\tusing sound_vector = std::vector<std::shared_ptr<SoundImpl>>;\n\tplaying_sounds.insert({category_t::GAME, sound_vector{}});\n\tplaying_sounds.insert({category_t::INTERFACE, sound_vector{}});\n\tplaying_sounds.insert({category_t::MUSIC, sound_vector{}});\n\tplaying_sounds.insert({category_t::TAUNT, sound_vector{}});\n\n\t// create buffer for mixing\n\tthis->mix_buffer = std::make_unique<int32_t[]>(\n\t\t4 * device_format->bytesPerSample() * device_format->channelCount());\n\n\tlog::log(MSG(info) << \"Using audio device: \"\n\t                   << (device_name.empty() ? \"default\" : device_name)\n\t                   << \" [sample rate=\" << device_format->sampleRate()\n\t                   << \", format=\" << device_format->sampleFormat()\n\t                   << \", channels=\" << device_format->channelCount()\n\t                   << \", samples=\" << device_format->bytesPerSample()\n\t                   << \"]\");\n\n\tthis->audio_sink->stop();\n\n\tthis->available = true;\n}\n\nAudioManager::~AudioManager() {\n\tthis->audio_sink->stop();\n}\n\nvoid AudioManager::load_resources(const std::vector<resource_def> &sound_files) {\n\tif (not this->available) {\n\t\treturn;\n\t}\n\n\tfor (auto &sound_file : sound_files) {\n\t\tauto key = std::make_tuple(sound_file.category, sound_file.id);\n\n\t\tif (this->resources.find(key) != std::end(this->resources)) {\n\t\t\t// sound is already loaded\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto resource = Resource::create_resource(this, sound_file);\n\t\tthis->resources.insert({key, resource});\n\t}\n}\n\nSound AudioManager::get_sound(category_t category, int id) {\n\tif (not this->available) {\n\t\tthrow error::Error{MSG(err) << \"audio manager not available, \"\n\t\t                            << \"but sound was requested!\"};\n\t}\n\n\tauto resource = resources.find(std::make_tuple(category, id));\n\tif (resource == std::end(resources)) {\n\t\tthrow audio::Error{\n\t\t\tMSG(err) << \"Sound resource does not exist: \"\n\t\t\t\t\t\t\"category=\"\n\t\t\t\t\t << category << \", \"\n\t\t\t\t\t << \"id=\" << id};\n\t}\n\n\tauto sound_impl = std::make_shared<SoundImpl>(resource->second);\n\treturn Sound{this, sound_impl};\n}\n\n\nvoid AudioManager::audio_callback(int16_t *stream, int length) {\n\tstd::memset(mix_buffer.get(), 0, length * 4);\n\n\t// iterate over all categories\n\tfor (auto &entry : this->playing_sounds) {\n\t\tauto &playing_list = entry.second;\n\t\t// iterate over all sounds in one category\n\t\tfor (size_t i = 0; i < playing_list.size(); i++) {\n\t\t\tauto &sound = playing_list[i];\n\t\t\tauto sound_finished = sound->mix_audio(mix_buffer.get(), length);\n\t\t\t// if the sound is finished,\n\t\t\t// it should be removed from the playing list\n\t\t\tif (sound_finished) {\n\t\t\t\tutil::vector_remove_swap_end(playing_list, i);\n\t\t\t\ti--;\n\t\t\t}\n\t\t}\n\t}\n\n\t// write the mix buffer to the output stream and adjust volume\n\tfor (int i = 0; i < length; i++) {\n\t\tauto value = mix_buffer[i] / 256;\n\t\tif (value > 32767) {\n\t\t\tvalue = 32767;\n\t\t}\n\t\telse if (value < -32768) {\n\t\t\tvalue = -32768;\n\t\t}\n\t\tstream[i] = static_cast<int16_t>(value);\n\t}\n}\n\nvoid AudioManager::add_sound(const std::shared_ptr<SoundImpl> &sound) {\n\tauto category = sound->get_category();\n\tauto &playing_list = this->playing_sounds.find(category)->second;\n\t// TODO probably check if sound already exists in playing list\n\tplaying_list.push_back(sound);\n}\n\nvoid AudioManager::remove_sound(const std::shared_ptr<SoundImpl> &sound) {\n\tauto category = sound->get_category();\n\tauto &playing_list = this->playing_sounds.find(category)->second;\n\n\tfor (size_t i = 0; i < playing_list.size(); i++) {\n\t\tif (playing_list[i] == sound) {\n\t\t\tutil::vector_remove_swap_end(playing_list, i);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nconst std::shared_ptr<QAudioFormat> &AudioManager::get_device_spec() const {\n\treturn this->device_format;\n}\n\nconst std::shared_ptr<job::JobManager> &AudioManager::get_job_manager() const {\n\treturn this->job_manager;\n}\n\nbool AudioManager::is_available() const {\n\treturn this->available;\n}\n\n\nstd::vector<std::string> AudioManager::get_devices() {\n\tauto devices = QMediaDevices::audioOutputs();\n\n\tstd::vector<std::string> device_list;\n\tdevice_list.reserve(devices.size());\n\n\tfor (auto &device : devices) {\n\t\tdevice_list.emplace_back(device.description().toStdString());\n\t}\n\n\treturn device_list;\n}\n\nstd::string AudioManager::get_default_device() {\n\treturn QMediaDevices::defaultAudioOutput().description().toStdString();\n}\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/audio_manager.h",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include <QObject>\n\n#include \"category.h\"\n#include \"hash_functions.h\"\n#include \"resource_def.h\"\n#include \"sound.h\"\n\nQ_FORWARD_DECLARE_OBJC_CLASS(QAudioDevice);\nQ_FORWARD_DECLARE_OBJC_CLASS(QAudioFormat);\nQ_FORWARD_DECLARE_OBJC_CLASS(QAudioSink);\n\nnamespace openage {\n\nnamespace job {\nclass JobManager;\n}\n\n\nnamespace audio {\n\n/**\n * This class provides audio functionality for openage.\n *\n * TODO: Finish porting to Qt.\n */\nclass AudioManager {\npublic:\n\t/**\n\t * Initializes the audio manager with the given device name.\n\t * If the name is empty, the default device is used.\n\t */\n\tAudioManager(const std::shared_ptr<job::JobManager> &job_manager,\n\t             const std::string &device_name = \"\");\n\n\t~AudioManager();\n\n\tAudioManager(const AudioManager &) = delete;\n\tAudioManager(AudioManager &&) = delete;\n\n\tAudioManager &operator=(const AudioManager &) = delete;\n\tAudioManager &operator=(AudioManager &&) = delete;\n\n\t/**\n\t * Loads all audio resources, that are specified in the sound_files vector.\n\t * @param sound_files a list of all sound resources\n\t */\n\tvoid load_resources(const std::vector<resource_def> &sound_files);\n\n\t/**\n\t * Returns a sound object with the given category and the given id. If no\n\t * such sound exists an Error will be thrown.\n\t * @param category the sound's category\n\t * @param id the sound's id\n\t */\n\tSound get_sound(category_t category, int id);\n\n\t/**\n\t * Called from the audio system once to request new data.\n\t */\n\tvoid audio_callback(int16_t *stream, int length);\n\n\t/**\n\t * Returns the currently used audio output format.\n\t */\n\tconst std::shared_ptr<QAudioFormat> &get_device_spec() const;\n\n\t/**\n\t * Return the game engine the audio manager is attached to.\n\t */\n\tconst std::shared_ptr<job::JobManager> &get_job_manager() const;\n\n\t/**\n\t * If this audio manager is available.\n\t * It's not available if\n\t */\n\tbool is_available() const;\n\n\t/**\n\t * Returns a vector of all available device names.\n\t */\n\tstatic std::vector<std::string> get_devices();\n\n\t/**\n\t * Returns the default device name.\n\t */\n\tstatic std::string get_default_device();\n\nprivate:\n\tvoid add_sound(const std::shared_ptr<SoundImpl> &sound);\n\tvoid remove_sound(const std::shared_ptr<SoundImpl> &sound);\n\n\t// Sound is the AudioManager's friend, so that only sounds can access the\n\t// add and remove sound method's\n\tfriend class Sound;\n\n\t/**\n\t * If no audio device was found, this audio manager\n\t * will be unavailable.\n\t */\n\tbool available;\n\n\t/**\n\t * The job manager used in this audio manager for job queuing.\n\t */\n\tstd::shared_ptr<job::JobManager> job_manager;\n\n\t/**\n\t * the used audio device's name\n\t */\n\tstd::string device_name;\n\n\t/**\n\t * the audio output format\n\t */\n\tstd::shared_ptr<QAudioFormat> device_format;\n\n\t/**\n\t * the used audio device's id\n\t */\n\tstd::shared_ptr<QAudioDevice> device;\n\n\t/**\n\t * the audio sink\n\t */\n\tstd::shared_ptr<QAudioSink> audio_sink;\n\n\t/**\n\t * Buffer used for mixing audio to one stream.\n\t */\n\tstd::unique_ptr<int32_t[]> mix_buffer;\n\n\tstd::unordered_map<std::tuple<category_t, int>, std::shared_ptr<Resource>> resources;\n\n\tstd::unordered_map<category_t, std::vector<std::shared_ptr<SoundImpl>>> playing_sounds;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/category.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"category.h\"\n\n#include \"../log/log.h\"\n#include \"../error/error.h\"\n#include \"../util/strings.h\"\n\nnamespace openage {\nnamespace audio {\n\n\nconst char *category_t_to_str(category_t val) {\n\tswitch (val) {\n\tcase category_t::GAME:      return \"GAME\";\n\tcase category_t::INTERFACE: return \"INTERFACE\";\n\tcase category_t::MUSIC:     return \"MUSIC\";\n\tcase category_t::TAUNT:     return \"TAUNT\";\n\tdefault:                    return \"unknown\";\n\t}\n}\n\n\nstd::ostream &operator <<(std::ostream &os, const category_t val) {\n\tos << category_t_to_str(val);\n\treturn os;\n}\n\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/category.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <iostream>\n\nnamespace openage::audio {\n\nenum class category_t {\n\tGAME,\n\tINTERFACE,\n\tMUSIC,\n\tTAUNT\n};\n\n\nconst char *category_t_to_str(category_t val);\nstd::ostream &operator<<(std::ostream &os, category_t val);\n\n\n} // namespace openage::audio\n"
  },
  {
    "path": "libopenage/audio/dynamic_loader.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"dynamic_loader.h\"\n\n#include \"error.h\"\n#include \"opus_dynamic_loader.h\"\n\nnamespace openage {\nnamespace audio {\n\n\nDynamicLoader::DynamicLoader(const util::Path &path)\n\t:\n\tpath{path} {}\n\n\nstd::unique_ptr<DynamicLoader> DynamicLoader::create(const util::Path &path,\n                                                     format_t format) {\n\tstd::unique_ptr<DynamicLoader> loader;\n\n\tswitch (format) {\n\tcase format_t::OPUS:\n\t\tloader = std::make_unique<OpusDynamicLoader>(path);\n\t\tbreak;\n\n\tdefault:\n\t\tthrow audio::Error{\n\t\t\tERR << \"No dynamic audio loader for format supported: \" << format\n\t\t};\n\t}\n\treturn loader;\n}\n\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/dynamic_loader.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n#include \"../util/path.h\"\n#include \"format.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * A DynamicLoader loads pcm chunks without loading the whole resource. A chunk\n * is a int16_t buffer with a fixed size that contains 16 bit signed integer\n * pcm data.\n */\nclass DynamicLoader {\nprotected:\n\t/**\n\t * the resource's location in the filesystem\n\t */\n\tutil::Path path;\n\npublic:\n\t/**\n\t * Initializes a new DynamicLoader.\n\t * @param path the resource's location in the filesystem\n\t */\n\tDynamicLoader(const util::Path &path);\n\tvirtual ~DynamicLoader() = default;\n\n\t/**\n\t * Loads a chunk of stereo pcm data from the resource. The chunk of data\n\t * begins at the given offset from the beginning of the resource. The actual\n\t * read number of int16_t values is returned.\n\t * TODO: Returns zero if the end of resource is reached.\n\t *\n\t * @param chunk_buffer the buffer to save the chunk into\n\t * @param offset the offset from the resource's beginning\n\t * @param chunk_size the number of int16_t values that fit in one chunk\n\t */\n\tvirtual size_t load_chunk(int16_t *chunk_buffer,\n\t                          size_t offset,\n\t                          size_t chunk_size) = 0;\n\n\t/**\n\t * Creates a DynamicLoader instance that supports the given format.\n\t * @param path the resource's location in the filesystem\n\t * @param format the resource's audio format\n\t */\n\tstatic std::unique_ptr<DynamicLoader> create(const util::Path &path,\n\t                                             format_t format);\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/dynamic_resource.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"dynamic_resource.h\"\n\n\n#include \"../job/job_manager.h\"\n#include \"../log/log.h\"\n#include \"audio_manager.h\"\n\nnamespace openage::audio {\n\nchunk_info_t::chunk_info_t(chunk_info_t::state_t state,\n                           size_t buffer_size) :\n\tstate{state},\n\tsize{0},\n\tbuffer{std::make_unique<int16_t[]>(buffer_size)} {}\n\n\nDynamicResource::DynamicResource(AudioManager *manager,\n                                 category_t category,\n                                 int id,\n                                 const util::Path &path,\n                                 format_t format,\n                                 int preload_amount,\n                                 size_t chunk_size,\n                                 size_t max_chunks) :\n\tResource{manager, category, id},\n\tpath{path},\n\tformat{format},\n\tpreload_amount{preload_amount},\n\tchunk_size{chunk_size},\n\tmax_chunks{max_chunks},\n\tuse_count{0} {}\n\nvoid DynamicResource::use() {\n\t// if the resource is new in use\n\tbool initialize_use = (this->use_count == 0);\n\tuse_count += 1;\n\n\tif (initialize_use) {\n\t\t// create loading job group\n\t\tthis->loading_job_group = this->manager->get_job_manager()->create_job_group();\n\n\t\t// create the fixed amount of chunk loading buffers\n\t\tfor (size_t i = 0; i < this->max_chunks; i++) {\n\t\t\tthis->decay_queue.push(\n\t\t\t\tstd::make_shared<chunk_info_t>(\n\t\t\t\t\tchunk_info_t::state_t::EMPTY,\n\t\t\t\t\tthis->chunk_size));\n\t\t}\n\n\t\t// create loader\n\t\tthis->loader = DynamicLoader::create(this->path, this->format);\n\t}\n}\n\nvoid DynamicResource::stop_using() {\n\t// if the resource is not used anymore\n\tif ((--this->use_count) == 0) {\n\t\t// clear the chunks\n\t\tthis->chunks.clear();\n\t\t// clear chunk information\n\t\tthis->decay_queue.clear();\n\t}\n}\n\naudio_chunk_t DynamicResource::get_data(size_t position, size_t data_length) {\n\tsize_t resource_chunk_index = position / this->chunk_size;\n\tsize_t chunk_offset = position % this->chunk_size;\n\n\t// check if the audio chunk is currently loading or already ready\n\tauto chunk_map_it = this->chunks.find(resource_chunk_index);\n\tif (chunk_map_it != std::end(this->chunks)) {\n\t\tauto chunk_info = chunk_map_it->second;\n\t\tint16_t *data = chunk_info->buffer.get() + chunk_offset;\n\n\t\tswitch (chunk_info->state.load()) {\n\t\tcase chunk_info_t::state_t::EMPTY:\n\t\t\tthis->chunks.erase(chunk_map_it);\n\t\t\t// signal end of stream\n\t\t\treturn {nullptr, 0};\n\n\t\tcase chunk_info_t::state_t::LOADING:\n\t\t\t// signal that resource is not ready yet\n\t\t\treturn {nullptr, 1};\n\n\t\tcase chunk_info_t::state_t::READY:\n\t\t\t// start to preload the next chunks!\n\t\t\tthis->start_preloading(resource_chunk_index);\n\n\t\t\t// calculate actual data length\n\t\t\tif (chunk_info->size - chunk_offset >= data_length) {\n\t\t\t\treturn {data, data_length};\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn {data, chunk_info->size - chunk_offset};\n\t\t\t}\n\t\t}\n\t}\n\n\t// guard the .empty(), .front() and .pop() calls\n\tauto chunk_info_lock = this->decay_queue.lock();\n\n\t// we don't have next data to load!\n\tif (this->decay_queue.empty()) {\n\t\tthrow Error{ERR << \"chunk decay queue ran dry, resource is dying.\"};\n\t}\n\n\t// obtain a chunk info element to be used and overwritten\n\tauto chunk_info = this->decay_queue.front();\n\tthis->decay_queue.pop();\n\n\tchunk_info_lock.unlock();\n\n\t// map it to the corresponding resource offset\n\tthis->chunks.insert({resource_chunk_index, chunk_info});\n\n\t// start a background job to get the data of that chunk\n\tsize_t resource_chunk_offset = resource_chunk_index * this->chunk_size;\n\tthis->load_chunk_async(chunk_info, resource_chunk_offset);\n\n\t// and also load more chunks starting at the current chunk index\n\tthis->start_preloading(resource_chunk_index);\n\n\treturn {nullptr, 1};\n}\n\n\nvoid DynamicResource::start_preloading(size_t resource_chunk_index) {\n\tsize_t resource_chunk_offset = resource_chunk_index * this->chunk_size;\n\n\t// from the current index, enqueue the load of `threshold` more chunks\n\tfor (int i = 1; i < this->preload_amount; i++) {\n\t\tresource_chunk_index += 1;\n\t\tresource_chunk_offset += this->chunk_size;\n\n\t\tauto chunk_map_it = this->chunks.find(resource_chunk_index);\n\n\t\t// the chunk is either in loading state or ready\n\t\t// we do this to no longer preload if the stream is at its end.\n\t\tif (chunk_map_it != std::end(this->chunks)) {\n\t\t\tauto chunk_info = chunk_map_it->second;\n\t\t\tif (chunk_info->state.load() == chunk_info_t::state_t::EMPTY) {\n\t\t\t\tthis->chunks.erase(chunk_map_it);\n\n\t\t\t\t// we encountered the end of stream!\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// else, the chunk is known already and loading or even loaded,\n\t\t\t// no need to trigger the load.\n\t\t}\n\t\telse {\n\t\t\t// chunk is unknown yet, so let's load it.\n\n\t\t\tauto decay_queue_lock = this->decay_queue.lock();\n\t\t\t// obtain the least-recently-used chunk to overwrite.\n\t\t\tauto local_chunk_info = this->decay_queue.front();\n\t\t\tthis->decay_queue.pop();\n\t\t\tdecay_queue_lock.unlock();\n\n\t\t\tthis->chunks.insert({resource_chunk_index, local_chunk_info});\n\t\t\tthis->load_chunk_async(local_chunk_info, resource_chunk_offset);\n\t\t}\n\t}\n}\n\n\nvoid DynamicResource::load_chunk_async(const std::shared_ptr<chunk_info_t> &chunk_info,\n                                       size_t resource_chunk_offset) {\n\tchunk_info->state.store(chunk_info_t::state_t::LOADING);\n\n\t// the `chunk_info` will stay out of the `decay_queue`\n\t// until it was loaded. So it's possible the queue runs dry.\n\n\tENSURE(this->loader, \"tried to load chunk without audio loader!\");\n\n\tauto loading_function = [this, chunk_info, resource_chunk_offset]() -> int {\n\t\tint16_t *buffer = chunk_info->buffer.get();\n\t\tsize_t loaded = this->loader->load_chunk(buffer, resource_chunk_offset, this->chunk_size);\n\t\tif (loaded == 0) {\n\t\t\tchunk_info->state.store(chunk_info_t::state_t::EMPTY);\n\t\t\tthis->decay_queue.push(chunk_info);\n\t\t}\n\t\telse {\n\t\t\tchunk_info->state.store(chunk_info_t::state_t::READY);\n\t\t\tchunk_info->size = loaded;\n\t\t\tthis->decay_queue.push(chunk_info);\n\t\t}\n\t\t// as the job manager currently does not support executing void\n\t\t// functions, we return zero\n\t\treturn 0;\n\t};\n\tthis->loading_job_group.enqueue<int>(loading_function);\n}\n\n\n} // namespace openage::audio\n"
  },
  {
    "path": "libopenage/audio/dynamic_resource.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <atomic>\n#include <memory>\n#include <tuple>\n#include <unordered_map>\n#include <vector>\n\n#include \"../datastructure/concurrent_queue.h\"\n#include \"../job/job.h\"\n#include \"../job/job_group.h\"\n#include \"../util/path.h\"\n#include \"category.h\"\n#include \"dynamic_loader.h\"\n#include \"format.h\"\n#include \"resource.h\"\n#include \"types.h\"\n\nnamespace openage {\nnamespace audio {\n\n/**\n * information storage about a piece of raw audio data.\n */\nstruct chunk_info_t {\n\tenum class state_t {\n\t\t/** The chunk is currently empty and unused. */\n\t\tEMPTY,\n\n\t\t/** The chunk is currently loading. */\n\t\tLOADING,\n\n\t\t/** The chunk is loaded and ready to be used. */\n\t\tREADY\n\t};\n\n\t/** The chunk's current state. */\n\tstd::atomic<state_t> state;\n\n\t/** The chunk's count of valid entries (samples). */\n\tsize_t size;\n\n\t/** The chunk's buffer. */\n\tstd::unique_ptr<int16_t[]> buffer;\n\n\tchunk_info_t(chunk_info_t::state_t state, size_t buffer_size);\n\t~chunk_info_t() = default;\n};\n\n\n/**\n * Audio data that is loaded dynamically when used.\n */\nclass DynamicResource : public Resource {\npublic:\n\tDynamicResource(AudioManager *manager,\n\t                category_t category,\n\t                int id,\n\t                const util::Path &path,\n\t                format_t format = format_t::OPUS,\n\t                int preload_amount = DEFAULT_PRELOAD_AMOUNT,\n\t                size_t chunk_size = DEFAULT_CHUNK_SIZE,\n\t                size_t max_chunks = DEFAULT_MAX_CHUNKS);\n\n\tvirtual ~DynamicResource() = default;\n\n\tvoid use() override;\n\tvoid stop_using() override;\n\n\taudio_chunk_t get_data(size_t position, size_t data_length) override;\n\nprivate:\n\t/**\n\t * Start to load audio chunks beginning at the given chunk index.\n\t * Continues to do so until the `preload_amount` is reached.\n\t */\n\tvoid start_preloading(size_t resource_chunk_index);\n\n\t/**\n\t * Load a single chunk in the background (through the job manager).\n\t * First, the given `chunk_info` is invalidated.\n\t * Then, after loading, it's pushed into the `decay_queue` so it\n\t * can decay again.\n\t */\n\tvoid load_chunk_async(const std::shared_ptr<chunk_info_t> &chunk_info,\n\t                      size_t resource_chunk_offset);\n\npublic:\n\t/**\n\t * The number of chunks that have to be loaded, before a sound actually\n\t * starts playing.\n\t */\n\tstatic constexpr int DEFAULT_PRELOAD_AMOUNT = 10;\n\n\t/** The default used chunk size in bytes (100ms for 48kHz stereo audio). */\n\tstatic constexpr size_t DEFAULT_CHUNK_SIZE = 9600 * 2;\n\n\t/** The default number of chunks, that can be loaded at the same time. */\n\tstatic constexpr size_t DEFAULT_MAX_CHUNKS = 100;\n\nprivate:\n\t/** The resource's path. */\n\tutil::Path path;\n\n\t/** The resource's audio format. */\n\tformat_t format;\n\n\t/** The number of chunks that should be preloaded. */\n\tint preload_amount;\n\n\t/** The size of one audio chunk in bytes. */\n\tsize_t chunk_size;\n\n\t/**\n\t * The maximum number of chunks that are used to store audio data.\n\t * This is the maximum amount stored in the `chunks` map.\n\t */\n\tsize_t max_chunks;\n\n\t/** The number of sounds that currently use this resource. */\n\tstd::atomic_int use_count;\n\n\t/** The background audio loader. */\n\tstd::unique_ptr<DynamicLoader> loader;\n\n\t/**\n\t * Queue of audio chunk information that manages the decay of buffer data.\n\t *\n\t * This implements \"forget least-recently-used chunks\" when a new one\n\t * shall be loaded.\n\t * This _must_ never run empty! Otherwise the audio system will lock up.\n\t */\n\tdatastructure::ConcurrentQueue<std::shared_ptr<chunk_info_t>> decay_queue;\n\n\t/**\n\t * Resource chunk index to chunk mapping.\n\t * Loading and usage state is reached through this.\n\t */\n\tstd::unordered_map<size_t, std::shared_ptr<chunk_info_t>> chunks;\n\n\t/** The background loading job group. */\n\tjob::JobGroup loading_job_group;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/error.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"error.h\"\n\nnamespace openage {\nnamespace audio {\n\nError::Error(const log::message &msg)\n\t:\n\terror::Error(msg) {}\n\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/error.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../error/error.h\"\n\nnamespace openage {\nnamespace audio {\n\nclass Error : public error::Error {\npublic:\n\tError(const log::message &msg);\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/format.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"format.h\"\n\n#include \"../error/error.h\"\n\nnamespace openage::audio {\n\n\nconst char *format_t_to_str(format_t val) {\n\tswitch (val) {\n\tcase format_t::OPUS: return \"OPUS\";\n\tcase format_t::WAV:  return \"WAV\";\n\tcase format_t::MP3:  return \"MP3\";\n\tcase format_t::FLAC: return \"FLAC\";\n\tdefault:             return \"unknown\";\n\t}\n}\n\n\nstd::ostream &operator <<(std::ostream &os, const format_t val) {\n\tos << format_t_to_str(val);\n\treturn os;\n}\n\n\n} // namespace openage::audio\n"
  },
  {
    "path": "libopenage/audio/format.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <ostream>\n\n\nnamespace openage {\nnamespace audio {\n\nenum class format_t {\n\tOPUS,\n\tWAV,\n\tMP3,\n\tFLAC\n};\n\nconst char *format_t_to_str(format_t val);\n\nstd::ostream &operator<<(std::ostream &os, format_t val);\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/hash_functions.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <tuple>\n\n#include \"category.h\"\n\nnamespace std {\n\ntemplate <>\nstruct hash<::openage::audio::category_t> {\n\tsize_t operator()(const ::openage::audio::category_t &c) const {\n\t\treturn static_cast<int>(c);\n\t}\n};\n\ntemplate <>\nstruct hash<std::tuple<::openage::audio::category_t, int>> {\n\tsize_t operator()(const std::tuple<::openage::audio::category_t, int> &t) const {\n\t\treturn static_cast<size_t>(std::get<0>(t)) << (sizeof(size_t) * 8 - 2) | std::get<1>(t);\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/audio/in_memory_loader.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"in_memory_loader.h\"\n\n#include \"error.h\"\n#include \"opus_in_memory_loader.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n\nInMemoryLoader::InMemoryLoader(const util::Path &path)\n\t:\n\tpath{path} {}\n\n\nstd::unique_ptr<InMemoryLoader> InMemoryLoader::create(const util::Path &path,\n                                                       format_t format) {\n\n\tstd::unique_ptr<InMemoryLoader> loader;\n\n\t// switch format and return an appropriate loader\n\tswitch (format) {\n\tcase format_t::OPUS:\n\t\tloader = std::make_unique<OpusInMemoryLoader>(path);\n\t\tbreak;\n\n\tdefault:\n\t\tthrow audio::Error{ERR << \"Not supported for format: \" << format};\n\t}\n\n\treturn loader;\n}\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/in_memory_loader.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <tuple>\n\n#include \"../util/path.h\"\n#include \"format.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * A InMemoryLoader loads a audio file into memory and converts it into 16 bit\n * signed integer pcm data.\n */\nclass InMemoryLoader {\nprotected:\n\t/*\n\t * The resource's location in the filesystem.\n\t */\n\tutil::Path path;\n\npublic:\n\t/**\n\t * Initializes a new InMemoryLoader.\n\t * @param path the resource's location in the filesystem\n\t */\n\tInMemoryLoader(const util::Path &path);\n\tvirtual ~InMemoryLoader() = default;\n\n\t/**\n\t * Returns the resource as pcm data buffer.\n\t */\n\tvirtual pcm_data_t get_resource() = 0;\n\n\t/**\n\t * Create a InMemoryLoader instance that supports the given format.\n\t * @param path the resource's location in the filesystem\n\t * @param format the resource's audio format\n\t */\n\tstatic std::unique_ptr<InMemoryLoader> create(const util::Path &path,\n\t                                              format_t format);\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/in_memory_resource.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"in_memory_resource.h\"\n\n#include \"in_memory_loader.h\"\n\nnamespace openage {\nnamespace audio {\n\nInMemoryResource::InMemoryResource(AudioManager *manager,\n                                   category_t category,\n                                   int id,\n                                   const util::Path &path,\n                                   format_t format)\n\t:\n\tResource{manager, category, id} {\n\n\tauto loader = InMemoryLoader::create(path, format);\n\tbuffer = loader->get_resource();\n}\n\n\nvoid InMemoryResource::use() {}\n\n\nvoid InMemoryResource::stop_using() {}\n\n\naudio_chunk_t InMemoryResource::get_data(size_t position,\n                                         size_t data_length) {\n\t// if the resource's end has been reached\n\tsize_t length = buffer.size();\n\tif (position >= length) {\n\t\treturn {nullptr, 0};\n\t}\n\n\tconst int16_t *buf_pos = &buffer[position];\n\tif (data_length > length - position) {\n\t\treturn {buf_pos, length - position};\n\t} else {\n\t\treturn {buf_pos, data_length};\n\t}\n}\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/in_memory_resource.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include \"../util/path.h\"\n#include \"format.h\"\n#include \"resource.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * An InMemoryResource loads the whole pcm data into memory and keeps it there.\n */\nclass InMemoryResource : public Resource {\nprivate:\n\t/** The resource's internal buffer. */\n\tpcm_data_t buffer;\n\npublic:\n\tInMemoryResource(AudioManager *manager,\n\t                 category_t category,\n\t                 int id,\n\t                 const util::Path &path,\n\t                 format_t format = format_t::OPUS);\n\tvirtual ~InMemoryResource() = default;\n\n\tvoid use() override;\n\tvoid stop_using() override;\n\n\taudio_chunk_t get_data(size_t position, size_t data_length) override;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/loader_policy.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"loader_policy.h\"\n\n#include \"../error/error.h\"\n\nnamespace openage {\nnamespace audio {\n\n\nconst char *loader_policy_t_to_str(loader_policy_t val) {\n\tswitch (val) {\n\tcase loader_policy_t::IN_MEMORY: return \"IN_MEMORY\";\n\tcase loader_policy_t::DYNAMIC:   return \"DYNAMIC\";\n\tdefault:                         return \"unknown\";\n\t}\n}\n\n\nstd::ostream &operator <<(std::ostream &os, const loader_policy_t val) {\n\tos << loader_policy_t_to_str(val);\n\treturn os;\n}\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/loader_policy.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n#include <ostream>\n\n\nnamespace openage {\nnamespace audio {\n\nenum class loader_policy_t {\n\tIN_MEMORY,\n\tDYNAMIC\n};\n\n\nconst char *loader_policy_t_to_str(loader_policy_t val);\n\nstd::ostream &operator<<(std::ostream &os, loader_policy_t val);\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/opus_dynamic_loader.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"opus_dynamic_loader.h\"\n\n#include <opus/opusfile.h>\n\n#include \"error.h\"\n#include \"../log/log.h\"\n\nnamespace openage::audio {\n\n\nOpusDynamicLoader::OpusDynamicLoader(const util::Path &path)\n\t:\n\tDynamicLoader{path},\n\tsource{open_opus_file(path)} {\n\n\t// read channels from the opus file\n\tchannels = op_channel_count(this->source.handle.get(), -1);\n\n\tint64_t pcm_length = op_pcm_total(this->source.handle.get(), -1);\n\tif (pcm_length < 0) {\n\t\tthrow audio::Error{\n\t\t\tERR << \"Could not seek in \"\n\t\t\t    << path << \": \" << pcm_length};\n\t}\n\n\tlength = static_cast<size_t>(pcm_length) * 2;\n}\n\n\nsize_t OpusDynamicLoader::load_chunk(int16_t *chunk_buffer,\n                                     size_t offset,\n                                     size_t chunk_size) {\n\n\t// if the requested offset is greater than the resource's length, there is\n\t// no chunk left to load\n\tif (offset > this->length) {\n\t\treturn 0;\n\t}\n\n\t// seek to the requested offset, the seek offset is given in samples\n\t// while the requested offset is given in int16_t values, so the division\n\t// by 2 is necessary\n\tauto pcm_offset = static_cast<int64_t>(offset / 2);\n\n\tint op_ret = op_pcm_seek(this->source.handle.get(), pcm_offset);\n\tif (op_ret < 0) {\n\t\tthrow audio::Error{\n\t\t\tERR << \"Could not seek in \" << this->path << \": \" << op_ret\n\t\t};\n\t}\n\n\t// read a chunk from the requested offset\n\t// if the opus file is a mono source, we read chunk_size / 2 values and\n\t// convert it to stereo directly\n\t// if the opus file is a stereo source, we read chunk_size directly\n\tint read_num_values = chunk_size / 2 * channels;\n\tint read_count = 0;\n\n\t// loop as long as there are samples left to read\n\twhile (read_count <= read_num_values) {\n\t\tint samples_read = op_read(\n\t\t\tthis->source.handle.get(),\n\t\t\tchunk_buffer + read_count,\n\t\t\tread_num_values - read_count,\n\t\t\tnullptr\n\t\t);\n\n\t\t// an error occurred\n\t\tif (samples_read < 0) {\n\t\t\tthrow audio::Error{\n\t\t\t\tERR << \"Could not read from \"\n\t\t\t\t    << this->path << \": \" << samples_read\n\t\t\t};\n\t\t}\n\t\t// end of the resource\n\t\telse if (samples_read == 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\t// increase read_count by the number of int16_t values that have been\n\t\t// read\n\t\tread_count += samples_read * channels;\n\t}\n\n\t// convert to stereo\n\tif (channels == 1) {\n\t\tfor (int i = read_count-1; i >= 0; i--) {\n\t\t\tauto value = chunk_buffer[i];\n\t\t\tchunk_buffer[i*2+1] = value;\n\t\t\tchunk_buffer[i*2] = value;\n\t\t}\n\t}\n\n\treturn (read_count * 2) / channels;\n}\n\n} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/opus_dynamic_loader.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n#include \"../util/path.h\"\n#include \"dynamic_loader.h\"\n#include \"opus_loading.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n\n/**\n * A OpusDynamicLoader load's opus encoded data.\n */\nclass OpusDynamicLoader : public DynamicLoader {\nprivate:\n\t/** The source file. */\n\topus_file_t source;\n\t/** The resource's length in int16_t values. */\n\tsize_t length;\n\t/** The resource's pcm channels. */\n\tint channels;\n\npublic:\n\t/**\n\t * Creates a new OpusDynamicLoader.\n\t * @param path the resource's location in the filesystem\n\t */\n\tOpusDynamicLoader(const util::Path &path);\n\tvirtual ~OpusDynamicLoader() = default;\n\n\tsize_t load_chunk(int16_t *chunk_buffer,\n\t                  size_t offset,\n\t                  size_t chunk_size) override;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/opus_in_memory_loader.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"opus_in_memory_loader.h\"\n\n#include <opus/opusfile.h>\n#include <string>\n\n#include \"error.h\"\n#include \"opus_loading.h\"\n#include \"../log/log.h\"\n\n\nnamespace openage {\nnamespace audio {\n\nOpusInMemoryLoader::OpusInMemoryLoader(const util::Path &path)\n\t:\n\tInMemoryLoader{path} {}\n\n\npcm_data_t OpusInMemoryLoader::get_resource() {\n\n\t// open the opus file\n\topus_file_t op_file = open_opus_file(this->path);\n\n\tauto op_channels = op_channel_count(op_file.handle.get(), -1);\n\tauto pcm_length = op_pcm_total(op_file.handle.get(), -1);\n\t// the stream is not seekable\n\tif (pcm_length < 0) {\n\t\tthrow audio::Error{ERR << \"Opus file is not seekable\"};\n\t}\n\n\t// calculate pcm buffer size depending on the number of channels\n\t// if the opus file only had one channel, the pcm buffer size must be\n\t// doubled\n\tsize_t length = static_cast<size_t>(pcm_length) * 2;\n\tpcm_data_t buffer(length, 0);\n\n\t// read data from opus file\n\tint position = 0;\n\twhile (true) {\n\t\tint samples_read = op_read(\n\t\t\top_file.handle.get(),\n\t\t\t&buffer.front() + position,\n\t\t\tlength-position, nullptr\n\t\t);\n\n\t\tif (samples_read < 0) {\n\t\t\tthrow audio::Error{\n\t\t\t\tERR << \"Failed to read from opus file: errorcode=\" << samples_read\n\t\t\t};\n\t\t}\n\t\telse if (samples_read == 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tposition += samples_read * op_channels;\n\t}\n\n\t// convert from mono to stereo\n\tif (op_channels == 1) {\n\t\tfor (int i = pcm_length-1; i >= 0; i--) {\n\t\t\tauto value = buffer[i];\n\t\t\tbuffer[i*2+1] = value;\n\t\t\tbuffer[i*2] = value;\n\t\t}\n\t}\n\n\treturn buffer;\n}\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/opus_in_memory_loader.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include \"../util/path.h\"\n#include \"in_memory_loader.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * A OpusInMemoryLoader load's opus encoded data from opus files. A opus file is\n * loaded completely at once and the loaded data will be kept in memory.\n */\nclass OpusInMemoryLoader : public InMemoryLoader {\npublic:\n\t/**\n\t * Creates a new OpusInMemoryLoader.\n\t * @param path the resource's location in the filesystem\n\t */\n\tOpusInMemoryLoader(const util::Path &path);\n\tvirtual ~OpusInMemoryLoader() = default;\n\n\tpcm_data_t get_resource() override;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/opus_loading.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include \"opus_loading.h\"\n\n#include \"error.h\"\n#include \"../log/log.h\"\n\n\nnamespace openage::audio {\n\n// custom deleter for OggOpusFile unique pointers\nstatic auto opus_deleter = [](OggOpusFile *op_file) {\n\tif (op_file != nullptr) {\n\t\top_free(op_file);\n\t}\n};\n\n\nstatic op_read_func opus_reader = [](void *stream,\n                                     unsigned char *buf,\n                                     int count) -> int {\n\n\tauto *file = reinterpret_cast<util::File *>(stream);\n\treturn file->read_to(buf, count);\n};\n\n\nstatic op_seek_func opus_seeker = [](void *stream,\n                                     opus_int64 offset,\n                                     int whence) -> int {\n\n\tauto *file = reinterpret_cast<util::File *>(stream);\n\tif (not file->seekable()) [[unlikely]] {\n\t\treturn -1;\n\t}\n\n\tfile->seek(offset, static_cast<util::File::seek_t>(whence));\n\treturn 0;\n};\n\n\nstatic op_tell_func opus_teller = [](void *stream) -> opus_int64 {\n\tauto *file = reinterpret_cast<util::File *>(stream);\n\treturn file->tell();\n};\n\n\nstatic OpusFileCallbacks opus_access_funcs{\n\topus_reader,\n\topus_seeker,\n\topus_teller,\n\tnullptr        // no close function needed (as opus_file_t owns the File)\n};\n\n\nopus_file_t open_opus_file(const util::Path &path) {\n\tif (not path.is_file()) {\n\t\tthrow audio::Error{\n\t\t\tERR << \"Audio file not found: \" << path\n\t\t};\n\t}\n\n\topus_file_t op_file;\n\tint op_err = 0;\n\n\t// check if the file can be opened directly\n\tauto native_path = path.resolve_native_path();\n\tif (native_path.size() > 0) {\n\t\top_file.handle = {\n\t\t\top_open_file(native_path.c_str(), &op_err),\n\t\t\topus_deleter\n\t\t};\n\t}\n\telse {\n\t\t// open the file and move the handle to the heap\n\t\top_file.file = std::make_unique<util::File>();\n\t\t*op_file.file = path.open_r();\n\n\t\top_file.handle = {\n\t\t\top_open_callbacks(op_file.file.get(), &opus_access_funcs,\n\t\t\t                  nullptr, 0, &op_err),\n\t\t\topus_deleter\n\t\t};\n\t}\n\n\tif (op_err != 0) {\n\n\t\tconst char *reason;\n\n\t\tswitch (op_err) {\n\t\tcase OP_EREAD:\n\t\t\treason = \"read/seek/tell failed or data has changed\"; break;\n\t\tcase OP_EFAULT:\n\t\t\treason = \"opus failed to allocate memory \"\n\t\t\t         \"or something else bad happened internally\"; break;\n\t\tcase OP_EIMPL:\n\t\t\treason = \"Stream used an unsupported feature\"; break;\n\t\tcase OP_EINVAL:\n\t\t\treason = \"seek() worked, but tell() did not, \"\n\t\t\t         \"or initial_bytes != start seek pos\"; break;\n\t\tcase OP_ENOTFORMAT:\n\t\t\treason = \"Data didn't contain opus stream\"; break;\n\t\tcase OP_EBADHEADER:\n\t\t\treason = \"Header packet was invalid or missing\"; break;\n\t\tcase OP_EVERSION:\n\t\t\treason = \"ID header has unrecognized version\"; break;\n\t\tcase OP_EBADLINK:\n\t\t\treason = \"Data we already saw before seeking not found\"; break;\n\t\tcase OP_EBADTIMESTAMP:\n\t\t\treason = \"Validity check for first/last timestamp failed\"; break;\n\n\t\tdefault:\n\t\t\treason = \"Unknown other error in opusfile\"; break;\n\t\t}\n\n\t\tthrow audio::Error{\n\t\t\tERR\n\t\t\t<< \"Could not open opus file: \"\n\t\t\t<< path << \" = '\" << native_path << \"': \"\n\t\t\t<< reason\n\t\t};\n\t}\n\treturn op_file;\n}\n\n} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/opus_loading.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <opus/opusfile.h>\n#include <string>\n\n#include \"../util/path.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * opusfile handle storage.\n */\nstruct opus_file_t {\n\t/**\n\t * The opusfile handle, with a custom deleter that frees\n\t * the opusfile memory.\n\t */\n\tstd::unique_ptr<OggOpusFile, std::function<void(OggOpusFile *)>> handle;\n\n\t/**\n\t * File used to supply the data.\n\t * This is unused if the file is a real file.\n\t */\n\tstd::unique_ptr<util::File> file;\n};\n\n\n/**\n * Opens a opus file. Its location is specified by the path stored in the\n * DynamicLoader.\n */\nopus_file_t open_opus_file(const util::Path &path);\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/resource.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include \"resource.h\"\n\n#include \"../error/error.h\"\n\n#include \"dynamic_resource.h\"\n#include \"in_memory_resource.h\"\n\nnamespace openage {\nnamespace audio {\n\n\nResource::Resource(AudioManager *manager, category_t category, int id) :\n\tmanager{manager},\n\tcategory{category},\n\tid{id} {}\n\n\ncategory_t Resource::get_category() const {\n\treturn category;\n}\n\n\nint Resource::get_id() const {\n\treturn id;\n}\n\n\nstd::shared_ptr<Resource> Resource::create_resource(AudioManager *manager,\n                                                    const resource_def &def) {\n\tif (not def.location.is_file()) [[unlikely]] {\n\t\tthrow Error{ERR << \"sound file does not exist: \" << def.location};\n\t}\n\n\tswitch (def.loader_policy) {\n\tcase loader_policy_t::IN_MEMORY:\n\t\treturn std::make_shared<InMemoryResource>(\n\t\t\tmanager, def.category, def.id, def.location, def.format);\n\n\tcase loader_policy_t::DYNAMIC:\n\t\treturn std::make_shared<DynamicResource>(\n\t\t\tmanager, def.category, def.id, def.location, def.format);\n\n\tdefault:\n\t\tthrow Error{ERR << \"Unsupported loader policy: \" << def.loader_policy};\n\t}\n}\n\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/resource.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n#include \"category.h\"\n#include \"resource_def.h\"\n#include \"types.h\"\n\n\nnamespace openage {\nnamespace audio {\n\nclass AudioManager;\n\n\n/**\n * A Resource contains 16 bit signed integer pcm data, that can be played by\n * sounds. Each Resource has an unique id within it's category. The category\n * specifies the type of sound, e. g. game sounds, background music, etc.\n * The id is a unique integer value.\n */\nclass Resource {\npublic:\n\tResource(AudioManager *manager, category_t category, int id);\n\tvirtual ~Resource() = default;\n\n\tResource(const Resource &) = delete;\n\tResource &operator=(const Resource &) = delete;\n\n\tResource(Resource &&) = delete;\n\tResource &operator=(Resource &&) = delete;\n\n\tvirtual category_t get_category() const;\n\tvirtual int get_id() const;\n\n\t/**\n\t * Tells the resource, that it will be used by a sound object, so it can\n\t * preload some pcm samples.\n\t */\n\tvirtual void use() = 0;\n\n\t/**\n\t * Tells the resource, that one sound object does not use this resource any\n\t * longer.\n\t */\n\tvirtual void stop_using() = 0;\n\n\t/**\n\t * Returns a pointer to the sample buffer at the given position and the\n\t * number of int16_t values that are actually available. If the end of the\n\t * resource is reached, 0 will be returned. If the resource is not ready\n\t * yet, a nullptr with a length, different to zero, will be returned.\n\t *\n\t * @param position the index of the first wanted int16_t value\n\t * @param data_length the number of int16_t values that should be returned\n\t */\n\tvirtual audio_chunk_t get_data(size_t position, size_t data_length) = 0;\n\n\t/**\n\t * create an audio resource, this produces a DynamicResource or a InMemoryResource\n\t */\n\tstatic std::shared_ptr<Resource> create_resource(AudioManager *manager,\n\t                                                 const resource_def &def);\n\nprotected:\n\t/**\n\t * Audiomanager in charge of this resource.\n\t */\n\tAudioManager *manager;\n\nprivate:\n\t/**\n\t * The resource's category.\n\t */\n\tcategory_t category;\n\n\t/**\n\t * The resource's id.\n\t */\n\tint id;\n};\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/resource_def.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"resource_def.h\"\n\nnamespace openage {\nnamespace audio {\n\n}} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/resource_def.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n#include \"../util/path.h\"\n#include \"category.h\"\n#include \"format.h\"\n#include \"loader_policy.h\"\n\n\nnamespace openage {\nnamespace audio {\n\n\n/**\n * Definition of a single sound resource file to load.\n */\nstruct resource_def {\n\tcategory_t category;\n\tint id;\n\tutil::Path location;\n\tformat_t format;\n\tloader_policy_t loader_policy;\n};\n\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/sound.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"sound.h\"\n\n#include <tuple>\n#include <utility>\n\n#include \"audio_manager.h\"\n#include \"resource.h\"\n\nnamespace openage::audio {\n\n\nSound::Sound(AudioManager *audio_manager, std::shared_ptr<SoundImpl> sound_impl)\n\t:\n\taudio_manager{audio_manager},\n\tsound_impl{std::move(sound_impl)} {\n}\n\n\ncategory_t Sound::get_category() const {\n\treturn sound_impl->get_category();\n}\n\n\nint Sound::get_id() const {\n\treturn sound_impl->get_id();\n}\n\n\nvoid Sound::set_volume(int32_t volume) {\n\tsound_impl->volume = volume;\n}\n\n\nint32_t Sound::get_volume() const {\n\treturn sound_impl->volume;\n}\n\n\nvoid Sound::set_looping(bool looping) {\n\tsound_impl->looping = looping;\n}\n\n\nbool Sound::is_looping() const {\n\treturn sound_impl->looping;\n}\n\n\nvoid Sound::play() {\n\tif (!sound_impl->in_use) {\n\t\tsound_impl->resource->use();\n\t\tsound_impl->in_use = true;\n\t}\n\tsound_impl->offset = 0;\n\tif (!sound_impl->playing) {\n\t\taudio_manager->add_sound(sound_impl);\n\t\tsound_impl->playing = true;\n\t}\n}\n\n\nvoid Sound::pause() {\n\tif (sound_impl->playing) {\n\t\taudio_manager->remove_sound(sound_impl);\n\t\tsound_impl->playing = false;\n\t}\n}\n\n\nvoid Sound::resume() {\n\tif (!sound_impl->in_use) {\n\t\tsound_impl->resource->use();\n\t\tsound_impl->in_use = true;\n\t}\n\tif (!sound_impl->playing) {\n\t\taudio_manager->add_sound(sound_impl);\n\t\tsound_impl->playing = true;\n\t}\n}\n\n\nvoid Sound::stop() {\n\tsound_impl->offset = 0;\n\tif (sound_impl->playing) {\n\t\taudio_manager->remove_sound(sound_impl);\n\t\tsound_impl->playing = false;\n\t}\n\tif (sound_impl->in_use) {\n\t\tsound_impl->resource->stop_using();\n\t\tsound_impl->in_use = false;\n\t}\n}\n\n\nbool Sound::is_playing() const {\n\treturn this->audio_manager->is_available() and sound_impl->playing;\n}\n\n\n// here begins the internal sound implementation\n\nSoundImpl::SoundImpl(std::shared_ptr<Resource> resource, int32_t volume)\n\t:\n\tresource{std::move(resource)},\n\tin_use{false},\n\tvolume{volume},\n\toffset{0},\n\tplaying{false},\n\tlooping{false} {\n}\n\n\nSoundImpl::~SoundImpl() {\n\tif (in_use) {\n\t\tresource->stop_using();\n\t}\n}\n\n\ncategory_t SoundImpl::get_category() const {\n\treturn resource->get_category();\n}\n\n\nint SoundImpl::get_id() const {\n\treturn resource->get_id();\n}\n\n\nbool SoundImpl::mix_audio(int32_t *stream, int length) {\n\tsize_t stream_index = 0;\n\twhile (length > 0) {\n\t\t// fetch the raw audio from the underlying resource\n\t\tauto chunk = resource->get_data(offset, length);\n\n\t\tif (chunk.length == 0) {\n\t\t\tif (this->looping) {\n\t\t\t\toffset = 0;\n\t\t\t} else {\n\t\t\t\tthis->playing = false;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else if (chunk.data == nullptr) {\n\t\t\treturn false;\n\t\t}\n\n\t\tfor (size_t i = 0; i < chunk.length; i++) {\n\t\t\tstream[i+stream_index] += this->volume * chunk.data[i];\n\t\t}\n\n\t\tthis->offset += chunk.length;\n\t\tlength -= chunk.length;\n\t\tstream_index += chunk.length;\n\t}\n\n\treturn false;\n}\n\n} // openage::audio\n"
  },
  {
    "path": "libopenage/audio/sound.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"category.h\"\n\nnamespace openage {\nnamespace audio {\n\n\n// forward declaration of AudioManager\nclass AudioManager;\nclass Resource;\n\n\n/**\n * A SoundImpl is the internal implementation of a sound. It stores the sound's\n * complete state. It is passed around as a shared_ptr, such that the same\n * SoundImpl instance can be shared between the Sound objects themselves and the\n * AudioManager.\n */\nclass SoundImpl {\npublic:\n\tSoundImpl(std::shared_ptr<Resource> resource, int32_t volume = 128);\n\t~SoundImpl();\n\n\t/**\n\t * The shared audio resource, which provides the pcm data to play.\n\t */\n\tstd::shared_ptr<Resource> resource;\n\n\t/**\n\t * Whether this sound currently actively uses it's shared audio resource.\n\t */\n\tbool in_use;\n\n\t/**\n\t * The sound's volume.\n\t */\n\n\tint32_t volume;\n\n\t/**\n\t * The sound's playing offset.\n\t */\n\tuint32_t offset;\n\n\t/**\n\t * Whether this sound is currently playing.\n\t */\n\tbool playing;\n\n\t/**\n\t * Whether this sound is currently looping.\n\t */\n\tbool looping;\n\n\t/**\n\t * Returns this sound's category.\n\t */\n\tcategory_t get_category() const;\n\n\t/**\n\t * Returns this sound's id.\n\t */\n\tint get_id() const;\n\n\t/*\n\t * Mix this sound with the given pcm stream and return whether it has\n\t * finished or not.\n\t *\n\t * @param stream the stream to mix with\n\t * @param length the number of values that should be mixed\n\t *\n\t * @returns if the sound was finished and should no longer be played.\n\t */\n\tbool mix_audio(int32_t *stream, int length);\n};\n\n\n/**\n * A sound object is the direct interface to the user. It provides methods to\n * control the sound, e.g. play, stop, etc. It is a lightweight object that\n * stores a shared_ptr to its internal SoundImpl and a pointer to its\n * AudioManager.\n */\nclass Sound {\n\tfriend class AudioManager;\n\nprivate:\n\tSound(AudioManager *audio_manager, std::shared_ptr<SoundImpl> sound_impl);\n\npublic:\n\t/**\n\t * Returns this sound's category.\n\t */\n\tcategory_t get_category() const;\n\n\t/**\n\t * Returns this sound's id.\n\t */\n\tint get_id() const;\n\n\t/**\n\t * Sets this sound's volume. The volume should be a value in range [0,256],\n\t * where 0 means silence and 256 the original pcm volume. A volume greater\n\t * than 256 could lead to overmodulation.\n\t * @param volume the new volume\n\t */\n\tvoid set_volume(int32_t volume);\n\n\t/**\n\t * Returns this sound's volume.\n\t */\n\tint32_t get_volume() const;\n\n\t/**\n\t * Sets whether this sound should be looping or not. A looping sound\n\t * restarts automatically after it has finished.\n\t * @param looping true, if this sound should be looping, otherwise false\n\t */\n\tvoid set_looping(bool looping);\n\n\t/**\n\t * Returns whether this sound is looping.\n\t */\n\tbool is_looping() const;\n\n\t/**\n\t * Resets the sound to it's beginning and starts playing it.\n\t */\n\tvoid play();\n\n\t/**\n\t * Pauses the sound at it's current playing offset.\n\t */\n\tvoid pause();\n\n\t/**\n\t * Resumes the sound at it's current playing offset.\n\t */\n\tvoid resume();\n\n\t/**\n\t * Resets the sound to it's beginning and stops playing it.\n\t */\n\tvoid stop();\n\n\t/**\n\t * Returns whether this sound is currently playing.\n\t */\n\tbool is_playing() const;\n\nprivate:\n\t/**\n\t * The audio manager that manages this sound.\n\t */\n\tAudioManager *audio_manager;\n\n\t/**\n\t * The internal sound implementation. This is where the internal state, e.g.\n\t * current playing offset, volume, etc., is stored.\n\t */\n\tstd::shared_ptr<SoundImpl> sound_impl;\n};\n\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/audio/types.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <vector>\n\n\nnamespace openage {\nnamespace audio {\n\n/**\n * A piece of raw audio data.\n *\n * special values:\n *   (nullptr, *) no data in the chunk\n *   (*, 0) end of stream\n */\nstruct audio_chunk_t {\n\tconst int16_t *data;\n\tsize_t length;\n};\n\n/**\n * pcm_data_t is a vector consisting of signed 16 bit integer samples. It is\n * used to represent one complete audio resource's buffer.\n */\nusing pcm_data_t = std::vector<int16_t>;\n\n/**\n * pcm_chunk_t is a vector consisting of signed 16 bit integer samples. It is\n * used to represent a chunk of a audio resource's buffer with a fixed size.\n */\nusing pcm_chunk_t = std::vector<int16_t>;\n\n} // namespace audio\n} // namespace openage\n"
  },
  {
    "path": "libopenage/config.cpp.in",
    "content": "// Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#include \"config.h\"\n\nnamespace openage {\nnamespace config {\n\nconst char *const config_option_string = \"${CONFIG_OPTION_STRING}\";\n\n}} // openage::config\n"
  },
  {
    "path": "libopenage/config.h.in",
    "content": "// Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#pragma once\n\n#define HAVE_THREAD_LOCAL_STORAGE ${HAVE_THREAD_LOCAL_STORAGE}\n\n#define WITH_BACKTRACE ${WITH_BACKTRACE}\n#define WITH_INOTIFY ${WITH_INOTIFY}\n#define WITH_OPENGL ${WITH_OPENGL}\n#define WITH_VULKAN ${WITH_VULKAN}\n#define WITH_GPERFTOOLS_PROFILER ${WITH_GPERFTOOLS_PROFILER}\n#define WITH_GPERFTOOLS_TCMALLOC ${WITH_GPERFTOOLS_TCMALLOC}\n#define WITH_NCURSES ${WITH_NCURSES}\n\n\nnamespace openage {\nnamespace config {\n\nextern const char *const config_option_string;\n\nconstexpr const char *buildsystem_sourcefile_dir = \"${CMAKE_SOURCE_DIR}/\";\n\n}} // openage::config\n"
  },
  {
    "path": "libopenage/console/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tbuf.cpp\n\tconsole.cpp\n\tdraw.cpp\n\ttests.cpp\n)\n"
  },
  {
    "path": "libopenage/console/buf.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"buf.h\"\n\n#include \"../util/unicode.h\"\n\n#include <cstdio>\n\nnamespace openage {\nnamespace console {\n\nusing namespace coord;\n\nBuf::Buf(term dims, term_t scrollback_lines, term_t min_width, buf_char default_char_fmt)\n\t:\n\tdims{dims},\n\tcursorpos{0, 0},\n\tsaved_cursorpos{0, 0},\n\tdefault_char_fmt{default_char_fmt}\n{\n\t//init all member variables\n\tthis->min_width = min_width;\n\n\tif (this->dims.x < this->min_width) {\n\t\tthis->dims.x = this->min_width;\n\t}\n\tif (this->dims.y < 1) {\n\t\tthis->dims.y = 1;\n\t}\n\tthis->scrollback_lines = scrollback_lines;\n\n\tthis->linedata_size = this->dims.y + this->scrollback_lines;\n\tthis->linedata = new buf_line[this->linedata_size];\n\tthis->linedata_end = this->linedata + this->linedata_size;\n\n\tthis->chrdata_size = this->dims.x * linedata_size;\n\tthis->chrdata = new buf_char[this->chrdata_size];\n\tthis->chrdata_end = this->chrdata + this->chrdata_size;\n\n\tthis->screen_chrdata = this->chrdata;\n\tthis->screen_linedata = this->linedata;\n\n\tthis->reset();\n}\n\nBuf::~Buf() {\n\t//free dynamically allocated members\n\tdelete[] this->linedata;\n\tdelete[] this->chrdata;\n}\n\nvoid Buf::reset() {\n\tthis->cursorpos = {0, 0};\n\tthis->saved_cursorpos = {0, 0};\n\tthis->cursor_visible = true;\n\tthis->cursor_special_lastcol = false;\n\n\tthis->escaped = false;\n\tthis->bell = false;\n\tthis->current_char_fmt = this->default_char_fmt;\n\n\tthis->scrollback_possible = 0;\n\tthis->scrollback_pos = 0;\n\n\t//fully clear data and linedata\n\tthis->clear({0, (term_t) -this->scrollback_lines}, {0, this->dims.y});\n}\n\n/*\n * this class merely exists as a helper for the next function,\n * Buf::resize(), and is thus defined and documented here in the implementation\n * cpp file.\n *\n * its purpose is to provide a new buffer, to where the old buffer may be\n * copied easily, avoiding code duplication.\n */\nclass NewBuf {\npublic:\n\tterm dims;\n\tterm_t scrollback_lines;\n\n\tbuf_line *linedata;\n\tsize_t linedata_size;\n\tbuf_line *linedata_end;\n\tbuf_line *linedata_ptr;\n\n\tbuf_char *chrdata;\n\tsize_t chrdata_size;\n\tbuf_char *chrdata_end;\n\tbuf_char *chrdata_ptr;\n\n\tterm_t screen_buf_start;\n\tsize_t screen_buf_size;\n\tterm_t scrollback_buf_start;\n\tsize_t scrollback_buf_size;\n\n\tbool current_line_is_screen_buf;\n\tterm current_pos;\n\n\tconst buf_char default_char_fmt;\n\n\tNewBuf(term dims, term_t scrollback_lines, buf_char default_char_fmt)\n\t\t:\n\t\tdims{dims},\n\t\tlinedata_ptr{nullptr},\n\t\tchrdata_ptr{nullptr},\n\t\tcurrent_pos{0, 0},\n\t\tdefault_char_fmt{default_char_fmt}\n\t{\n\t\tthis->scrollback_lines = scrollback_lines;\n\n\t\tthis->linedata_size = this->dims.y + this->scrollback_lines;\n\t\tthis->linedata = new buf_line[this->linedata_size];\n\t\tthis->linedata_end = this->linedata + this->linedata_size;\n\n\t\tthis->chrdata_size = this->linedata_size * this->dims.x;\n\t\tthis->chrdata = new buf_char[this->chrdata_size];\n\t\tthis->chrdata_end = this->chrdata + this->chrdata_size;\n\n\t\tthis->screen_buf_start = 0;\n\t\tthis->screen_buf_size = 0;\n\t\tthis->scrollback_buf_start = 0;\n\t\tthis->scrollback_buf_size = 0;\n\n\t\tthis->current_line_is_screen_buf = false;\n\t}\n\n\t~NewBuf() {\n\t\tdelete[] this->linedata;\n\t\tdelete[] this->chrdata;\n\t}\n\n\tvoid apply_to_buf(Buf *b) {\n\t\tb->dims = this->dims;\n\n\t\tdelete b->linedata;\n\t\tb->linedata = this->linedata;\n\t\tthis->linedata = nullptr;\n\t\tb->linedata_size = this->linedata_size;\n\t\tb->linedata_end = this->linedata_end;\n\n\t\tdelete b->chrdata;\n\t\tb->chrdata = this->chrdata;\n\t\tthis->chrdata = nullptr;\n\t\tb->chrdata_size = this->chrdata_size;\n\t\tb->chrdata_end = this->chrdata_end;\n\n\t\t// TODO set the following members of b:\n\t\t// screen_chrdata, screen_linedata\n\t\t// cursorpos, saved_cursorpos,\n\t\t// scrollback_possible, scrollback_pos\n\t\t// TODO call b->clear() with correct\n\t\t// screen buffer part as arguments\n\t}\n\n\tvoid move_ptrs_to_next_line() {\n\t\t//all three if clauses should have identical results...\n\t\tthis->current_pos.y++;\n\t\tif (this->current_pos.y >= this->dims.y) {\n\t\t\tthis->current_pos.y = 0;\n\t\t}\n\t\tthis->linedata_ptr++;\n\t\tif (this->linedata_ptr >= this->linedata_end) {\n\t\t\tthis->linedata_ptr = this->linedata;\n\t\t}\n\t\tthis->chrdata_ptr += this->dims.x;\n\t\tif (this->chrdata_ptr >= this->chrdata_end) {\n\t\t\tthis->chrdata_ptr = this->chrdata;\n\t\t}\n\t\t*(this->linedata_ptr) = BUF_LINE_DEFAULT;\n\t\tthis->current_pos.x = 0;\n\t}\n\n\t/**\n\t * starts a new line (as by newline character)\n\t */\n\tvoid new_line(bool part_of_screen_buf) {\n\t\t//clear the remaining chars of current line\n\t\tfor (; this->current_pos.x < this->dims.x; this->current_pos.x++) {\n\t\t\tthis->chrdata_ptr[this->current_pos.x] = this->default_char_fmt;\n\t\t}\n\n\t\tmove_ptrs_to_next_line();\n\n\t\tthis->current_line_is_screen_buf = part_of_screen_buf;\n\t}\n\n\t/**\n\t * writes a char\n\t */\n\tvoid new_chr(buf_char c) {\n\t\tthis->chrdata_ptr[this->current_pos.x++] = c;\n\n\t\tif (c != this->default_char_fmt) {\n\t\t\tthis->linedata_ptr->type = LINE_REGULAR;\n\n\t\t\tif (this->current_line_is_screen_buf) {\n\t\t\t\t//TODO write down somewhere the fact that this\n\t\t\t\t//line constitutes a part of the screen buffer\n\t\t\t}\n\t\t\telse {\n\t\t\t\t//TODO write down somewhere the fact that this\n\t\t\t\t//line constitutes a part of the scrollback buffer\n\t\t\t}\n\t\t}\n\n\t\tif (this->current_pos.x == this->dims.x) {\n\t\t\tthis->linedata_ptr->type = LINE_WRAPPED;\n\t\t\tmove_ptrs_to_next_line();\n\t\t}\n\t}\n};\n\n//TODO unfinished implementation\nvoid Buf::resize(term new_dims) {\n\tterm old_dims = this->dims;\n\n\tif (new_dims.x < this->min_width) {\n\t\tnew_dims.x = this->min_width;\n\t}\n\tif (new_dims.y < 1) {\n\t\tnew_dims.y = 1;\n\t}\n\n\t// allocate new buffers\n\tsize_t new_linedata_size = new_dims.y + this->scrollback_lines;\n\tbuf_line *new_linedata = new buf_line[new_linedata_size];\n\tbuf_line *new_linedata_end = new_linedata + new_linedata_size;\n\tsize_t new_chrdata_size = new_dims.x * new_linedata_size;\n\tbuf_char *new_chrdata = new buf_char[new_chrdata_size];\n\tbuf_char *new_chrdata_end = new_chrdata + new_chrdata_size;\n\n\t// working positions in new buffers: start at beginning of buffer\n\tbuf_line *new_linedata_pos = new_linedata;\n\tbuf_char *new_chrdata_pos = new_chrdata;\n\t// working positions in old buffers: start with oldest line of\n\t// scrollback buffer\n\tbuf_line *old_linedata_scrollbackstart = this->linedataptr(-scrollback_lines);\n\tbuf_char *old_chrdata_scrollbackstart = this->chrdataptr({0, (term_t) -scrollback_lines});\n\tbuf_line *old_linedata_pos = old_linedata_scrollbackstart;\n\tbuf_char *old_chrdata_pos = old_chrdata_scrollbackstart;\n\n\t// copy line by line, considering the value stored in\n\t// old_linedata_pos->auto_wrapped.\n\t// start with the first line of the scrollback buffer, and stop with the\n\t// last line of the screen buffer.\n\t// do not copy empty characters, unless they are followed by filled\n\t// characters in the same line, or a continued wrapped line.\n\t// store in term_t variables the locations in the new buffer of\n\t//   the first/last line of the old scrollback buffer\n\t//   the first/last line of the old screen buffer\n\t// these variables need to take into account events where upmost\n\t// lines of a buffer are overwritten by new lines.\n\t// commented out for now //TODO\n\n\t// term_t scrollback_buf_start;\n\t// term_t scrollback_buf_end;\n\t// term_t screen_buf_start;\n\t// term_t screen_buf_end;\n\n\t// count the number of empty chars that we've found in this line,\n\t// and we may or may not still have to copy.\n\tsize_t empty_chars = 0;\n\twhile (old_linedata_pos != old_linedata_scrollbackstart) {\n\t\t// should never be >, always ==\n\t\t// also, both checks should always yield identical results\n\t\t// (TODO do an ASSERT to assure this?)\n\t\tif (old_linedata_pos >= this->linedata_end) {\n\t\t\told_linedata_pos = this->linedata;\n\t\t}\n\t\tif (old_chrdata_pos >= this->chrdata_end) {\n\t\t\told_chrdata_pos = this->chrdata;\n\t\t}\n\t\t// should never be >, always ==\n\t\t// also, both checks should always yield identical results\n\t\t// (TODO do an ASSERT to assure this?)\n\t\tif (new_linedata_pos >= new_linedata_end) {\n\t\t\tnew_linedata_pos = new_linedata;\n\t\t}\n\t\tif (new_chrdata_pos >= new_chrdata_end) {\n\t\t\tnew_chrdata_pos = new_chrdata;\n\t\t}\n\n\t\tfor (term_t x = 0; x < old_dims.x; x++) {\n\t\t\tif (*old_chrdata_pos == this->default_char_fmt) {\n\t\t\t\tempty_chars++;\n\t\t\t} else {\n\t\t\t\twhile (empty_chars > 0) {\n\t\t\t\t\t// TODO write the empty chars\n\t\t\t\t}\n\t\t\t\t// TODO write the char\n\t\t\t}\n\n\t\t\told_chrdata_pos++;\n\t\t}\n\t}\n\n\t// TODO\n\t// depending on the variables defined in the previous section,\n\t// decide which line is the first line of the screen buffer\n\t// int new_chrbuf, and which parts of new_chrbuf must be cleared.\n\n\t// TODO\n\t// copy\n\n\tthis->dims = new_dims;\n}\n\nvoid Buf::write(const char *c, ssize_t len) {\n\tif (len >= 0) {\n\t\tfor (; len > 0; len--) {\n\t\t\tthis->write(*c);\n\t\t\tc++;\n\t\t}\n\t} else {\n\t\tfor (; *c; c++) {\n\t\t\tthis->write(*c);\n\t\t}\n\t}\n}\n\nvoid Buf::scroll(term_t lines) {\n\tif (lines < 0) {\n\t\t// scroll down\n\t\tlines = -lines;\n\n\t\tif ((term_t) this->scrollback_pos < lines) {\n\t\t\tthis->scrollback_pos = 0;\n\t\t} else {\n\t\t\tthis->scrollback_pos -= lines;\n\t\t}\n\t} else {\n\t\t// scroll up\n\t\tthis->scrollback_pos += lines;\n\t\tif (this->scrollback_pos > this->scrollback_possible) {\n\t\t\tthis->scrollback_pos = this->scrollback_possible;\n\t\t}\n\t}\n}\n\nvoid Buf::advance(unsigned linecount) {\n\t// sanitize linecount\n\tif (linecount == 0) {\n\t\treturn;\n\t}\n\tif ((term_t) linecount > this->dims.y + this->scrollback_lines) {\n\t\tlinecount = this->dims.y + this->scrollback_lines;\n\t}\n\n\t// update scrollback_possible\n\tthis->scrollback_possible += linecount;\n\tif (this->scrollback_possible > (term_t) this->scrollback_lines) {\n\t\tthis->scrollback_possible = this->scrollback_lines;\n\t}\n\n\t// update scrollback position, to remain at the currently scrolled-to\n\t// position\n\tif (this->scrollback_pos > 0) {\n\t\tthis->scrollback_pos += linecount;\n\t\tif (this->scrollback_pos > this->scrollback_possible) {\n\t\t\t//seems like we're already on top of the scrollback buffer\n\t\t\tthis->scrollback_pos = this->scrollback_possible;\n\t\t}\n\t}\n\n\t// clear the new lines. that's scrollback_buffer[0:linecount]\n\tthis->clear({0, (term_t) -this->scrollback_lines}, {0, (term_t) ((term_t) linecount - (term_t) this->scrollback_lines)});\n\n\t// move the screen buffer by updating the screen_chrdata pointer\n\tthis->screen_chrdata += linecount * this->dims.x;\n\tif (this->screen_chrdata >= this->chrdata_end) {\n\t\tthis->screen_chrdata -= this->chrdata_size;\n\t}\n\n\t// also update the screen_linedata pointer\n\tthis->screen_linedata += linecount;\n\tif (this->screen_linedata >= this->linedata_end) {\n\t\tthis->screen_linedata -= this->linedata_size;\n\t}\n}\n\nvoid Buf::write(char c) {\n\tif (!this->streamdecoder.feed(c)) {\n\t\t// an error has been detected in the input stream\n\t\tthis->process_codepoint(0xFFFD);\n\t};\n\n\tif (this->streamdecoder.remaining == 0 && this->streamdecoder.out >= 0) {\n\t\tthis->process_codepoint(streamdecoder.out);\n\t}\n}\n\nvoid Buf::pop_last_char() {\n\tif (this->cursorpos.x > 0) {\n\t\tif (this->cursor_special_lastcol) {\n\t\t\tthis->cursor_special_lastcol = false;\n\t\t}\n\t\telse {\n\t\t\tthis->cursorpos.x -= 1;\n\t\t}\n\n\t\tbuf_char *ptr = this->chrdataptr(this->cursorpos);\n\t\t*ptr = this->current_char_fmt;\n\t\tptr->cp = ' ';\n\n\t\tif (this->cursorpos.x == 0 && this->linedataptr(this->cursorpos.y - 1)->type == LINE_WRAPPED) {\n\t\t\tthis->linedataptr(this->cursorpos.y)->type = LINE_EMPTY;\n\t\t\tthis->cursorpos.y--;\n\t\t\tthis->linedataptr(this->cursorpos.y)->type = LINE_REGULAR;\n\t\t\tthis->cursorpos.x = this->dims.x - 1;\n\t\t\tthis->cursor_special_lastcol = true;\n\t\t}\n\t}\n}\n\nvoid Buf::process_codepoint(int cp) {\n\t// if the terminal is currently in escaped state, tread the codepoint as\n\t// part of the current escape sequence.\n\tif (this->escaped) {\n\t\tthis->escape_sequence.push_back(cp);\n\n\t\tsize_t len = this->escape_sequence.size();\n\t\tint first = this->escape_sequence[0];\n\t\tint previous = -1;\n\t\tif (len > 1) {\n\t\t\tprevious = this->escape_sequence[len - 2];\n\t\t}\n\n\t\t// the first char of the escape sequence determines\n\t\t// its length, terminators and allowed characters.\n\t\tswitch (first) {\n\t\tcase '[':\n\t\t\t// CSI\n\t\t\tif (len == 1 or (cp >= 0x20 and cp < 0x40)) {\n\t\t\t\t// regular, allowed char\n\t\t\t} else if (cp >= 0x40 and cp < 0x7f) {\n\t\t\t\t// terminator\n\t\t\t\tthis->process_csi_escape_sequence();\n\t\t\t} else {\n\t\t\t\t// illegal char, abortabortabort\n\t\t\t\tthis->escape_sequence_aborted();\n\t\t\t}\n\t\t\tbreak;\n\t\tcase ']':  // OSC\n\t\tcase 'P':  // DCS\n\t\tcase '_':  // APC\n\t\tcase '^':  // PM\n\t\t\t// terminated by ESC \\ or BEL\n\t\t\tif ((previous == 0x1b and cp == '\\\\') or cp == 0x07) {\n\t\t\t\tthis->process_text_escape_sequence();\n\t\t\t}\n\t\t\tbreak;\n\t\tcase '(':  // non-utf8-related stuff\n\t\tcase ')':  // character-set selection etc...\n\t\tcase '*':  // these are all 2-char sequences\n\t\tcase '+':\n\t\tcase '-':\n\t\tcase '/':\n\t\tcase '.':\n\t\tcase '%':\n\t\tcase '#':\n\t\tcase ' ':\n\t\t\t// not implemented\n\t\t\tif (len == 2) {\n\t\t\t\tthis->escape_sequence_aborted();\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault: // no known multi-cp sequence, treat as single escaped cp\n\t\t\tthis->process_escaped_cp(first);\n\t\t\tbreak;\n\t\t}\n\t} else {\n\t\t// we're not currently escaped, so the char is printed...\n\t\t// at least if it's a printable character.\n\t\tthis->print_codepoint(cp);\n\t}\n}\n\nvoid Buf::print_codepoint(int cp) {\n\tswitch (cp) {\n\t// control characters\n\tcase 0x00: // NUL: ignore\n\tcase 0x01: // SOH: ignore\n\tcase 0x02: // STX: ignore\n\tcase 0x03: // ETX: ignore\n\tcase 0x04: // EOT: ignore\n\tcase 0x05: // ENQ: ignore (empty response)\n\tcase 0x06: // ACK: ignore\n\t\tbreak;\n\tcase 0x07: // BEL: set bell flag\n\t\tthis->bell = true;\n\t\tbreak;\n\tcase 0x08: // BS: backspace\n\t\t// move cursor 1 left.\n\t\t// if cursor pos is 0, move to end of previous line\n\t\t// if already at (0,0), move to (0,0)\n\t\tif (this->cursor_special_lastcol && (this->cursorpos.x == this->dims.x - 1)) {\n\t\t\t// if we are in the special last-column state, only unset that flag,\n\t\t\t// but don't move the cursor.\n\t\t\tthis->cursor_special_lastcol = false;\n\t\t} else {\n\t\t\tthis->cursorpos.x -= 1;\n\t\t\tif (this->cursorpos.x < 0) {\n\t\t\t\tthis->cursorpos.x += this->dims.x;\n\t\t\t\tthis->cursorpos.y -= 1;\n\t\t\t\tif (this->cursorpos.y < 0) {\n\t\t\t\t\tthis->cursorpos = {0, 0};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tcase 0x09: // HT: horizontal tab\n\t\t// move cursor to next multiple of 8\n\t\t// at least move by 1, at most by 8\n\t\t// if next multiple of 8 is greater than terminal width,\n\t\t// move to end of this line\n\t\t// never move to next line\n\t\tthis->cursorpos.x = ((this->cursorpos.x + 8) / 8) * 8;\n\t\tif (this->cursorpos.x >= this->dims.x) {\n\t\t\tthis->cursorpos.x = this->dims.x - 1;\n\t\t}\n\t\tbreak;\n\tcase 0x0a: // LF: linefeed\n\tcase 0x0b: // VT: vertical tab\n\tcase 0x0c: // FF: form feed\n\t\t//all these do the same: move to beginning of next line\n\t\tthis->cursor_special_lastcol = false;\n\t\tthis->cursorpos.x = 0;\n\n\t\tif (this->cursorpos.y == this->dims.y - 1) {\n\t\t\tthis->advance(1);\n\t\t} else {\n\t\t\tthis->cursorpos.y += 1;\n\t\t}\n\t\tbreak;\n\tcase 0x0d: // CR: carriage return\n\t\tthis->cursorpos.x = 0;\n\t\tthis->cursor_special_lastcol = false;\n\t\tbreak;\n\tcase 0x0e: // SO: ignore\n\tcase 0x0f: // SI: ignore\n\tcase 0x10: // DLE: ignore\n\tcase 0x11: // DC1: ignore\n\tcase 0x12: // DC2: ignore\n\tcase 0x13: // DC3: ignore\n\tcase 0x14: // DC4: ignore\n\tcase 0x15: // NAK: ignore\n\tcase 0x16: // SYN: ignore\n\tcase 0x17: // ETB: ignore\n\tcase 0x18: // CAN: ignore\n\tcase 0x19: // EM: ignore\n\tcase 0x1a: // SUB: ignore\n\t\tbreak;\n\tcase 0x1b: // ESC: escape sequence start\n\t\tthis->escaped = true;\n\t\tbreak;\n\tcase 0x1c: // FS: ignore\n\tcase 0x1d: // GS: ignore\n\tcase 0x1e: // RS: ignore\n\tcase 0x1f: // US: ignore\n\tcase 0x7f: // DEL: ignore\n\t\tbreak;\n\tdefault:   // regular, printable character\n\t\tif (this->cursor_special_lastcol && (this->cursorpos.x == this->dims.x - 1)) {\n\t\t\tthis->cursor_special_lastcol = false;\n\t\t\t// store the fact that this line was auto-wrapped\n\t\t\t// and will continue in the next line\n\t\t\tthis->linedataptr(this->cursorpos.y)->type = LINE_WRAPPED;\n\t\t\tthis->cursorpos.x = 0;\n\t\t\tif (this->cursorpos.y == this->dims.y - 1) {\n\t\t\t\tthis->advance(1);\n\t\t\t} else {\n\t\t\t\tthis->cursorpos.y += 1;\n\t\t\t}\n\t\t}\n\n\t\t// set char at current cursor pos\n\t\tbuf_char *ptr = this->chrdataptr(this->cursorpos);\n\t\t*ptr = this->current_char_fmt;\n\t\tptr->cp = cp;\n\t\tbuf_line *lineptr = this->linedataptr(this->cursorpos.y);\n\n\t\t// store the fact that this line has been written to\n\t\tif (lineptr->type == LINE_EMPTY) {\n\t\t\tlineptr->type = LINE_REGULAR;\n\t\t}\n\n\t\t// advance cursor to the right\n\t\tthis->cursorpos.x++;\n\t\tif (this->cursorpos.x == this->dims.x) {\n\t\t\tthis->cursorpos.x -= 1;\n\t\t\tthis->cursor_special_lastcol = true;\n\t\t}\n\n\t\tbreak;\n\t}\n}\n\nvoid print_cps(FILE *f, std::vector<int> *v) {\n\tfor (int i: *v) {\n\t\tif (i >= 0x20 and i < 0x7f) {\n\t\t\tfprintf(f, \"%c\", (char) i);\n\t\t} else {\n\t\t\tfprintf(f, \"?\");\n\t\t}\n\t}\n}\n\nvoid Buf::escape_sequence_aborted() {\n\tthis->escaped = false;\n\tprint_cps(stderr, &this->escape_sequence);\n\tfprintf(stderr, \"\\n\");\n\tthis->escape_sequence.clear();\n}\n\nvoid Buf::escape_sequence_processed() {\n\tthis->escaped = false;\n\tthis->escape_sequence.clear();\n}\n\nvoid Buf::process_escaped_cp(int cp) {\n\tswitch (cp) {\n\tcase 'c': //RIS (full reset)\n\t\tthis->reset();\n\t\tbreak;\n\tdefault:\n\t\t//unimplemented or invalid codepoint\n\t\tthis->escape_sequence_aborted();\n\t\tbreak;\n\t}\n}\n\nvoid Buf::process_text_escape_sequence() {\n\tsize_t len = this->escape_sequence.size();\n\tswitch (this->escape_sequence[0]) {\n\tcase ']': //OSC\n\t\tswitch (this->escape_sequence[1]) {\n\t\tcase '0':\n\t\tcase '2':\n\t\t\tif (this->escape_sequence[2] == ';') {\n\t\t\t\t//set window title\n\t\t\t\tint maxidx = len - 1;\n\t\t\t\tif (this->escape_sequence[len - 1] == '\\\\') {\n\t\t\t\t\tmaxidx = len - 2;\n\t\t\t\t}\n\t\t\t\tthis->title.clear();\n\t\t\t\tfor (int i = 3; i < maxidx; i++) {\n\t\t\t\t\tthis->title.push_back(this->escape_sequence[i]);\n\t\t\t\t}\n\t\t\t\tthis->escape_sequence_processed();\n\t\t\t}\n\t\t}\n\t\tbreak;\n\tdefault:\n\t\tbreak;\n\t}\n\t//not implemented or invalid\n\tthis->escape_sequence_aborted();\n}\n\nvoid Buf::process_csi_escape_sequence() {\n\tsize_t len = this->escape_sequence.size();\n\tint type = this->escape_sequence[len - 1];\n\tbool starts_with_questionmark = false;\n\tstd::vector<int> params;\n\tbool in_param = false;\n\tint currentparam;\n\n\tfor (size_t pos = 1; pos < len - 1; pos++) {\n\t\tint cp = this->escape_sequence[pos];\n\t\tif (cp == '?' && pos == 1) {\n\t\t\tstarts_with_questionmark = true;\n\t\t} else if (cp >= '0' && cp <= '9') {\n\t\t\tif (!in_param) {\n\t\t\t\tin_param = true;\n\t\t\t\tcurrentparam = 0;\n\t\t\t}\n\n\t\t\tcurrentparam = 10 * currentparam + (cp - '0');\n\t\t} else if (cp == ';') {\n\t\t\tif (!in_param) {\n\t\t\t\t// this ';' did not have a number before it\n\t\t\t\t// (ommited)\n\t\t\t\tcurrentparam = -1;\n\t\t\t}\n\n\t\t\tparams.push_back(currentparam);\n\t\t\tin_param = false;\n\t\t} else {\n\t\t\t// unexpected character, we want a nice\n\t\t\t// semicolon-separated list of decimal numbers.\n\t\t\t// abortabortabort\n\t\t\tthis->escape_sequence_aborted();\n\t\t\treturn;\n\t\t}\n\t}\n\n\tif (in_param) {\n\t\t// there's no semicolon at the end, finish the param\n\t\tparams.push_back(currentparam);\n\t} else {\n\t\t// there's a semicolon at the end, or we had no params\n\t\t// at all. add param with default value.\n\t\tparams.push_back(-1);\n\t}\n\n\t// default values\n\tint defaultval;\n\tunsigned minparamcount;\n\tswitch (type) {\n\tcase 'H':\n\tcase 'f':\n\t\tminparamcount = 2;\n\t\tdefaultval = 1;\n\t\tbreak;\n\tcase 'J':\n\tcase 'K':\n\tcase 'm':\n\t\tminparamcount = 1;\n\t\tdefaultval = 0;\n\t\tbreak;\n\tcase 's':\n\tcase 'u':\n\t\tminparamcount = 0;\n\t\tdefaultval = 0;\n\t\tbreak;\n\tdefault:\n\t\tminparamcount = 1;\n\t\tdefaultval = 1;\n\t\tbreak;\n\t}\n\n\t// apply default value requirements\n\twhile (params.size() < minparamcount) {\n\t\tparams.push_back(-1);\n\t}\n\tfor (int &p: params) {\n\t\tif (p == -1) {\n\t\t\tp = defaultval;\n\t\t}\n\t}\n\n\t// execute the escape sequence\n\tswitch (type) {\n\tcase '@': // ICH: insert n blank characters\n\t\tfor (int i = 0; i < params[0]; i++) {\n\t\t\tthis->print_codepoint(0x20);\n\t\t}\n\t\tbreak;\n\tcase 'A': // CUU: move cursor up\n\t\tthis->cursorpos.y -= params[0];\n\t\tif (this->cursorpos.y < 0) {\n\t\t\tthis->cursorpos.y = 0;\n\t\t}\n\t\tbreak;\n\tcase 'e': // VPR: vertical position relative\n\t\t//fall through\n\tcase 'B': // CUD: move cursor down\n\t\tthis->cursorpos.y += params[0];\n\t\tif (this->cursorpos.y >= this->dims.y) {\n\t\t\tthis->cursorpos.y = this->dims.y - 1;\n\t\t}\n\t\tbreak;\n\tcase 'C': // CUF: move cursor to the right\n\t\tthis->cursorpos.x += params[0];\n\t\tif (this->cursorpos.x >= this->dims.x) {\n\t\t\tthis->cursorpos.x = this->dims.x - 1;\n\t\t}\n\t\tbreak;\n\tcase 'D': // CUB: move cursor to the left\n\t\tthis->cursorpos.x -= params[0];\n\t\tif (this->cursorpos.x < 0) {\n\t\t\tthis->cursorpos.x = 0;\n\t\t}\n\t\tbreak;\n\tcase 'E': // CNL: move cursor down and to beginning of line\n\t\tthis->cursorpos.x = 0;\n\t\tthis->cursorpos.y += params[0];\n\t\tif (this->cursorpos.y >= this->dims.y) {\n\t\t\tthis->cursorpos.y = this->dims.y - 1;\n\t\t}\n\t\tbreak;\n\tcase 'F': // CPL: move cursor up and to beginning of line\n\t\tthis->cursorpos.x = 0;\n\t\tthis->cursorpos.y -= params[0];\n\t\tif (this->cursorpos.y < 0) {\n\t\t\tthis->cursorpos.y = 0;\n\t\t}\n\t\tbreak;\n\tcase 'G': // CHA: set cursorpos.x\n\t\tthis->cursorpos.x = params[0] - 1;\n\t\tif (this->cursorpos.x < 0) {\n\t\t\tthis->cursorpos.x = 0;\n\t\t}\n\t\tif (this->cursorpos.x >= this->dims.x) {\n\t\t\tthis->cursorpos.x = this->dims.x - 1;\n\t\t}\n\t\tbreak;\n\tcase 'f': // HVP: set cursorpos\n\t\t//fall through\n\tcase 'H': // CUP: set cursorpos\n\t\tthis->cursorpos.y = params[0] - 1;\n\t\tthis->cursorpos.x = params[1] - 1;\n\t\tif (this->cursorpos.x < 0) {\n\t\t\tthis->cursorpos.x = 0;\n\t\t}\n\t\tif (this->cursorpos.x >= this->dims.x) {\n\t\t\tthis->cursorpos.x = this->dims.x - 1;\n\t\t}\n\t\tif (this->cursorpos.y < 0) {\n\t\t\tthis->cursorpos.y = 0;\n\t\t}\n\t\tif (this->cursorpos.y >= this->dims.y) {\n\t\t\tthis->cursorpos.y = this->dims.y - 1;\n\t\t}\n\t\tbreak;\n\tcase 'd': // VPA: vertical position absolute\n\t\tthis->cursorpos.y = params[0] - 1;\n\t\tif (this->cursorpos.y < 0) {\n\t\t\tthis->cursorpos.y = 0;\n\t\t}\n\t\tif (this->cursorpos.y >= this->dims.y) {\n\t\t\tthis->cursorpos.y = this->dims.y - 1;\n\t\t}\n\t\tbreak;\n\tcase 'J': // ED: erase display\n\t\tswitch (params[0]) {\n\t\tcase 0: // clear screen buffer from cursor to end\n\t\t\tthis->clear(this->cursorpos, {0, this->dims.y});\n\t\t\tbreak;\n\t\tcase 1: // clear screen buffer from beginning to cursor\n\t\t\tthis->clear({0, 0}, this->cursorpos, true);\n\t\t\tbreak;\n\t\tcase 2: // clear screen buffer\n\t\t\tthis->clear({0, 0}, {0, this->dims.y});\n\t\t\tbreak;\n\t\tdefault: // unknown/unimplemented parameter\n\t\t\tbreak;\n\t\t}\n\t\tbreak;\n\tcase 'K': // EL: erase line\n\t\tswitch (params[0]) {\n\t\tcase 0: // clear current line from cursor to end\n\t\t\tthis->clear(this->cursorpos, {0, (term_t) (this->cursorpos.y + 1)});\n\t\t\tbreak;\n\t\tcase 1: // clear current line from beginning to cursor\n\t\t\tthis->clear({0, this->cursorpos.y}, this->cursorpos, true);\n\t\t\tbreak;\n\t\tcase 2: // clear current line\n\t\t\tthis->clear({0, this->cursorpos.y}, {0, (term_t) (this->cursorpos.y + 1)});\n\t\t\tbreak;\n\t\tdefault: // unknown/unimplemented parameter\n\t\t\tbreak;\n\t\t}\n\t\tbreak;\n\tcase 'm': // SGR: set graphics rendition\n\t\tthis->process_sgr_code(params);\n\t\tbreak;\n\tcase 's': // SCP: save cursor position\n\t\tthis->saved_cursorpos = this->cursorpos;\n\t\tbreak;\n\tcase 'u': // RCP: restore cursor position\n\t\tthis->cursorpos = this->saved_cursorpos;\n\t\tbreak;\n\tcase 'l': // set mode\n\t\tif (starts_with_questionmark) {\n\t\t\tswitch(params[0]) {\n\t\t\tcase 25: // cursor invisible\n\t\t\t\tthis->cursor_visible = false;\n\t\t\t\tbreak;\n\t\t\tcase 1049: // switch to alternate screen\n\t\t\t\t// TBI\n\t\t\t\t// idea: use last dims.y lines of scrollback\n\t\t\t\t// buffer to save regular screen data\n\t\t\t\tbreak;\n\t\t\tdefault: // unknown/unimplemented parameter\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\tswitch(params[0]) {\n\t\t\tdefault: // unknown/unimplemented parameter\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tbreak;\n\tcase 'h': // reset mode\n\t\tif (starts_with_questionmark) {\n\t\t\tswitch(params[0]) {\n\t\t\tcase 25: // cursor visible\n\t\t\t\tthis->cursor_visible = true;\n\t\t\t\tbreak;\n\t\t\tcase 1049: // restore regular screen\n\t\t\t\t// TBI\n\t\t\t\tbreak;\n\t\t\tdefault: // unknown/unimplemented parameter\n\t\t\t\tbreak;\n\t\t\t}\n\t\t} else {\n\t\t\tswitch(params[0]) {\n\t\t\tdefault: // unknown/unimplemented parameter\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tbreak;\n\tcase 'S': // SU\n\tcase 'T': // SD\n\tcase 'n': // DSR\n\t\t// not implemented; fall through.\n\tdefault:\n\t\t// not implemented (or nonexisting)\n\t\tthis->escape_sequence_aborted();\n\t\treturn;\n\t}\n\tthis->escape_sequence_processed();\n}\n\nvoid Buf::process_sgr_code(const std::vector<int> &params) {\n\tfor (size_t i = 0; i < params.size(); i++) {\n\t\tint p = params[i];\n\t\tswitch (p) {\n\t\tcase 0: // reset\n\t\t\tthis->current_char_fmt = this->default_char_fmt;\n\t\t\tbreak;\n\t\tcase 1: // bold\n\t\t\tthis->current_char_fmt.flags |= CHR_BOLD;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FAINT;\n\t\t\tbreak;\n\t\tcase 2: // faint\n\t\t\tthis->current_char_fmt.flags |= CHR_FAINT;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BOLD;\n\t\t\tbreak;\n\t\tcase 3: // italic\n\t\t\tthis->current_char_fmt.flags |= CHR_ITALIC;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FRAKTUR;\n\t\t\tbreak;\n\t\tcase 4: // underline\n\t\t\tthis->current_char_fmt.flags |= CHR_UNDERLINED;\n\t\t\tbreak;\n\t\tcase 5: // blink slowly\n\t\t\tthis->current_char_fmt.flags |= CHR_BLINKING;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BLINKINGFAST;\n\t\t\tbreak;\n\t\tcase 6: // blink fast\n\t\t\tthis->current_char_fmt.flags |= CHR_BLINKINGFAST;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BLINKING;\n\t\t\tbreak;\n\t\tcase 7: // negative\n\t\t\tthis->current_char_fmt.flags |= CHR_NEGATIVE;\n\t\t\tbreak;\n\t\tcase 8: // invisible\n\t\t\tthis->current_char_fmt.flags |= CHR_INVISIBLE;\n\t\t\tbreak;\n\t\tcase 9: // struck out\n\t\t\tthis->current_char_fmt.flags |= CHR_STRUCKOUT;\n\t\t\tbreak;\n\t\t// cases 10-19: font selectors. not implemented.\n\t\tcase 20: // fraktur\n\t\t\tthis->current_char_fmt.flags |= CHR_FRAKTUR;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_ITALIC;\n\t\t\tbreak;\n\t\tcase 21: // bold off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BOLD;\n\t\t\tbreak;\n\t\tcase 22: // bold and faint off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BOLD;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FAINT;\n\t\t\tbreak;\n\t\tcase 23: // italic and fraktur off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_ITALIC;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FRAKTUR;\n\t\t\tbreak;\n\t\tcase 24: // underline off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_UNDERLINED;\n\t\t\tbreak;\n\t\tcase 25: // blinking and blinkingfast off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BLINKING;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_BLINKINGFAST;\n\t\t\tbreak;\n\t\t// case 26: not yet standardized\n\t\tcase 27: // negative off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_NEGATIVE;\n\t\t\tbreak;\n\t\tcase 28: // invisible off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_INVISIBLE;\n\t\t\tbreak;\n\t\tcase 29: // struck out off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_STRUCKOUT;\n\t\t\tbreak;\n\t\tcase 30:\n\t\tcase 31:\n\t\tcase 32:\n\t\tcase 33:\n\t\tcase 34:\n\t\tcase 35:\n\t\tcase 36:\n\t\tcase 37: // foreground color (8 colors)\n\t\t\tthis->current_char_fmt.fgcol = (p - 30);\n\t\t\tbreak;\n\t\tcase 38: // foreground color (256 colors)\n\t\t\ti += 2;\n\t\t\tif (i >= params.size() || params[i] < 0 || params[i] >= 256) {\n\t\t\t\t// invalid 256-color SGR code.\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis->current_char_fmt.fgcol = params[i];\n\t\t\tbreak;\n\t\tcase 39: // reset foreground color\n\t\t\tthis->current_char_fmt.fgcol = this->default_char_fmt.fgcol;\n\t\t\tbreak;\n\t\tcase 40:\n\t\tcase 41:\n\t\tcase 42:\n\t\tcase 43:\n\t\tcase 44:\n\t\tcase 45:\n\t\tcase 46:\n\t\tcase 47: // background color (8 colors)\n\t\t\tthis->current_char_fmt.bgcol = (p - 40);\n\t\t\tbreak;\n\t\tcase 48: // background color (256 colors)\n\t\t\ti += 2;\n\t\t\tif (i >= params.size() || params[i] < 0 || params[i] >= 256) {\n\t\t\t\t// invalid 256-color SGR code.\n\t\t\t\t// abortabortabort\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis->current_char_fmt.bgcol = params[i];\n\t\t\tbreak;\n\t\tcase 49: // reset background color\n\t\t\tthis->current_char_fmt.bgcol = this->default_char_fmt.bgcol;\n\t\t\tbreak;\n\t\t// case 50: not yet standardized\n\t\tcase 51: // framed\n\t\t\tthis->current_char_fmt.flags |= CHR_FRAMED;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_ENCIRCLED;\n\t\t\tbreak;\n\t\tcase 52: // encircled\n\t\t\tthis->current_char_fmt.flags |= CHR_ENCIRCLED;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FRAMED;\n\t\t\tbreak;\n\t\tcase 53: // overlined\n\t\t\tthis->current_char_fmt.flags |= CHR_OVERLINED;\n\t\t\tbreak;\n\t\tcase 54: // framed and encircled off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_FRAMED;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_ENCIRCLED;\n\t\t\tbreak;\n\t\tcase 55: // overlined off\n\t\t\tthis->current_char_fmt.flags &= ~CHR_OVERLINED;\n\t\t\tbreak;\n\t\t// cases 56-59: not yet standardized\n\t\tcase 60: // right side line\n\t\t\tthis->current_char_fmt.flags |= CHR_RIGHTLINED;\n\t\t\tbreak;\n\t\t// case 61: double right side line. not implemented.\n\t\tcase 62: // left side line\n\t\t\tthis->current_char_fmt.flags |= CHR_LEFTLINED;\n\t\t\tbreak;\n\t\t// case 63: double left side line. not implemented.\n\t\tcase 64: // stress ideogram (whatever that is)\n\t\t\tthis->current_char_fmt.flags |= CHR_STRESS_IDEOGRAM;\n\t\t\tbreak;\n\t\tcase 65: // disables effects 60-64\n\t\t\tthis->current_char_fmt.flags &= ~CHR_RIGHTLINED;\n\t\t\tthis->current_char_fmt.flags &= ~CHR_LEFTLINED;\n\t\t\tbreak;\n\t\t// cases above 65 are not standardized.\n\t\tdefault:\n\t\t\t// not implemented or not defined.\n\t\t\t// abortabortabort\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid Buf::clear(term start, term end, bool clear_end) {\n\t// apply clear_end\n\tif (clear_end) {\n\t\tend.x++;\n\t\tif (end.x > this->dims.x) {\n\t\t\tend.x = 0;\n\t\t\tend.y++;\n\t\t}\n\t}\n\n\tif (start.y > end.y or (start.y == end.y and start.x >= end.x)) {\n\t\treturn;\n\t}\n\n\t// clear char info\n\tchrdata_clear(chrdataptr(start), chrdataptr(end));\n\n\t// calculate lines to clear\n\t// a line is cleared iff all of its characters are cleared\n\tterm_t line_start = start.y;\n\tif (start.x > 0) {\n\t\tline_start++;\n\t}\n\tterm_t line_end = end.y;\n\n\tif (line_start >= line_end) {\n\t\treturn;\n\t}\n\n\t// clear line info\n\tlinedata_clear(linedataptr(line_start), linedataptr(line_end));\n}\n\nvoid Buf::chrdata_clear(buf_char *start, buf_char *end) {\n\tif (start < end) {\n\t\tfor (; start < end; start++) {\n\t\t\t*start = this->current_char_fmt;\n\t\t}\n\t} else {\n\t\tfor (buf_char *c = start; c < this->chrdata_end; c++) {\n\t\t\t*c = this->current_char_fmt;\n\t\t}\n\t\tfor (buf_char *c = this->chrdata; c < end; c++) {\n\t\t\t*c = this->current_char_fmt;\n\t\t}\n\t}\n}\n\nvoid Buf::linedata_clear(buf_line *start, buf_line *end) {\n\tif (start < end) {\n\t\tfor (; start < end; start++) {\n\t\t\t*start = BUF_LINE_DEFAULT;\n\t\t}\n\t} else {\n\t\tfor (buf_line *l = start; l < this->linedata_end; l++) {\n\t\t\t*l = BUF_LINE_DEFAULT;\n\t\t}\n\t\tfor (buf_line *l = this->linedata; l < end; l++) {\n\t\t\t*l = BUF_LINE_DEFAULT;\n\t\t}\n\t}\n}\n\nbuf_char *Buf::chrdataptr(term pos) {\n\tbuf_char *result = this->screen_chrdata + pos.x + pos.y * this->dims.x;\n\tif (result < this->chrdata) {\n\t\tresult += this->chrdata_size;\n\t} else if (result >= this->chrdata_end) {\n\t\tresult -= this->chrdata_size;\n\t}\n\treturn result;\n}\n\nbuf_line *Buf::linedataptr(term_t lineno) {\n\tbuf_line *result = this->screen_linedata + lineno;\n\tif (result < this->linedata) {\n\t\tresult += this->linedata_size;\n\t}\n\tif (result > this->linedata_end) {\n\t\tresult -= this->linedata_size;\n\t}\n\treturn result;\n}\n\nconst coord::term &Buf::get_dims() const {\n\treturn this->dims;\n}\n\n\n}} // openage::console\n"
  },
  {
    "path": "libopenage/console/buf.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <stdint.h>\n#include <stdlib.h>\n#include <sys/types.h>\n\n#include <vector>\n\n#include \"../coord/term.h\"\n#include \"../util/compiler.h\"\n#include \"../util/unicode.h\"\n\nnamespace openage {\nnamespace console {\n\nclass NewBuf;\n\n\n/**\n * the length of the escape sequence buffer, and thus the maximum length\n * of any escape sequence that will be successfully processed.\n *\n * longer escape sequences will be ignored.\n */\nusing chrcol_t = uint8_t;\nusing chrflags_t = uint16_t;\n\nconstexpr chrflags_t CHR_BOLD = (1 << 0);\nconstexpr chrflags_t CHR_FAINT = (1 << 1);\nconstexpr chrflags_t CHR_ITALIC = (1 << 2);\nconstexpr chrflags_t CHR_BLINKING = (1 << 3);\nconstexpr chrflags_t CHR_BLINKINGFAST = (1 << 4);\nconstexpr chrflags_t CHR_UNDERLINED = (1 << 5);\nconstexpr chrflags_t CHR_STRUCKOUT = (1 << 6);\nconstexpr chrflags_t CHR_FRAKTUR = (1 << 7);\nconstexpr chrflags_t CHR_NEGATIVE = (1 << 8);\nconstexpr chrflags_t CHR_INVISIBLE = (1 << 9);\nconstexpr chrflags_t CHR_FRAMED = (1 << 10);\nconstexpr chrflags_t CHR_ENCIRCLED = (1 << 11);       // because framing stuff is so 20th century\nconstexpr chrflags_t CHR_OVERLINED = (1 << 12);       // whoever knows that this exists\nconstexpr chrflags_t CHR_RIGHTLINED = (1 << 13);      // whoever wants that\nconstexpr chrflags_t CHR_LEFTLINED = (1 << 14);       // whoever needs that\nconstexpr chrflags_t CHR_STRESS_IDEOGRAM = (1 << 15); // whatever that is\n\n/**\n * a single character in the buffer\n */\nstruct buf_char {\n\t/**\n\t * Default copy constructor\n\t */\n\tbuf_char(int cp, chrcol_t fgcol, chrcol_t bgcol, chrflags_t flags) :\n\t\tcp{cp},\n\t\tfgcol{fgcol},\n\t\tbgcol{bgcol},\n\t\tflags{flags} {\n\t}\n\n\tbuf_char() = default;\n\n\t/**\n\t * unicode codepoint of the character\n\t */\n\tint cp;\n\t/**\n\t * (256-col) color of the character\n\t */\n\tchrcol_t fgcol;\n\t/**\n\t * (256-col) background color\n\t */\n\tchrcol_t bgcol;\n\t/**\n\t * flags of the character\n\t */\n\tchrflags_t flags;\n\n\tbool operator==(const buf_char &other) const {\n\t\treturn (this->cp == other.cp) && (this->fgcol == other.fgcol) && (this->bgcol == other.bgcol) && (this->flags == other.flags);\n\t}\n\n\tbool operator!=(const buf_char &other) const {\n\t\treturn not(*this == other);\n\t}\n};\n\nusing linetype_t = uint8_t;\n\n/**\n * the line has not been written yet (or cleared in between)\n */\nconstexpr linetype_t LINE_EMPTY = 0;\n/**\n * the line has some contents\n */\nconstexpr linetype_t LINE_REGULAR = 1;\n/**\n * the line has been written to until it auto-wrapped;\n * its continuation can be found in the next line\n *\n * this value is required for re-calculating the buffer on resize\n */\nconstexpr linetype_t LINE_WRAPPED = 2;\n\n/**\n * metadata for a single line in the buffer\n */\nstruct buf_line {\n\tlinetype_t type;\n};\n\nconstexpr buf_line BUF_LINE_DEFAULT{LINE_EMPTY};\n\nclass Buf {\n\tfriend class NewBuf;\n\npublic:\n\tBuf(coord::term dims, coord::term_t scrollback_lines, coord::term_t min_width, buf_char default_char_fmt = {0x20, 254, 0, 0});\n\t~Buf();\n\n\t/*\n\t * we don't want this object to be copyable\n\t */\n\tBuf &operator=(const Buf &) = delete;\n\tBuf(const Buf &) = delete;\n\n\n\t/**\n\t * resets the buffer to its constructor defaults\n\t */\n\tvoid reset();\n\n\t/**\n\t * write a byte string to the buffer\n\t *\n\t * the string is assumed to be UTF-8 encoded.\n\t *\n\t * if len >= 0, it describes the length of the string.\n\t * otherwise, the string is assumed to be null-terminated.\n\t */\n\tvoid write(const char *c, ssize_t len = -1);\n\n\t/**\n\t * write a single byte to the buffer.\n\t *\n\t * assumed to be UTF-8 encoded.\n\t */\n\tvoid write(char c);\n\n\t/**\n\t * Pop the last char of the current line\n\t *\n\t * Will correctly handle UTF-8 chars and wrapped lines.\n\t */\n\tvoid pop_last_char();\n\n\t/**\n\t * resizes the screen buffer\n\t */\n\tvoid resize(coord::term new_dims);\n\n\t/**\n\t * scrolls up or down by the given number of lines\n\t *\n\t * lines\n\t *   amount of lines to scroll up - may be negative\n\t *   if the upper or lower limit is reached, this is capped.\n\t */\n\tvoid scroll(coord::term_t lines);\n\n\t// the following members could/should be private, but we don't believe in withholding\n\t// crucial information from the public. besides, sooner or later some whistleblower\n\t// would release the pointers anyway.\n\t// also, I don't believe in friends.\n\n\t/**\n\t * utf-8 state-machine that reads input bytes\n\t * and outputs unicode codepoints\n\t */\n\tutil::utf8_decoder streamdecoder;\n\n\t/**\n\t * processes one unicode codepoint\n\t * internally called by write(char) after feeding the\n\t * char into streamdecoder\n\t */\n\tvoid process_codepoint(int cp);\n\n\t/**\n\t * prints one unicode codepoint\n\t * interally called by process_codepoint(int cp) after sorting out escape sequences, and by some escape sequences such as CSI @\n\t */\n\tvoid print_codepoint(int cp);\n\n\t/**\n\t * aborts the current escape sequence\n\t * (e.g. because it contained an illegal\n\t * character, or is not implemented)\n\t */\n\tvoid escape_sequence_aborted();\n\n\t/**\n\t * called when a escape sequence has been\n\t * successfully processed, clears the\n\t * sequence. either this or abort_escape_sequence\n\t * is called for each escape sequence.\n\t */\n\tvoid escape_sequence_processed();\n\n\t/**\n\t * processes the CSI escape sequence currently stored in\n\t * escape_sequence.\n\t * invalid sequences are ignored.\n\t */\n\tvoid process_csi_escape_sequence();\n\n\t/**\n\t * processes a single-codepoint escape sequence.\n\t * invalid codepoints are ignored.\n\t */\n\tvoid process_escaped_cp(int cp);\n\n\t/**\n\t * processes a text escape sequence\n\t * that is one of OSC, DCS, APC or PM\n\t */\n\tvoid process_text_escape_sequence();\n\n\t/**\n\t * sets graphics rendition parameters; called by\n\t * process_escape_sequence\n\t *\n\t * params:\n\t *   numerical params list from escape sequence\n\t */\n\tvoid process_sgr_code(const std::vector<int> &params);\n\n\t/**\n\t * advances the buffer by the specified number of lines.\n\t *\n\t * the top lines are moved to the scrollback buffer.\n\t * the top lines of the scrollback buffer are discarded.\n\t * the bottom lines of the buffer are empty after this.\n\t * the cursor position is not changed by this.\n\t *\n\t * linecount:\n\t *   the number of lines to advance the buffer;\n\t *   defaults to one.\n\t *   may be > buffer height.\n\t */\n\tvoid advance(unsigned linecount = 1);\n\n\t/**\n\t * clears the screen character and line buffers.\n\t * lines are cleared iff all of their characters are cleared.\n\t *\n\t * if start >= end, nothing happens.\n\t *\n\t * inputs are not sanitized.\n\t *\n\t * start\n\t *   screen buffer coordinates of first character to be deleted\n\t *   negative values of y indicate scrollback buffer\n\t *   -scrollback_lines <= y < dims.y\n\t *   0 <= x < dims.x\n\t * end\n\t *   screen buffer coordinates of first character not to be deleted\n\t *   negative values of y indicate scrollback buffer\n\t *   -scrollback_lines <= y < dims.y\n\t *   0 <= x < dims.x\n\t *   {x, y} = {0, dims.y} is allowed.\n\t * clear_end\n\t *   if true, end is incremented by 1. this affects all constraints.\n\t */\n\tvoid clear(coord::term start, coord::term end, bool clear_end = false);\n\n\t/**\n\t * clears a range of the data buffer.\n\t * end is the first character that is not cleared.\n\t * end and start both must be valid pointers within data.\n\t */\n\tvoid chrdata_clear(buf_char *start, buf_char *end);\n\n\t/**\n\t * clears a range of the linedata buffer.\n\t * end is the first line that is not cleared.\n\t * end and start both must be valid pointers within data.\n\t */\n\tvoid linedata_clear(buf_line *start, buf_line *end);\n\n\t/**\n\t * returns a valid pointer to the character info for the character\n\t * designated by pos\n\t *\n\t * pos\n\t *   screen buffer coordinates\n\t *   negative values of y indicate scrollback buffer\n\t *   -scrollback_lines <= pos.y <= dims.y\n\t *   0 <= pos.x < dims.x\n\t *   if y == dims.y, the return ptr is not guaranteed to be valid.\n\t */\n\tbuf_char *chrdataptr(coord::term pos);\n\n\t/**\n\t * returns a valid pointer to the line info for the line designated\n\t * by lineno\n\t *\n\t * lineno\n\t *   number of line in screen buffer\n\t *   negative numbers indicate scrollback buffer\n\t *   -scrollback_lines <= lineno <= dims.y\n\t *   if lineno == dims.y, the return ptr is not guaranteed to be valid.\n\t */\n\tbuf_line *linedataptr(coord::term_t lineno);\n\n\t/**\n\t * Return the dimensions of the visible console area.\n\t */\n\tconst coord::term &get_dims() const;\n\npublic:\n\t// following this line are all terminal buffer related variables\n\n\t/**\n\t * the complete (2-dimensional) terminal output buffer content\n\t *\n\t * contains the screen buffer (where the cursor may move),\n\t * and the scrollback buffer (which is invisible while not scrolled back)\n\t *\n\t * thus, its size is always\n\t *   screen buffer size + scrollback buffer size\n\t *   (dims.x * dims.y + dims.x * scrollback_lines)\n\t *\n\t * on resize, this buffer is completely re-created.\n\t *\n\t * the first entry of data is not neccesarily the first character of the\n\t * screen or scrollback buffer; see screen_chrdata.\n\t */\n\tbuf_char *chrdata;\n\n\t/**\n\t * stores the (array) size of data, for performance and convenience\n\t * reasons.\n\t *\n\t * always has the value\n\t *   dims.x * (dims.y + scrollback_lines)\n\t */\n\tsize_t chrdata_size;\n\n\t/**\n\t * stores the end of the region allocated for data, for performance\n\t * and convenience reasons.\n\t * points to the first memory region outside the data buffer.\n\t *\n\t * always has the value\n\t *   data + chrdata_size\n\t */\n\tbuf_char *chrdata_end;\n\n\t/**\n\t * points to the first character that belongs to the screen buffer,\n\t * inside the data buffer.\n\t * this pointer is changed when the terminal buffer advances one line.\n\t *\n\t * note that screen_chrdata may NOT be directly indexed, as *screen_chrdata[k]\n\t * might be >= chrdata_end. in this case, the correct memory location will\n\t * be *screen_chrdata[k - chrdata_size].\n\t *\n\t * *screen_chrdata[0] always is the correct memory location of the screen\n\t * buffer top left corner.\n\t *\n\t * *screen_chrdata[dims.x * dims.y] is the memory location of the scrollback\n\t * buffer top left corner, but it might be neccesary to subtract\n\t * chrdata_size to get an actually valid memory location.\n\t */\n\tbuf_char *screen_chrdata;\n\n\t/**\n\t * similar to how data holds information about all characters,\n\t * linedata holds information about all lines.\n\t * currently, the only held information is whether the line has been started\n\t * by wrapping an existing line (and thus is not a 'real' new line)\n\t */\n\tbuf_line *linedata;\n\n\t/**\n\t * see chrdata_size\n\t */\n\tsize_t linedata_size;\n\n\t/**\n\t * see chrdata_end\n\t */\n\tbuf_line *linedata_end;\n\n\t/**\n\t * as an analog to screen_chrdata, this points to the entry in linedata that\n\t * holds metadata about the top line of the screen buffer.\n\t */\n\tbuf_line *screen_linedata;\n\n\t// following this line are all terminal size related variables\n\n\t/**\n\t * minimum screen buffer width\n\t */\n\tcoord::term_t min_width;\n\n\t/**\n\t * screen buffer dimensions (in characters)\n\t */\n\tcoord::term dims;\n\n\t/**\n\t * scrollback buffer height;\n\t * scrollback buffer width is identical to screen buffer width.\n\t */\n\tcoord::term_t scrollback_lines;\n\n\t// following this line are all cursor state related variables\n\n\t/**\n\t * cursor position\n\t */\n\tcoord::term cursorpos;\n\n\t/**\n\t * saved cursor position\n\t */\n\tcoord::term saved_cursorpos;\n\n\t/**\n\t * true if the cursor is visible\n\t */\n\tbool cursor_visible;\n\n\t/**\n\t * true if cursor is in the last column while whole line is filled\n\t * this is very special.\n\t * this allows CR after fully-filled lines to return to the same line,\n\t * instead of being in the next line.\n\t */\n\tbool cursor_special_lastcol;\n\n\t// following this line are misc variables\n\n\t/**\n\t * true if we are currently reading an escape sequence\n\t */\n\tbool escaped;\n\n\t/**\n\t * current escape sequence\n\t */\n\tstd::vector<int> escape_sequence;\n\n\t/**\n\t * true if an bell ('\\a') has been received, but not cleared\n\t */\n\tbool bell;\n\n\t/**\n\t * window title\n\t */\n\tstd::vector<int> title;\n\n\t/**\n\t * surrently selected formatting (colors, flags).\n\t * all printed chars, as well as all buffer clearing,\n\t * will use this until it is changed by an SGR escape sequence.\n\t *\n\t * the codepoint is the codepoint used for filling empty chars\n\t * (e.g. 0x20 ('space'))\n\t */\n\tbuf_char current_char_fmt;\n\n\t/**\n\t * the default buf char, which will be used for filling\n\t * new lines, or determining whether a line is empty.\n\t * should have space (0x20) as its char, and the desired\n\t * default values for fgcol and bgcol.\n\t */\n\tconst buf_char default_char_fmt;\n\n\t/**\n\t * how far it's currently possible to scroll back.\n\t * this value steadily increases when the buffer advances.\n\t *\n\t * if this is 0, all lines outside the screen buffer are empty.\n\t *\n\t * must be >= 0 and <= scrollback_lines\n\t */\n\tcoord::term_t scrollback_possible;\n\n\t/**\n\t * how many lines the buffer is currently scrolled back.\n\t * if   0, the screen buffer is wholly rendered.\n\t *         if the terminal is being advanced, this stays 0.\n\t *         this makes the terminal follow newly printed text.\n\t * if > 0, the bottom lines of the screen buffer are not rendered,\n\t *         and the bottom lines of the scrollback buffer are rendered\n\t *         instead.\n\t *         if screen buffer advances, this is increased accordingly\n\t *         (unless it would become >= scrollback_lines).\n\t *         this causes the currently scrolled-to position to remain\n\t *         scrolled-to even when new text is printed.\n\t *\n\t * must be >= 0 and <= scrollback_possible.\n\t */\n\tcoord::term_t scrollback_pos;\n};\n\n} // namespace console\n} // namespace openage\n"
  },
  {
    "path": "libopenage/console/console.cpp",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#include \"console.h\"\n\n#include \"../error/error.h\"\n#include \"../log/log.h\"\n#include \"../util/strings.h\"\n#include \"../util/unicode.h\"\n\n#include \"draw.h\"\n\nnamespace openage {\nnamespace console {\n\n/*\n * TODO:\n * multiple terminals on screen\n * resizable terminals\n * scrollbars\n * console input\n * log console, command console\n */\n\nConsole::Console(/* presenter::LegacyDisplay *display */) :\n\t// display{display},\n\tbottomleft{0, 0},\n\ttopright{1, 1},\n\tcharsize{1, 1},\n\tvisible(false),\n\tbuf{{80, 25}, 1337, 80},\n\tfont{{\"DejaVu Sans Mono\", \"Book\", 12}} {\n\ttermcolors.reserve(256);\n\n\t// this better be representative for the width of all other characters\n\tcharsize.x = ceilf(font.get_advance_width(\"W\"));\n\tcharsize.y = ceilf(font.get_line_height());\n\n\tlog::log(MSG(dbg) << \"Console font character size: \" << charsize.x << \"x\" << charsize.y);\n\n\t// Adjust the corners of the console based on the default buffer size and char size\n\ttopright.x = charsize.x * this->buf.get_dims().x;\n\ttopright.y = charsize.y * this->buf.get_dims().y;\n}\n\nConsole::~Console() {}\n\n/*\nvoid Console::load_colors(std::vector<gamedata::palette_color> &colortable) {\n\tfor (auto &c : colortable) {\n\t\tthis->termcolors.emplace_back(c);\n\t}\n\n\tif (termcolors.size() != 256) {\n\t\tthrow Error(MSG(err) << \"Exactly 256 terminal colors are required.\");\n\t}\n}\n*/\n\nvoid Console::register_to_engine() {\n\t// TODO: Use new renderer\n\t/*\n\tthis->display->register_input_action(this);\n\tthis->display->register_tick_action(this);\n\tthis->display->register_drawhud_action(this);\n\tthis->display->register_resize_action(this);\n\n\tBind the console toggle key globally\n\tauto &action = this->display->get_action_manager();\n\tauto &global = this->display->get_input_manager().get_global_context();\n\n\tglobal.bind(action.get(\"TOGGLE_CONSOLE\"), [this](const input::legacy::action_arg_t &) {\n\t\tthis->set_visible(!this->visible);\n\t});\n\n\n\tTODO: bind any needed input to InputContext\n\n\ttoggle console will take highest priority\n\tthis->input_context.bind(action.get(\"TOGGLE_CONSOLE\"), [this](const input::legacy::action_arg_t &) {\n\t\tthis->set_visible(false);\n\t});\n\tthis->input_context.bind(input::legacy::event_class::UTF8, [this](const input::legacy::action_arg_t &arg) {\n\t\t// a single char typed into the console\n\t\tstd::string utf8 = arg.e.as_utf8();\n\t\tthis->buf.write(utf8.c_str());\n\t\tcommand += utf8;\n\t\treturn true;\n\t});\n\tthis->input_context.bind(input::legacy::event_class::NONPRINT, [this](const input::legacy::action_arg_t &arg) {\n\t\tswitch (arg.e.as_char()) {\n\t\tcase 8: // remove a single UTF-8 character\n\t\t\tif (this->command.size() > 0) {\n\t\t\t\tutil::utf8_pop_back(this->command);\n\t\t\t\tthis->buf.pop_last_char();\n\t\t\t}\n\t\t\treturn true;\n\n\t\tcase 13: // interpret command\n\t\t\tthis->buf.write('\\n');\n\t\t\tthis->interpret(this->command);\n\t\t\tthis->command = \"\";\n\t\t\treturn true;\n\n\t\tdefault:\n\t\t\treturn false;\n\t\t}\n\t});\n\tthis->input_context.utf8_mode = true;\n    */\n}\n\nvoid Console::set_visible(bool /* make_visible */) {\n\t// TODO: Use new renderer\n\t/*\n\tif (make_visible) {\n\t\tthis->display->get_input_manager().push_context(&this->input_context);\n\t\tthis->visible = true;\n\t}\n\telse {\n\t\tthis->display->get_input_manager().remove_context(&this->input_context);\n\t\tthis->visible = false;\n\t}\n    */\n}\n\nvoid Console::write(const char *text) {\n\tthis->buf.write(text);\n\tthis->buf.write('\\n');\n}\n\nvoid Console::interpret(const std::string &command) {\n\tif (command == \"exit\") {\n\t\tthis->set_visible(false);\n\t}\n\telse if (command == \"list\") {\n\t\t// TODO: Use new renderer\n\n\t\t// for (auto &line : this->display->list_options()) {\n\t\t// \tthis->write(line.c_str());\n\t\t// }\n\t}\n\telse if (command.substr(0, 3) == \"set\") {\n\t\tstd::size_t first_space = command.find(\" \");\n\t\tstd::size_t second_space = command.find(\" \", first_space + 1);\n\t\tif (first_space != std::string::npos && second_space != std::string::npos) {\n\t\t\tstd::string name = command.substr(first_space + 1, second_space - first_space - 1);\n\t\t\tstd::string value = command.substr(second_space + 1, std::string::npos);\n\t\t\t// TODO: Use new engine class\n\t\t\t// this->engine->get_cvar_manager().set(name, value);\n\t\t\tlog::log(MSG(dbg) << \"Tried setting cvar \" << name << \" with \" << value\n\t\t\t                  << \" but engine is not implemented yet.\");\n\t\t}\n\t}\n\telse if (command.substr(0, 3) == \"get\") {\n\t\tstd::size_t first_space = command.find(\" \");\n\t\tif (first_space != std::string::npos) {\n\t\t\tstd::string name = command.substr(first_space + 1, std::string::npos);\n\t\t\t// TODO: Use new engine class\n\t\t\t// std::string value = this->engine->get_cvar_manager().get(name);\n\t\t\t// this->write(value.c_str());\n\t\t\tlog::log(MSG(dbg) << \"Tried getting cvar \" << name\n\t\t\t                  << \" but engine is not implemented yet.\");\n\t\t}\n\t}\n}\n/*\nbool Console::on_tick() {\n\tif (!this->visible) {\n\t\treturn true;\n\t}\n\n\t// TODO: handle stuff such as cursor blinking,\n\t// repeating held-down keys\n\treturn true;\n}\n\nbool Console::on_drawhud() {\n\tif (!this->visible) {\n\t\treturn true;\n\t}\n\n\t// TODO: Use new renderer\n\n\t// draw::to_opengl(this->display, this);\n\n\treturn true;\n}\n\nbool Console::on_input(SDL_Event *e) {\n\t// only handle inputs if the console is visible\n\tif (!this->visible) {\n\t\treturn true;\n\t}\n\n\tswitch (e->type) {\n\tcase SDL_KEYDOWN:\n\t\t//TODO handle key inputs\n\n\t\t//do not allow anyone else to handle this input\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\nbool Console::on_resize(coord::viewport_delta new_size) {\n\tcoord::pixel_t w = this->buf.get_dims().x * this->charsize.x;\n\tcoord::pixel_t h = this->buf.get_dims().y * this->charsize.y;\n\n\tthis->bottomleft = {(new_size.x - w) / 2, (new_size.y - h) / 2};\n\tthis->topright = {this->bottomleft.x + w, this->bottomleft.y - h};\n\n\treturn true;\n}\n*/\n} // namespace console\n} // namespace openage\n"
  },
  {
    "path": "libopenage/console/console.h",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"../coord/pixel.h\"\n#include \"../renderer/font/font.h\"\n#include \"../util/color.h\"\n#include \"buf.h\"\n\nnamespace openage {\n\n/**\n * In-game console subsystem. Featuring a full terminal emulator.\n *\n * TODO: Adapt to new engine subsystems.\n */\nnamespace console {\n\nclass Console {\npublic:\n\tConsole(/* presenter::LegacyDisplay *display */);\n\t~Console();\n\n\t/**\n\t * load the consoles color table\n\t */\n\t// void load_colors(std::vector<gamedata::palette_color> &colortable);\n\n\t/**\n\t * register this console to the renderer.\n\t * this leads to the drawing calls, and input handling.\n\t */\n\tvoid register_to_engine();\n\n\tvoid set_visible(bool make_visible);\n\n\t/**\n\t * prints the given text on the console.\n\t */\n\tvoid write(const char *text);\n\n\t/**\n\t * a temporary console command interpreter\n\t */\n\tvoid interpret(const std::string &command);\n\n\t// bool on_drawhud() override;\n\t// bool on_tick() override;\n\t// bool on_input(SDL_Event *event) override;\n\t// bool on_resize(coord::viewport_delta new_size) override;\n\nprotected:\n\t// TODO: Replace with new renderer\n\t// presenter::LegacyDisplay *display;\n\npublic:\n\tcoord::camhud bottomleft;\n\tcoord::camhud topright;\n\tcoord::camhud charsize;\n\n\tstd::vector<util::col> termcolors;\n\n\tbool visible;\n\n\tBuf buf;\n\trenderer::Font font;\n\n\t// input::legacy::InputContext input_context;\n\n\t// the command state\n\tstd::string command;\n};\n\n} // namespace console\n} // namespace openage\n"
  },
  {
    "path": "libopenage/console/draw.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include \"draw.h\"\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include \"../util/fds.h\"\n#include \"../util/timing.h\"\n#include <epoxy/gl.h>\n\n#include \"buf.h\"\n#include \"console.h\"\n\nnamespace openage {\nnamespace console {\nnamespace draw {\n\n/*\nvoid to_opengl(presenter::LegacyDisplay *engine, Console *console) {\n\tcoord::camhud topleft{\n\t\tconsole->bottomleft.x,\n\t\t// TODO This should probably just be console->topright.y\n\t\tconsole->bottomleft.y + console->charsize.y * console->buf.dims.y};\n\tcoord::pixel_t ascender = static_cast<coord::pixel_t>(console->font.get_ascender());\n\n\trenderer::TextRenderer *text_renderer = engine->get_text_renderer();\n\ttext_renderer->set_font(&console->font);\n\n\tint64_t monotime = timing::get_monotonic_time();\n\n\tbool fastblinking_visible = (monotime % 600000000 < 300000000);\n\tbool slowblinking_visible = (monotime % 300000000 < 150000000);\n\n\tfor (coord::term_t x = 0; x < console->buf.dims.x; x++) {\n\t\tcoord::camhud chartopleft{topleft.x + console->charsize.x * x, 0};\n\n\t\tfor (coord::term_t y = 0; y < console->buf.dims.y; y++) {\n\t\t\tchartopleft.y = topleft.y - console->charsize.y * y;\n\t\t\tbuf_char p = *(console->buf.chrdataptr({x, y - console->buf.scrollback_pos}));\n\n\t\t\tint fgcolid, bgcolid;\n\n\t\t\tbool cursor_visible_at_current_pos = (console->buf.cursorpos == coord::term{x, y - console->buf.scrollback_pos});\n\n\t\t\tcursor_visible_at_current_pos &= console->buf.cursor_visible;\n\n\t\t\tif (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) {\n\t\t\t\tbgcolid = p.fgcol;\n\t\t\t\tfgcolid = p.bgcol;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tbgcolid = p.bgcol;\n\t\t\t\tfgcolid = p.fgcol;\n\t\t\t}\n\n\t\t\tif ((p.flags & CHR_INVISIBLE)\n\t\t\t    or (p.flags & CHR_BLINKING and not slowblinking_visible)\n\t\t\t    or (p.flags & CHR_BLINKINGFAST and not fastblinking_visible)) {\n\t\t\t\tfgcolid = bgcolid;\n\t\t\t}\n\n\t\t\tconsole->termcolors[bgcolid].use(0.8);\n\n\t\t\tglBegin(GL_QUADS);\n\t\t\t{\n\t\t\t\tglVertex3f(chartopleft.x, chartopleft.y, 0);\n\t\t\t\tglVertex3f(chartopleft.x, chartopleft.y - console->charsize.y, 0);\n\t\t\t\tglVertex3f(chartopleft.x + console->charsize.x, chartopleft.y - console->charsize.y, 0);\n\t\t\t\tglVertex3f(chartopleft.x + console->charsize.x, chartopleft.y, 0);\n\t\t\t}\n\t\t\tglEnd();\n\n\t\t\tconsole->termcolors[fgcolid].use(1);\n\n\t\t\tchar utf8buf[5];\n\t\t\tif (util::utf8_encode(p.cp, utf8buf) == 0) {\n\t\t\t\t//unrepresentable character (question mark in black rhombus)\n\t\t\t\ttext_renderer->draw(chartopleft.x, chartopleft.y - ascender, \"\\uFFFD\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttext_renderer->draw(chartopleft.x, chartopleft.y - ascender, utf8buf);\n\t\t\t}\n\t\t}\n\t}\n}\n*/\n\nvoid to_terminal(Buf *buf, util::FD *fd, bool clear) {\n\t//move cursor, draw top left corner\n\tfd->puts(\"\\x1b[H\\u250c\");\n\tif (clear) {\n\t\tfd->puts(\"\\x1b[J\");\n\t}\n\t//draw top line, including title\n\tfor (coord::term_t x = 0; x < buf->dims.x; x++) {\n\t\tif (x >= 3 && (x - 3) < (int)buf->title.size()) {\n\t\t\tfd->putcp(buf->title[x - 3]);\n\t\t}\n\t\telse if (x == 2) {\n\t\t\tfd->putbyte('[');\n\t\t}\n\t\telse if (x - 3 == (int)buf->title.size()) {\n\t\t\tfd->putbyte(']');\n\t\t}\n\t\telse {\n\t\t\tfd->puts(\"\\u2500\");\n\t\t}\n\t}\n\t//draw top right corner\n\tfd->puts(\"\\u2510\\n\");\n\t//calculate pos/size of scrollbar\n\t//scrollbar_top is the first line that has the scrollbar displayed\n\t//scrollbar_bottom is the first line that doesn't have the scrollbar displayed\n\tcoord::term_t lines_total = buf->scrollback_possible + buf->dims.y;\n\tcoord::term_t pos_total = buf->scrollback_possible - buf->scrollback_pos;\n\tcoord::term_t scrollbar_top = (buf->dims.y * pos_total) / lines_total;\n\tcoord::term_t scrollbar_bottom = (buf->dims.y * (pos_total + buf->dims.y)) / lines_total;\n\tif (scrollbar_bottom == scrollbar_top) {\n\t\tif (scrollbar_bottom < buf->dims.y) {\n\t\t\tscrollbar_bottom++;\n\t\t}\n\t\telse {\n\t\t\tscrollbar_top--;\n\t\t}\n\t}\n\n\t//print lines -scrollback_pos to dims.y - scrollback_pos - 1\n\tfor (coord::term_t y = 0; y < buf->dims.y; y++) {\n\t\t//draw left line\n\t\tfd->puts(\"\\u2502\");\n\t\t//draw chars of this line\n\t\tfor (coord::term_t x = 0; x < buf->dims.x; x++) {\n\t\t\tbuf_char p = *(buf->chrdataptr({x, y - buf->scrollback_pos}));\n\t\t\tif (p.cp < 32) {\n\t\t\t\tp.cp = '?';\n\t\t\t}\n\t\t\tfd->printf(\"\\x1b[38;5;%dm\\x1b[48;5;%dm\", p.fgcol, p.bgcol);\n\t\t\tif (p.flags & CHR_BOLD) {\n\t\t\t\tfd->puts(\"\\x1b[1m\");\n\t\t\t}\n\t\t\tif (p.flags & CHR_BLINKING) {\n\t\t\t\tfd->puts(\"\\x1b[5m\");\n\t\t\t}\n\t\t\tbool cursor_visible_at_current_pos = buf->cursorpos == coord::term{x, y - buf->scrollback_pos};\n\t\t\tcursor_visible_at_current_pos &= buf->cursor_visible;\n\t\t\tif (((p.flags & CHR_NEGATIVE) != 0) xor cursor_visible_at_current_pos) {\n\t\t\t\t//print char negative\n\t\t\t\tfd->puts(\"\\x1b[7m\");\n\t\t\t}\n\t\t\tfd->putcp(p.cp);\n\t\t\tfd->puts(\"\\x1b[m\");\n\t\t}\n\t\t//draw right line\n\t\tif (y >= scrollbar_top and y < scrollbar_bottom) {\n\t\t\t//draw scrollbar on this part of right line\n\t\t\tfd->puts(\"\\u2503\");\n\t\t}\n\t\telse {\n\t\t\tfd->puts(\"\\u2502\");\n\t\t}\n\t\tfd->putbyte('\\n');\n\t}\n\t//draw bottom left corner\n\tfd->puts(\"\\u2514\");\n\t//draw bottom line\n\tfor (coord::term_t x = 0; x < buf->dims.x; x++) {\n\t\tfd->puts(\"\\u2500\");\n\t}\n\t//draw bottom right corner\n\tfd->puts(\"\\u2518\\n\");\n}\n\n} // namespace draw\n} // namespace console\n} // namespace openage\n"
  },
  {
    "path": "libopenage/console/draw.h",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage {\n\nnamespace util {\nclass FD;\n} // namespace util\n\nnamespace console {\n\nclass Buf;\nclass Console;\n\nnamespace draw {\n\n/**\n * experimental and totally inefficient opengl draw of a terminal buffer.\n */\n// void to_opengl(presenter::LegacyDisplay *engine, Console *console);\n\n/**\n * very early and inefficient printing of the console to a pty.\n */\nvoid to_terminal(Buf *buf, util::FD *fd, bool clear = false);\n\n} // namespace draw\n} // namespace console\n} // namespace openage\n"
  },
  {
    "path": "libopenage/console/tests.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include <string>\n#include <vector>\n\n#ifdef _MSC_VER\n\t#define STDOUT_FILENO 1\n#else\n\t#include <unistd.h>\n#endif\n#include \"../util/fds.h\"\n#include \"../util/pty.h\"\n#include <cerrno>\n#include <cstdio>\n#include <cstdlib>\n#include <fcntl.h>\n\n#include \"../error/error.h\"\n#include \"../log/log.h\"\n\n#include \"buf.h\"\n#include \"console.h\"\n#include \"draw.h\"\n\nnamespace openage::console::tests {\n\n\n// TODO: move to util\nint max(int a, int b) {\n\treturn (a > b) ? a : b;\n}\n\n\n// TODO: test for console coordinates and resizing\n\n\nvoid render() {\n\tconsole::Buf buf{{80, 25}, 1337, 80};\n\n\tbuf.write(\"Hello, brave new console world!\\n\\n\\n\\n\");\n\tbuf.write(\"stuff, lol.\\n\\n\");\n\tbuf.write(\"\\x1b[1mbold stuff, lol.\\x1b[m\\n\\n\");\n\tbuf.write(\"\\x1b[5;31;1mred bold blinking stuff, lol, ... also, this text seems to be quite long, maybe even wider than the terminal width. i wonder what could \\x1b[7mhappen.\\x1b[m\\n\\n\");\n\tfor (int i = 0; i < 18; i++) {\n\t\tbuf.write(\"rofl\\n\");\n\t}\n\tbuf.scroll(100);\n\tutil::FD outfd{STDOUT_FILENO};\n\tconsole::draw::to_terminal(&buf, &outfd, true);\n}\n\n\nvoid interactive() {\n#ifndef _WIN32\n\n\tconsole::Buf buf{{80, 25}, 1337, 80};\n\tstruct winsize ws;\n\n\tws.ws_col = buf.dims.x;\n\tws.ws_row = buf.dims.y;\n\tws.ws_xpixel = buf.dims.x * 8;\n\tws.ws_ypixel = buf.dims.y * 13;\n\n\tint amaster;\n\tswitch (forkpty(&amaster, nullptr, nullptr, &ws)) {\n\tcase -1:\n\t\tthrow Error(MSG(err) << \"fork() failed: \" << strerror(errno));\n\tcase 0: {\n\t\t// we are the child, spawn a shell\n\t\tconst char *shell = getenv(\"SHELL\");\n\t\tif (shell == nullptr) {\n\t\t\tshell = \"/bin/sh\";\n\t\t}\n\t\texecl(shell, shell, nullptr);\n\t\tthrow Error(MSG(err) << \"execl(\\\"\" << shell << \"\\\", \\\"\" << shell << \"\\\", nullptr) failed: \" << strerror(errno));\n\t}\n\tdefault:\n\t\t// we are the parent\n\t\tbreak;\n\t}\n\n\tutil::FD ptyout{amaster};            // for writing to the slave\n\tutil::FD ptyin{&ptyout, true};       // for reading the slave's output\n\tutil::FD termout{STDOUT_FILENO};     // for writing to the terminal\n\tutil::FD termin{STDIN_FILENO, true}; // for reading the user input\n\n\t// hide cursor\n\ttermout.puts(\"\\x1b[?25l\");\n\t// set canon input mode\n\ttermin.setinputmodecanon();\n\t// set amaster to auto-close\n\tptyout.close_on_destroy = true;\n\n\tconstexpr int rdbuf_size = 4096;\n\tchar rdbuf[rdbuf_size];\n\n\tint nfds = max(termin.fd, ptyin.fd) + 1;\n\n\tbool loop = true;\n\n\tconsole::draw::to_terminal(&buf, &termout, true);\n\n\twhile (loop) {\n\t\tfd_set rfds;\n\t\tstruct timeval tv;\n\n\t\t// Watch stdin (fd 0) to see when it has input.\n\t\tFD_ZERO(&rfds);\n\t\tFD_SET(ptyin.fd, &rfds);\n\t\tFD_SET(termin.fd, &rfds);\n\n\t\ttv.tv_sec = 0;\n\t\ttv.tv_usec = 1000000 / 60;\n\n\t\tswitch (select(nfds, &rfds, nullptr, nullptr, &tv)) {\n\t\tcase -1:\n\t\t\t// error\n\t\t\tbreak;\n\t\tcase 0:\n\t\t\t// timeout\n\t\t\tbreak;\n\t\tdefault:\n\t\t\t// success\n\t\t\tif (FD_ISSET(termin.fd, &rfds)) {\n\t\t\t\tssize_t retval = read(termin.fd, rdbuf, rdbuf_size);\n\t\t\t\tswitch (retval) {\n\t\t\t\tcase -1:\n\t\t\t\t\t// error... probably EWOULDBLOCK. ignore.\n\t\t\t\t\tbreak;\n\t\t\t\tcase 0:\n\t\t\t\t\t// EOF on stdin... huh... well... that was unexpected... TODO\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tif (ptyout.write(rdbuf, retval) != retval) {\n\t\t\t\t\t\t// for some reason, we couldn't write all input to\n\t\t\t\t\t\t// a master.\n\t\t\t\t\t\tloop = false;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (FD_ISSET(ptyin.fd, &rfds)) {\n\t\t\t\tssize_t retval = read(ptyin.fd, rdbuf, rdbuf_size);\n\t\t\t\tswitch (retval) {\n\t\t\t\tcase -1:\n\t\t\t\t\tswitch (errno) {\n\t\t\t\t\tcase EIO:\n\t\t\t\t\t\tloop = false;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// probably EWOULDBLOCK. ignore.\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tcase 0:\n\t\t\t\t\t// EOF on amaster\n\t\t\t\t\tloop = false;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tfor (int i = 0; i < retval; i++) {\n\t\t\t\t\t\tbuf.write(rdbuf[i]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\tif (tv.tv_usec == 0) {\n\t\t\tconsole::draw::to_terminal(&buf, &termout, false);\n\n\t\t\ttv.tv_usec = 1000000 / 60;\n\t\t}\n\t}\n\n\t// show cursor\n\ttermout.puts(\"\\x1b[?25h\");\n\n#endif /* _WIN32 */\n}\n\n\n} // namespace openage::console::tests\n"
  },
  {
    "path": "libopenage/coord/CMakeLists.txt",
    "content": "# Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\nadd_sources(libopenage\n\tchunk.cpp\n\tcoord_test.cpp\n\tdeclarations.cpp\n\tphys.cpp\n\tpixel.cpp\n    scene.cpp\n\tterm.cpp\n\ttile.cpp\n)\n\ntarget_include_directories(libopenage\n\tPRIVATE\n\t\t${CMAKE_CURRENT_BINARY_DIR}\n)\n"
  },
  {
    "path": "libopenage/coord/chunk.cpp",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#include \"chunk.h\"\n\n#include \"coord/tile.h\"\n\n\nnamespace openage {\nnamespace coord {\n\ntile_delta chunk_delta::to_tile(tile_t tiles_per_chunk) const {\n\treturn tile_delta{this->ne * tiles_per_chunk, this->se * tiles_per_chunk};\n}\n\ntile chunk::to_tile(tile_t tiles_per_chunk) const {\n\treturn tile{this->ne * tiles_per_chunk, this->se * tiles_per_chunk};\n}\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/chunk.h",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"coord_nese.gen.h\"\n#include \"coord_neseup.gen.h\"\n#include \"declarations.h\"\n\nnamespace openage {\nnamespace coord {\n\n\nstruct chunk_delta : CoordNeSeRelative<chunk_t, chunk, chunk_delta> {\n\tusing CoordNeSeRelative<chunk_t, chunk, chunk_delta>::CoordNeSeRelative;\n\n\ttile_delta to_tile(tile_t tiles_per_chunk) const;\n};\n\nstruct chunk : CoordNeSeAbsolute<chunk_t, chunk, chunk_delta> {\n\tusing CoordNeSeAbsolute<chunk_t, chunk, chunk_delta>::CoordNeSeAbsolute;\n\n\ttile to_tile(tile_t tiles_per_chunk) const;\n};\n\nstruct chunk3_delta : CoordNeSeUpRelative<chunk_t, chunk3, chunk3_delta> {\n\tusing CoordNeSeUpRelative<chunk_t, chunk3, chunk3_delta>::CoordNeSeUpRelative;\n};\n\nstruct chunk3 : CoordNeSeUpAbsolute<chunk_t, chunk3, chunk3_delta> {\n\tusing CoordNeSeUpAbsolute<chunk_t, chunk3, chunk3_delta>::CoordNeSeUpAbsolute;\n};\n\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/coord.cpp.template",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n${\"#\"}include \"coord_${formatted_members(\"{}\", join_with=\"\")}.gen.h\"\n\n#include <cstdio>\n\nnamespace openage {\nnamespace coord {\n\n// fully implemented as templated code in the header file.\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/coord.h.template",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <ostream>\n\nnamespace openage {\nnamespace coord {\n\n/**\n * Template class for all coordinate types that have the members (${formatted_members(\"{}\")}).\n *\n * There is a distinction between relative and absolute coordinate values\n * to disallow semantically-nonsensical origin-sensitive operations:\n *\n * absolute value + relative value -> absolute value\n * absolute value - absolute value -> relative value\n * relative value + relative value -> relative value\n * absolute value + absolute value -> undefined\n * relative value * scalar -> relative value\n * absolute value * scalar -> undefined\n *\n * This is the 'Absolute' version.\n *\n * 'Absolute' and 'Relative' are the absolute and relative types of the\n * derived class (CRTP).\n *\n * If you change this class, remember to update the gdb pretty printers\n * in etc/gdb_pretty/printers.py.\n */\ntemplate<typename CoordType, typename Absolute, typename Relative>\nstruct Coord${camelcase}Absolute {\n\n\tusing elem_t = CoordType;\n\n\t// The member variables to store the actual data.\n\t${formatted_members(\"CoordType {0};\", join_with=\"\\n\\t\")}\n\n\t/**\n\t * We don't want to have a default constructor,\n\t * because zero-initialization of absolute coordinates is\n\t * origin-sensitive and thus non-sensical.\n\t */\n\tCoord${camelcase}Absolute() = delete;\n\n\t/**\n\t * From-individual-values constructor\n\t */\n\tconstexpr Coord${camelcase}Absolute(${formatted_members(\"const CoordType &{}\")}) : ${formatted_members(\"{0}{{{0}}}\")} {}\n\n\t// copy constructor + assignment operator\n\tconstexpr Coord${camelcase}Absolute(const Coord${camelcase}Absolute &other) = default;\n\tconstexpr Coord${camelcase}Absolute &operator =(const Coord${camelcase}Absolute &other) = default;\n\n\tconstexpr Absolute operator +(const Relative &other) const {\n\t\treturn Absolute(${formatted_members(\"this->{0} + other.{0}\")});\n\t}\n\n\tconstexpr Relative operator -(const Absolute &other) const {\n\t\treturn Relative(${formatted_members(\"this->{0} - other.{0}\")});\n\t}\n\n\tconstexpr Absolute operator -(const Relative &other) const {\n\t\treturn Absolute(${formatted_members(\"this->{0} - other.{0}\")});\n\t}\n\n\tconstexpr Absolute &operator +=(const Relative &other) {\n\t\t*this = *this + other;\n\t\treturn static_cast<Absolute &>(*this);\n\t}\n\n\tconstexpr Absolute &operator -=(const Relative &other) {\n\t\t*this = *this - other;\n\t\treturn static_cast<Absolute &>(*this);\n\t}\n\n\tfriend constexpr bool operator ==(const Absolute &lhs, const Absolute &rhs) {\n\t\treturn ${formatted_members(\"(lhs.{0} == rhs.{0})\", join_with=\" && \")};\n\t}\n};\n\n\n/**\n * Template class for all coordinate types that have the members x and y.\n * This is the 'Relative' version.\n *\n * 'Absolute' and 'Relative' are the absolute and relative types of the\n * derived class (CRTP).\n *\n * If you change this class, remember to update the gdb pretty printers\n * in etc/gdb_pretty/printers.py.\n */\ntemplate<typename CoordType, typename Absolute, typename Relative>\nstruct Coord${camelcase}Relative {\n\tusing elem_t = CoordType;\n\n\t// The member variables to store the actual data.\n\t${formatted_members(\"CoordType {0};\", join_with=\"\\n\\t\")}\n\n\t/**\n\t * Default constructor: sets the values to their defaults.\n\t */\n\tCoord${camelcase}Relative() : ${formatted_members(\"{}{{}}\")} {}\n\n\t/**\n\t * From-individual-values constructor\n\t */\n\tconstexpr Coord${camelcase}Relative(${formatted_members(\"const CoordType &{}\")}) : ${formatted_members(\"{0}{{{0}}}\")} {}\n\n\t// copy constructor and assignment operator\n\tconstexpr Coord${camelcase}Relative(const Coord${camelcase}Relative &other) = default;\n\tconstexpr Coord${camelcase}Relative &operator =(const Coord${camelcase}Relative &other) = default;\n\n\tconstexpr Relative operator +() const {\n\t\treturn Relative(${formatted_members(\"+this->{}\")});\n\t}\n\n\tconstexpr Relative operator -() const {\n\t\treturn Relative(${formatted_members(\"-this->{}\")});\n\t}\n\n\tconstexpr Absolute operator +(const Absolute &other) const {\n\t\treturn Absolute(${formatted_members(\"this->{0} + other.{0}\")});\n\t}\n\n\tconstexpr Relative operator +(const Relative &other) const {\n\t\treturn Relative(${formatted_members(\"this->{0} + other.{0}\")});\n\t}\n\n\tconstexpr Relative operator -(const Relative &other) const {\n\t\treturn Relative(${formatted_members(\"this->{0} - other.{0}\")});\n\t}\n\n\ttemplate<typename ScalarType>\n\tconstexpr Relative operator *(const ScalarType &scalar) const {\n\t\treturn Relative(${formatted_members(\"this->{0} * scalar\")});\n\t}\n\n\ttemplate<typename ScalarType>\n\tconstexpr Relative operator /(const ScalarType &scalar) const {\n\t\treturn Relative(${formatted_members(\"this->{} / scalar\")});\n\t}\n\n\tconstexpr Relative &operator +=(const Relative &other) {\n\t\t*this += other;\n\t\treturn static_cast<Relative &>(*this);\n\t}\n\n\tconstexpr Relative &operator -=(const Relative &other) {\n\t\t*this -= other;\n\t\treturn static_cast<Relative &>(*this);\n\t}\n\n\ttemplate<typename ScalarType>\n\tconstexpr Relative &operator *=(const ScalarType &scalar) {\n\t\t*this *= scalar;\n\t\treturn static_cast<Relative &>(*this);\n\t}\n\n\ttemplate<typename ScalarType>\n\tconstexpr Relative &operator /=(const ScalarType &scalar) {\n\t\t*this /= scalar;\n\t\treturn static_cast<Relative &>(*this);\n\t}\n\n\tfriend constexpr bool operator ==(const Relative &lhs, const Relative &rhs) {\n\t\treturn ${formatted_members(\"(lhs.{0} == rhs.{0})\", join_with=\" && \")};\n\t}\n};\n\n\ntemplate<typename CoordType, typename Absolute, typename Relative>\nstd::ostream &operator <<(std::ostream &os, Coord${camelcase}Absolute<CoordType, Absolute, Relative> coord) {\n\tos << \"[${formatted_members('{0}: \" << coord.{0} << \"')}]\";\n\treturn os;\n}\n\n\ntemplate<typename CoordType, typename Absolute, typename Relative>\nstd::ostream &operator <<(std::ostream &os, Coord${camelcase}Relative<CoordType, Absolute, Relative> coord) {\n\tos << \"(${formatted_members('{0}: \" << coord.{0} << \"')})\";\n\treturn os;\n}\n\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/coord_test.cpp",
    "content": "// Copyright 2016-2016 the openage authors. See copying.md for legal info.\n\n#include \"coord_xy.gen.h\"\n\n#include <cstdint>\n\n#include \"../testing/testing.h\"\n#include \"../util/stringformatter.h\"\n\nnamespace openage {\nnamespace coord {\nnamespace tests {\n\nnamespace {\n\n// pack these test types into an anonynous namespace to prevent them from\n// polluting the rest of the project.\n\nstruct TestCoordsRelative;\nstruct TestCoordsAbsolute;\n\nstruct TestCoordsAbsolute : CoordXYAbsolute<int, TestCoordsAbsolute, TestCoordsRelative> {\n    using CoordXYAbsolute<int, TestCoordsAbsolute, TestCoordsRelative>::CoordXYAbsolute;\n};\n\nstruct TestCoordsRelative : CoordXYRelative<int, TestCoordsAbsolute, TestCoordsRelative> {\n    using CoordXYRelative<int, TestCoordsAbsolute, TestCoordsRelative>::CoordXYRelative;\n};\n\n} // anonymous namespace\n\n\n/**\n * test method for the base CoordXY* classes.\n */\nvoid coord() {\n    // test comparision\n    TESTEQUALS(TestCoordsAbsolute(3, 4) == TestCoordsAbsolute(3, 4), true);\n    TESTEQUALS(TestCoordsAbsolute(3, 4) != TestCoordsAbsolute(3, 4), false);\n    TESTEQUALS(TestCoordsAbsolute(3, 4) == TestCoordsAbsolute(3, 5), false);\n    TESTEQUALS(TestCoordsAbsolute(3, 4) == TestCoordsAbsolute(4, 4), false);\n    TESTEQUALS(TestCoordsAbsolute(3, 4) != TestCoordsAbsolute(3, 5), true);\n\n    TESTEQUALS(TestCoordsRelative(3, 4) == TestCoordsRelative(3, 4), true);\n    TESTEQUALS(TestCoordsRelative(3, 4) != TestCoordsRelative(3, 4), false);\n    TESTEQUALS(TestCoordsRelative(3, 4) == TestCoordsRelative(3, 5), false);\n    TESTEQUALS(TestCoordsRelative(3, 4) == TestCoordsRelative(4, 4), false);\n    TESTEQUALS(TestCoordsRelative(3, 4) != TestCoordsRelative(3, 5), true);\n\n    // test copy constructor / assignment operator\n    TestCoordsAbsolute a0(1, 2);\n    TestCoordsRelative r0(3, 4);\n\n    TESTEQUALS(a0, TestCoordsAbsolute(1, 2));\n    TESTEQUALS(r0, TestCoordsRelative(3, 4));\n\n    TestCoordsAbsolute a1{a0};\n    TestCoordsRelative r1{r0};\n\n    TESTEQUALS(a0, a1);\n    TESTEQUALS(r0, r1);\n\n    a1 = a0;\n    r1 = r0;\n\n    TESTEQUALS(a0, a1);\n    TESTEQUALS(r0, r1);\n\n    // test unary +/-\n    TESTEQUALS(-r0, TestCoordsRelative(-3, -4));\n    TESTEQUALS(+r0, TestCoordsRelative(3, 4));\n\n    // test addition\n    TESTEQUALS(r0 + r0, TestCoordsRelative(6, 8));\n    TESTEQUALS(a0 + r0, TestCoordsAbsolute(4, 6));\n    TESTEQUALS(r0 + a0, TestCoordsAbsolute(4, 6));\n\n    // test scalar multiplication and division\n    TESTEQUALS(r0 * 3, TestCoordsRelative(9, 12));\n    TESTEQUALS(r0 / 3, TestCoordsRelative(1, 1));\n\n    // test string representation\n    util::FString s;\n    s << TestCoordsAbsolute(1, 2);\n    TESTEQUALS(std::string(s), \"[x: 1, y: 2]\");\n\n    s.reset();\n    s << TestCoordsRelative(3, 4);\n    TESTEQUALS(std::string(s), \"(x: 3, y: 4)\");\n}\n\n}}} // openage::coord::tests\n"
  },
  {
    "path": "libopenage/coord/declarations.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"declarations.h\"\n\nnamespace openage::coord {\n\n// This file is intended to be empty.\n\n}\n"
  },
  {
    "path": "libopenage/coord/declarations.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/**\n * This file contains forward declarations for all the coordinate types,\n * as well as their scalar types.\n *\n * It was created to escape circular-include hell.\n */\n\n#include <cstdint>\n\n#include \"util/fixed_point.h\"\n\nnamespace openage {\nnamespace coord {\n\n// forward declarations of phys.h types\nconstexpr unsigned int phys_t_radix_pos = 16;\n\nusing phys_t = util::FixedPoint<int64_t, phys_t_radix_pos>;\n\nstruct phys2_delta;\nstruct phys2;\nstruct phys3_delta;\nstruct phys3;\n\nusing phys_angle_t = util::FixedPoint<int32_t, 16>;\n\n// forward declarations of scene.h types\nconstexpr unsigned int scene_t_radix_pos = 16;\n\nusing scene_t = util::FixedPoint<int64_t, scene_t_radix_pos>;\n\nstruct scene2_delta;\nstruct scene2;\nstruct scene3_delta;\nstruct scene3;\n\n// forward declarations of tile.h types\nusing tile_t = int64_t;\n\nconstexpr tile_t tiles_per_chunk = 16;\n\nstruct tile_delta;\nstruct tile;\nstruct tile3_delta;\nstruct tile3;\n\n// forward declarations of pixel.h types\nusing pixel_t = int32_t;\n\nstruct camgame_delta;\nstruct camgame;\nstruct viewport_delta;\nstruct viewport;\nstruct camhud_delta;\nstruct camhud;\nstruct input_delta;\nstruct input;\n\n// forward declarations of chunk.h types\nusing chunk_t = int32_t;\n\nstruct chunk_delta;\nstruct chunk;\nstruct chunk3_delta;\nstruct chunk3;\n\n// forward declarations of term.h types\nusing term_t = int;\n\nstruct term_delta;\nstruct term;\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/phys.cpp",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#include \"phys.h\"\n\n#include \"coord/pixel.h\"\n#include \"coord/scene.h\"\n#include \"coord/tile.h\"\n#include \"util/math.h\"\n#include \"util/math_constants.h\"\n\n\nnamespace openage::coord {\n\n\ndouble phys2_delta::length() const {\n\treturn std::hypot(this->ne.to_double(), this->se.to_double());\n}\n\n\nphys2_delta phys2_delta::normalize(double length) const {\n\treturn *this * (length / this->length());\n}\n\n\nphys3_delta phys2_delta::to_phys3() const {\n\treturn phys3_delta{this->ne, this->se, 0};\n}\n\n\nscene2_delta phys2_delta::to_scene2() const {\n\treturn scene2_delta(this->ne, this->se);\n}\n\nphys_angle_t phys2_delta::to_angle(const coord::phys2_delta &other) const {\n\t// TODO: Using floats here will result in inaccuracies.\n\t//       Better use a fixed point version of atan2.\n\tauto det = other.ne.to_float() * this->se.to_float() - this->ne.to_float() * other.se.to_float();\n\tauto dot = this->ne.to_float() * other.ne.to_float() + this->se.to_float() * other.se.to_float();\n\n\tauto angle = std::atan2(det, dot) * 180 / math::PI;\n\tif (angle < 0) {\n\t\tangle += 360;\n\t}\n\n\treturn phys_angle_t::from_float(angle);\n}\n\n\ndouble phys2::distance(phys2 other) const {\n\treturn (*this - other).length();\n}\n\n\ntile phys2::to_tile() const {\n\treturn tile{this->ne.to_int(), this->se.to_int()};\n}\n\nphys3 phys2::to_phys3(phys_t up) const {\n\treturn phys3(this->ne, this->se, up);\n}\n\nscene2 phys2::to_scene2() const {\n\treturn scene2(this->ne, this->se);\n}\n\ndouble phys3_delta::length() const {\n\treturn math::hypot3(this->ne.to_double(), this->se.to_double(), this->up.to_double());\n}\n\n\nphys3_delta phys3_delta::normalize(double length) const {\n\treturn *this * (length / this->length());\n}\n\n\nphys2_delta phys3_delta::to_phys2() const {\n\treturn phys2_delta{this->ne, this->se};\n}\n\n\nscene3_delta phys3_delta::to_scene3() const {\n\treturn scene3_delta{this->ne, this->se, this->up};\n}\n\nphys_angle_t phys3_delta::to_angle(const coord::phys2_delta &other) const {\n\t// TODO: Using floats here will result in inaccuracies.\n\t//       Better use a fixed point version of atan2.\n\tauto det = other.ne.to_float() * this->se.to_float() - this->ne.to_float() * other.se.to_float();\n\tauto dot = this->ne.to_float() * other.ne.to_float() + this->se.to_float() * other.se.to_float();\n\n\tauto angle = std::atan2(det, dot) * 180 / math::PI;\n\tif (angle < 0) {\n\t\tangle += 360;\n\t}\n\n\treturn phys_angle_t::from_float(angle);\n}\n\ntile3 phys3::to_tile3() const {\n\treturn tile3{this->ne.to_int(), this->se.to_int(), this->up.to_int()};\n}\n\n\ntile phys3::to_tile() const {\n\treturn this->to_tile3().to_tile();\n}\n\n\nphys2 phys3::to_phys2() const {\n\treturn phys2{this->ne, this->se};\n}\n\n\nscene3 phys3::to_scene3() const {\n\treturn scene3{this->ne, this->se, this->up};\n}\n\n} // namespace openage::coord\n"
  },
  {
    "path": "libopenage/coord/phys.h",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"coord/coord_nese.gen.h\"\n#include \"coord/coord_neseup.gen.h\"\n#include \"coord/declarations.h\"\n#include \"util/hash.h\"\n#include \"util/misc.h\"\n\nnamespace openage {\n\nclass Terrain;\n\nnamespace coord {\n\n\n/*\n * Gameworld-aligned coordinate systems.\n * See doc/code/coordinate-systems.md for more information.\n */\n\n\nstruct phys2_delta : CoordNeSeRelative<phys_t, phys2, phys2_delta> {\n\tusing CoordNeSeRelative<phys_t, phys2, phys2_delta>::CoordNeSeRelative;\n\n\tdouble length() const;\n\tphys2_delta normalize(double length = 1) const;\n\n\t// coordinate conversions\n\tphys3_delta to_phys3() const;\n\tscene2_delta to_scene2() const;\n\n\t// TODO: This DOES NOT use fixed point math currently\n\tphys_angle_t to_angle(const coord::phys2_delta &other = {-1, 1}) const;\n};\n\nstruct phys2 : CoordNeSeAbsolute<phys_t, phys2, phys2_delta> {\n\tusing CoordNeSeAbsolute<phys_t, phys2, phys2_delta>::CoordNeSeAbsolute;\n\n\tdouble distance(phys2 other) const;\n\n\t// coordinate conversions\n\ttile to_tile() const;\n\tphys3 to_phys3(phys_t up = 0) const;\n\tscene2 to_scene2() const;\n};\n\nstruct phys3_delta : CoordNeSeUpRelative<phys_t, phys3, phys3_delta> {\n\tusing CoordNeSeUpRelative<phys_t, phys3, phys3_delta>::CoordNeSeUpRelative;\n\n\t// there's no converter to convert phys3 deltas to tile3 deltas because\n\t// phys3_delta{0.5, 0, 0} might result in tile3_delta{0, 0, 0} or\n\t// tile3_delta{1, 0, 0} depending on the absolute position.\n\t// we don't allow ambiguous conversions.\n\n\tdouble length() const;\n\tphys3_delta normalize(double length = 1) const;\n\n\t// coordinate conversions\n\tphys2_delta to_phys2() const;\n\tscene3_delta to_scene3() const;\n\n\t// TODO: This DOES NOT use fixed point math currently\n\tphys_angle_t to_angle(const coord::phys2_delta &other = {-1, 1}) const;\n};\n\nstruct phys3 : CoordNeSeUpAbsolute<phys_t, phys3, phys3_delta> {\n\tusing CoordNeSeUpAbsolute<phys_t, phys3, phys3_delta>::CoordNeSeUpAbsolute;\n\n\t// coordinate conversions\n\ttile3 to_tile3() const;\n\ttile to_tile() const;\n\tphys2 to_phys2() const;\n\tscene3 to_scene3() const;\n};\n\n\n} // namespace coord\n} // namespace openage\n\nnamespace std {\n\ntemplate <>\nstruct hash<openage::coord::phys3> {\n\tsize_t operator()(const openage::coord::phys3 &pos) const {\n\t\tsize_t hash = openage::util::type_hash<openage::coord::phys3>();\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::phys_t>{}(pos.ne));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::phys_t>{}(pos.se));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::phys_t>{}(pos.up));\n\t\treturn hash;\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/coord/pixel.cpp",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#include \"pixel.h\"\n\n#include \"coord/phys.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/camera/definitions.h\"\n\n\nnamespace openage {\nnamespace coord {\n\nviewport_delta camhud_delta::to_viewport() const {\n\treturn viewport_delta{this->x, this->y};\n}\n\n\nviewport camhud::to_viewport() const {\n\t// reverse of viewport::to_camhud\n\treturn (*this - camhud{0, 0}).to_viewport() + viewport{0, 0};\n}\n\n\ncamhud viewport::to_camhud() const {\n\treturn camhud{0, 0} + (*this - viewport{0, 0}).to_camhud();\n}\n\n\nEigen::Vector2f viewport::to_ndc_space(const std::shared_ptr<renderer::camera::Camera> &camera) const {\n\treturn Eigen::Vector2f(\n\t\t2.0f * this->x / camera->get_viewport_size()[0] - 1,\n\t\t2.0f * this->y / camera->get_viewport_size()[1] - 1);\n}\n\n\nviewport_delta input_delta::to_viewport(const std::shared_ptr<renderer::camera::Camera> &camera) const {\n\tviewport_delta result;\n\n\tresult.x = this->x;\n\tresult.y = camera->get_viewport_size()[1] - this->y;\n\n\treturn result;\n}\n\n\nviewport input::to_viewport(const std::shared_ptr<renderer::camera::Camera> &camera) const {\n\treturn viewport{0, 0} + (*this - input{0, 0}).to_viewport(camera);\n}\n\n\nphys3 input::to_phys3(const std::shared_ptr<renderer::camera::Camera> &camera) const {\n\treturn this->to_scene3(camera).to_phys3();\n}\n\n\nscene3 input::to_scene3(const std::shared_ptr<renderer::camera::Camera> &camera) const {\n\t// Use raycasting to find the position\n\t// Direction and origin point are fetched from the camera\n\tauto cam_dir = renderer::camera::CAM_DIRECTION;\n\tauto ray_origin = camera->get_input_pos(*this);\n\n\t// xz plane that we want to intersect with\n\t// TODO: Account for different terrain heights (move code somewhere else?)\n\tauto plane_pos = Eigen::Vector3f(0.0f, 0.0f, 0.0f);\n\tauto plane_dir = Eigen::Vector3f(0.0f, 1.0f, 0.0f);\n\n\t// calculate intersection point\n\tEigen::Vector3f p_intersect = ray_origin + cam_dir * ((plane_pos - ray_origin).dot(plane_dir) / cam_dir.dot(plane_dir));\n\n\treturn scene3(-p_intersect.z(), p_intersect.x(), 0.0f);\n}\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/pixel.h",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord_xy.gen.h\"\n#include \"declarations.h\"\n\nnamespace openage {\n\nnamespace renderer::camera {\nclass Camera;\n}\n\nnamespace coord {\n\n\n/*\n * Pixel-aligned coordinate systems.\n * See doc/code/coordinate-systems.md for more information.\n */\n\nstruct camhud_delta : CoordXYRelative<pixel_t, camhud, camhud_delta> {\n\tusing CoordXYRelative<pixel_t, camhud, camhud_delta>::CoordXYRelative;\n\n\t// coordinate conversions\n\tviewport_delta to_viewport() const;\n};\n\n\nstruct camhud : CoordXYAbsolute<pixel_t, camhud, camhud_delta> {\n\tusing CoordXYAbsolute<pixel_t, camhud, camhud_delta>::CoordXYAbsolute;\n\n\t// coordinate conversions\n\tviewport to_viewport() const;\n};\n\n\nstruct viewport_delta : CoordXYRelative<pixel_t, viewport, viewport_delta> {\n\tusing CoordXYRelative<pixel_t, viewport, viewport_delta>::CoordXYRelative;\n\n\t// coordinate conversions\n\tcamhud_delta to_camhud() const {\n\t\treturn camhud_delta{this->x, this->y};\n\t}\n};\n\n\nstruct viewport : CoordXYAbsolute<pixel_t, viewport, viewport_delta> {\n\tusing CoordXYAbsolute<pixel_t, viewport, viewport_delta>::CoordXYAbsolute;\n\n\t// coordinate conversions\n\tcamhud to_camhud() const;\n\n\t// renderer conversions\n\tEigen::Vector2f to_ndc_space(const std::shared_ptr<renderer::camera::Camera> &camera) const;\n};\n\n\nstruct input_delta : CoordXYRelative<pixel_t, input, input_delta> {\n\tusing CoordXYRelative<pixel_t, input, input_delta>::CoordXYRelative;\n\n\t// coordinate conversions\n\tviewport_delta to_viewport(const std::shared_ptr<renderer::camera::Camera> &camera) const;\n};\n\n\nstruct input : CoordXYAbsolute<pixel_t, input, input_delta> {\n\tusing CoordXYAbsolute<pixel_t, input, input_delta>::CoordXYAbsolute;\n\n\t// coordinate conversions\n\tviewport to_viewport(const std::shared_ptr<renderer::camera::Camera> &camera) const;\n\tphys3 to_phys3(const std::shared_ptr<renderer::camera::Camera> &camera) const;\n\tscene3 to_scene3(const std::shared_ptr<renderer::camera::Camera> &camera) const;\n};\n\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/scene.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"scene.h\"\n\n#include <numbers>\n\n#include \"coord/pixel.h\"\n#include \"coord/tile.h\"\n#include \"util/math.h\"\n#include \"util/math_constants.h\"\n\n\nnamespace openage::coord {\n\n/**\n * Ratio for converting the scene coordinate UP direction to OpenGL/Vulkan\n * coordinates, i.e. translating ingame \"height\" to displayed height.\n * The ratio is 1 / sqrt(8), which is an approximation of the ratio used in AoE2.\n *\n * @return Multiplier for the UP direction.\n */\nconstexpr float up_ratio() {\n\t// approx. 1.0 / sqrt(8)\n\treturn 0.353553391f;\n}\n\n\ndouble scene2_delta::length() const {\n\treturn std::hypot(this->ne.to_double(), this->se.to_double());\n}\n\n\nscene2_delta scene2_delta::normalize(double length) const {\n\treturn *this * (length / this->length());\n}\n\n\nscene3_delta scene2_delta::to_scene3() const {\n\treturn scene3_delta{this->ne, this->se, 0};\n}\n\n\nphys2_delta scene2_delta::to_phys2() const {\n\treturn phys2_delta(this->ne, this->se);\n}\n\n\nEigen::Vector3f scene2_delta::to_world_space() const {\n\treturn Eigen::Vector3f(this->se.to_float(), 0.0f, -this->ne.to_float());\n}\n\nfloat scene2_delta::to_angle(const coord::scene2_delta &other) const {\n\tauto det = other.ne.to_float() * this->se.to_float() - this->ne.to_float() * other.se.to_float();\n\tauto dot = this->ne.to_float() * other.ne.to_float() + this->se.to_float() * other.se.to_float();\n\n\tauto angle = std::atan2(det, dot) * 180 / math::PI;\n\tif (angle < 0) {\n\t\tangle += 360;\n\t}\n\n\treturn angle;\n}\n\n\ndouble scene2::distance(scene2 other) const {\n\treturn (*this - other).length();\n}\n\n\nscene3 scene2::to_scene3(phys_t altitude) const {\n\treturn scene3{this->ne, this->se, altitude};\n}\n\n\nphys2 scene2::to_phys2() const {\n\treturn phys2(this->ne, this->se);\n}\n\n\nEigen::Vector3f scene2::to_world_space() const {\n\treturn Eigen::Vector3f(this->se.to_float(), 0.0f, -this->ne.to_float());\n}\n\n\ndouble scene3_delta::length() const {\n\treturn math::hypot3(this->ne.to_double(), this->se.to_double(), this->up.to_double());\n}\n\n\nscene3_delta scene3_delta::normalize(double length) const {\n\treturn *this * (length / this->length());\n}\n\n\nscene2_delta scene3_delta::to_scene2() const {\n\treturn scene2_delta(this->ne, this->se);\n}\n\n\nphys3_delta scene3_delta::to_phys3() const {\n\treturn phys3_delta(this->ne, this->se, this->up);\n}\n\n\nEigen::Vector3f scene3_delta::to_world_space() const {\n\treturn Eigen::Vector3f(this->se.to_float(), this->up.to_float() * up_ratio(), -this->ne.to_float());\n}\n\nfloat scene3_delta::to_angle(const coord::scene2_delta &other) const {\n\treturn this->to_scene2().to_angle(other);\n}\n\nscene2 scene3::to_scene2() const {\n\treturn scene2{this->ne, this->se};\n}\n\n\nphys3 scene3::to_phys3() const {\n\treturn phys3(this->ne, this->se, this->up);\n}\n\n\nEigen::Vector3f scene3::to_world_space() const {\n\treturn Eigen::Vector3f(this->se.to_float(), this->up.to_float() * up_ratio(), -this->ne.to_float());\n}\n\n} // namespace openage::coord\n"
  },
  {
    "path": "libopenage/coord/scene.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <eigen3/Eigen/Dense>\n\n#include \"util/hash.h\"\n#include \"util/misc.h\"\n\n#include \"coord/coord_nese.gen.h\"\n#include \"coord/coord_neseup.gen.h\"\n#include \"coord/declarations.h\"\n#include \"coord/phys.h\"\n\nnamespace openage {\n\nnamespace renderer::camera {\nclass Camera;\n}\n\nnamespace coord {\n\n\n/*\n * Gameworld-aligned coordinate systems.\n * See doc/code/coordinate-systems.md for more information.\n */\n\n\nstruct scene2_delta : CoordNeSeRelative<scene_t, scene2, scene2_delta> {\n\tusing CoordNeSeRelative<scene_t, scene2, scene2_delta>::CoordNeSeRelative;\n\n\tdouble length() const;\n\tscene2_delta normalize(double length = 1) const;\n\n\t// coordinate conversions\n\tscene3_delta to_scene3() const;\n\tphys2_delta to_phys2() const;\n\n\t// renderer conversions\n\tEigen::Vector3f to_world_space() const;\n\tfloat to_angle(const coord::scene2_delta &other = {-1, 1}) const; // TODO: other = cam direction?\n};\n\nstruct scene2 : CoordNeSeAbsolute<scene_t, scene2, scene2_delta> {\n\tusing CoordNeSeAbsolute<scene_t, scene2, scene2_delta>::CoordNeSeAbsolute;\n\n\tdouble distance(scene2 other) const;\n\n\t// coordinate conversions\n\tscene3 to_scene3(phys_t altitude = 0) const;\n\tphys2 to_phys2() const;\n\n\t// renderer conversions\n\tEigen::Vector3f to_world_space() const;\n};\n\nstruct scene3_delta : CoordNeSeUpRelative<scene_t, scene3, scene3_delta> {\n\tusing CoordNeSeUpRelative<scene_t, scene3, scene3_delta>::CoordNeSeUpRelative;\n\n\tdouble length() const;\n\tscene3_delta normalize(double length = 1) const;\n\n\t// coordinate conversions\n\tscene2_delta to_scene2() const;\n\tphys3_delta to_phys3() const;\n\n\t// renderer conversions\n\tEigen::Vector3f to_world_space() const;\n\tfloat to_angle(const coord::scene2_delta &other = {-1, 1}) const; // TODO: other = cam direction?\n};\n\nstruct scene3 : CoordNeSeUpAbsolute<scene_t, scene3, scene3_delta> {\n\tusing CoordNeSeUpAbsolute<scene_t, scene3, scene3_delta>::CoordNeSeUpAbsolute;\n\n\t// coordinate conversions\n\tscene2 to_scene2() const;\n\tphys3 to_phys3() const;\n\n\t// renderer conversions\n\tEigen::Vector3f to_world_space() const;\n};\n\n\n} // namespace coord\n} // namespace openage\n\nnamespace std {\n\ntemplate <>\nstruct hash<openage::coord::scene3> {\n\tsize_t operator()(const openage::coord::scene3 &pos) const {\n\t\tsize_t hash = openage::util::type_hash<openage::coord::scene3>();\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::scene_t>{}(pos.ne));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::scene_t>{}(pos.se));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::scene_t>{}(pos.up));\n\t\treturn hash;\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/coord/term.cpp",
    "content": "// Copyright 2016-2018 the openage authors. See copying.md for legal info.\n\n#include \"term.h\"\n\nnamespace openage {\nnamespace coord {\n\n}} // namespace openage::coord\n"
  },
  {
    "path": "libopenage/coord/term.h",
    "content": "// Copyright 2016-2018 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"coord_xy.gen.h\"\n#include \"declarations.h\"\n\nnamespace openage {\nnamespace coord {\n\n\n/*\n * Terminal-related coordinate systems.\n * See doc/code/coordinate-systems.md for more information.\n */\n\n\nstruct term_delta : CoordXYRelative<term_t, term, term_delta> {\n\tusing CoordXYRelative<term_t, term, term_delta>::CoordXYRelative;\n};\n\nstruct term : CoordXYAbsolute<term_t, term, term_delta> {\n\tusing CoordXYAbsolute<term_t, term, term_delta>::CoordXYAbsolute;\n};\n\n\n} // namespace coord\n} // namespace openage\n"
  },
  {
    "path": "libopenage/coord/tile.cpp",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#include \"tile.h\"\n\n#include \"chunk.h\"\n#include \"phys.h\"\n\n\nnamespace openage::coord {\n\nphys2_delta tile_delta::to_phys2() const {\n\treturn phys2_delta{phys2::elem_t::from_int(this->ne),\n\t                   phys2::elem_t::from_int(this->se)};\n}\n\nphys3_delta tile_delta::to_phys3(tile_t up) const {\n\treturn phys3_delta{phys3::elem_t::from_int(this->ne),\n\t                   phys3::elem_t::from_int(this->se),\n\t                   phys3::elem_t::from_int(up)};\n}\n\ntile3 tile::to_tile3(tile_t up) const {\n\treturn tile3{this->ne, this->se, up};\n}\n\nphys2 tile::to_phys2() const {\n\treturn phys2{phys3::elem_t::from_int(this->ne),\n\t             phys3::elem_t::from_int(this->se)};\n}\n\nphys3 tile::to_phys3(tile_t up) const {\n\treturn this->to_tile3(up).to_phys3();\n}\n\nphys2 tile::to_phys2_center() const {\n\treturn phys2{phys3::elem_t::from_int(this->ne) + 0.5,\n\t             phys3::elem_t::from_int(this->se) + 0.5};\n}\n\nphys3 tile::to_phys3_center(tile_t up) const {\n\treturn phys3{phys3::elem_t::from_int(this->ne) + 0.5,\n\t             phys3::elem_t::from_int(this->se) + 0.5,\n\t             phys3::elem_t::from_int(up)};\n}\n\nchunk tile::to_chunk() const {\n\treturn chunk{\n\t\tstatic_cast<chunk::elem_t>(util::div(this->ne, tiles_per_chunk)),\n\t\tstatic_cast<chunk::elem_t>(util::div(this->se, tiles_per_chunk))};\n}\n\ntile_delta tile::get_pos_on_chunk() const {\n\treturn tile_delta{\n\t\tutil::mod(this->ne, tiles_per_chunk),\n\t\tutil::mod(this->se, tiles_per_chunk)};\n}\n\ntile_delta tile3_delta::to_tile() const {\n\treturn tile_delta{this->ne, this->se};\n}\n\nphys3_delta tile3_delta::to_phys3() const {\n\treturn phys3_delta{phys3::elem_t::from_int(this->ne),\n\t                   phys3::elem_t::from_int(this->se),\n\t                   phys3::elem_t::from_int(up)};\n}\n\ntile tile3::to_tile() const {\n\treturn tile{this->ne, this->se};\n}\n\nphys2 tile3::to_phys2() const {\n\treturn this->to_tile().to_phys2();\n}\n\nphys3 tile3::to_phys3() const {\n\treturn phys3{phys3::elem_t::from_int(this->ne),\n\t             phys3::elem_t::from_int(this->se),\n\t             phys3::elem_t::from_int(this->up)};\n}\n\nphys2 tile3::to_phys2_center() const {\n\treturn phys2{phys3::elem_t::from_int(this->ne) + 0.5,\n\t             phys3::elem_t::from_int(this->se) + 0.5};\n}\n\nphys3 tile3::to_phys3_center() const {\n\treturn phys3{phys3::elem_t::from_int(this->ne) + 0.5,\n\t             phys3::elem_t::from_int(this->se) + 0.5,\n\t             phys3::elem_t::from_int(this->up)};\n}\n\n} // namespace openage::coord\n"
  },
  {
    "path": "libopenage/coord/tile.h",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../util/hash.h\"\n#include \"../util/misc.h\"\n#include \"coord_nese.gen.h\"\n#include \"coord_neseup.gen.h\"\n#include \"declarations.h\"\n\nnamespace openage {\nnamespace coord {\n\n/*\n * Gameworld tile-related coordinate systems.\n * See doc/code/coordinate-systems.md for more information.\n */\n\n\nstruct tile_delta : CoordNeSeRelative<tile_t, tile, tile_delta> {\n\tusing CoordNeSeRelative<tile_t, tile, tile_delta>::CoordNeSeRelative;\n\n\t// coordinate conversions\n\tphys2_delta to_phys2() const;\n\tphys3_delta to_phys3(tile_t up = 0) const;\n};\n\nstruct tile : CoordNeSeAbsolute<tile_t, tile, tile_delta> {\n\tusing CoordNeSeAbsolute<tile_t, tile, tile_delta>::CoordNeSeAbsolute;\n\n\t// coordinate conversions\n\t/**\n\t * adds an UP component to the coordinate.\n\t * the resulting UP component will be 'altitude' tiles above ground\n\t * elevation.\n\t */\n\ttile3 to_tile3(tile_t up = 0) const;\n\n\tphys2 to_phys2() const;\n\tphys3 to_phys3(tile_t up = 0) const;\n\tphys2 to_phys2_center() const;\n\tphys3 to_phys3_center(tile_t up = 0) const;\n\n\tchunk to_chunk() const;\n\ttile_delta get_pos_on_chunk() const;\n};\n\nstruct tile3_delta : CoordNeSeUpRelative<tile_t, tile3, tile3_delta> {\n\tusing CoordNeSeUpRelative<tile_t, tile3, tile3_delta>::CoordNeSeUpRelative;\n\n\t// coordinate conversions\n\t// simply discards the UP component of the coordinate delta.\n\ttile_delta to_tile() const;\n\tphys3_delta to_phys3() const;\n};\n\nstruct tile3 : CoordNeSeUpAbsolute<tile_t, tile3, tile3_delta> {\n\tusing CoordNeSeUpAbsolute<tile_t, tile3, tile3_delta>::CoordNeSeUpAbsolute;\n\n\t// coordinate conversions\n\t// simply discards the UP component of the coordinate.\n\ttile to_tile() const;\n\n\tphys2 to_phys2() const;\n\tphys3 to_phys3() const;\n\tphys2 to_phys2_center() const;\n\tphys3 to_phys3_center() const;\n};\n\n\n} // namespace coord\n} // namespace openage\n\nnamespace std {\n\ntemplate <>\nstruct hash<openage::coord::tile> {\n\tsize_t operator()(const openage::coord::tile &pos) const {\n\t\tsize_t hash = openage::util::type_hash<openage::coord::tile>();\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::tile_t>{}(pos.ne));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::tile_t>{}(pos.se));\n\t\treturn hash;\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/curve/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tbase_curve.cpp\n\tcontinuous.cpp\n\tdiscrete.cpp\n\tdiscrete_mod.cpp\n\tinterpolated.cpp\n\tkeyframe.cpp\n\tkeyframe_container.cpp\n\tsegmented.cpp\n)\n\nadd_subdirectory(\"container\")\nadd_subdirectory(\"tests\")\n"
  },
  {
    "path": "libopenage/curve/base_curve.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"base_curve.h\"\n\nnamespace openage::curve {\n\n} // openage::curve\n"
  },
  {
    "path": "libopenage/curve/base_curve.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <functional>\n#include <memory>\n#include <sstream>\n#include <string>\n#include <utility>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"curve/keyframe_container.h\"\n#include \"event/evententity.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage {\nnamespace event {\nclass EventLoop;\n}\n\nnamespace curve {\n\ntemplate <typename T>\nclass BaseCurve : public event::EventEntity {\npublic:\n\tBaseCurve(const std::shared_ptr<event::EventLoop> &loop,\n\t          size_t id,\n\t          const std::string &idstr = \"\",\n\t          const EventEntity::single_change_notifier &notifier = nullptr,\n\t          const T &defaultval = T()) :\n\t\tEventEntity(loop, notifier),\n\t\tcontainer{defaultval},\n\t\t_id{id},\n\t\t_idstr{idstr},\n\t\tloop{loop},\n\t\tlast_element{this->container.size()} {}\n\n\tvirtual ~BaseCurve() = default;\n\n\t// prevent copying because it invalidates the usage of unique ids and event\n\t// registration. If you need to copy a curve, use the sync() method.\n\t// TODO: if copying is enabled again, these members have to be reassigned: _id, _idstr, last_element\n\tBaseCurve(const BaseCurve &) = delete;\n\tBaseCurve &operator=(const BaseCurve &) = delete;\n\n\tBaseCurve(BaseCurve &&) = default;\n\n\tvirtual T get(const time::time_t &t) const = 0;\n\n\tvirtual T operator()(const time::time_t &now) {\n\t\treturn get(now);\n\t}\n\n\t/**\n\t * Get the closest keyframe with t <= \\p time.\n\t *\n\t * @return Keyframe time and value.\n\t */\n\tvirtual std::pair<time::time_t, const T> frame(const time::time_t &time) const;\n\n\t/**\n\t * Get the closest keyframe with t > \\p time.\n\t *\n\t * @return Keyframe time and value.\n\t */\n\tvirtual std::pair<time::time_t, const T> next_frame(const time::time_t &time) const;\n\n\t/**\n\t * Insert/overwrite given value at given time and erase all elements\n\t * that follow at a later time.\n\t * If multiple elements exist at the given time,\n\t * overwrite the last one.\n\t */\n\tvirtual void set_last(const time::time_t &at, const T &value);\n\n\t/**\n\t * Insert a value at the given time.\n\t * If there already is a value at this time,\n\t * the value is inserted directly after the existing one.\n\t */\n\tvirtual void set_insert(const time::time_t &at, const T &value);\n\n\t/**\n\t * Insert a value at the given time.\n\t * If there already is a value at this time,\n\t * the given value will replace the first value with the same time.\n\t */\n\tvirtual void set_replace(const time::time_t &at, const T &value);\n\n\t/**\n\t * Remove all values that have the given time.\n\t */\n\tvirtual void erase(const time::time_t &at);\n\n\t/**\n\t * Integrity check, for debugging/testing reasons only.\n\t */\n\tvoid check_integrity() const;\n\n\t/**\n\t * Copy keyframes from another curve to this curve. After syncing, the two curves\n\t * are guaranteed to return the same values for t >= start.\n\t *\n\t * The operation may insert new keyframes at \\p start on the curve.\n\t *\n\t * @param other Curve that keyframes are copied from.\n\t * @param start Start time at which keyframes are replaced (default = -INF).\n\t *              Using the default value replaces ALL keyframes of \\p this with\n\t *              the keyframes of \\p other.\n\t */\n\tvoid sync(const BaseCurve<T> &other,\n\t          const time::time_t &start = time::TIME_MIN);\n\n\t/**\n\t * Copy keyframes from another curve (with a different element type) to this curve.\n\t * After syncing, the two curves are guaranteed to return the same values\n\t * for t >= start.\n\t *\n\t * The operation may insert new keyframes at \\p start on the curve.\n\t *\n\t * @param other Curve that keyframes are copied from.\n\t * @param converter Function that converts the value type of \\p other to the\n\t *                  value type of \\p this.\n\t * @param start Start time at which keyframes are replaced (default = -INF).\n\t *              Using the default value replaces ALL keyframes of \\p this with\n\t *              the keyframes of \\p other.\n\t */\n\ttemplate <typename O>\n\tvoid sync(const BaseCurve<O> &other,\n\t          const std::function<T(const O &)> &converter,\n\t          const time::time_t &start = time::TIME_MIN);\n\n\t/**\n\t * Get the identifier of this curve.\n\t *\n\t * @return Identifier.\n\t */\n\tsize_t id() const override {\n\t\treturn this->_id;\n\t}\n\n\t/**\n\t * Get the human-readable identifier of this curve.\n\t *\n\t * @return Human-readable identifier.\n\t */\n\tstd::string idstr() const override {\n\t\tif (this->_idstr.size() == 0) {\n\t\t\treturn std::to_string(this->id());\n\t\t}\n\t\treturn this->_idstr;\n\t}\n\n\t/**\n\t * Get a string representation of the curve.\n\t */\n\tstd::string str() const;\n\n\t/**\n\t * Get the container containing all keyframes of this curve.\n\t *\n\t * @return Keyframe container.\n\t */\n\tconst KeyframeContainer<T> &get_container() const {\n\t\treturn this->container;\n\t}\n\nprotected:\n\t/**\n\t * Stores all the keyframes\n\t */\n\tKeyframeContainer<T> container;\n\n\t/**\n\t * Identifier for the container\n\t */\n\tconst size_t _id;\n\n\t/**\n\t * Human-readable identifier for the container\n\t */\n\tconst std::string _idstr;\n\n\t/**\n\t * The eventloop this curve was registered to\n\t */\n\tconst std::shared_ptr<event::EventLoop> loop;\n\n\t/**\n\t * Cache the index of the last accessed element (usually the end).\n\t */\n\tmutable typename KeyframeContainer<T>::elem_ptr last_element;\n};\n\n\ntemplate <typename T>\nvoid BaseCurve<T>::set_last(const time::time_t &at, const T &value) {\n\tauto hint = this->container.last(at, this->last_element);\n\n\t// erase max one same-time value\n\tif (this->container.get(hint).time() == at) {\n\t\thint--;\n\t}\n\n\thint = this->container.erase_after(hint);\n\n\tthis->container.insert_before(at, value, hint);\n\tthis->last_element = hint;\n\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nvoid BaseCurve<T>::set_insert(const time::time_t &at, const T &value) {\n\tauto hint = this->container.insert_after(at, value, this->last_element);\n\t// check if this is now the final keyframe\n\tif (this->container.get(hint).time() > this->container.get(this->last_element).time()) {\n\t\tthis->last_element = hint;\n\t}\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nvoid BaseCurve<T>::set_replace(const time::time_t &at, const T &value) {\n\tthis->container.insert_overwrite(at, value, this->last_element);\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nvoid BaseCurve<T>::erase(const time::time_t &at) {\n\tthis->last_element = this->container.erase(at, this->last_element);\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nstd::pair<time::time_t, const T> BaseCurve<T>::frame(const time::time_t &time) const {\n\tauto e = this->container.last(time, this->container.size());\n\tauto elem = this->container.get(e);\n\treturn elem.as_pair();\n}\n\n\ntemplate <typename T>\nstd::pair<time::time_t, const T> BaseCurve<T>::next_frame(const time::time_t &time) const {\n\tauto e = this->container.last(time, this->container.size());\n\te++;\n\tauto elem = this->container.get(e);\n\treturn elem.as_pair();\n}\n\ntemplate <typename T>\nstd::string BaseCurve<T>::str() const {\n\tstd::stringstream ss;\n\tss << \"Curve[\" << this->idstr() << \"]{\" << std::endl;\n\tfor (const auto &keyframe : this->container) {\n\t\tss << \"    \" << keyframe.time() << \": \" << keyframe.val() << \",\" << std::endl;\n\t}\n\tss << \"}\";\n\n\treturn ss.str();\n}\n\ntemplate <typename T>\nvoid BaseCurve<T>::check_integrity() const {\n\ttime::time_t last_time = time::TIME_MIN;\n\tfor (const auto &keyframe : this->container) {\n\t\tif (keyframe.time() < last_time) {\n\t\t\tthrow Error{MSG(err) << \"curve is broken after t=\" << last_time << \": \" << this->str()};\n\t\t}\n\t\tlast_time = keyframe.time();\n\t}\n}\n\ntemplate <typename T>\nvoid BaseCurve<T>::sync(const BaseCurve<T> &other,\n                        const time::time_t &start) {\n\t// Copy keyframes between containers for t >= start\n\tthis->last_element = this->container.sync(other.container, start);\n\n\t// Check if this->get() returns the same value as other->get() for t = start\n\t// If not, insert a new keyframe at start\n\tauto get_other = other.get(start);\n\tif (this->get(start) != get_other) {\n\t\tthis->set_insert(start, get_other);\n\t}\n\n\tthis->changes(start);\n}\n\n\ntemplate <typename T>\ntemplate <typename O>\nvoid BaseCurve<T>::sync(const BaseCurve<O> &other,\n                        const std::function<T(const O &)> &converter,\n                        const time::time_t &start) {\n\t// Copy keyframes between containers for t >= start\n\tthis->last_element = this->container.sync(other.get_container(), converter, start);\n\n\t// Check if this->get() returns the same value as other->get() for t = start\n\t// If not, insert a new keyframe at start\n\tauto get_other = converter(other.get(start));\n\tif (this->get(start) != get_other) {\n\t\tthis->set_insert(start, get_other);\n\t}\n\n\tthis->changes(start);\n}\n\n} // namespace curve\n} // namespace openage\n"
  },
  {
    "path": "libopenage/curve/container/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tarray.cpp\n    element_wrapper.cpp\n\titerator.cpp\n\tmap.cpp\n\tmap_filter_iterator.cpp\n\tqueue.cpp\n\tqueue_filter_iterator.cpp\n)\n"
  },
  {
    "path": "libopenage/curve/container/array.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n\n#include \"array.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/array.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <array>\n\n#include \"curve/container/iterator.h\"\n#include \"curve/keyframe_container.h\"\n#include \"event/evententity.h\"\n\n\nnamespace openage {\nnamespace curve {\n\ntemplate <typename T, size_t Size>\nconstexpr std::array<KeyframeContainer<T>, Size> init_default_vals(const std::array<T, Size> &default_vals) {\n\tstd::array<KeyframeContainer<T>, Size> containers;\n\tfor (size_t i = 0; i < Size; i++) {\n\t\tcontainers[i] = KeyframeContainer<T>(default_vals[i]);\n\t}\n\treturn containers;\n}\n\ntemplate <typename T, size_t Size>\nclass Array : event::EventEntity {\npublic:\n\t/// Underlying container type.\n\tusing container_t = std::array<KeyframeContainer<T>, Size>;\n\n\t/// Uderlying iterator type\n\tusing const_iterator = typename std::array<KeyframeContainer<T>, Size>::const_iterator;\n\n\t/// Index type to access elements in the container.\n\tusing index_t = typename container_t::size_type;\n\n\t/**\n\t * Create a new array curve container.\n\t *\n\t * @param loop Event loop this curve is registered on for notifications.\n\t * @param id Unique identifier for this curve.\n\t * @param idstr Human-readable identifier for this curve.\n\t * @param notifier Function to call when this curve changes.\n\t * @param default_vals Default values for the array elements.\n\t */\n\tArray(const std::shared_ptr<event::EventLoop> &loop,\n\t      size_t id,\n\t      const std::string &idstr = \"\",\n\t      const EventEntity::single_change_notifier &notifier = nullptr,\n\t      const std::array<T, Size> &default_vals = {}) :\n\t\tEventEntity(loop, notifier),\n\t\tcontainers{init_default_vals(default_vals)},\n\t\t_id{id},\n\t\t_idstr{idstr},\n\t\tloop{loop},\n\t\tlast_elements{} {}\n\n\t// prevent copying because it invalidates the usage of unique ids and event\n\t// registration. If you need to copy a curve, use the sync() method.\n\tArray(const Array &) = delete;\n\tArray &operator=(const Array &) = delete;\n\n\tArray(Array &&) = default;\n\n\t/**\n\t * Get the last element with elem->time <= time.\n\t *\n\t * @param t Time of access.\n\t * @param index Index of the array element.\n\t *\n\t * @return Value of the last element with time <= t.\n\t */\n\tT at(const time::time_t &t, const index_t index) const;\n\n\t/**\n\t * Get all elements at time t.\n\t *\n\t * @param t Time of access.\n\t *\n\t * @return Array of values at time t.\n\t */\n\tstd::array<T, Size> get(const time::time_t &t) const;\n\n\t/**\n\t * Get the size of the array.\n\t *\n\t * @return Array size.\n\t */\n\tconsteval size_t size() const;\n\n\t/**\n\t * Get the last keyframe value and time with elem->time <= time.\n\t *\n\t * @param t Time of access.\n\t * @param index Index of the array element.\n\t *\n\t * @return Time-value pair of the last keyframe with time <= t.\n\t */\n\tstd::pair<time::time_t, T> frame(const time::time_t &t, const index_t index) const;\n\n\t/**\n\t * Get the first keyframe value and time with elem->time > time.\n\t *\n\t * If there is no keyframe with time > t, the behavior is undefined.\n\t *\n\t * @param t Time of access.\n\t * @param index Index of the array element.\n\t *\n\t * @return Time-value pair of the first keyframe with time > t.\n\t */\n\tstd::pair<time::time_t, T> next_frame(const time::time_t &t, const index_t index) const;\n\n\t/**\n\t * Insert a new keyframe value at time t.\n\t *\n\t * If there is already a keyframe at time t, the new keyframe is inserted after the existing one.\n\t *\n\t * @param t Time of insertion.\n\t * @param index Index of the array element.\n\t * @param value Keyframe value.\n\t */\n\tvoid set_insert(const time::time_t &t, const index_t index, T value);\n\n\t/**\n\t * Insert a new keyframe value at time t. Erase all other keyframes with elem->time > t.\n\t *\n\t * @param t Time of insertion.\n\t * @param index Index of the array element.\n\t * @param value Keyframe value.\n\t */\n\tvoid set_last(const time::time_t &t, const index_t index, T value);\n\n\t/**\n\t * Replace all keyframes at elem->time == t with a new keyframe value.\n\t *\n\t * @param t Time of insertion.\n\t * @param index Index of the array element.\n\t * @param value Keyframe value.\n\t */\n\tvoid set_replace(const time::time_t &t, const index_t index, T value);\n\n\t/**\n\t * Copy keyframes from another container to this container.\n\t *\n\t * Replaces all keyframes beginning at t >= start with keyframes from \\p other.\n\t *\n\t * @param other Curve that keyframes are copied from.\n\t * @param start Start time at which keyframes are replaced (default = -INF).\n\t *              Using the default value replaces ALL keyframes of \\p this with\n\t *              the keyframes of \\p other.\n\t */\n\tvoid sync(const Array<T, Size> &other, const time::time_t &start);\n\n\t/**\n\t * Get the identifier of this curve.\n\t *\n\t * @return Identifier.\n\t */\n\tsize_t id() const override {\n\t\treturn this->_id;\n\t}\n\n\t/**\n\t * Get the human-readable identifier of this curve.\n\t *\n\t * @return Human-readable identifier.\n\t */\n\tstd::string idstr() const override {\n\t\tif (this->_idstr.size() == 0) {\n\t\t\treturn std::to_string(this->id());\n\t\t}\n\t\treturn this->_idstr;\n\t}\n\n\n\tclass ArrayIterator : public CurveIterator<T, Array<T, Size>> {\n\tpublic:\n\t\t/**\n\t\t * Construct the iterator from its boundary conditions: time and container\n\t\t */\n\t\tArrayIterator(const const_iterator &base,\n\t\t              const Array<T, Size> *base_container,\n\t\t              const time::time_t &to) :\n\t\t\tCurveIterator<T, Array<T, Size>>(base, base_container, -time::TIME_MAX, to) {\n\t\t}\n\n\n\t\tvirtual bool valid() const override {\n\t\t\tif (this->container->end().get_base() != this->get_base() && this->get_base()->begin()->time() <= this->to) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t/**\n\t\t * Get the keyFrame with a time <= this->to from the KeyframeContainer\n\t\t * that this iterator currently points at\n\t\t */\n\t\tconst T &value() const override {\n\t\t\tconst auto &key_frame_container = *this->get_base();\n\t\t\tsize_t hint_index = std::distance(this->container->begin().get_base(), this->get_base());\n\t\t\tsize_t e = key_frame_container.last(this->to, last_elements[hint_index]);\n\t\t\tthis->last_elements[hint_index] = e;\n\t\t\treturn key_frame_container.get(e).val();\n\t\t}\n\n\t\t/**\n\t\t * Cache hints for containers. Stores the index of the last keyframe accessed in each container.\n\t\t *\n\t\t * hints is used to speed up the search for keyframes.\n\t\t *\n\t\t * mutable as hints are updated by const read-only functions.\n\t\t */\n\t\tmutable std::array<typename KeyframeContainer<T>::elem_ptr, Size> last_elements = {};\n\t};\n\n\n\t/**\n\t * iterator pointing to a keyframe of the first KeyframeContainer in the curve at a given time\n\t */\n\tArrayIterator begin(const time::time_t &time = time::TIME_MIN) const;\n\n\n\t/**\n\t *  iterator pointing after the last KeyframeContainer in the curve at a given time\n\t */\n\tArrayIterator end(const time::time_t &time = time::TIME_MAX) const;\n\n\nprivate:\n\t/**\n\t * get a copy to the KeyframeContainer at index\n\t */\n\tKeyframeContainer<T> operator[](index_t index) const {\n\t\treturn this->containers.at(index);\n\t}\n\n\t/**\n\t * Containers for each array element.\n\t *\n\t * Each element is managed by a KeyframeContainer.\n\t */\n\tcontainer_t containers;\n\n\t/**\n\t * Identifier for the container\n\t */\n\tconst size_t _id;\n\n\t/**\n\t * Human-readable identifier for the container\n\t */\n\tconst std::string _idstr;\n\n\t/**\n\t * The eventloop this curve was registered to\n\t */\n\tconst std::shared_ptr<event::EventLoop> loop;\n\n\t/**\n\t * Cache hints for containers. Stores the index of the last keyframe accessed in each container.\n\t *\n\t * hints is used to speed up the search for keyframes.\n\t *\n\t * mutable as hints are updated by const read-only functions.\n\t */\n\tmutable std::array<typename KeyframeContainer<T>::elem_ptr, Size> last_elements = {};\n};\n\n\ntemplate <typename T, size_t Size>\nstd::pair<time::time_t, T> Array<T, Size>::frame(const time::time_t &t,\n                                                 const index_t index) const {\n\t// find elem_ptr in container to get the last keyframe\n\tauto hint = this->last_elements[index];\n\tauto frame_index = this->containers.at(index).last(t, hint);\n\n\t// update the hint\n\tthis->last_elements[index] = frame_index;\n\n\treturn this->containers.at(index).get(frame_index).as_pair();\n}\n\ntemplate <typename T, size_t Size>\nstd::pair<time::time_t, T> Array<T, Size>::next_frame(const time::time_t &t,\n                                                      const index_t index) const {\n\t// find elem_ptr in container to get the last keyframe with time <= t\n\tauto hint = this->last_elements[index];\n\tauto frame_index = this->containers.at(index).last(t, hint);\n\n\t// increment the index to get the next keyframe\n\tframe_index++;\n\n\t// update the hint\n\tthis->last_elements[index] = frame_index;\n\n\treturn this->containers.at(index).get(frame_index).as_pair();\n}\n\ntemplate <typename T, size_t Size>\nT Array<T, Size>::at(const time::time_t &t,\n                     const index_t index) const {\n\t// find elem_ptr in container to get the last keyframe with time <= t\n\tauto hint = this->last_elements[index];\n\tauto e = this->containers.at(index).last(t, hint);\n\n\t// update the hint\n\tthis->last_elements[index] = e;\n\n\treturn this->containers.at(index).get(e).val();\n}\n\ntemplate <typename T, size_t Size>\nstd::array<T, Size> Array<T, Size>::get(const time::time_t &t) const {\n\treturn [&]<auto... I>(std::index_sequence<I...>) {\n\t\treturn std::array<T, Size>{this->at(t, I)...};\n\t}(std::make_index_sequence<Size>{});\n}\n\ntemplate <typename T, size_t Size>\nconsteval size_t Array<T, Size>::size() const {\n\treturn Size;\n}\n\ntemplate <typename T, size_t Size>\nvoid Array<T, Size>::set_insert(const time::time_t &t,\n                                const index_t index,\n                                T value) {\n\t// find elem_ptr in container to get the last keyframe with time <= t\n\tauto hint = this->last_elements[index];\n\tauto e = this->containers.at(index).insert_after(Keyframe{t, value}, hint);\n\n\t// update the hint\n\tthis->last_elements[index] = e;\n\n\tthis->changes(t);\n}\n\ntemplate <typename T, size_t Size>\nvoid Array<T, Size>::set_last(const time::time_t &t,\n                              const index_t index,\n                              T value) {\n\t// find elem_ptr in container to get the last keyframe with time <= t\n\tauto hint = this->last_elements[index];\n\tauto e = this->containers.at(index).last(t, hint);\n\n\t// erase max one same-time value\n\tif (this->containers.at(index).get(e).time() == t) {\n\t\te--;\n\t}\n\n\t// erase all keyframes with time > t\n\tthis->containers.at(index).erase_after(e);\n\n\t// insert the new keyframe at the end\n\tthis->containers.at(index).insert_before(Keyframe{t, value}, e);\n\n\t// update the hint\n\tthis->last_elements[index] = hint;\n\n\tthis->changes(t);\n}\n\ntemplate <typename T, size_t Size>\nvoid Array<T, Size>::set_replace(const time::time_t &t,\n                                 const index_t index,\n                                 T value) {\n\t// find elem_ptr in container to get the last keyframe with time <= t\n\tauto hint = this->last_elements[index];\n\tauto e = this->containers.at(index).insert_overwrite(Keyframe{t, value}, hint);\n\n\t// update the hint\n\tthis->last_elements[index] = e;\n\n\tthis->changes(t);\n}\n\ntemplate <typename T, size_t Size>\nvoid Array<T, Size>::sync(const Array<T, Size> &other,\n                          const time::time_t &start) {\n\tfor (index_t i = 0; i < Size; i++) {\n\t\tthis->containers[i].sync(other.containers[i], start);\n\t}\n\n\tthis->changes(start);\n}\n\ntemplate <typename T, size_t Size>\nauto Array<T, Size>::begin(const time::time_t &t) const -> ArrayIterator {\n\treturn ArrayIterator(this->containers.begin(), this, t);\n}\n\n\ntemplate <typename T, size_t Size>\nauto Array<T, Size>::end(const time::time_t &t) const -> ArrayIterator {\n\treturn ArrayIterator(this->containers.end(), this, t);\n}\n\n} // namespace curve\n} // namespace openage\n"
  },
  {
    "path": "libopenage/curve/container/element_wrapper.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"element_wrapper.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/element_wrapper.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"time/time.h\"\n\nnamespace openage::curve {\n\n/**\n * Wrapper for elements in a curve container.\n *\n * Stores the lifetime of the element (insertion time and erasure time) alongside the value.\n */\ntemplate <typename T>\nclass element_wrapper {\npublic:\n\t/**\n\t * Create a new element with insertion time \\p time and a given value.\n\t *\n\t * Erasure time is set to time::TIME_MAX, i.e. the element is alive indefinitely.\n\t *\n\t * @param time Insertion time of the element.\n\t * @param value Element value.\n\t */\n\telement_wrapper(const time::time_t &time, const T &value) :\n\t\t_alive{time},\n\t\t_dead{time::TIME_MAX},\n\t\t_value{value} {}\n\n\t/**\n\t * Create a new element with insertion time \\p alive and erasure time \\p dead and a given value.\n\t *\n\t * @param alive Insertion time of the element.\n\t * @param dead Erasure time of the element.\n\t * @param value Element value.\n\t */\n\telement_wrapper(const time::time_t &alive, const time::time_t &dead, const T &value) :\n\t\t_alive{alive},\n\t\t_dead{dead},\n\t\t_value{value} {}\n\n\t/**\n\t * Get the insertion time of this element.\n\t *\n\t * @return Time when the element was inserted into the container.\n\t */\n\tconst time::time_t &alive() const {\n\t\treturn _alive;\n\t}\n\n\t/**\n\t * Get the erasure time of this element.\n\t *\n\t * @return Time when the element was erased from the container.\n\t */\n\tconst time::time_t &dead() const {\n\t\treturn _dead;\n\t}\n\n\t/**\n\t * Set the erasure time of this element.\n\t *\n\t * @param time Time when the element was erased from the container.\n\t */\n\tvoid set_dead(const time::time_t &time) {\n\t\t_dead = time;\n\t}\n\n\t/**\n\t * Get the value of this element.\n\t *\n\t * @return Value of the element.\n\t */\n\tconst T &value() const {\n\t\treturn _value;\n\t}\n\nprivate:\n\t/**\n\t * Time of insertion of the element into the container\n\t */\n\ttime::time_t _alive;\n\n\t/**\n\t * Time of erasure of the element from the container\n\t */\n\ttime::time_t _dead;\n\n\t/**\n\t * Element value\n\t */\n\tT _value;\n};\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/iterator.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"iterator.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/iterator.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * Default interface for curve containers\n */\ntemplate <typename val_t,\n          typename container_t,\n          typename iterator_t = typename container_t::const_iterator>\nclass CurveIterator {\npublic:\n\t/**\n\t * access the value of the iterator\n\t */\n\tvirtual const val_t &value() const = 0;\n\n\t/**\n\t * Check if the iterator is still valid\n\t * (this breaks from the stl - in the best way)\n\t */\n\tvirtual bool valid() const = 0;\n\n\t/**\n\t * The iterator needs a reference to the container\n\t */\n\texplicit CurveIterator(const container_t *c) :\n\t\tbase{},\n\t\tcontainer{c},\n\t\tfrom{-time::TIME_MAX},\n\t\tto{+time::TIME_MAX} {}\n\nprotected:\n\t/**\n\t * Can only be constructed from the referenced container\n\t */\n\tCurveIterator(const iterator_t &base,\n\t              const container_t *container,\n\t              const time::time_t &from,\n\t              const time::time_t &to) :\n\t\tbase{base},\n\t\tcontainer{container},\n\t\tfrom{from},\n\t\tto{to} {}\n\npublic:\n\t/** Default copy c'tor */\n\tCurveIterator(const CurveIterator &) = default;\n\n\tvirtual ~CurveIterator() = default;\n\n\t/** Default assignment operator */\n\tCurveIterator<val_t, container_t, iterator_t> &operator=(\n\t\tconst CurveIterator<val_t, container_t, iterator_t> &) = default;\n\n\t/** Dereference will call the virtual function */\n\tvirtual const val_t &operator*() const {\n\t\treturn this->value();\n\t}\n\n\t/** Dereference will call the virutal function */\n\tvirtual const val_t *operator->() const {\n\t\treturn &this->value();\n\t}\n\n\t/**\n\t * For equalness only the base iterator will be testet - not the timespans\n\t * this is defined in.\n\t */\n\tvirtual bool operator==(const CurveIterator<val_t, container_t> &rhs) const {\n\t\treturn this->base == rhs.base;\n\t}\n\n\t/**\n\t * For unequalness only the base iterator will be testet - not the timespans\n\t * this is defined in.\n\t */\n\tvirtual bool operator!=(const CurveIterator<val_t, container_t> &rhs) const {\n\t\treturn this->base != rhs.base;\n\t}\n\n\t/**\n\t * Advance to the next valid element.\n\t */\n\tvirtual CurveIterator<val_t, container_t> &operator++() {\n\t\tdo {\n\t\t\t++this->base;\n\t\t}\n\t\twhile (this->container->end().base != this->base and not this->valid());\n\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Access the underlying iterator.\n\t */\n\tconst iterator_t &get_base() const {\n\t\treturn base;\n\t}\n\n\t/**\n\t * Access the lower end value of the defined time frame\n\t */\n\tconst time::time_t &get_from() const {\n\t\treturn from;\n\t}\n\n\t/**\n\t * Access the higher end value of the defined time frame\n\t */\n\tconst time::time_t &get_to() const {\n\t\treturn to;\n\t}\n\nprotected:\n\t/// The iterator this is currently referring to.\n\titerator_t base;\n\n\t/// The base container.\n\tconst container_t *container;\n\n\t/// The time, from where this iterator started to iterate.\n\ttime::time_t from;\n\n\t/// The time, to where this iterator will iterate.\n\ttime::time_t to;\n};\n\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/map.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"map.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/map.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <iostream>\n#include <optional>\n#include <unordered_map>\n#include <utility>\n\n#include \"curve/container/element_wrapper.h\"\n#include \"curve/container/map_filter_iterator.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * Map that keeps track of the lifetime of the contained elements.\n * Make sure that no key is reused.\n */\ntemplate <typename key_t, typename val_t>\nclass UnorderedMap {\n\t/**\n\t * Data holder. Maps keys to map elements.\n\t * Map elements themselves store when they are valid.\n\t */\n\tstd::unordered_map<key_t, element_wrapper<val_t>> container;\n\npublic:\n\tusing const_iterator = typename std::unordered_map<key_t, element_wrapper<val_t>>::const_iterator;\n\n\tstd::optional<MapFilterIterator<key_t, val_t, UnorderedMap>>\n\toperator()(const time::time_t &, const key_t &) const;\n\n\tstd::optional<MapFilterIterator<key_t, val_t, UnorderedMap>>\n\tat(const time::time_t &, const key_t &) const;\n\n\tMapFilterIterator<key_t, val_t, UnorderedMap>\n\tbegin(const time::time_t &e = time::TIME_MAX) const;\n\n\tMapFilterIterator<key_t, val_t, UnorderedMap>\n\tend(const time::time_t &e = time::TIME_MAX) const;\n\n\tMapFilterIterator<key_t, val_t, UnorderedMap>\n\tinsert(const time::time_t &birth, const key_t &, const val_t &);\n\n\tMapFilterIterator<key_t, val_t, UnorderedMap>\n\tinsert(const time::time_t &birth, const time::time_t &death, const key_t &key, const val_t &value);\n\n\tMapFilterIterator<key_t, val_t, UnorderedMap>\n\tbetween(const time::time_t &start, const time::time_t &to) const;\n\n\tvoid birth(const time::time_t &, const key_t &);\n\tvoid birth(const time::time_t &,\n\t           const MapFilterIterator<val_t, val_t, UnorderedMap> &);\n\n\tvoid kill(const time::time_t &, const key_t &);\n\tvoid kill(const time::time_t &,\n\t          const MapFilterIterator<val_t, val_t, UnorderedMap> &);\n\n\t// remove all dead elements before that point in time\n\tvoid clean(const time::time_t &);\n\n\t/**\n\t * gdb helper method.\n\t */\n\tvoid dump() {\n\t\tfor (auto i : container) {\n\t\t\tstd::cout << \"Element: \" << i.second.value << std::endl;\n\t\t}\n\t}\n};\n\ntemplate <typename key_t, typename val_t>\nstd::optional<MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>>\nUnorderedMap<key_t, val_t>::operator()(const time::time_t &time,\n                                       const key_t &key) const {\n\treturn this->at(time, key);\n}\n\ntemplate <typename key_t, typename val_t>\nstd::optional<MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>>\nUnorderedMap<key_t, val_t>::at(const time::time_t &time,\n                               const key_t &key) const {\n\tauto e = this->container.find(key);\n\tif (e != this->container.end() and e->second.alive() <= time and e->second.dead() > time) {\n\t\treturn MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>(\n\t\t\te,\n\t\t\tthis,\n\t\t\ttime,\n\t\t\ttime::TIME_MAX);\n\t}\n\telse {\n\t\treturn {};\n\t}\n}\n\ntemplate <typename key_t, typename val_t>\nMapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>\nUnorderedMap<key_t, val_t>::begin(const time::time_t &time) const {\n\treturn MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>(\n\t\tthis->container.begin(),\n\t\tthis,\n\t\ttime,\n\t\ttime::TIME_MAX);\n}\n\ntemplate <typename key_t, typename val_t>\nMapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>\nUnorderedMap<key_t, val_t>::end(const time::time_t &time) const {\n\treturn MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>(\n\t\tthis->container.end(),\n\t\tthis,\n\t\t-time::TIME_MAX,\n\t\ttime);\n}\n\ntemplate <typename key_t, typename val_t>\nMapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>\nUnorderedMap<key_t, val_t>::between(const time::time_t &from, const time::time_t &to) const {\n\tauto it = MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>(\n\t\tthis->container.begin(),\n\t\tthis,\n\t\tfrom,\n\t\tto);\n\n\tif (!it.valid()) {\n\t\t++it;\n\t}\n\treturn it;\n}\n\ntemplate <typename key_t, typename val_t>\nMapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>\nUnorderedMap<key_t, val_t>::insert(const time::time_t &alive,\n                                   const key_t &key,\n                                   const val_t &value) {\n\treturn this->insert(\n\t\talive,\n\t\ttime::TIME_MAX,\n\t\tkey,\n\t\tvalue);\n}\n\ntemplate <typename key_t, typename val_t>\nMapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>\nUnorderedMap<key_t, val_t>::insert(const time::time_t &alive,\n                                   const time::time_t &dead,\n                                   const key_t &key,\n                                   const val_t &value) {\n\telement_wrapper<val_t> e{alive, dead, value};\n\tauto it = this->container.insert(std::make_pair(key, e));\n\treturn MapFilterIterator<key_t, val_t, UnorderedMap<key_t, val_t>>(\n\t\tit.first,\n\t\tthis,\n\t\talive,\n\t\tdead);\n}\n\ntemplate <typename key_t, typename val_t>\nvoid UnorderedMap<key_t, val_t>::birth(const time::time_t &time,\n                                       const key_t &key) {\n\tauto it = this->container.find(key);\n\tif (it != this->container.end()) {\n\t\tit->second.alive = time;\n\t}\n}\n\ntemplate <typename key_t, typename val_t>\nvoid UnorderedMap<key_t, val_t>::birth(const time::time_t &time,\n                                       const MapFilterIterator<val_t, val_t, UnorderedMap> &it) {\n\tit->second.alive = time;\n}\n\ntemplate <typename key_t, typename val_t>\nvoid UnorderedMap<key_t, val_t>::kill(const time::time_t &time,\n                                      const key_t &key) {\n\tauto it = this->container.find(key);\n\tif (it != this->container.end()) {\n\t\tit->second.dead = time;\n\t}\n}\n\ntemplate <typename key_t, typename val_t>\nvoid UnorderedMap<key_t, val_t>::kill(const time::time_t &time,\n                                      const MapFilterIterator<val_t, val_t, UnorderedMap> &it) {\n\tit->second.dead = time;\n}\n\ntemplate <typename key_t, typename val_t>\nvoid UnorderedMap<key_t, val_t>::clean(const time::time_t &) {\n\t// TODO save everything to a file and be happy.\n}\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/map_filter_iterator.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"map_filter_iterator.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/map_filter_iterator.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"curve/container/iterator.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::curve {\n\n/**\n * A filtering operator to iterate over all elements of a map whose elements\n * exist for a certain livespan. The range where to iterate is given at\n * construction.\n *\n * It depends on key_t and val_t as map-parameters, container_t is the container\n * to operate on and the function valid_f, that checks if an element is alive.\n */\ntemplate <typename key_t,\n          typename val_t,\n          typename container_t>\nclass MapFilterIterator : public CurveIterator<val_t, container_t> {\npublic:\n\tusing iterator_t = typename container_t::const_iterator;\n\n\t/**\n\t * Construct the iterator from its boundary conditions: time and container\n\t */\n\tMapFilterIterator(const iterator_t &base,\n\t                  const container_t *container,\n\t                  const time::time_t &from,\n\t                  const time::time_t &to) :\n\t\tCurveIterator<val_t, container_t>(base, container, from, to) {}\n\n\tusing CurveIterator<val_t, container_t>::operator=;\n\n\tvirtual bool valid() const override {\n\t\treturn (this->get_base()->second.alive() >= this->from\n\t\t        and this->get_base()->second.dead() < this->to);\n\t}\n\n\t/**\n\t * Get the value behind the iterator.\n\t * Nicer way of accessing it beside operator *.\n\t */\n\tval_t const &value() const override {\n\t\treturn this->get_base()->second.value();\n\t}\n\n\t/**\n\t * Get the key pointed to by this iterator.\n\t */\n\tconst key_t &key() const {\n\t\treturn this->get_base()->first;\n\t}\n};\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/queue.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"queue.h\"\n\nnamespace openage::curve {\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/queue.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <iostream>\n#include <iterator>\n#include <list>\n#include <memory>\n#include <string>\n\n#include \"error/error.h\"\n\n#include \"curve/container/element_wrapper.h\"\n#include \"curve/container/iterator.h\"\n#include \"curve/container/queue_filter_iterator.h\"\n#include \"event/evententity.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage {\nnamespace event {\nclass EventLoop;\n}\n\nnamespace curve {\n\n/**\n * A container that manages events on a timeline. Every event has exactly one\n * time it will happen.\n * This container can be used to store interactions\n */\ntemplate <class T>\nclass Queue : public event::EventEntity {\npublic:\n\t/**\n\t * The underlaying container type.\n\t */\n\tusing container_t = typename std::vector<element_wrapper<T>>;\n\n\t/**\n\t * The index type to access elements in the container\n\t */\n\tusing elem_ptr = typename container_t::size_type;\n\n\t/**\n\t * The iterator type to access elements in the container\n\t */\n\tusing const_iterator = typename container_t::const_iterator;\n\tusing iterator = typename container_t::iterator;\n\n\tQueue(const std::shared_ptr<event::EventLoop> &loop,\n\t      size_t id,\n\t      const std::string &idstr = \"\") :\n\t\tEventEntity{loop},\n\t\t_id{id},\n\t\t_idstr{idstr},\n\t\tlast_change{time::TIME_ZERO},\n\t\tfront_start{0} {}\n\n\t// prevent accidental copy of queue\n\tQueue(const Queue &) = delete;\n\n\t// Reading Access\n\n\t/**\n\t * Get the first element inserted at t <= time.\n\t *\n\t * Ignores dead elements.\n\t *\n\t * @param time The time to get the element at.\n\t *\n\t * @return Queue element.\n\t */\n\tconst T &front(const time::time_t &time) const;\n\n\t/**\n\t * Check if the queue is empty at the given time (no elements alive\n\t * before t <= time).\n\t *\n\t * Ignores dead elements.\n\t *\n\t * @param time The time to check at.\n\t *\n\t * @return true if the queue is empty, false otherwise.\n\t */\n\tbool empty(const time::time_t &time) const;\n\n\t// Modifying access\n\n\t/**\n\t * Get the first element inserted at t <= time and erase it from the\n\t * queue.\n\t *\n\t * Ignores dead elements.\n\t *\n\t * @param time The time to get the element at.\n\t * @param value Queue element.\n\t */\n\tconst T &pop_front(const time::time_t &time);\n\n\t/**\n\t * Get an iterator to the first element inserted at t >= time.\n\t *\n\t * Does not ignore dead elements.\n\t *\n\t * @param time The time to get the element at (default: \\p time::TIME_MIN ).\n\t *\n\t * @return Iterator to the first element.\n\t */\n\tQueueFilterIterator<T, Queue<T>> begin(const time::time_t &time = time::TIME_MIN) const;\n\n\t/**\n\t * Get an iterator to the last element in the queue at the given time.\n\t *\n\t * Does not ignore dead elements.\n\t *\n\t * @param t The time to get the element at (default: \\p time::TIME_MAX ).\n\t *\n\t * @return Iterator to the last element.\n\t */\n\tQueueFilterIterator<T, Queue<T>> end(const time::time_t &time = time::TIME_MAX) const;\n\n\t/**\n\t * Get an iterator to elements that are in the queue between two time frames.\n\t *\n\t * Does not ignore dead elements.\n\t *\n\t * @param begin Start time (default: \\p time::TIME_MIN ).\n\t * @param end End time (default: \\p time::TIME_MAX ).\n\t *\n\t * @return Iterator to the first element in the time frame.\n\t */\n\tQueueFilterIterator<T, Queue<T>> between(\n\t\tconst time::time_t &begin = time::TIME_MIN,\n\t\tconst time::time_t &end = time::TIME_MAX) const;\n\n\t/**\n\t * Erase an element from the queue.\n\t *\n\t * Does not ignore dead elements.\n\t *\n\t * @param it The iterator to the element to erase.\n\t */\n\tvoid erase(const CurveIterator<T, Queue<T>> &it);\n\n\t/**\n\t * Insert a new element into the queue.\n\t *\n\t * @param time The time to insert at.\n\t * @param e The element to insert.\n\t *\n\t * @return Iterator to the inserted element.\n\t */\n\tQueueFilterIterator<T, Queue<T>> insert(const time::time_t &time, const T &e);\n\n\t/**\n\t * Erase all elements at t <= time.\n\t *\n\t * @param time The time to clear at.\n\t */\n\tvoid clear(const time::time_t &time);\n\n\t/**\n\t * Print the queue to stdout.\n\t */\n\tvoid dump() {\n\t\tfor (auto i : container) {\n\t\t\tstd::cout << i.value << \" at \" << i.alive() << std::endl;\n\t\t}\n\t}\n\n\t/**\n\t * Get the identifier of this curve.\n\t *\n\t * @return Identifier.\n\t */\n\tsize_t id() const override {\n\t\treturn this->_id;\n\t}\n\n\t/**\n\t * Get the human-readable identifier of this curve.\n\t *\n\t * @return Human-readable identifier.\n\t */\n\tstd::string idstr() const override {\n\t\tif (this->_idstr.size() == 0) {\n\t\t\treturn std::to_string(this->id());\n\t\t}\n\t\treturn this->_idstr;\n\t}\n\nprivate:\n\t/**\n\t * Kill an element from the queue at the given time.\n\t *\n\t * The element is set to dead at the given time and is not accessible\n\t * for pops with t > time.\n\t *\n\t * @param time The time to kill at.\n\t * @param at The index of the element to kill.\n\t */\n\tvoid kill(const time::time_t &time, elem_ptr at);\n\n\t/**\n\t * Get the first alive element inserted at t <= time.\n\t *\n\t * @param time The time to get the element at.\n\t *\n\t * @return Index of the first alive element or end() if no such element exists.\n\t */\n\telem_ptr first_alive(const time::time_t &time) const;\n\n\t/**\n\t * Identifier for the container\n\t */\n\tconst size_t _id;\n\n\t/**\n\t * Human-readable identifier for the container\n\t */\n\tconst std::string _idstr;\n\n\t/**\n\t * The container that stores the queue elements.\n\t */\n\tcontainer_t container;\n\n\t/**\n\t * Simulation time of the last modifying change to the queue.\n\t */\n\ttime::time_t last_change;\n\n\t/**\n\t * Caches the search start position for the next front() call.\n\t *\n\t * All positions before the index are guaranteed to be dead at t >= last_change.\n\t */\n\telem_ptr front_start;\n};\n\n\ntemplate <class T>\ntypename Queue<T>::elem_ptr Queue<T>::first_alive(const time::time_t &time) const {\n\telem_ptr hint = 0;\n\n\t// check if the access is later than the last change\n\tif (this->last_change <= time) {\n\t\t// start searching from the last front position\n\t\thint = this->front_start;\n\t}\n\t// else search from the beginning\n\n\t// Iterate until we find an alive element\n\twhile (hint != this->container.size()\n\t       and this->container.at(hint).alive() <= time) {\n\t\tif (this->container.at(hint).dead() > time) {\n\t\t\treturn hint;\n\t\t}\n\t\t++hint;\n\t}\n\n\treturn this->container.size();\n}\n\n\ntemplate <typename T>\nconst T &Queue<T>::front(const time::time_t &time) const {\n\telem_ptr at = this->first_alive(time);\n\tENSURE(at < this->container.size(),\n\t       \"Tried accessing front at \" << time << \" but index \" << at << \" is invalid. \"\n\t                                   << \"The queue may be empty.\"\n\t                                   << \"(last_change: \" << this->last_change\n\t                                   << \", front_start: \" << this->front_start\n\t                                   << \", container size: \" << this->container.size()\n\t                                   << \")\");\n\n\treturn this->container.at(at).value();\n}\n\n\ntemplate <class T>\nconst T &Queue<T>::pop_front(const time::time_t &time) {\n\telem_ptr at = this->first_alive(time);\n\tENSURE(at < this->container.size(),\n\t       \"Tried accessing front at \" << time << \" but index \" << at << \" is invalid. \"\n\t                                   << \"The queue may be empty.\"\n\t                                   << \"(last_change: \" << this->last_change\n\t                                   << \", front_start: \" << this->front_start\n\t                                   << \", container size: \" << this->container.size()\n\t                                   << \")\");\n\n\t// kill the element at time t\n\tthis->kill(time, at);\n\n\t// cache the search start position for the next front() call\n\t// for pop time t, there should be no more elements alive before t\n\t// so we can advance the front to the next element\n\tthis->last_change = time;\n\tthis->front_start = at + 1;\n\n\tthis->changes(time);\n\n\treturn this->container.at(at).value();\n}\n\n\ntemplate <class T>\nbool Queue<T>::empty(const time::time_t &time) const {\n\tif (this->container.empty()) {\n\t\treturn true;\n\t}\n\n\treturn this->first_alive(time) == this->container.size();\n}\n\n\ntemplate <typename T>\nQueueFilterIterator<T, Queue<T>> Queue<T>::begin(const time::time_t &t) const {\n\tfor (auto it = this->container.begin(); it != this->container.end(); ++it) {\n\t\tif (it->alive() >= t) {\n\t\t\treturn QueueFilterIterator<T, Queue<T>>(\n\t\t\t\tit,\n\t\t\t\tthis,\n\t\t\t\tt,\n\t\t\t\ttime::TIME_MAX);\n\t\t}\n\t}\n\n\treturn this->end(t);\n}\n\n\ntemplate <typename T>\nQueueFilterIterator<T, Queue<T>> Queue<T>::end(const time::time_t &t) const {\n\treturn QueueFilterIterator<T, Queue<T>>(\n\t\tcontainer.end(),\n\t\tthis,\n\t\tt,\n\t\ttime::TIME_MAX);\n}\n\n\ntemplate <typename T>\nQueueFilterIterator<T, Queue<T>> Queue<T>::between(const time::time_t &begin,\n                                                   const time::time_t &end) const {\n\tauto it = QueueFilterIterator<T, Queue<T>>(\n\t\tcontainer.begin(),\n\t\tthis,\n\t\tbegin,\n\t\tend);\n\tif (not it.valid()) {\n\t\t++it;\n\t}\n\treturn it;\n}\n\n\ntemplate <typename T>\nvoid Queue<T>::erase(const CurveIterator<T, Queue<T>> &it) {\n\tcontainer.erase(it.get_base());\n}\n\n\ntemplate <class T>\nvoid Queue<T>::kill(const time::time_t &time,\n                    elem_ptr at) {\n\tthis->container[at].set_dead(time);\n}\n\n\ntemplate <typename T>\nQueueFilterIterator<T, Queue<T>> Queue<T>::insert(const time::time_t &time,\n                                                  const T &e) {\n\telem_ptr at = this->container.size();\n\twhile (at > 0) {\n\t\t--at;\n\t\tif (this->container.at(at).alive() <= time) {\n\t\t\t++at;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Get the iterator to the insertion point\n\titerator insertion_point = std::next(this->container.begin(), at);\n\tinsertion_point = this->container.insert(insertion_point, element_wrapper<T>{time, e});\n\n\t// TODO: Inserting before any dead elements shoud reset their death time\n\t//       since by definition, they cannot be popped before the new element\n\n\t// cache the insertion time\n\tthis->last_change = time;\n\n\t// if the new element is inserted before the current front element\n\t// cache it as the new front element\n\tif (at < this->front_start) {\n\t\tthis->front_start = at;\n\t}\n\n\tauto ct = QueueFilterIterator<T, Queue<T>>(\n\t\tinsertion_point,\n\t\tthis,\n\t\ttime,\n\t\ttime::TIME_MAX);\n\n\tif (!ct.valid()) {\n\t\t++ct;\n\t}\n\n\tthis->changes(time);\n\n\treturn ct;\n}\n\n\ntemplate <typename T>\nvoid Queue<T>::clear(const time::time_t &time) {\n\telem_ptr at = this->first_alive(time);\n\n\t// no elements alive at t <= time\n\t// so we don't have any changes\n\tif (at == this->container.size()) {\n\t\treturn;\n\t}\n\n\t// erase all elements alive at t <= time\n\twhile (this->container.at(at).alive() <= time\n\t       and at != this->container.size()) {\n\t\tif (this->container.at(at).dead() > time) {\n\t\t\tthis->container[at].set_dead(time);\n\t\t}\n\t\t++at;\n\t}\n\n\t// cache the search start position for the next front() call\n\tthis->last_change = time;\n\tthis->front_start = at;\n\n\tthis->changes(time);\n}\n\n\n} // namespace curve\n} // namespace openage\n"
  },
  {
    "path": "libopenage/curve/container/queue_filter_iterator.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"queue_filter_iterator.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/container/queue_filter_iterator.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"curve/container/iterator.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::curve {\n\n/**\n * A filtering operator to iterate over all elements of a queue whose elements\n * exist at exactly one point of time, the range where to iterate is given at\n * construction.\n *\n * It depends on val_t as its value type, container_t is the container\n * to operate on and the function valid_f, that checks if an element is alive.\n */\ntemplate <typename val_t,\n          typename container_t>\nclass QueueFilterIterator : public CurveIterator<val_t, container_t, typename container_t::const_iterator> {\npublic:\n\tusing const_iterator = typename container_t::const_iterator;\n\n\t/**\n\t * Construct the iterator from its boundary conditions: time and container\n\t */\n\tQueueFilterIterator(const const_iterator &base,\n\t                    const container_t *base_container,\n\t                    const time::time_t &from,\n\t                    const time::time_t &to) :\n\t\tCurveIterator<val_t, container_t>(base, base_container, from, to) {}\n\n\tvirtual bool valid() const override {\n\t\tif (this->container->end().get_base() != this->get_base()) {\n\t\t\treturn (this->get_base()->alive() >= this->from and this->get_base()->alive() < this->to);\n\t\t}\n\t\treturn false;\n\t}\n\n\tconst val_t &value() const override {\n\t\tconst auto &a = *this->get_base();\n\t\treturn a.value();\n\t}\n};\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/continuous.cpp",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#include \"continuous.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // openage::curve\n"
  },
  {
    "path": "libopenage/curve/continuous.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <sstream>\n#include <string>\n\n#include \"curve/interpolated.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::curve {\n\n\n/**\n * Continuous linear curve.\n *\n * At one point in time, there can be only one value,\n * thus, there can't be jumps. All values are connected through linear\n * interpolation.\n *\n * The bound template type T has to implement `operator+(T)` and\n * `operator*(time::time_t)`.\n */\ntemplate <typename T>\nclass Continuous : public Interpolated<T> {\npublic:\n\tusing Interpolated<T>::Interpolated;\n\n\t/**\n\t * Insert/overwrite given value at given time and erase all elements\n\t * that follow at a later time.\n\t * If multiple elements exist at the given time,\n\t * overwrite all of them.\n\t */\n\tvoid set_last(const time::time_t &t, const T &value) override;\n\n\t/** This just calls set_replace in order to guarantee the continuity. */\n\tvoid set_insert(const time::time_t &t, const T &value) override;\n\n\t/** human readable identifier */\n\tstd::string idstr() const override;\n};\n\n\ntemplate <typename T>\nvoid Continuous<T>::set_last(const time::time_t &at, const T &value) {\n\tauto hint = this->container.last(at, this->last_element);\n\n\t// erase all same-time entries\n\twhile (this->container.get(hint).time() == at) {\n\t\thint--;\n\t}\n\n\thint = this->container.erase_after(hint);\n\n\tthis->container.insert_before(at, value, hint);\n\tthis->last_element = hint;\n\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nvoid Continuous<T>::set_insert(const time::time_t &t, const T &value) {\n\tthis->set_replace(t, value);\n}\n\n\ntemplate <typename T>\nstd::string Continuous<T>::idstr() const {\n\tstd::stringstream ss;\n\tss << \"ContinuousCurve[\";\n\tif (this->_idstr.size()) {\n\t\tss << this->_idstr;\n\t}\n\telse {\n\t\tss << this->id();\n\t}\n\tss << \"]\";\n\treturn ss.str();\n}\n\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/discrete.cpp",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#include \"discrete.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // openage::curve\n"
  },
  {
    "path": "libopenage/curve/discrete.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n#include <sstream>\n#include <string>\n#include <type_traits>\n#include <utility>\n\n#include \"curve/base_curve.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::curve {\n\n/**\n * Does not interpolate between values. The template type does only need to\n * implement `operator=` and copy ctor.\n */\ntemplate <typename T>\nclass Discrete : public BaseCurve<T> {\n\tstatic_assert(std::is_copy_assignable<T>::value,\n\t              \"Template type is not copy assignable\");\n\tstatic_assert(std::is_copy_constructible<T>::value,\n\t              \"Template type is not copy constructible\");\n\npublic:\n\tusing BaseCurve<T>::BaseCurve;\n\n\t/**\n\t * Does not interpolate anything,\n\t * just returns gives the raw value of the last keyframe with time <= t.\n\t */\n\tT get(const time::time_t &t) const override;\n\n\t/**\n\t * Get a human readable id string.\n\t */\n\tstd::string idstr() const override;\n\n\t/**\n\t * Return the last time and keyframe with time <= t.\n\t */\n\tstd::pair<time::time_t, T> get_time(const time::time_t &t) const;\n\n\t/**\n\t * Return, if existing, the time and value of keyframe with time < t\n\t */\n\tstd::optional<std::pair<time::time_t, T>> get_previous(const time::time_t &t) const;\n};\n\n\ntemplate <typename T>\nT Discrete<T>::get(const time::time_t &time) const {\n\tauto e = this->container.last(time, this->last_element);\n\tthis->last_element = e; // TODO if Caching?\n\treturn this->container.get(e).val();\n}\n\n\ntemplate <typename T>\nstd::string Discrete<T>::idstr() const {\n\tstd::stringstream ss;\n\tss << \"DiscreteCurve[\";\n\tif (this->_idstr.size()) {\n\t\tss << this->_idstr;\n\t}\n\telse {\n\t\tss << this->id();\n\t}\n\tss << \"]\";\n\treturn ss.str();\n}\n\n\ntemplate <typename T>\nstd::pair<time::time_t, T> Discrete<T>::get_time(const time::time_t &time) const {\n\tauto e = this->container.last(time, this->last_element);\n\tthis->last_element = e;\n\n\tauto elem = this->container.get(e);\n\treturn elem.as_pair();\n}\n\n\ntemplate <typename T>\nstd::optional<std::pair<time::time_t, T>> Discrete<T>::get_previous(const time::time_t &time) const {\n\tauto e = this->container.last(time, this->last_element);\n\tthis->last_element = e;\n\n\t// if we're not at the container head\n\t// go back one entry.\n\tif (e == 0) {\n\t\treturn {};\n\t}\n\n\te--;\n\tauto elem = this->container.get(e);\n\treturn elem.as_pair();\n}\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/discrete_mod.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"discrete_mod.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/discrete_mod.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n#include <sstream>\n#include <string>\n#include <type_traits>\n#include <utility>\n\n#include \"curve/base_curve.h\"\n#include \"curve/discrete.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * Specialized discrete curve that allows wrapping simulation time values into an\n * interval defined by the first and last keyframe. This is useful for\n * data mapping to (pre-defined) consistent intervals that repeat very often, e.g.\n * animation keyframes.\n *\n * When using this curve, consider that all keyframe times should be relative to\n * the local interval time (and not simulation time), so the first keyframe should\n * always be inserted at t = 0. Also, the last keyframe should have the same value\n * as the first keyframe as a convention.\n */\ntemplate <typename T>\nclass DiscreteMod : public Discrete<T> {\n\tstatic_assert(std::is_copy_assignable<T>::value,\n\t              \"Template type is not copy assignable\");\n\tstatic_assert(std::is_copy_constructible<T>::value,\n\t              \"Template type is not copy constructible\");\n\npublic:\n\tusing Discrete<T>::Discrete;\n\n\t// Override insertion/erasure to get interval time\n\n\tvoid set_last(const time::time_t &at, const T &value) override;\n\tvoid set_insert(const time::time_t &at, const T &value) override;\n\tvoid erase(const time::time_t &at) override;\n\n\t/**\n\t * Get a human readable id string.\n\t */\n\tstd::string idstr() const override;\n\n\t/**\n\t * Get the raw value of the last keyframe with time <= (t - start) % interval_length.\n\t */\n\tT get_mod(const time::time_t &t, const time::time_t &start) const;\n\n\t/**\n\t * Get the last time and keyframe with time <= (t - start) % interval_length.\n\t */\n\tstd::pair<time::time_t, T> get_time_mod(const time::time_t &t, const time::time_t &start) const;\n\n\t/**\n\t * Get, if existing, the time and value of keyframe with time < (t - start) % interval_length.\n\t */\n\tstd::optional<std::pair<time::time_t, T>> get_previous_mod(const time::time_t &t, const time::time_t &start) const;\n\nprivate:\n\t/**\n\t * Length of the time interval of this curve (time between first and last keyframe).\n\t */\n\ttime::time_t time_length;\n};\n\n\ntemplate <typename T>\nvoid DiscreteMod<T>::set_last(const time::time_t &at, const T &value) {\n\tBaseCurve<T>::set_last(at, value);\n\tthis->time_length = at;\n}\n\n\ntemplate <typename T>\nvoid DiscreteMod<T>::set_insert(const time::time_t &at, const T &value) {\n\tBaseCurve<T>::set_insert(at, value);\n\n\tif (this->time_length < at) {\n\t\tthis->time_length = at;\n\t}\n}\n\n\ntemplate <typename T>\nvoid DiscreteMod<T>::erase(const time::time_t &at) {\n\tBaseCurve<T>::erase(at);\n\n\tif (this->time_length == at) {\n\t\tthis->time_length = this->container.get(this->container.size() - 1).time();\n\t}\n}\n\n\ntemplate <typename T>\nstd::string DiscreteMod<T>::idstr() const {\n\tstd::stringstream ss;\n\tss << \"DiscreteRingCurve[\";\n\tif (this->_idstr.size()) {\n\t\tss << this->_idstr;\n\t}\n\telse {\n\t\tss << this->id();\n\t}\n\tss << \"]\";\n\treturn ss.str();\n}\n\n\ntemplate <typename T>\nT DiscreteMod<T>::get_mod(const time::time_t &time, const time::time_t &start) const {\n\ttime::time_t offset = time - start;\n\tif (this->time_length == 0) {\n\t\t// modulo would fail here so return early\n\t\treturn Discrete<T>::get(0);\n\t}\n\n\ttime::time_t mod = offset % this->time_length;\n\treturn Discrete<T>::get(mod);\n}\n\n\ntemplate <typename T>\nstd::pair<time::time_t, T> DiscreteMod<T>::get_time_mod(const time::time_t &time, const time::time_t &start) const {\n\ttime::time_t offset = time - start;\n\tif (this->time_length == 0) {\n\t\t// modulo would fail here so return early\n\t\treturn Discrete<T>::get_time(0);\n\t}\n\n\ttime::time_t mod = offset % this->time_length;\n\treturn Discrete<T>::get_time(mod);\n}\n\n\ntemplate <typename T>\nstd::optional<std::pair<time::time_t, T>> DiscreteMod<T>::get_previous_mod(const time::time_t &time, const time::time_t &start) const {\n\ttime::time_t offset = time - start;\n\tif (this->time_length == 0) {\n\t\t// modulo would fail here so return early\n\t\treturn Discrete<T>::get_previous(0);\n\t}\n\n\ttime::time_t mod = offset % this->time_length;\n\treturn Discrete<T>::get_previous(mod);\n}\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/interpolated.cpp",
    "content": "// Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n#include \"interpolated.h\"\n"
  },
  {
    "path": "libopenage/curve/interpolated.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"curve/base_curve.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * Interpolation base class.\n *\n * Extends the value container to support interpolation between values.\n *\n * The bound template type T has to implement `operator +(T)` and\n * `operator *(time::time_t)`.\n */\ntemplate <typename T>\nclass Interpolated : public BaseCurve<T> {\npublic:\n\tusing BaseCurve<T>::BaseCurve;\n\n\t/**\n\t * Will interpolate between the keyframes linearly based on the time.\n\t * Picks two adjacent keyframes for interpolation.\n\t * Uses the leftmost element of the right keyframe group with same times\n\t * and the rightmost element of the left keyframe group with same times.\n\t * A curve of [0:0, 1:5, 1:10, 2:20] evaluated at t=0.5 is 2.5, t=1 is 10,\n\t * at t=1.5 is 15.\n\t *\n\t * example for a <= t <= b:\n\t * val([a:x, b:y], t) = x + (y - x)/(b - a) * (t - a)\n\t */\n\n\tT get(const time::time_t &) const override;\n};\n\n\ntemplate <typename T>\nT Interpolated<T>::get(const time::time_t &time) const {\n\tconst auto e = this->container.last(time, this->last_element);\n\tthis->last_element = e;\n\n\tauto nxt = e;\n\t++nxt;\n\n\ttime::time_t interval = 0;\n\n\tauto offset = time - this->container.get(e).time();\n\n\tif (nxt != this->container.size()) {\n\t\tinterval = this->container.get(nxt).time() - this->container.get(e).time();\n\t}\n\n\t// here, offset > interval will never hold.\n\t// otherwise the underlying storage is broken.\n\n\t// If the next element is at the same time, just return the value of this one.\n\tif (nxt == this->container.size() // use the last curve value\n\t    || offset == 0 // values equal -> don't need to interpolate\n\t    || interval == 0) { // values at the same time -> division-by-zero-error\n\n\t\treturn this->container.get(e).val();\n\t}\n\telse {\n\t\t// Interpolation between time(now) and time(next) that has elapsed\n\t\t// TODO: Elapsed time does not use fixed point arithmetic\n\t\tdouble elapsed_frac = offset.to_double() / interval.to_double();\n\n\t\t// TODO: nxt->value - e->value will produce wrong results if\n\t\t//       the nxt->value < e->value and curve element type is unsigned\n\t\t//       Example: nxt = 2, e = 4; type = uint8_t ==> 2 - 4 = 254\n\t\tauto diff_value = (this->container.get(nxt).val() - this->container.get(e).val()) * elapsed_frac;\n\t\treturn this->container.get(e).val() + diff_value;\n\t}\n}\n\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/keyframe.cpp",
    "content": "// Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n#include \"keyframe.h\"\n"
  },
  {
    "path": "libopenage/curve/keyframe.h",
    "content": "// Copyright 2019-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * A element of the curvecontainer. This is especially used to keep track of\n * the value-timing.\n *\n * If you change this class, remember to update the gdb pretty printers\n * in etc/gdb_pretty/printers.py.\n */\ntemplate <typename T>\nclass Keyframe {\npublic:\n\t/**\n\t * New default object at numericlimits<time>::min.\n\t */\n\tKeyframe() {}\n\n\t/**\n\t * New, default-constructed element at the given time\n\t */\n\tKeyframe(const time::time_t &time) :\n\t\ttimestamp{time} {}\n\n\t/**\n\t * New element fron time and value\n\t */\n\tKeyframe(const time::time_t &time, const T &value) :\n\t\tvalue{value},\n\t\ttimestamp{time} {}\n\n\t/**\n\t * Get the time of this keyframe.\n\t *\n\t * @return Keyframe time.\n\t */\n\tconst time::time_t &time() const {\n\t\treturn this->timestamp;\n\t}\n\n\t/**\n\t * Get the value of this keyframe.\n\t *\n\t * @return Keyframe value.\n\t */\n\tconst T &val() const {\n\t\treturn this->value;\n\t}\n\n\t/**\n\t * Get a time-value pair of this keyframe.\n\t *\n\t * @return Keyframe time-value pair.\n\t */\n\tstd::pair<time::time_t, T> as_pair() const {\n\t\treturn {this->timestamp, this->value};\n\t}\n\npublic:\n\t/**\n\t * Value of the keyframe.\n\t *\n\t * Can be modified by the curve if necessary.\n\t */\n\tT value = T{};\n\nprivate:\n\t/**\n\t * Time of the keyframe.\n\t */\n\ttime::time_t timestamp = time::TIME_MIN;\n};\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/keyframe_container.cpp",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#include \"keyframe_container.h\"\n\nnamespace openage::curve {\n\n// nothing to see here, keep walking (to the header)\n\n} // openage::curve\n"
  },
  {
    "path": "libopenage/curve/keyframe_container.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <functional>\n#include <iostream>\n#include <list>\n\n#include \"curve/keyframe.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::curve {\n\n/**\n * A timely ordered list with several management functions\n *\n * This class manages different time-based management functions for list\n * approach that lies underneath. It contains list to be accessed via a\n * non-accurate timing functionality, this means, that for getting a value, not\n * the exact timestamp has to be known, it will always return the one closest,\n * less or equal to the requested one.\n **/\ntemplate <typename T>\nclass KeyframeContainer {\npublic:\n\t/**\n\t * A element of the curvecontainer. This is especially used to keep track of\n\t * the value-timing.\n\t */\n\tusing keyframe_t = Keyframe<T>;\n\n\t/**\n\t * The underlaying container type.\n\t */\n\tusing container_t = std::vector<keyframe_t>;\n\n\t/**\n\t * The index type to access elements in the container\n\t */\n\tusing elem_ptr = typename container_t::size_type;\n\n\t/**\n\t * The iterator type to access elements in the container\n\t */\n\tusing iterator = typename container_t::const_iterator;\n\n\t/**\n\t * Create a new container.\n\t *\n\t * Inserts a default element with value \\p T() at \\p time = -INF to ensure\n\t * that accessing the container always returns an element.\n\t *\n\t * TODO: need the datamanger for change management\n\t */\n\tKeyframeContainer();\n\n\t/**\n\t * Create a new container.\n\t *\n\t * Inserts a default element at \\p time = -INF to ensure\n\t * that accessing the container always returns an element.\n\t *\n\t * @param defaultval Value of default element at -INF.\n\t *\n\t * TODO: need the datamanger for change management\n\t */\n\tKeyframeContainer(const T &defaultval);\n\n\t/**\n\t * Return the number of elements in this container.\n\t * One element is always added at -Inf by default,\n\t * so this is usually your_added_elements + 1.\n\t */\n\tsize_t size() const;\n\n\tconst keyframe_t &get(const elem_ptr &idx) const {\n\t\treturn this->container.at(idx);\n\t}\n\n\t/**\n\t * Get the last element in the curve which is at or before the given time.\n\t * (i.e. elem->time <= time). Given a hint where to start the search.\n\t */\n\telem_ptr last(const time::time_t &time,\n\t              const elem_ptr &hint) const;\n\n\t/**\n\t * Get the last element with elem->time <= time, without a hint where to start\n\t * searching.\n\t *\n\t * The usage of this method is discouraged - except if there is absolutely\n\t * no chance for you to have a hint (or the container is known to be nearly\n\t * empty)\n\t */\n\telem_ptr last(const time::time_t &time) const {\n\t\treturn this->last(time, this->container.size());\n\t}\n\n\t/**\n\t * Get the last element in the curve which is before the given time.\n\t * (i.e. elem->time < time). Given a hint where to start the search.\n\t */\n\telem_ptr last_before(const time::time_t &time,\n\t                     const elem_ptr &hint) const;\n\n\t/**\n\t * Get the last element with elem->time < time, without a hint where to start\n\t * searching.\n\t */\n\telem_ptr last_before(const time::time_t &time) const {\n\t\treturn this->last_before(time, this->container.size());\n\t}\n\n\t/**\n\t * Insert a new element without a hint.\n\t *\n\t * Starts the search for insertion at the end of the data.\n\t * This function is not recommended for use, whenever possible, keep a hint\n\t * to insert the data.\n\t */\n\telem_ptr insert_before(const keyframe_t &value) {\n\t\treturn this->insert_before(value, this->container.size());\n\t}\n\n\t/**\n\t * Insert a new element. The hint shall give an approximate location, where\n\t * the inserter will start to look for a insertion point. If a good hint is\n\t * given, the runtime of this function will not be affected by the current\n\t * history size. If there is a keyframe with identical time, this will\n\t * insert the new keyframe before the old one.\n\t */\n\telem_ptr insert_before(const keyframe_t &value, const elem_ptr &hint);\n\n\t/**\n\t * Create and insert a new element without submitting a hint. The search is\n\t * started from the end of the data. The use of this function is\n\t * discouraged, use it only, if your really do not have the possibility to\n\t * get a hint.\n\t */\n\telem_ptr insert_before(const time::time_t &time, const T &value) {\n\t\treturn this->insert_before(keyframe_t(time, value), this->container.size());\n\t}\n\n\t/**\n\t * Create and insert a new element. The hint gives an approximate location.\n\t * If there is a value with identical time, this will insert the new value\n\t * before the old one.\n\t */\n\telem_ptr insert_before(const time::time_t &time, const T &value, const elem_ptr &hint) {\n\t\treturn this->insert_before(keyframe_t(time, value), hint);\n\t}\n\n\t/**\n\t * Insert a new element, overwriting elements that have a\n\t * time conflict. Give an approximate insertion location to minimize runtime\n\t * on big-history curves.\n\t * `overwrite_all` == true -> overwrite all same-time elements.\n\t * `overwrite_all` == false -> overwrite the last of the time-conflict elements.\n\t */\n\telem_ptr insert_overwrite(const keyframe_t &value,\n\t                          const elem_ptr &hint,\n\t                          bool overwrite_all = false);\n\n\t/**\n\t * Insert a new value at given time which will overwrite the last of the\n\t * elements with the same time. This function will start to search the time\n\t * from the end of the data. The use of this function is discouraged, use it\n\t * only, if your really do not have the possibility to get a hint.\n\t */\n\telem_ptr insert_overwrite(const time::time_t &time, const T &value) {\n\t\treturn this->insert_overwrite(keyframe_t(time, value),\n\t\t                              this->container.size());\n\t}\n\n\t/**\n\t * Insert a new value at given time, which overwrites element(s) with\n\t * identical time. If `overwrite_all` is false, overwrite the last same-time\n\t * element. If `overwrite_all` is true, overwrite all elements with same-time.\n\t * Provide a insertion hint to abbreviate the search for the\n\t * insertion point.\n\t */\n\telem_ptr insert_overwrite(const time::time_t &time,\n\t                          const T &value,\n\t                          const elem_ptr &hint,\n\t                          bool overwrite_all = false) {\n\t\treturn this->insert_overwrite(keyframe_t(time, value), hint, overwrite_all);\n\t}\n\n\t/**\n\t * Insert a new element, after a previous element when there's a time\n\t * conflict. Give an approximate insertion location to minimize runtime on\n\t * big-history curves.\n\t */\n\telem_ptr insert_after(const keyframe_t &value, const elem_ptr &hint);\n\n\t/**\n\t * Insert a new value at given time which will be prepended to the block of\n\t * elements that have the same time. This function will start to search the\n\t * time from the end of the data. The use of this function is discouraged,\n\t * use it only, if your really do not have the possibility to get a hint.\n\t */\n\telem_ptr insert_after(const time::time_t &time, const T &value) {\n\t\treturn this->insert_after(keyframe_t(time, value),\n\t\t                          this->container.size());\n\t}\n\n\t/**\n\t * Create and insert a new element, which is added after a previous element with\n\t * identical time. Provide a insertion hint to abbreviate the search for the insertion point.\n\t */\n\telem_ptr insert_after(const time::time_t &time, const T &value, const elem_ptr &hint) {\n\t\treturn this->insert_after(keyframe_t(time, value), hint);\n\t}\n\n\t/**\n\t * Erase all elements that come after this last valid element.\n\t */\n\telem_ptr erase_after(elem_ptr last_valid);\n\n\t/**\n\t * Erase a single element from the curve.\n\t * Returns the element after the deleted one.\n\t */\n\telem_ptr erase(elem_ptr it);\n\n\t/**\n\t * Erase all elements with given time.\n\t * Variant without hint, starts the search at the end of the container.\n\t * Returns the iterator after the deleted elements.\n\t */\n\telem_ptr erase(const time::time_t &time) {\n\t\treturn this->erase(time, this->container.size());\n\t}\n\n\t/**\n\t * Erase all element with given time.\n\t * `hint` is an iterator pointing hopefully close to the searched\n\t * elements.\n\t *\n\t * Returns the iterator after the deleted elements.\n\t * Or, if no elements with this time exist,\n\t * the iterator to the first element after the requested time is returned\n\t */\n\telem_ptr erase(const time::time_t &time,\n\t               const elem_ptr &hint) {\n\t\treturn this->erase_group(time, this->last(time, hint));\n\t}\n\n\t/**\n\t * Obtain an iterator to the first value with the smallest timestamp.\n\t */\n\titerator begin() const {\n\t\treturn this->container.begin();\n\t}\n\n\t/**\n\t * Obtain an iterator to the position after the last value.\n\t */\n\titerator end() const {\n\t\treturn this->container.end();\n\t}\n\n\t/**\n\t * Remove all keyframes from the container, EXCEPT for the default value\n\t * at -INF.\n\t *\n\t * Essentially, the container is reset to the state immediately after construction.\n\t */\n\tvoid clear() {\n\t\tthis->container.erase(++this->begin(), this->end());\n\t}\n\n\t/**\n\t * Copy keyframes from another container to this container.\n\t *\n\t * Replaces all keyframes beginning at t >= start with keyframes from \\p other.\n\t *\n\t * @param other Curve that keyframes are copied from.\n\t * @param start Start time at which keyframes are replaced (default = -INF).\n\t *              Using the default value replaces ALL keyframes of \\p this with\n\t *              the keyframes of \\p other.\n\t */\n\telem_ptr sync(const KeyframeContainer<T> &other,\n\t              const time::time_t &start = time::TIME_MIN);\n\n\t/**\n\t * Copy keyframes from another container (with a different element type) to this container.\n\t *\n\t * Replaces all keyframes beginning at t >= start with keyframes from \\p other.\n\t *\n\t * @param other Curve that keyframes are copied from.\n\t * @param converter Function that converts the value type of \\p other to the\n\t *                  value type of \\p this.\n\t * @param start Start time at which keyframes are replaced (default = -INF).\n\t *              Using the default value replaces ALL keyframes of \\p this with\n\t *              the keyframes of \\p other.\n\t */\n\ttemplate <typename O>\n\telem_ptr sync(const KeyframeContainer<O> &other,\n\t              const std::function<T(const O &)> &converter,\n\t              const time::time_t &start = time::TIME_MIN);\n\n\t/**\n\t * Debugging method to be used from gdb to understand bugs better.\n\t */\n\tvoid dump() const {\n\t\tfor (auto &e : container) {\n\t\t\tstd::cout << \"Element: time: \" << e.time << \" v: \" << e.value << std::endl;\n\t\t}\n\t}\n\nprivate:\n\t/**\n\t * Erase elements with this time.\n\t * The iterator has to point to the last element of the same-time group.\n\t */\n\telem_ptr erase_group(const time::time_t &time,\n\t                     const elem_ptr &last_elem);\n\n\t/**\n\t * The data store.\n\t */\n\tcontainer_t container;\n};\n\n\ntemplate <typename T>\nKeyframeContainer<T>::KeyframeContainer() {\n\t// Create a default element at -Inf, that can always be dereferenced - so\n\t// there will by definition never be a element that cannot be dereferenced\n\tthis->container.push_back(keyframe_t(time::TIME_MIN, T()));\n}\n\n\ntemplate <typename T>\nKeyframeContainer<T>::KeyframeContainer(const T &defaultval) {\n\t// Create a default element at -Inf, that can always be dereferenced - so\n\t// there will by definition never be a element that cannot be dereferenced\n\tthis->container.push_back(keyframe_t(time::TIME_MIN, defaultval));\n}\n\n\ntemplate <typename T>\nsize_t KeyframeContainer<T>::size() const {\n\treturn this->container.size();\n}\n\n\n/*\n * Select the last element that is <= a given time.\n * If there is multiple elements with the same time, return the last of them.\n * If there is no element with such time, return the next element before the time.\n *\n * Intuitively, this function returns the element that set the last value\n * that determines the curve value for a searched time.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::last(const time::time_t &time,\n                           const KeyframeContainer<T>::elem_ptr &hint) const {\n\telem_ptr at = hint;\n\tconst elem_ptr end = this->container.size();\n\n\tif (at != end and this->container.at(at).time() <= time) {\n\t\t// walk to the right until the time is larget than the searched\n\t\twhile (at != end and this->container.at(at).time() <= time) {\n\t\t\t++at;\n\t\t}\n\t\t// go one back, because we want the last element that is <= time\n\t\t--at;\n\t}\n\telse { // idx == end or idx->time > time\n\t\t// walk to the left until the element time is smaller than the searched time\n\t\twhile (at > 0 and (at == end or this->container.at(at).time() > time)) {\n\t\t\t--at;\n\t\t}\n\t}\n\n\treturn at;\n}\n\n\n/*\n * Select the last element that is < a given time.\n * If there is multiple elements with the same time, return the last of them.\n * If there is no element with such time, return the next element before the time.\n *\n * Intuitively, this function returns the element that comes right before the\n * first element that matches the search time.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::last_before(const time::time_t &time,\n                                  const KeyframeContainer<T>::elem_ptr &hint) const {\n\telem_ptr at = hint;\n\tconst elem_ptr end = this->container.size();\n\n\tif (at != end and this->container.at(at).time() < time) {\n\t\t// walk to the right until the time is larget than the searched\n\t\twhile (at != end and this->container.at(at).time() <= time) {\n\t\t\t++at;\n\t\t}\n\t\t// go one back, because we want the last element that is <= time\n\t\t--at;\n\t}\n\telse { // idx == end or idx->time > time\n\t\t// walk to the left until the element time is smaller than the searched time\n\t\twhile (at > 0 and (at == end or this->container.at(at).time() >= time)) {\n\t\t\t--at;\n\t\t}\n\t}\n\n\treturn at;\n}\n\n\n/*\n * Determine where to insert based on time, and insert.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::insert_before(const KeyframeContainer<T>::keyframe_t &e,\n                                    const KeyframeContainer<T>::elem_ptr &hint) {\n\telem_ptr at = this->last(e.time(), hint);\n\n\tif (at == this->container.size()) {\n\t\tthis->container.push_back(e);\n\t\treturn at;\n\t}\n\n\t// seek over all same-time elements, so we can insert before the first one\n\twhile (this->container.at(at).time() == e.time() and at > 0) {\n\t\tat--;\n\t}\n\n\t++at;\n\n\tthis->container.insert(this->begin() + at, e);\n\treturn at;\n}\n\n\n/*\n * Determine where to insert based on time, and insert, overwriting value(s) with same time.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::insert_overwrite(const KeyframeContainer<T>::keyframe_t &e,\n                                       const KeyframeContainer<T>::elem_ptr &hint,\n                                       bool overwrite_all) {\n\telem_ptr at = this->last(e.time(), hint);\n\tconst elem_ptr end = this->container.size();\n\n\tif (overwrite_all) {\n\t\tat = this->erase_group(e.time(), at);\n\t}\n\telse if (at != end) {\n\t\t// overwrite the same-time element\n\t\tif (this->get(at).time() == e.time()) {\n\t\t\tthis->container.erase(this->begin() + at);\n\t\t}\n\t\telse {\n\t\t\t++at;\n\t\t}\n\t}\n\n\tthis->container.insert(this->begin() + at, e);\n\treturn at;\n}\n\n\n/*\n * Determine where to insert based on time, and insert.\n * If there is a time conflict, insert after the existing element.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::insert_after(const KeyframeContainer<T>::keyframe_t &e,\n                                   const KeyframeContainer<T>::elem_ptr &hint) {\n\telem_ptr at = this->last(e.time(), hint);\n\tconst elem_ptr end = this->container.size();\n\n\tif (at != end) {\n\t\t++at;\n\t}\n\n\tthis->container.insert(this->begin() + at, e);\n\treturn at;\n}\n\n\n/*\n * Go from the end to the last_valid element, and call erase on all of them\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::erase_after(KeyframeContainer<T>::elem_ptr last_valid) {\n\t// exclude the last_valid element from deletion\n\tif (last_valid != this->container.size()) {\n\t\t// Delete everything to the end.\n\t\tconst elem_ptr delete_start = last_valid + 1;\n\t\tthis->container.erase(this->begin() + delete_start, this->end());\n\t}\n\n\treturn last_valid;\n}\n\n\n/*\n * Delete the element from the list and call delete on it.\n */\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::erase(KeyframeContainer<T>::elem_ptr e) {\n\tthis->container.erase(this->begin() + e);\n\treturn e;\n}\n\n\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::sync(const KeyframeContainer<T> &other,\n                           const time::time_t &start) {\n\t// Delete elements after start time\n\telem_ptr at = this->last_before(start, this->container.size());\n\tat = this->erase_after(at);\n\n\tauto at_other = 1; // always skip the first element (because it's the default value)\n\n\t// Copy all elements from other with time >= start\n\tfor (size_t i = at_other; i < other.size(); i++) {\n\t\tif (other.get(i).time() >= start) {\n\t\t\tat = this->insert_after(other.get(i), at);\n\t\t}\n\t}\n\n\treturn this->container.size();\n}\n\n\ntemplate <typename T>\ntemplate <typename O>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::sync(const KeyframeContainer<O> &other,\n                           const std::function<T(const O &)> &converter,\n                           const time::time_t &start) {\n\t// Delete elements after start time\n\telem_ptr at = this->last_before(start, this->container.size());\n\tat = this->erase_after(at);\n\n\tauto at_other = 1; // always skip the first element (because it's the default value)\n\n\t// Copy all elements from other with time >= start\n\tfor (size_t i = at_other; i < other.size(); i++) {\n\t\tif (other.get(i).time() >= start) {\n\t\t\tat = this->insert_after(keyframe_t(other.get(i).time(), converter(other.get(i).val())), at);\n\t\t}\n\t}\n\n\treturn this->container.size();\n}\n\n\ntemplate <typename T>\ntypename KeyframeContainer<T>::elem_ptr\nKeyframeContainer<T>::erase_group(const time::time_t &time,\n                                  const KeyframeContainer<T>::elem_ptr &last_elem) {\n\tsize_t at = last_elem;\n\n\t// if the time what we're looking for\n\t// erase elements until all element with that time are purged\n\twhile (at != this->container.size() and this->container.at(at).time() == time) {\n\t\tthis->container.erase(this->container.begin() + at);\n\t\t--at;\n\t}\n\n\t// we have to cancel one --at in order to return\n\t// the element after the group we deleted.\n\tif (at != this->container.size()) {\n\t\t++at;\n\t}\n\n\treturn at;\n}\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/segmented.cpp",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#include \"segmented.h\"\n\nnamespace openage::curve {\n\n// This file is intended to be empty\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/segmented.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <sstream>\n#include <string>\n\n#include \"curve/interpolated.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::curve {\n\n\n/**\n * Linear curve type.\n *\n * Stores linearly interpolated line segments.\n * In contrast to `curve::Continuous`, it is not continuous and there can be jumps.\n * At one point in time, there can be multiple values. For interpolations between set values,\n * the left and rightmost one of a group of values at the same time are evaluated as fix points.\n *\n * Use the insertion operators of `ValueContainer`: `set_last`, `set_insert` and `set_replace`.\n *\n * The bound template type T has to implement `operator +(T)` and\n * `operator *(time::time_t)`.\n */\ntemplate <typename T>\nclass Segmented : public Interpolated<T> {\npublic:\n\tusing Interpolated<T>::Interpolated;\n\n\t/**\n\t * Insert/replace a value jump with the left and right values into the curve.\n\t * The right value is used for queries at >= time,\n\t * the left value for queries at < time.\n\t */\n\tvoid set_insert_jump(const time::time_t &, const T &leftval, const T &rightval);\n\n\t/**\n\t * Insert/replace a value jump with given left and right values.\n\t * All following curve keyframes will be deleted, so the\n\t * last two values of the curve will be `leftval` and `rightval`.\n\t */\n\tvoid set_last_jump(const time::time_t &, const T &leftval, const T &rightval);\n\n\t/** human readable identifier */\n\tstd::string idstr() const override;\n};\n\n\ntemplate <typename T>\nvoid Segmented<T>::set_insert_jump(const time::time_t &at, const T &leftval, const T &rightval) {\n\tauto hint = this->container.insert_overwrite(at, leftval, this->last_element, true);\n\tthis->container.insert_after(at, rightval, hint);\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nvoid Segmented<T>::set_last_jump(const time::time_t &at, const T &leftval, const T &rightval) {\n\tauto hint = this->container.last(at, this->last_element);\n\n\t// erase all one same-time values\n\twhile (this->container.get(hint).time() == at) {\n\t\thint--;\n\t}\n\n\thint = this->container.erase_after(hint);\n\n\tthis->container.insert_before(at, rightval, hint);\n\tthis->container.insert_before(at, leftval, hint);\n\tthis->last_element = hint;\n\n\tthis->changes(at);\n}\n\n\ntemplate <typename T>\nstd::string Segmented<T>::idstr() const {\n\tstd::stringstream ss;\n\tss << \"SegmentedCurve[\";\n\tif (this->_idstr.size()) {\n\t\tss << this->_idstr;\n\t}\n\telse {\n\t\tss << this->id();\n\t}\n\tss << \"]\";\n\treturn ss.str();\n}\n\n\n} // namespace openage::curve\n"
  },
  {
    "path": "libopenage/curve/tests/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tcurve_types.cpp\n\tcontainer.cpp\n)\n"
  },
  {
    "path": "libopenage/curve/tests/container.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include <algorithm>\n#include <deque>\n#include <iostream>\n#include <memory>\n#include <optional>\n#include <type_traits>\n#include <unordered_map>\n#include <unordered_set>\n\n#include \"curve/container/array.h\"\n#include \"curve/container/iterator.h\"\n#include \"curve/container/map.h\"\n#include \"curve/container/map_filter_iterator.h\"\n#include \"curve/container/queue.h\"\n#include \"curve/container/queue_filter_iterator.h\"\n#include \"event/event_loop.h\"\n#include \"testing/testing.h\"\n\n\nnamespace openage::curve::tests {\n\nstruct map_test_element {\n\tint value;\n\n\texplicit map_test_element(int v) :\n\t\tvalue(v) {}\n\n\tbool operator!=(int rhs) {\n\t\treturn this->value != rhs;\n\t}\n};\n\nstd::ostream &operator<<(std::ostream &o, const map_test_element &e) {\n\to << e.value;\n\treturn o;\n}\n\ntemplate <typename key_t, typename val_t>\nvoid dump(const std::unordered_map<key_t, val_t> &map) {\n\tfor (const auto &i : map) {\n\t\tstd::cout << i.first << \": \" << i.second << std::endl;\n\t}\n}\n\nvoid test_map() {\n\tstatic_assert(std::is_copy_constructible<MapFilterIterator<int, int, UnorderedMap<int, int>>>::value,\n\t              \"UnorderedMapIterator not Copy Constructable able\");\n\tstatic_assert(std::is_copy_assignable<MapFilterIterator<int, int, UnorderedMap<int, int>>>::value,\n\t              \"UnorderedMapIterator not Copy Assignable\");\n\n\tUnorderedMap<int, int> map;\n\tmap.insert(0, 10, 0, 0);\n\tmap.insert(5, 10, 5, 1);\n\tmap.insert(100, 200, 200, 2);\n\n\t// Basic tests test lookup in the middle of the range.\n\t{\n\t\tauto t = map.at(2, 0); // At timestamp 2 element 0\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 0);\n\t\tt = map.at(20, 5);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t{\n\t\tauto t = map.at(7, 5);\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 1);\n\t\tt = map.at(20, 5);\n\t\tTESTEQUALS(t.has_value(), false);\n\t\tt = map.at(2, 5);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t{\n\t\tauto t = map.at(150, 200);\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 2);\n\t\tt = map.at(500, 200);\n\t\tTESTEQUALS(t.has_value(), false);\n\t\tt = map.at(5, 200);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t// test 2.0: test at the boundaries\n\t{\n\t\tauto t = map.at(0, 0);\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 0);\n\t\tt = map.at(10, 0);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t{\n\t\tauto t = map.at(5, 5);\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 1);\n\t\tt = map.at(10, 5);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t{\n\t\tauto t = map.at(100, 200);\n\t\tTESTEQUALS(t.has_value(), true);\n\t\tTESTEQUALS(t.value().value(), 2);\n\t\tt = map.at(200, 200);\n\t\tTESTEQUALS(t.has_value(), false);\n\t}\n\t// Test 3.0 Iterations\n\t{\n\t\t// Iteration tests\n\t\tstd::unordered_map<int, int> reference;\n\t\treference[0] = 0;\n\t\treference[5] = 1;\n\t\treference[200] = 2;\n\t\tfor (auto it = map.begin(0); it != map.end(); ++it) { // Get all\n\t\t\tauto ri = reference.find(it.key());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\n\t\treference[5] = 5;\n\t\tfor (auto it = map.begin(1); it != map.end(90); ++it) {\n\t\t\tauto ri = reference.find(it.key());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\n\t\treference[5] = 5;\n\t\tfor (auto it = map.between(1, 90); it != map.end(); ++it) {\n\t\t\tauto ri = reference.find(it.key());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\t}\n}\n\nvoid test_list() {\n}\n\nvoid test_queue() {\n\tstatic_assert(std::is_copy_constructible<QueueFilterIterator<int, Queue<int>>>::value,\n\t              \"QueueIterator not Copy Constructable able\");\n\tstatic_assert(std::is_copy_assignable<QueueFilterIterator<int, Queue<int>>>::value,\n\t              \"QueueIterator not Copy Assignable\");\n\n\tauto loop = std::make_shared<event::EventLoop>();\n\n\tQueue<int> q{loop, 0};\n\n\tTESTEQUALS(q.empty(0), true);\n\tTESTEQUALS(q.empty(1), true);\n\tTESTEQUALS(q.empty(100001), true);\n\n\tq.insert(2, 2);\n\tq.insert(4, 3);\n\tq.insert(10, 4);\n\tq.insert(100001, 5);\n\tq.insert(100001, 6);\n\n\tTESTEQUALS(q.empty(0), true);\n\tTESTEQUALS(q.empty(1), true);\n\tTESTEQUALS(q.empty(2), false);\n\tTESTEQUALS(q.empty(100001), false);\n\tTESTEQUALS(q.empty(100002), false);\n\n\tq.insert(0, 1);\n\n\tTESTEQUALS(*q.begin(0), 1);\n\tTESTEQUALS(*q.begin(1), 2);\n\tTESTEQUALS(*q.begin(2), 2);\n\tTESTEQUALS(*q.begin(3), 3);\n\tTESTEQUALS(*q.begin(4), 3);\n\tTESTEQUALS(*q.begin(5), 4);\n\tTESTEQUALS(*q.begin(10), 4);\n\tTESTEQUALS(*q.begin(12), 5);\n\tTESTEQUALS(*q.begin(100000), 5);\n\n\t{\n\t\tstd::unordered_set<int> reference = {1, 2, 3};\n\t\tfor (auto it = q.between(0, 6); it != q.end(); ++it) {\n\t\t\tauto ri = reference.find(it.value());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\t}\n\t{\n\t\tstd::unordered_set<int> reference = {2, 3, 4};\n\t\tfor (auto it = q.between(1, 40); it != q.end(); ++it) {\n\t\t\tauto ri = reference.find(it.value());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\t}\n\t{\n\t\tstd::unordered_set<int> reference = {};\n\t\tfor (auto it = q.between(30, 40); it != q.end(); ++it) {\n\t\t\tauto ri = reference.find(it.value());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\t}\n\t{\n\t\tstd::unordered_set<int> reference = {1, 2, 3, 4};\n\t\tfor (auto it = q.between(0, 40); it != q.end(); ++it) {\n\t\t\tauto ri = reference.find(it.value());\n\t\t\tif (ri != reference.end()) {\n\t\t\t\treference.erase(ri);\n\t\t\t}\n\t\t}\n\t\tTESTEQUALS(reference.empty(), true);\n\t}\n\n\tTESTEQUALS(q.front(0), 1);\n\tTESTEQUALS(q.front(2), 1);\n\tTESTEQUALS(q.front(5), 1);\n\tTESTEQUALS(q.front(10), 1);\n\tTESTEQUALS(q.front(100001), 1);\n\n\tTESTEQUALS(q.pop_front(0), 1);\n\tTESTEQUALS(q.empty(0), true);\n\n\tTESTEQUALS(q.pop_front(12), 2);\n\tTESTEQUALS(q.empty(12), false);\n\n\tTESTEQUALS(q.pop_front(12), 3);\n\tTESTEQUALS(q.empty(12), false);\n\n\tTESTEQUALS(q.pop_front(12), 4);\n\tTESTEQUALS(q.empty(12), true);\n\n\tq.clear(0);\n\tTESTEQUALS(q.empty(0), true);\n\tTESTEQUALS(q.empty(100001), false);\n}\n\nvoid test_array() {\n\tauto loop = std::make_shared<event::EventLoop>();\n\n\tArray<int, 4> a(loop, 0);\n\ta.set_insert(1, 0, 0);\n\ta.set_insert(1, 1, 1);\n\ta.set_insert(1, 2, 2);\n\ta.set_insert(1, 3, 3);\n\t// a = [[0:0, 1:0],[0:0, 1:1],[0:0, 1:2],[0:0, 1:3]]\n\n\t// test size\n\tTESTEQUALS(a.size(), 4);\n\n\t// extracting array at time t == 1\n\tauto res = a.get(1);\n\tauto expected = std::array<int, 4>{0, 1, 2, 3};\n\tTESTEQUALS(res.at(0), expected.at(0));\n\tTESTEQUALS(res.at(1), expected.at(1));\n\tTESTEQUALS(res.at(2), expected.at(2));\n\tTESTEQUALS(res.at(3), expected.at(3));\n\tTESTEQUALS(res.size(), expected.size());\n\n\t// extracting array at time t == 0\n\t// array should have default values (== 0) for all keyframes\n\tres = a.get(0);\n\tTESTEQUALS(res.at(0), 0);\n\tTESTEQUALS(res.at(1), 0);\n\tTESTEQUALS(res.at(2), 0);\n\tTESTEQUALS(res.at(3), 0);\n\n\tArray<int, 4> other(loop, 0);\n\tother.set_last(0, 0, 999);\n\tother.set_last(0, 1, 999);\n\tother.set_last(0, 2, 999);\n\tother.set_last(0, 3, 999);\n\n\t// inserting keyframes at time t == 1\n\tother.set_insert(1, 0, 4);\n\tother.set_insert(1, 1, 5);\n\tother.set_insert(1, 2, 6);\n\tother.set_insert(1, 3, 7);\n\t// other = [[0:999, 1:4],[0:999, 1:5],[0:999, 1:6],[0:999, 1:7]]\n\n\tTESTEQUALS(other.at(0, 0), 999);\n\tTESTEQUALS(other.at(0, 1), 999);\n\tTESTEQUALS(other.at(0, 2), 999);\n\tTESTEQUALS(other.at(0, 3), 999);\n\tTESTEQUALS(other.at(1, 0), 4);\n\tTESTEQUALS(other.at(1, 1), 5);\n\tTESTEQUALS(other.at(1, 2), 6);\n\tTESTEQUALS(other.at(1, 3), 7);\n\n\t// sync keyframes from other to a\n\ta.sync(other, 1);\n\t// a = [[0:0, 1:4],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]]\n\n\tres = a.get(0);\n\tTESTEQUALS(res.at(0), 0);\n\tTESTEQUALS(res.at(1), 0);\n\tTESTEQUALS(res.at(2), 0);\n\tTESTEQUALS(res.at(3), 0);\n\tres = a.get(1);\n\tTESTEQUALS(res.at(0), 4);\n\tTESTEQUALS(res.at(1), 5);\n\tTESTEQUALS(res.at(2), 6);\n\tTESTEQUALS(res.at(3), 7);\n\n\t// replace keyframes at time t == 2\n\ta.set_insert(2, 0, 15);\n\ta.set_insert(2, 0, 20);\n\ta.set_replace(2, 0, 25);\n\tTESTEQUALS(a.at(2, 0), 25);\n\t// a = [[0:0, 1:4, 2:25],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]]\n\n\t// set last keyframe at time t == 3\n\ta.set_insert(3, 0, 30); // a = [[0:0, 1:4, 2:25, 3:30], ...\n\ta.set_insert(4, 0, 40); // a = [[0:0, 1:4, 2:25, 3:30, 4:40], ...\n\ta.set_last(3, 0, 35);   // a = [[0:0, 1:4, 2:25, 3:35],...\n\tTESTEQUALS(a.at(4, 0), 35);\n\t// a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7]]\n\n\t// test frame and next_frame\n\tauto frame = a.frame(1, 2);\n\tTESTEQUALS(frame.first, 1);  // time\n\tTESTEQUALS(frame.second, 6); // value\n\n\ta.set_insert(5, 3, 40);\n\t// a = [[0:0, 1:4, 2:25, 3:35],[0:0, 1:5],[0:0, 1:6],[0:0, 1:7, 5:40]]\n\tauto next_frame = a.next_frame(1, 3);\n\tTESTEQUALS(next_frame.first, 5);   // time\n\tTESTEQUALS(next_frame.second, 40); // value\n\n\t// Test begin and end\n\tauto it = a.begin(1);\n\tTESTEQUALS(*it, 4);\n\t++it;\n\tTESTEQUALS(*it, 5);\n\t++it;\n\tTESTEQUALS(*it, 6);\n\t++it;\n\tTESTEQUALS(*it, 7);\n}\n\n\nvoid container() {\n\ttest_map();\n\ttest_list();\n\ttest_queue();\n\ttest_array();\n}\n\n\n} // namespace openage::curve::tests\n"
  },
  {
    "path": "libopenage/curve/tests/curve_types.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include <iterator>\n#include <list>\n#include <memory>\n#include <string>\n\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"curve/discrete_mod.h\"\n#include \"curve/keyframe.h\"\n#include \"curve/keyframe_container.h\"\n#include \"curve/segmented.h\"\n#include \"event/event_loop.h\"\n#include \"testing/testing.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\nnamespace openage::curve::tests {\n\nvoid curve_types() {\n\t// Check the base container type\n\t{\n\t\tauto loop = std::make_shared<event::EventLoop>();\n\t\tKeyframeContainer<int> c;\n\n\t\tauto p0 = c.insert_before(0, 0);\n\t\tauto p1 = c.insert_before(1, 1);\n\t\tauto p2 = c.insert_before(10, 2);\n\t\tauto ib = 0;\n\t\tauto ie = c.size();\n\n\t\t// now contains: [-inf: 0, 0:0, 1:1, 10:2]\n\n\t\tTESTEQUALS(c.size(), 4);\n\n\t\t{\n\t\t\tauto it = c.begin();\n\t\t\tTESTEQUALS(it->val(), 0);\n\t\t\tTESTEQUALS(it->time(), time::TIME_MIN);\n\t\t\tTESTEQUALS((++it)->time(), 0);\n\t\t\tTESTEQUALS(it->val(), 0);\n\t\t\tTESTEQUALS((++it)->time(), 1);\n\t\t\tTESTEQUALS(it->val(), 1);\n\t\t\tTESTEQUALS((++it)->time(), 10);\n\t\t\tTESTEQUALS(it->val(), 2);\n\t\t}\n\n\t\t// last function tests without hints\n\t\tTESTEQUALS(c.get(c.last(0)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47)).val(), 2);\n\n\t\t// last() with hints.\n\t\tTESTEQUALS(c.get(c.last(0, ib)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1, ib)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5, ib)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10, ib)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47, ib)).val(), 2);\n\n\t\tTESTEQUALS(c.get(c.last(0, p0)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1, p0)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5, p0)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10, p0)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47, p0)).val(), 2);\n\n\t\tTESTEQUALS(c.get(c.last(0, p1)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1, p1)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5, p1)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10, p1)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47, p1)).val(), 2);\n\n\t\tTESTEQUALS(c.get(c.last(0, p2)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1, p2)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5, p2)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10, p2)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47, p2)).val(), 2);\n\n\t\tTESTEQUALS(c.get(c.last(0, ie)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1, ie)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(5, ie)).val(), 1);\n\t\tTESTEQUALS(c.get(c.last(10, ie)).val(), 2);\n\t\tTESTEQUALS(c.get(c.last(47, ie)).val(), 2);\n\n\t\t// Now test the basic erase() function\n\t\t// Delete the 1-element, new values should be [-inf:0, 0:0, 10:2]\n\t\tc.erase(c.last(1));\n\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(5)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(47)).val(), 2);\n\n\t\t// should do nothing, since we delete all at > 99,\n\t\t// but the last element is at 10. should still be [-inf:0, 0:0, 10:2]\n\t\tc.erase_after(c.last(99));\n\t\tTESTEQUALS(c.get(c.last(47)).val(), 2);\n\n\t\t// now since 5 < 10, element with value 2 has to be gone\n\t\t// result should be [-inf:0, 0:0]\n\t\tc.erase_after(c.last(5));\n\t\tTESTEQUALS(c.get(c.last(47)).val(), 0);\n\n\t\tc.insert_overwrite(0, 42);\n\t\tTESTEQUALS(c.get(c.last(100)).val(), 42);\n\t\tTESTEQUALS(c.get(c.last(100)).time(), 0);\n\n\t\t// the curve now contains [-inf:0, 0:42]\n\t\t// let's change/add some more elements\n\t\tc.insert_overwrite(0, 10);\n\t\tTESTEQUALS(c.get(c.last(100)).val(), 10);\n\n\t\tc.insert_after(0, 11);\n\t\tc.insert_after(0, 12);\n\t\t// now: [-inf:0, 0:10, 0:11, 0:12]\n\t\tTESTEQUALS(c.get(c.last(0)).val(), 12);\n\t\tTESTEQUALS(c.get(c.last(10)).val(), 12);\n\n\t\tc.insert_before(0, 2);\n\t\t// all the values at t=0 should be 2, 10, 11, 12\n\n\t\tc.insert_after(1, 15);\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 15);\n\t\tTESTEQUALS(c.get(c.last(10)).val(), 15);\n\n\t\tc.insert_overwrite(2, 20);\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 15);\n\t\tTESTEQUALS(c.get(c.last(2)).val(), 20);\n\t\tTESTEQUALS(c.get(c.last(10)).val(), 20);\n\n\t\tc.insert_before(3, 25);\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 15);\n\t\tTESTEQUALS(c.get(c.last(2)).val(), 20);\n\t\tTESTEQUALS(c.get(c.last(3)).val(), 25);\n\t\tTESTEQUALS(c.get(c.last(10)).val(), 25);\n\n\t\t// now it should be [-inf: 0, 0: 2, 0: 10, 0: 11, 0: 12, 1: 15, 2: 20,\n\t\t// 3: 25]\n\n\t\t{\n\t\t\tauto it = c.begin();\n\t\t\tTESTEQUALS(it->time(), time::TIME_MIN);\n\t\t\tTESTEQUALS(it->val(), 0);\n\n\t\t\tTESTEQUALS((++it)->time(), 0);\n\t\t\tTESTEQUALS(it->val(), 2);\n\n\t\t\tTESTEQUALS((++it)->time(), 0);\n\t\t\tTESTEQUALS(it->val(), 10);\n\n\t\t\tTESTEQUALS((++it)->time(), 0);\n\t\t\tTESTEQUALS(it->val(), 11);\n\n\t\t\tTESTEQUALS((++it)->time(), 0);\n\t\t\tTESTEQUALS(it->val(), 12);\n\n\t\t\tTESTEQUALS((++it)->time(), 1);\n\t\t\tTESTEQUALS(it->val(), 15);\n\n\t\t\tTESTEQUALS((++it)->time(), 2);\n\t\t\tTESTEQUALS(it->val(), 20);\n\n\t\t\tTESTEQUALS((++it)->time(), 3);\n\t\t\tTESTEQUALS(it->val(), 25);\n\t\t}\n\n\t\t// TODO: test c.insert_overwrite and c.insert_after\n\n\t\tKeyframeContainer<int> c2;\n\t\tc2.sync(c, 1);\n\t\t// now c2 should be [-inf: 0, 1: 15, 2: 20, 3: 25]\n\t\tTESTEQUALS(c2.get(c2.last(0)).val(), 0);\n\t\tTESTEQUALS(c2.get(c2.last(1)).val(), 15);\n\t\tTESTEQUALS(c2.get(c2.last(2)).val(), 20);\n\t\tTESTEQUALS(c2.get(c2.last(3)).val(), 25);\n\t\tTESTEQUALS(c2.get(c2.last(10)).val(), 25);\n\t\tTESTEQUALS(c2.size(), 4);\n\n\t\tc.clear();\n\t\t// now it should be [-inf: 0]\n\t\tTESTEQUALS(c.get(c.last(0)).val(), 0);\n\t\tTESTEQUALS(c.get(c.last(1)).val(), 0);\n\t\tTESTEQUALS(c.size(), 1);\n\t}\n\n\t// Check the Simple Continuous type\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tContinuous<float> c(f, 0);\n\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(10, 1);\n\n\t\tTESTEQUALS(c.get(0), 0);\n\n\t\tTESTEQUALS_FLOAT(c.get(1), 0.1, 1e-7);\n\n\t\tContinuous<float> c2(f, 0);\n\t\tc2.sync(c, 0);\n\n\t\tc2.set_insert(0, 5);\n\t\tc2.set_insert(10, 0);\n\n\t\tTESTEQUALS(c2.get(0), 5);\n\t\tTESTEQUALS(c2.get(10), 0);\n\n\t\tTESTEQUALS_FLOAT(c2.get(1), 4.5, 1e-7);\n\n\t\tc2.sync(c, 5);\n\t\tTESTEQUALS(c2.get(10), 1);\n\n\t\t// for t >= 5 c and c2 should have the same values after sync\n\t\tTESTEQUALS_FLOAT(c.get(5), c2.get(5), 1e-7);\n\t\tTESTEQUALS_FLOAT(c.get(7), c2.get(7), 1e-7);\n\t\tTESTEQUALS_FLOAT(c.get(10), c2.get(10), 1e-7);\n\t}\n\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tContinuous<float> c(f, 0);\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(20, 20);\n\n\t\tTESTEQUALS(c.get(0), 0);\n\t\tTESTEQUALS(c.get(1), 1);\n\t\tTESTEQUALS(c.get(7), 7);\n\n\t\tc.set_last(20, 10);\n\t\tTESTEQUALS(c.get(0), 0);\n\t\tTESTEQUALS(c.get(2), 1);\n\t\tTESTEQUALS(c.get(8), 4);\n\t}\n\n\t// Check the discrete type\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tDiscrete<int> c(f, 0);\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(10, 10);\n\n\t\tTESTEQUALS(c.get(0), 0);\n\t\tTESTEQUALS(c.get(1), 0);\n\t\tTESTEQUALS(c.get(10), 10);\n\t\tTESTEQUALS(c.get(15), 10);\n\n\t\tc.erase(10);\n\t\tTESTEQUALS(c.get(15), 0);\n\n\t\tDiscrete<std::string> complex(f, 0);\n\n\t\tcomplex.set_insert(0, \"Test 0\");\n\t\tcomplex.set_insert(10, \"Test 10\");\n\n\t\tTESTEQUALS(complex.get(0), \"Test 0\");\n\t\tTESTEQUALS(complex.get(1), \"Test 0\");\n\t\tTESTEQUALS(complex.get(10), \"Test 10\");\n\t}\n\n\t// Check the discrete mod type\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tDiscreteMod<int> c(f, 0);\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(5, 20);\n\t\tc.set_insert(10, 10);\n\n\t\tTESTEQUALS(c.get(0), 0);\n\t\tTESTEQUALS(c.get(1), 0);\n\t\tTESTEQUALS(c.get(5), 20);\n\t\tTESTEQUALS(c.get(10), 10);\n\t\tTESTEQUALS(c.get(15), 10);\n\n\t\tTESTEQUALS(c.get_mod(0, 0), 0);\n\t\tTESTEQUALS(c.get_mod(1, 0), 0);\n\t\tTESTEQUALS(c.get_mod(5, 0), 20);\n\n\t\t// wraparound\n\t\tTESTEQUALS(c.get_mod(10, 0), 0);\n\t\tTESTEQUALS(c.get_mod(11, 0), 0);\n\t\tTESTEQUALS(c.get_mod(16, 0), 20);\n\n\t\t// start offsets\n\t\tTESTEQUALS(c.get_mod(101, 100), 0);\n\t\tTESTEQUALS(c.get_mod(1337, 1000), 20);\n\n\t\tc.erase(10);\n\t\tTESTEQUALS(c.get(15), 20);\n\t\tTESTEQUALS(c.get_mod(5, 0), 0);\n\t\tTESTEQUALS(c.get_mod(15, 0), 0);\n\t}\n\n\t// check set_last\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tDiscrete<int> c(f, 0);\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(1, 1);\n\t\tc.set_insert(3, 3);\n\n\t\tTESTEQUALS(c.get(3), 3);\n\n\t\tc.set_last(2, 10);\n\t\tTESTEQUALS(c.get(2), 10);\n\t}\n\n\t// Encountered Errors\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tContinuous<int> c(f, 0);\n\t\tc.set_insert(0, 1);\n\t\tc.set_insert(1, 1);\n\t\tc.set_last(1, -5);\n\n\t\t// [0:1, 1:-5]\n\t\tTESTEQUALS(c.get(1), -5);\n\n\t\tc.set_replace(1, 10);\n\t\t// [0:1, 1:10]\n\t\tTESTEQUALS(c.get(1), 10);\n\n\t\tc.set_insert(20, 20);\n\t\tc.set_insert(1, 1);\n\t\tc.set_insert(2, 2);\n\n\t\t// [0:1, 1:1, 2:2, 20:20]\n\t\tTESTEQUALS(c.get(0.5), 1);\n\t\tTESTEQUALS(c.get(1), 1);\n\n\t\tTESTEQUALS(c.get(5), 5);\n\t\tTESTEQUALS(c.get(20), 20);\n\t\tTESTEQUALS(c.get(25), 20);\n\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(1, 10);\n\t\tc.set_insert(20, 21);\n\t\tc.erase(2);\n\t\t// [0:0, 1:10, 20:21]\n\t\tTESTEQUALS(c.get(0), 0);\n\t\tTESTEQUALS(c.get(1), 10);\n\t\t// 10 + (20 - 10)/(21 - 1) * (5 - 1) = 12\n\t\tTESTEQUALS(c.get(5), 12);\n\t\tTESTEQUALS(c.get(0.5), 5);\n\t}\n\n\t// check jumps of Segmented\n\t{\n\t\tauto f = std::make_shared<event::EventLoop>();\n\t\tSegmented<int> c(f, 0);\n\n\t\tc.set_insert(0, 0);\n\t\tc.set_insert(2, 2);\n\t\tc.set_insert(10, 10);\n\n\t\t// [0:0, 2:2, 10:10]\n\t\tTESTEQUALS(c.get(5), 5);\n\t\tTESTEQUALS(c.get(1), 1);\n\n\t\tc.set_insert(1, 10);\n\t\t// [0:0, 1:10, 2:2, 10:10]\n\t\tTESTNOEXCEPT(c.check_integrity());\n\t\tTESTEQUALS(c.get(1), 10);\n\t\tTESTEQUALS(c.get(5), 5);\n\n\t\tc.set_insert_jump(1, 0, 20);\n\t\t// [0:0, 1:0, 1:20, 2:2, 10:10]\n\t\tTESTNOEXCEPT(c.check_integrity());\n\t\tTESTEQUALS(c.get(0.5), 0);\n\t\tTESTEQUALS(c.get(1), 20);\n\n\t\tc.erase(2);\n\t\t// [0:0, 1:0, 1:20, 10:10]\n\t\tTESTNOEXCEPT(c.check_integrity());\n\t\tc.str();\n\t\tTESTEQUALS(c.get(5), 15);\n\n\t\tc.set_last_jump(1, 4, 10);\n\t\t// [0:0, 1:4, 1:10]\n\t\tTESTNOEXCEPT(c.check_integrity());\n\t\tTESTEQUALS(c.get(0.5), 2);\n\t\tTESTEQUALS(c.get(1), 10);\n\t\tTESTEQUALS(c.get(5), 10);\n\n\t\tc.erase(1);\n\t\t// [0:0]\n\t\tTESTEQUALS(c.get(1), 0);\n\t\tTESTEQUALS(c.get(5), 0);\n\t}\n}\n\n} // namespace openage::curve::tests\n"
  },
  {
    "path": "libopenage/cvar/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tcvar.cpp\n)\n\npxdgen(\n\tcvar.h\n)\n"
  },
  {
    "path": "libopenage/cvar/cvar.cpp",
    "content": "// Copyright 2013-2020 the openage authors. See copying.md for legal info.\n\n#include \"cvar.h\"\n\n#include \"../log/log.h\"\n\n\nnamespace openage {\nnamespace cvar {\n\nCVarManager::CVarManager(const util::Path &path)\n\t:\n\tpath{path} {}\n\n\nbool CVarManager::create(const std::string &name,\n                         const std::pair<get_func, set_func> &accessors) {\n\tauto it = this->store.find(name);\n\tif (it == this->store.end()) {\n\t\tthis->store[name] = accessors;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\n\nstd::string CVarManager::get(const std::string &name) const {\n\tauto it = this->store.find(name);\n\tif (it != this->store.end()) {\n\t\treturn it->second.first();\n\t}\n\treturn \"\";\n}\n\n\nvoid CVarManager::set(const std::string &name, const std::string &value) const {\n\tauto it = store.find(name);\n\tif (it != store.end()) {\n\t\tit->second.second(value);\n\t}\n}\n\n\nvoid CVarManager::load_config(const util::Path &path) {\n\tpyx_load_config_file.call(this, path);\n}\n\n\nvoid CVarManager::load_all() {\n\tlog::log(INFO << \"loading configuration files...\");\n\tthis->load_config(this->path[\"keybinds.oac\"]);\n}\n\npyinterface::PyIfFunc<void, CVarManager *, const util::Path &> pyx_load_config_file;\n\n}} // openage::cvar\n"
  },
  {
    "path": "libopenage/cvar/cvar.h",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <stdlib.h>\n// pxd: from libcpp.string cimport string\n#include <string>\n#include <unordered_map>\n#include <utility>\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc0, PyIfFunc2\n#include \"../pyinterface/functional.h\"\n// pxd: from libopenage.util.path cimport Path\n#include \"../util/path.h\"\n\n\nnamespace openage {\nnamespace cvar {\n\n/** function type used to get a configuration value */\nusing get_func = std::function<std::string()>;\n\n/** function type to set the value of a config entry */\nusing set_func = std::function<void(std::string)>;\n\n\n/**\n * Configuration manager.\n *\n * Stores key-value pairs of config data.\n * Actually it doesn't store data, instead functions\n * that perform/fetch the configuration at the appropriate place.\n *\n * pxd:\n *\n * cppclass CVarManager:\n *     string get(string name) except +\n *     void set(string name, string value) except +\n *     void load_config(Path path) except +\n */\nclass OAAPI CVarManager {\npublic:\n\tCVarManager(const util::Path &path);\n\n\t/**\n\t * Creates a configuration entry\n\t * @returns if the entry name was successful.\n\t *          It won't be if the name was used before.\n\t */\n\tbool create(const std::string &name,\n\t            const std::pair<get_func, set_func> &accessors);\n\n\t/**\n\t * Gets the value of a config entry.\n\t * Internally calls the stored get function.\n\t */\n\tstd::string get(const std::string &name) const;\n\n\t/**\n\t * Sets the config entry value.\n\t */\n\tvoid set(const std::string &name, const std::string &value) const;\n\n\t/**\n\t * Performs the loading of a configuration file\n\t * via the Python implementation.\n\t */\n\tvoid load_config(const util::Path &path);\n\n\t/**\n\t * Perform the load of the default config files.\n\t */\n\tvoid load_all();\n\nprivate:\n\t/**\n\t * Store the key-value pair of config options.\n\t * The clue is to store the \"what does the config do\",\n\t * not the actual value.\n\t * That way the system is universal.\n\t */\n\tstd::unordered_map<std::string, std::pair<get_func, set_func>> store;\n\n\t/**\n\t * Magic path that stores config files.\n\t * Auto-redirects to the default and write paths in the home folder.\n\t * It is set up in openage/cvar/location.py and openage/game/main.py\n\t */\n\tutil::Path path;\n};\n\n\n/**\n * Python function to load a configuration file.\n * The config manager is passed into it.\n *\n * pxd:\n * ctypedef CVarManager * CVarManagerPtr\n * PyIfFunc2[void, CVarManagerPtr, const Path&] pyx_load_config_file\n */\nextern OAAPI pyinterface::PyIfFunc<void, CVarManager *, const util::Path &> pyx_load_config_file;\n\n} // namespace cvar\n} // namespace openage\n"
  },
  {
    "path": "libopenage/datastructure/CMakeLists.txt",
    "content": "add_sources(libopenage\n\ttests.cpp\n)\n"
  },
  {
    "path": "libopenage/datastructure/concurrent_queue.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <condition_variable>\n#include <mutex>\n#include <queue>\n#include <thread>\n#include <type_traits>\n\nnamespace openage::datastructure {\n/**\n * A threadsafe queue.\n * Wraps the std::queue with a mutex.\n *\n * Supports waiting for data from another thread.\n */\ntemplate <typename T>\nclass ConcurrentQueue {\n\t/**\n\t * Type of mutex used for the queue.\n\t */\n\tusing mutex_t = std::recursive_mutex;\n\npublic:\n\t/** Removes all elements from the queue. */\n\tvoid clear() {\n\t\tstd::scoped_lock lock{this->mutex};\n\t\twhile (!this->queue.empty()) {\n\t\t\tthis->queue.pop();\n\t\t}\n\t}\n\n\t/** Returns whether the queue is empty. */\n\tbool empty() {\n\t\tstd::scoped_lock lock{this->mutex};\n\t\treturn this->queue.empty();\n\t}\n\n\t/** Returns the front item of the queue without removing it. */\n\tT &front() {\n\t\tstd::unique_lock<mutex_t> lock{this->mutex};\n\t\twhile (this->queue.empty()) {\n\t\t\tthis->elements_available.wait(lock);\n\t\t}\n\t\treturn this->queue.front();\n\t}\n\n\t/** Copies the front item in the queue and removes it from the queue. */\n\ttemplate <typename... None, typename U = T>\n\tT pop([[maybe_unused]] typename std::enable_if_t<!std::is_move_constructible_v<U> and std::is_copy_constructible_v<U>> *t = nullptr) {\n\t\tstatic_assert(sizeof...(None) == 0, \"User-specified template arguments are prohibited.\");\n\t\tstd::scoped_lock lock{this->mutex};\n\t\tT ret = this->front();\n\t\tthis->queue.pop();\n\n\t\t// Explicitly call the copy constructor since T cannot be move-constructed\n\t\t// TODO: change to `return ret;` when we migrate to GCC 10\n\t\treturn T(ret);\n\t}\n\n\t/** Moves the front item in the queue and removes it from the queue. */\n\ttemplate <typename... None, typename U = T>\n\tT pop([[maybe_unused]] typename std::enable_if_t<std::is_move_constructible_v<U>> *t = nullptr) {\n\t\tstatic_assert(sizeof...(None) == 0, \"User-specified template arguments are prohibited.\");\n\t\tstd::scoped_lock lock{this->mutex};\n\t\tT ret = std::move(this->front());\n\t\tthis->queue.pop();\n\t\treturn ret;\n\t}\n\n\t/** Appends the given item to the queue by copying it. */\n\ttemplate <typename... None, typename U = T>\n\tvoid push(typename std::enable_if_t<std::is_copy_constructible_v<U>, const T &> item) {\n\t\tstatic_assert(sizeof...(None) == 0, \"User-specified template arguments are prohibited.\");\n\t\tstd::unique_lock<mutex_t> lock{this->mutex};\n\t\tthis->queue.push(item);\n\t\tlock.unlock();\n\t\tthis->elements_available.notify_one();\n\t}\n\n\t/** Appends the given item to the queue by moving it. */\n\ttemplate <typename... None, typename U = T>\n\tvoid push(typename std::enable_if_t<std::is_move_constructible_v<U>, T &&> item) {\n\t\tstatic_assert(sizeof...(None) == 0, \"User-specified template arguments are prohibited.\");\n\t\tstd::unique_lock<mutex_t> lock{this->mutex};\n\t\tthis->queue.push(std::move(item));\n\t\tlock.unlock();\n\t\tthis->elements_available.notify_one();\n\t}\n\n\t/**\n\t * Return a lock to the queue so multiple\n\t * of the above operations can be done sequentially\n\t */\n\tstd::unique_lock<mutex_t> lock() {\n\t\treturn std::unique_lock<mutex_t>{this->mutex};\n\t}\n\nprivate:\n\t/** The internally used queue. */\n\tstd::queue<T> queue;\n\n\t/** The mutex to synchronize the queue. */\n\tmutex_t mutex;\n\n\t/**\n\t * Condition variable to signal, whether elements are avaiable from the\n\t * queue.\n\t */\n\tstd::condition_variable_any elements_available;\n};\n\n} // namespace openage::datastructure\n"
  },
  {
    "path": "libopenage/datastructure/constexpr_map.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <array>\n#include <type_traits>\n#include <utility>\n\n#include \"error/error.h\"\n\n\nnamespace openage::datastructure {\n\n/**\n * Compiletime generic lookup map.\n *\n * Stores the map entries in an array and uses constexpr methods\n * to search and retrieve them at compile-time. Note that for this to\n * work, the keys and values put into the map must be avilable at\n * compile time (obviously).\n *\n * If you experience compiler errors, make sure you request _existing_ keys.\n * We intentionally trigger compiler failures when a key doesn't exist.\n *\n * Messages include: \"error: ‘*0u’ is not a constant expression\"\n * -> nonexistant key\n */\ntemplate <typename K, typename V, size_t count>\nclass ConstMap {\npublic:\n\ttemplate <class... Entries>\n\tconstexpr ConstMap(Entries &&...entries) :\n\t\tvalues{std::forward<Entries>(entries)...} {\n\t\tthis->verify_no_duplicates();\n\t}\n\n\t/**\n\t * Return the number of entries in this map.\n\t */\n\tconstexpr int size() const {\n\t\treturn count;\n\t}\n\n\t/**\n\t * Tests if the given key is in this map.\n\t */\n\tconstexpr bool contains(const K &key) const {\n\t\treturn this->find(key) != values.end();\n\t}\n\n\t/**\n\t * Return the stored value for the given key.\n\t */\n\tconstexpr const V &get(const K &key) const {\n\t\tauto iter = this->find(key);\n\t\tif (iter == values.end()) {\n\t\t\tthrow Error(MSG(err) << \"The key is not in the map.\");\n\t\t}\n\n\t\treturn iter->second;\n\t}\n\n\t/**\n\t * Access entries by map[key].\n\t */\n\tconstexpr const V &operator[](const K &key) const {\n\t\treturn this->get(key);\n\t}\n\nprivate:\n\t/**\n\t * Abort when the map uses the same key more than once.\n\t */\n\tconstexpr void verify_no_duplicates() const {\n\t\tfor (auto iter1 = values.begin(); iter1 != values.end(); ++iter1) {\n\t\t\tfor (auto iter2 = iter1 + 1; iter2 != values.end(); ++iter2) {\n\t\t\t\tif (iter1->first == iter2->first) {\n\t\t\t\t\tthrow Error(MSG(err) << \"There is a duplicate key in the map.\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Returns the iterator matching key from the array.\n\t *  - `values.end()` if the key is not found.\n\t */\n\tconstexpr auto find(const K &key) const {\n\t\tfor (auto iter = values.begin(); iter != values.end(); ++iter) {\n\t\t\tif (iter->first == key) {\n\t\t\t\treturn iter;\n\t\t\t}\n\t\t}\n\t\treturn values.end();\n\t}\n\n\t/**\n\t * The entries associated with this map.\n\t */\n\tstd::array<std::pair<K, V>, count> values;\n};\n\n\n/**\n * Creates a compiletime lookup table from\n * K to V, where all entries of K must be unique.\n *\n * usage: constexpr auto bla = create_const_map<type0, type1>(entry0, entry1, ...);\n */\ntemplate <typename K, typename V, typename... Entries>\nconstexpr auto create_const_map(Entries &&...entry) {\n\treturn ConstMap<K, V, sizeof...(entry)>{entry...};\n}\n\n/**\n * Template deduction guide to deduce the Key-Value types\n * for the ConstMap from the paired entries passed.\n *\n * usage: constexpr ConstMap boss{std::pair{k0, v0}, std::pair{k1, v1}, ...};\n *\n * Note: Use when automatic type deduction is desirable.\n *       For manually specifying types, use the other method.\n */\ntemplate <typename Entry, typename... Rest>\n\trequires std::conjunction_v<std::is_same<Entry, Rest>...>\nConstMap(Entry, Rest &&...) -> ConstMap<typename Entry::first_type,\n                                        typename Entry::second_type,\n                                        1 + sizeof...(Rest)>;\n} // namespace openage::datastructure\n"
  },
  {
    "path": "libopenage/datastructure/pairing_heap.h",
    "content": "// Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/** @file\n * This file contains the implementation of a pairing heap.\n * It is a priority queue with near-optimal runtime.\n *\n * The main advantage over the STL heap is the presence\n * of the decrease_key operation.\n *\n * Literature:\n *\n * Fredman, Michael L., Robert Sedgewick, Daniel D. Sleator, and Robert\n * E. Tarjan. \"The pairing heap: A new form of self-adjusting heap.\"\n * Algorithmica 1, no. 1-4 (1986): 111-129.\n */\n\n#include <functional>\n#include <iterator>\n#include <memory>\n#include <type_traits>\n#include <unordered_set>\n\n#include \"../error/error.h\"\n#include \"../util/compiler.h\"\n\n\n#define OPENAGE_PAIRINGHEAP_DEBUG false\n\n\nnamespace openage::datastructure {\n\n\ntemplate <typename T,\n          typename compare,\n          typename heapnode_t>\nclass PairingHeap;\n\ntemplate <typename T, typename compare>\nclass PairingHeapIterator;\n\n\ntemplate <typename T, typename compare = std::less<T>>\nclass PairingHeapNode {\npublic:\n\tusing this_type = PairingHeapNode<T, compare>;\n\n\tfriend PairingHeap<T, compare, this_type>;\n\tfriend PairingHeapIterator<T, compare>;\n\n\tT data;\n\tcompare cmp;\n\n\tPairingHeapNode(const T &data) :\n\t\tdata{data} {}\n\n\tPairingHeapNode(T &&data) :\n\t\tdata{std::move(data)} {}\n\n\t~PairingHeapNode() = default;\n\n\tPairingHeapNode(const this_type &other) = delete;\n\n\tthis_type &operator=(const this_type &other) = delete;\n\n\t/**\n\t * Get contained node data.\n\t */\n\tconst T &get_data() const {\n\t\treturn this->data;\n\t}\n\n\t/**\n\t * Let this node become a child of the given one.\n\t */\n\tvoid become_child_of(this_type *const node) {\n\t\tnode->add_child(this);\n\t}\n\n\t/**\n\t * Add the given node as a child to this one.\n\t */\n\tvoid add_child(this_type *const new_child) {\n\t\t// first child is the most recently attached one\n\t\t// it must not have siblings as they will get lost.\n\n\t\tnew_child->prev_sibling = nullptr;\n\t\tnew_child->next_sibling = this->first_child;\n\n\t\tif (this->first_child != nullptr) {\n\t\t\tthis->first_child->prev_sibling = new_child;\n\t\t}\n\n\t\tthis->first_child = new_child;\n\t\tnew_child->parent = this;\n\t}\n\n\t/**\n\t * This method decides which node becomes the new root node\n\t * by comparing `this` with `node`.\n\t * The new root is returned, it has the other node as child.\n\t */\n\tthis_type *link_with(this_type *const node) {\n\t\tthis_type *new_root;\n\t\tthis_type *new_child;\n\n\t\tif (this->cmp(this->data, node->data)) {\n\t\t\tnew_root = this;\n\t\t\tnew_child = node;\n\t\t}\n\t\telse {\n\t\t\tnew_root = node;\n\t\t\tnew_child = this;\n\t\t}\n\n\t\t// children of new root become siblings of new new_child\n\t\t// -> parent of new child = new root\n\n\t\t// this will be set by the add_child method\n\t\tnew_child->prev_sibling = nullptr;\n\t\tnew_child->next_sibling = nullptr;\n\n\t\t// this is then set to the previous pair root:\n\t\tnew_root->prev_sibling = nullptr;\n\t\tnew_root->next_sibling = nullptr;\n\n\t\t// set up the child\n\t\tnew_root->add_child(new_child);\n\n\t\treturn new_root;\n\t}\n\n\t/**\n\t * Link all siblings backwards from right to left.\n\t * Recursive call, one stage for each all childs of the root node.\n\t * This results in the computation of the new subtree root.\n\t */\n\tthis_type *link_backwards() {\n\t\tif (this->next_sibling == nullptr) {\n\t\t\t// reached end, return this as current root,\n\t\t\t// the previous siblings will be linked to it.\n\t\t\treturn this;\n\t\t}\n\n\t\t// recurse to last sibling,\n\t\tthis_type *node = this->next_sibling->link_backwards();\n\n\t\t// then link ourself to the new root.\n\t\tthis->next_sibling = nullptr;\n\t\tthis->prev_sibling = nullptr;\n\t\tnode->next_sibling = nullptr;\n\t\tnode->prev_sibling = nullptr;\n\t\treturn this->link_with(node);\n\t}\n\n\t/**\n\t * Cut this node from all parent and sibling connections,\n\t * but keeps the child pointer.\n\t * This effectively cuts out the subtree.\n\t */\n\tvoid loosen() {\n\t\t// release us from some other node\n\t\tif (this->parent and this->parent->first_child == this) {\n\t\t\t// we are child\n\t\t\t// make the next sibling child\n\t\t\tthis->parent->first_child = this->next_sibling;\n\t\t}\n\t\t// if we have a previous sibling\n\t\tif (this->prev_sibling != nullptr) {\n\t\t\t// set its next sibling to skip us.\n\t\t\tthis->prev_sibling->next_sibling = this->next_sibling;\n\t\t}\n\t\t// if we have a next sibling\n\t\tif (this->next_sibling != nullptr) {\n\t\t\t// tell its previous sibling to skip us.\n\t\t\tthis->next_sibling->prev_sibling = this->prev_sibling;\n\t\t}\n\n\t\t// reset sibling and parent ptrs.\n\t\tthis->prev_sibling = nullptr;\n\t\tthis->next_sibling = nullptr;\n\t\tthis->parent = nullptr;\n\t}\n\nprivate:\n\tthis_type *first_child = nullptr;\n\tthis_type *prev_sibling = nullptr;\n\tthis_type *next_sibling = nullptr;\n\tthis_type *parent = nullptr; // for decrease-key and delete\n};\n\n\n/**\n * @brief Iterator class for PairingHeap.\n *\n * This class provides a bidirectional iterator for the PairingHeap data structure.\n * It allows traversal of the heap in both forward and backward directions.\n * It is depth-first traversal.\n *\n * @tparam T The type of elements stored in the heap.\n * @tparam compare The comparison functor used to order the elements.\n */\ntemplate <typename T, typename compare = std::less<T>>\nclass PairingHeapIterator {\npublic:\n\tusing iterator_category = std::bidirectional_iterator_tag;\n\tusing value_type = T;\n\tusing difference_type = std::ptrdiff_t;\n\tusing pointer = T *;\n\tusing reference = T &;\n\n\t/**\n\t * @brief Constructs an iterator starting at the given node.\n\t *\n\t * @param node The starting node for the iterator.\n\t */\n\tPairingHeapIterator(PairingHeapNode<T, compare> *node) :\n\t\tcurrent(node) {}\n\n\t/**\n\t * @brief Dereference operator.\n\t *\n\t * @return A reference to the data stored in the current node.\n\t */\n\treference operator*() const {\n\t\treturn current->data;\n\t}\n\n\t/**\n\t * @brief Member access operator.\n\t *\n\t * @return A pointer to the data stored in the current node.\n\t */\n\tpointer operator->() const {\n\t\treturn &(current->data);\n\t}\n\n\n\t/**\n\t * @brief Get current node.\n\t *\n\t * @return The current node.\n\t */\n\tPairingHeapNode<T, compare> *node() {\n\t\treturn current;\n\t}\n\n\n\t/**\n\t * @brief Pre-increment operator.\n\t *\n\t * Moves the iterator to the next node in the heap.\n\t *\n\t * @return A reference to the incremented iterator.\n\t */\n\tPairingHeapIterator &operator++() {\n\t\tif (current->first_child) {\n\t\t\tcurrent = current->first_child;\n\t\t}\n\t\telse if (current->next_sibling) {\n\t\t\tcurrent = current->next_sibling;\n\t\t}\n\t\telse {\n\t\t\twhile (current->parent && !current->parent->next_sibling) {\n\t\t\t\tcurrent = current->parent;\n\t\t\t}\n\t\t\tif (current->parent) {\n\t\t\t\tcurrent = current->parent->next_sibling;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tcurrent = nullptr;\n\t\t\t}\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * @brief Post-increment operator.\n\t *\n\t * Moves the iterator to the next node in the heap.\n\t *\n\t * @return A copy of the iterator before incrementing.\n\t */\n\tPairingHeapIterator operator++(int) {\n\t\tPairingHeapIterator tmp = *this;\n\t\t++(*this);\n\t\treturn tmp;\n\t}\n\n\t/**\n\t * @brief Pre-decrement operator.\n\t *\n\t * Moves the iterator to the previous node in the heap.\n\t *\n\t * @return A reference to the decremented iterator.\n\t */\n\tPairingHeapIterator &operator--() {\n\t\tif (current->prev_sibling) {\n\t\t\tcurrent = current->prev_sibling;\n\t\t\twhile (current->first_child) {\n\t\t\t\tcurrent = current->first_child;\n\t\t\t\twhile (current->next_sibling) {\n\t\t\t\t\tcurrent = current->next_sibling;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse if (current->parent) {\n\t\t\tcurrent = current->parent;\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * @brief Post-decrement operator.\n\t *\n\t * Moves the iterator to the previous node in the heap.\n\t *\n\t * @return A copy of the iterator before decrementing.\n\t */\n\tPairingHeapIterator operator--(int) {\n\t\tPairingHeapIterator tmp = *this;\n\t\t--(*this);\n\t\treturn tmp;\n\t}\n\n\t/**\n\t * @brief Equality comparison operator.\n\t *\n\t * @param a The first iterator to compare.\n\t * @param b The second iterator to compare.\n\t * @return True if both iterators point to the same node, false otherwise.\n\t */\n\tfriend bool operator==(const PairingHeapIterator &a, const PairingHeapIterator &b) {\n\t\treturn a.current == b.current;\n\t}\n\n\t/**\n\t * @brief Inequality comparison operator.\n\t *\n\t * @param a The first iterator to compare.\n\t * @param b The second iterator to compare.\n\t * @return True if the iterators point to different nodes, false otherwise.\n\t */\n\tfriend bool operator!=(const PairingHeapIterator &a, const PairingHeapIterator &b) {\n\t\treturn a.current != b.current;\n\t}\n\nprivate:\n\tPairingHeapNode<T, compare> *current; ///< Pointer to the current node in the heap.\n};\n\n\n/**\n * (Quite) efficient heap implementation.\n */\ntemplate <typename T,\n          typename compare = std::less<T>,\n          typename heapnode_t = PairingHeapNode<T, compare>>\nclass PairingHeap final {\npublic:\n\tusing element_t = heapnode_t *;\n\tusing this_type = PairingHeap<T, compare, heapnode_t>;\n\tusing iterator = PairingHeapIterator<T, compare>;\n\n\t/**\n\t * create a empty heap.\n\t */\n\tPairingHeap() :\n\t\tnode_count(0),\n\t\troot_node(nullptr) {\n\t}\n\n\t~PairingHeap() {\n\t\tthis->clear();\n\t};\n\n\t/**\n\t * adds the given item to the heap.\n\t * O(1)\n\t */\n\telement_t push(const T &item) {\n\t\telement_t new_node = new heapnode_t(item);\n\t\tthis->push_node(new_node);\n\t\treturn new_node;\n\t}\n\n\t/**\n\t * moves the given item to the heap.\n\t * O(1)\n\t */\n\telement_t push(T &&item) {\n\t\telement_t new_node = new heapnode_t(std::move(item));\n\t\tthis->push_node(new_node);\n\t\treturn new_node;\n\t}\n\n\t/**\n\t * returns the smallest item on the heap and deletes it.\n\t * also known as delete_min.\n\t *                       _________\n\t * Ω(log log n), O(2^(2*√log log n'))\n\t */\n\tT pop() {\n\t\tif (this->root_node == nullptr) {\n\t\t\tthrow Error{MSG(err) << \"Can't pop an empty heap!\"};\n\t\t}\n\n\t\t// 0. remove tree root, it's the minimum.\n\t\telement_t ret = this->root_node;\n\t\telement_t current_sibling = this->root_node->first_child;\n\t\tthis->root_node = nullptr;\n\n\t\t// 1. link root children pairwise, last node may be alone\n\t\telement_t first_pair = nullptr;\n\t\telement_t previous_pair = nullptr;\n\n\t\twhile (current_sibling != nullptr) [[unlikely]] {\n\t\t\telement_t link0 = current_sibling;\n\t\t\telement_t link1 = current_sibling->next_sibling;\n\n\t\t\t// pair link0 and link1\n\t\t\tif (link1 != nullptr) {\n\t\t\t\t// get the first sibling for next pair, just in advance.\n\t\t\t\tcurrent_sibling = link1->next_sibling;\n\n\t\t\t\t// do the link: merges two nodes, smaller one = root.\n\t\t\t\telement_t link_root = link0->link_with(link1);\n\t\t\t\tlink_root->parent = nullptr;\n\n\t\t\t\tif (previous_pair == nullptr) {\n\t\t\t\t\t// this was the first pair\n\t\t\t\t\tfirst_pair = link_root;\n\t\t\t\t\tfirst_pair->prev_sibling = nullptr;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// store node as next sibling in previous pair\n\t\t\t\t\tprevious_pair->next_sibling = link_root;\n\t\t\t\t\tlink_root->prev_sibling = previous_pair;\n\t\t\t\t}\n\n\t\t\t\tprevious_pair = link_root;\n\t\t\t\tlink_root->next_sibling = nullptr;\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// link0 is the last and unpaired root child.\n\t\t\t\tlink0->parent = nullptr;\n\t\t\t\tif (previous_pair == nullptr) {\n\t\t\t\t\t// link0 was the only node\n\t\t\t\t\tfirst_pair = link0;\n\t\t\t\t\tlink0->prev_sibling = nullptr;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tprevious_pair->next_sibling = link0;\n\t\t\t\t\tlink0->prev_sibling = previous_pair;\n\t\t\t\t}\n\t\t\t\tlink0->next_sibling = nullptr;\n\t\t\t\tcurrent_sibling = nullptr;\n\t\t\t}\n\t\t}\n\n\n\t\t// 2. then link remaining trees to the last one, from right to left\n\t\tif (first_pair != nullptr) {\n\t\t\tthis->root_node = first_pair->link_backwards();\n\t\t}\n\n\t\tthis->node_count -= 1;\n\n#if OPENAGE_PAIRINGHEAP_DEBUG\n\t\tif (1 != this->nodes.erase(ret)) {\n\t\t\tthrow Error{ERR << \"didn't remove node\"};\n\t\t}\n#endif\n\n\t\t// (to find those two lines, 14h of debugging passed)\n\t\tret->loosen();\n\t\tret->first_child = nullptr;\n\n\t\t// and it's done!\n\t\tT data = std::move(ret->data);\n\t\tdelete ret;\n\t\treturn data;\n\t}\n\n\t/**\n\t * Returns the smallest item on the heap.\n\t * O(1)\n\t */\n\tconst T &top() const {\n\t\treturn this->root_node->get_data();\n\t}\n\n\t/**\n\t * Returns the smallest node on the heap.\n\t *\n\t * O(1)\n\t */\n\tconst element_t &top_node() const {\n\t\treturn this->root_node;\n\t}\n\n\t/**\n\t * You must call this after the node data decreased.\n\t * This cuts the subtree and links the subtree again.\n\t * If the node value _increased_ and you call this,\n\t * the heap is corrupted.\n\t * Also known as the decrease_key operation.\n\t *\n\t * O(1)\n\t */\n\tvoid decrease(const element_t &node) {\n\t\tif (node != this->root_node) [[likely]] {\n\t\t\t// cut out the node and its subtree\n\t\t\tnode->loosen();\n\t\t\tthis->root_node = node->link_with(this->root_node);\n\t\t}\n\t\t// decreasing the root node won't change it, so we do nothing.\n\t}\n\n\t/**\n\t * After a change, call this to reorganize the given node.\n\t * Support increase and decrease of values.\n\t *\n\t * Use `decrease` instead when you know the value decreased.\n\t *\n\t * O(1) (but slower than decrease), and O(pop) when node is the root.\n\t */\n\tvoid update(element_t &node) {\n\t\tif (node != this->root_node) [[likely]] {\n\t\t\tnode = this->push(this->remove_node(node));\n\t\t}\n\t\telse {\n\t\t\t// it's the root node, so we just pop and push it.\n\t\t\tnode = this->push(this->pop());\n\t\t}\n\t}\n\n\t/**\n\t * remove a node from the heap. Return its data.\n\t *\n\t * If the item is the current root, just pop().\n\t * else, cut the node from its parent, pop() that subtree\n\t * and merge these trees.\n\t *\n\t * O(pop_node)\n\t */\n\tT remove_node(const element_t &node) {\n\t\tif (node == this->root_node) {\n\t\t\treturn this->pop();\n\t\t}\n\t\telse {\n\t\t\tnode->loosen();\n\n\t\t\telement_t real_root = this->root_node;\n\t\t\tthis->root_node = node;\n\t\t\tT data = this->pop();\n\n\t\t\telement_t new_root = this->root_node;\n\t\t\tthis->root_node = real_root;\n\n\t\t\tif (new_root != nullptr) {\n\t\t\t\tthis->root_insert(new_root);\n\t\t\t}\n\t\t\treturn data;\n\t\t}\n\t}\n\n\t/**\n\t * erase all elements on the heap.\n\t */\n\tvoid clear() {\n\t\tstd::vector<element_t> to_delete;\n\t\tto_delete.reserve(this->size());\n\n\t\t// collect all node pointers to delete\n\t\tfor (iterator it = this->begin(); it != this->end(); it++) {\n\t\t\tto_delete.push_back(it.node());\n\t\t}\n\n\t\t// delete all nodes\n\t\tfor (element_t node : to_delete) {\n\t\t\tdelete node;\n\t\t}\n\n\t\t// reset heap state to empty\n\t\tthis->root_node = nullptr;\n\t\tthis->node_count = 0;\n#if OPENAGE_PAIRINGHEAP_DEBUG\n\t\t// clear the node set for debugging\n\t\tthis->nodes.clear();\n#endif\n\t}\n\n\t/**\n\t * @returns the number of nodes stored on the heap.\n\t */\n\tsize_t size() const {\n\t\treturn this->node_count;\n\t}\n\n\t/**\n\t * @returns whether there are no nodes stored on the heap.\n\t */\n\tbool empty() const {\n\t\treturn this->node_count == 0;\n\t}\n\n#if OPENAGE_PAIRINGHEAP_DEBUG\n\t/**\n\t * verify the consistency of the heap.\n\t * - check if each node is only present once.\n\t * - check if the nodes are referenced at their correct place only\n\t */\n\tstd::unordered_set<element_t> check_consistency() const {\n\t\tstd::unordered_set<element_t> found_nodes;\n\n\t\tauto func = [&](const element_t &root) {\n\t\t\tif (not root) {\n\t\t\t\tthrow Error{ERR << \"test function called with nullptr node\"};\n\t\t\t}\n\n\t\t\tif (found_nodes.find(root) == std::end(found_nodes)) {\n\t\t\t\tfound_nodes.insert(root);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow Error{ERR << \"encountered node twice\"};\n\t\t\t}\n\n\t\t\tif (this->node_count != this->nodes.size()) {\n\t\t\t\tthrow Error{ERR << \"node count fail\"};\n\t\t\t}\n\n\t\t\tif (root->next_sibling) {\n\t\t\t\tif (not root->next_sibling->prev_sibling) {\n\t\t\t\t\tthrow Error{ERR << \"node has next sibling, which has no prev sibling\"};\n\t\t\t\t}\n\t\t\t\tif (root->next_sibling->prev_sibling != root) {\n\t\t\t\t\tthrow Error{ERR << \"node not referenced by next.prev\"};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (root->prev_sibling) {\n\t\t\t\tif (not root->prev_sibling->next_sibling) {\n\t\t\t\t\tthrow Error{ERR << \"node has prev sibling, which has no next sibling\"};\n\t\t\t\t}\n\t\t\t\tif (root->prev_sibling->next_sibling != root) {\n\t\t\t\t\tthrow Error{ERR << \"node not referenced by prev.next\"};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (root->first_child) {\n\t\t\t\tif (root->first_child == root->next_sibling) {\n\t\t\t\t\tthrow Error{ERR << \"first_child is next_sibling\"};\n\t\t\t\t}\n\t\t\t\tif (root->first_child == root->prev_sibling) {\n\t\t\t\t\tthrow Error{ERR << \"first_child is prev_sibling\"};\n\t\t\t\t}\n\t\t\t\tif (root->first_child == root->parent) {\n\t\t\t\t\tthrow Error{ERR << \"first_child is parent\"};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (root->parent) {\n\t\t\t\tif (found_nodes.find(root->parent) == std::end(found_nodes)) {\n\t\t\t\t\tthrow Error{ERR << \"parent node is not known\"};\n\t\t\t\t}\n\t\t\t\telement_t child = root->parent->first_child;\n\t\t\t\telement_t lastchild;\n\n\t\t\t\tbool foundvianext = false, foundviaprev = false;\n\t\t\t\tstd::unordered_set<element_t> loopprotect;\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (not child) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (loopprotect.find(child) == std::end(loopprotect)) {\n\t\t\t\t\t\tloopprotect.insert(child);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow Error{ERR << \"child reencountered when walking forward\"};\n\t\t\t\t\t}\n\n\t\t\t\t\tif (child == root) {\n\t\t\t\t\t\tfoundvianext = true;\n\t\t\t\t\t}\n\n\t\t\t\t\t// if both are equal, cmp will still be false.\n\t\t\t\t\tif (this->cmp(child->data, root->parent->data)) {\n\t\t\t\t\t\tthrow Error{ERR << \"tree invariant violated\"};\n\t\t\t\t\t}\n\n\t\t\t\t\tlastchild = child;\n\t\t\t\t\tchild = child->next_sibling;\n\t\t\t\t}\n\n\t\t\t\tloopprotect.clear();\n\t\t\t\twhile (true) {\n\t\t\t\t\tif (not lastchild) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (loopprotect.find(lastchild) == std::end(loopprotect)) {\n\t\t\t\t\t\tloopprotect.insert(lastchild);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow Error{ERR << \"child reencountered when walking back\"};\n\t\t\t\t\t}\n\n\t\t\t\t\tif (lastchild == root) {\n\t\t\t\t\t\tfoundviaprev = true;\n\t\t\t\t\t}\n\n\t\t\t\t\tlastchild = lastchild->prev_sibling;\n\t\t\t\t}\n\n\t\t\t\tif (not foundvianext and not foundviaprev) {\n\t\t\t\t\tthrow Error{ERR << \"node not found as parent's child at all\"};\n\t\t\t\t}\n\t\t\t\telse if (not foundvianext) {\n\t\t\t\t\tthrow Error{ERR << \"node not found via parent's next childs\"};\n\t\t\t\t}\n\t\t\t\telse if (not foundviaprev) {\n\t\t\t\t\tthrow Error{ERR << \"node not found via parent's prev childs\"};\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tif (this->root_node) {\n\t\t\tthis->walk_tree(this->root_node, func);\n\n\t\t\tif (found_nodes.size() != this->size()) {\n\t\t\t\tfor (auto &it : found_nodes) {\n\t\t\t\t\tconst element_t &elem = it;\n\t\t\t\t\tif (this->nodes.find(elem) == std::end(this->nodes)) {\n\t\t\t\t\t\tthrow Error{ERR << \"node not recorded but found\"};\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (auto &it : this->nodes) {\n\t\t\t\t\tconst element_t &elem = it;\n\t\t\t\t\tif (found_nodes.find(elem) == std::end(found_nodes)) {\n\t\t\t\t\t\tthrow Error{ERR << \"node recorded but not found\"};\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthrow Error{ERR << \"node count inconsistent\"};\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (not this->empty()) {\n\t\t\t\tthrow Error{ERR << \"root missing but heap not empty\"};\n\t\t\t}\n\t\t}\n\n\t\treturn found_nodes;\n\t}\n#endif\n\n\t/**\n\t * Apply the given function to all nodes in the tree.\n\t *\n\t * @tparam reverse If true, the function is applied to the nodes in reverse order.\n\t * @param func Function to apply to each node.\n\t */\n\ttemplate <bool reverse = false>\n\tvoid iter_all(const std::function<void(const element_t &)> &func) const {\n\t\tthis->walk_tree<reverse>(this->root_node, func);\n\t}\n\n\titerator begin() const {\n\t\treturn iterator(this->root_node);\n\t}\n\n\titerator end() const {\n\t\treturn iterator(nullptr);\n\t}\n\nprivate:\n\t/**\n\t * Apply the given function to all nodes in the tree.\n\t *\n\t * @tparam reverse If true, the function is applied to the nodes in reverse order.\n\t * @param start Starting node.\n\t * @param func Function to apply to each node.\n\t */\n\ttemplate <bool reverse = false>\n\tvoid walk_tree(const element_t &start,\n\t               const std::function<void(const element_t &)> &func) const {\n\t\tif constexpr (not reverse) {\n\t\t\tfunc(start);\n\t\t}\n\n\t\tif (start) {\n\t\t\tauto node = start->first_child;\n\t\t\twhile (true) {\n\t\t\t\tif (not node) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthis->walk_tree<reverse>(node, func);\n\t\t\t\tnode = node->next_sibling;\n\t\t\t}\n\t\t\tif constexpr (reverse) {\n\t\t\t\tfunc(start);\n\t\t\t}\n\t\t}\n\t}\n\n\n\t/**\n\t * adds the given node to the heap.\n\t * use this if the node was not in the heap before.\n\t * O(1)\n\t */\n\tvoid push_node(const element_t &node) {\n\t\tthis->root_insert(node);\n#if OPENAGE_PAIRINGHEAP_DEBUG\n\t\tauto [iter, result] = this->nodes.insert(node);\n\t\tif (not result) {\n\t\t\tthrow Error{ERR << \"node already known\"};\n\t\t}\n#endif\n\n\t\tthis->node_count += 1;\n\t}\n\n\t/**\n\t * insert a node into the heap.\n\t */\n\tvoid root_insert(const element_t &node) {\n\t\tif (this->root_node == nullptr) [[unlikely]] {\n\t\t\tthis->root_node = node;\n\t\t}\n\t\telse {\n\t\t\tthis->root_node = this->root_node->link_with(node);\n\t\t}\n\t}\n\n\tcompare cmp;\n\tsize_t node_count;\n\telement_t root_node;\n\n#if OPENAGE_PAIRINGHEAP_DEBUG\n\tstd::unordered_set<element_t> nodes;\n#endif\n};\n\n} // namespace openage::datastructure\n"
  },
  {
    "path": "libopenage/datastructure/tests.cpp",
    "content": "// Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n#include \"tests.h\"\n\n#include <algorithm>\n#include <memory>\n#include <mutex>\n#include <utility>\n\n#include \"testing/testing.h\"\n\n#include \"datastructure/concurrent_queue.h\"\n#include \"datastructure/constexpr_map.h\"\n#include \"datastructure/pairing_heap.h\"\n\n\nnamespace openage::datastructure::tests {\n\n\nvoid pairing_heap_0() {\n\tPairingHeap<int> heap{};\n\n\t(heap.size() == 0) or TESTFAIL;\n\n\theap.push(0);\n\theap.push(1);\n\theap.push(2);\n\theap.push(3);\n\theap.push(4);\n\n\t// state: 01234\n\tTESTEQUALS(heap.size(), 5);\n\tTESTEQUALS(heap.top(), 0);\n\n\tTESTEQUALS(heap.pop(), 0);\n\tTESTEQUALS(heap.pop(), 1);\n\tTESTEQUALS(heap.pop(), 2);\n\tTESTEQUALS(heap.pop(), 3);\n\n\tTESTEQUALS(heap.size(), 1);\n\n\theap.push(0);\n\theap.push(10);\n\n\t// state: 0 4 10\n\n\tTESTEQUALS(heap.pop(), 0);\n\tTESTEQUALS(heap.pop(), 4);\n\tTESTEQUALS(heap.pop(), 10);\n\tTESTEQUALS(heap.size(), 0);\n\n\theap.push(5);\n\theap.push(5);\n\theap.push(0);\n\theap.push(5);\n\theap.push(5);\n\tTESTEQUALS(heap.pop(), 0);\n\tTESTEQUALS(heap.pop(), 5);\n\tTESTEQUALS(heap.pop(), 5);\n\tTESTEQUALS(heap.pop(), 5);\n\tTESTEQUALS(heap.pop(), 5);\n\n\tTESTEQUALS(heap.size(), 0);\n}\n\n\nvoid pairing_heap_1() {\n\tPairingHeap<heap_elem> heap{};\n\theap.push(heap_elem{1});\n\tauto node_u1 = heap.push(heap_elem{2});\n\theap.push(heap_elem{3});\n\n\t// 1 2 3\n\tnode_u1->data.data = 0;\n\theap.decrease(node_u1);\n\n\t// 0 1 3\n\tTESTEQUALS(heap.pop().data, 0);\n\tTESTEQUALS(heap.pop().data, 1);\n\n\t// 3\n\theap.push(heap_elem{4});\n\theap.push(heap_elem{0});\n\theap.push(heap_elem{2});\n\theap.push(heap_elem{5});\n\theap.push(heap_elem{1});\n\theap.push(heap_elem{6});\n\tauto node_u2 = heap.push(heap_elem{7});\n\theap.push(heap_elem{8});\n\theap.push(heap_elem{9});\n\theap.push(heap_elem{10});\n\n\tTESTEQUALS(heap.pop().data, 0);\n\n\tTESTEQUALS(heap.pop().data, 1);\n\n\t// now update the 7-node to 8\n\tnode_u2->data.data = 8;\n\theap.update(node_u2);\n\n\t// 2 3 4 5 6 8 8 9 10\n\tTESTEQUALS(heap.pop().data, 2);\n\tTESTEQUALS(heap.pop().data, 3);\n\tTESTEQUALS(heap.pop().data, 4);\n\tTESTEQUALS(heap.pop().data, 5);\n\tTESTEQUALS(heap.pop().data, 6);\n\tTESTEQUALS(heap.pop().data, 8);\n\tTESTEQUALS(heap.pop().data, 8);\n\tTESTEQUALS(heap.pop().data, 9);\n\tTESTEQUALS(heap.pop().data, 10);\n}\n\n\nvoid pairing_heap_2() {\n\tPairingHeap<heap_elem> heap{};\n\theap.push(heap_elem{1});\n\tauto node = heap.push(heap_elem{2});\n\theap.push(heap_elem{3});\n\n\t// state: 1 2 3, now remove 2\n\tauto data = heap.remove_node(node);\n\tTESTEQUALS(data.data, 2);\n\n\t// state: 1 3\n\tTESTEQUALS(heap.pop().data, 1);\n\tTESTEQUALS(heap.pop().data, 3);\n}\n\n\nvoid pairing_heap_3() {\n\tPairingHeap<heap_elem> heap{};\n\theap.push(heap_elem{0});\n\theap.push(heap_elem{1});\n\theap.push(heap_elem{2});\n\theap.push(heap_elem{3});\n\theap.push(heap_elem{4});\n\theap.push(heap_elem{5});\n\n\tsize_t i = 0;\n\tstd::array<int, 6> expected{0, 5, 4, 3, 2, 1};\n\tfor (auto &elem : heap) {\n\t\tTESTEQUALS(elem.data, expected.at(i));\n\t\ti++;\n\t}\n\n\theap.pop(); // trigger pairing\n\n\theap.clear();\n\n\tTESTEQUALS(heap.size(), 0);\n\tTESTEQUALS(heap.empty(), true);\n}\n\n\n// exported test\nvoid pairing_heap() {\n\tpairing_heap_0();\n\tpairing_heap_1();\n\tpairing_heap_2();\n\tpairing_heap_3();\n}\n\n\n// exported test\nvoid constexpr_map() {\n\tstatic_assert(create_const_map<int, int>().size() == 0, \"wrong size\");\n\tstatic_assert(create_const_map<int, int>(std::make_pair(0, 42)).size() == 1,\n\t              \"wrong size\");\n\tstatic_assert(create_const_map<int, int>(std::make_pair(0, 42),\n\t                                         std::make_pair(13, 37))\n\t                      .size()\n\t                  == 2,\n\t              \"wrong size\");\n\n\tstatic_assert(not create_const_map<int, int>().contains(9001),\n\t              \"empty map doesn't contain anything\");\n\tstatic_assert(create_const_map<int, int>(std::make_pair(42, 0),\n\t                                         std::make_pair(13, 37))\n\t                  .contains(42),\n\t              \"contained element missing\");\n\tstatic_assert(create_const_map<int, int>(std::make_pair(42, 0),\n\t                                         std::make_pair(13, 37))\n\t                  .contains(13),\n\t              \"contained element missing\");\n\tstatic_assert(not create_const_map<int, int>(std::make_pair(42, 0),\n\t                                             std::make_pair(13, 37))\n\t                      .contains(9001),\n\t              \"uncontained element seems to be contained.\");\n\n\tstatic_assert(create_const_map<int, int>(std::make_pair(42, 9001),\n\t                                         std::make_pair(13, 37))\n\t                      .get(42)\n\t                  == 9001,\n\t              \"fetched wrong value\");\n\tstatic_assert(create_const_map<int, int>(std::make_pair(42, 9001),\n\t                                         std::make_pair(13, 37))\n\t                      .get(13)\n\t                  == 37,\n\t              \"fetched wrong value\");\n\n\tconstexpr ConstMap cmap{\n\t\tstd::pair(0, 0),\n\t\tstd::pair(13, 37),\n\t\tstd::pair(42, 9001)};\n\n\tcmap.contains(0) or TESTFAIL;\n\tnot cmap.contains(18) or TESTFAIL;\n\n\tTESTEQUALS(cmap.size(), 3);\n\tTESTEQUALS(cmap.get(13), 37);\n\tTESTEQUALS(cmap.get(42), 9001);\n}\n\n/**\n * A simple class that can be move-constructed but not copy-constructed\n */\nclass MoveOnly {\nprivate:\n\tint data;\n\n\tint release() {\n\t\tint ret = this->data;\n\t\tthis->data = 0;\n\t\treturn ret;\n\t}\n\npublic:\n\tMoveOnly(int data) :\n\t\tdata(data) {}\n\tMoveOnly(const MoveOnly &) = delete;\n\tMoveOnly(MoveOnly &&other) :\n\t\tdata(other.release()) {}\n\t~MoveOnly() {}\n\n\tint get() const {\n\t\treturn this->data;\n\t}\n};\n\n/**\n * A simple class that can be copy-constructed but not move-constructed\n */\nclass CopyOnly {\nprivate:\n\tint data;\n\npublic:\n\tCopyOnly(int data) :\n\t\tdata(data) {}\n\tCopyOnly(const CopyOnly &other) :\n\t\tdata(other.get()) {}\n\tCopyOnly(CopyOnly &&) = delete;\n\t~CopyOnly() {}\n\n\tint get() const {\n\t\treturn this->data;\n\t}\n};\n\n/**\n * A simple class that can be both copy-constructed and move-constructed\n */\nclass CopyMove {\nprivate:\n\tint data;\n\n\tstatic std::mutex s_mutex;\n\tstatic int s_accumulatedConstructionCounter;\n\n\tint release() {\n\t\tint ret = this->data;\n\t\tthis->data = 0;\n\t\treturn ret;\n\t}\n\n\tvoid atomic_inc() {\n\t\tstd::scoped_lock lock(s_mutex);\n\t\ts_accumulatedConstructionCounter++;\n\t}\n\npublic:\n\tCopyMove(int data) :\n\t\tdata(data) {\n\t\tatomic_inc();\n\t}\n\n\tCopyMove(const CopyMove &other) :\n\t\tdata(other.get()) {\n\t\tatomic_inc();\n\t}\n\n\t// Move constructor does not increase global counter\n\tCopyMove(CopyMove &&other) :\n\t\tdata(other.release()) {}\n\n\t~CopyMove() {}\n\n\tint get() const {\n\t\treturn this->data;\n\t}\n\n\tstatic int get_accumulated_construction_counter() {\n\t\treturn CopyMove::s_accumulatedConstructionCounter;\n\t}\n\n\tstatic void reset_accumulated_construction_counter() {\n\t\tCopyMove::s_accumulatedConstructionCounter = 0;\n\t}\n};\n\nstd::mutex CopyMove::s_mutex = std::mutex();\nint CopyMove::s_accumulatedConstructionCounter = 0;\n\nvoid concurrent_queue_copy_only_elements_compilation() {\n\tconst int test_data = 157;\n\topenage::datastructure::ConcurrentQueue<CopyOnly> queue;\n\n\t{\n\t\tCopyOnly tmp(test_data);\n\t\tqueue.push(std::move(tmp));\n\t}\n\n\tCopyOnly res(queue.pop());\n\tTESTEQUALS(res.get(), test_data);\n}\n\nvoid concurrent_queue_move_only_elements_compilation() {\n\tconst int test_data = 157;\n\topenage::datastructure::ConcurrentQueue<MoveOnly> queue;\n\n\t{\n\t\tMoveOnly tmp(test_data);\n\t\tqueue.push(std::move(tmp));\n\t}\n\n\tMoveOnly res(queue.pop());\n\tTESTEQUALS(res.get(), test_data);\n}\n\nvoid concurrent_queue_copy_move_elements_compilation() {\n\tconst int test_data = 157;\n\topenage::datastructure::ConcurrentQueue<CopyMove> queue;\n\n\t{\n\t\tCopyMove tmp(test_data);\n\t\tqueue.push(std::move(tmp));\n\t}\n\n\tCopyMove res(queue.pop());\n\tTESTEQUALS(res.get(), test_data);\n\n\t// The second instance should be move-constructed\n\tTESTEQUALS(CopyMove::get_accumulated_construction_counter(), 1);\n\tres.reset_accumulated_construction_counter();\n}\n\n// exported test\nvoid concurrent_queue() {\n\tconcurrent_queue_copy_only_elements_compilation();\n\tconcurrent_queue_move_only_elements_compilation();\n\tconcurrent_queue_copy_move_elements_compilation();\n}\n\n} // namespace openage::datastructure::tests\n"
  },
  {
    "path": "libopenage/datastructure/tests.h",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <functional>\n\n\nnamespace openage::datastructure::tests {\n\n/**\n * simplest priority queue element that supports reordering.\n */\nstruct heap_elem {\n\tint data;\n\n\tbool operator<(const heap_elem &other) const {\n\t\treturn this->data < other.data;\n\t}\n\n\tbool operator==(const heap_elem &other) const {\n\t\treturn this->data == other.data;\n\t}\n};\n\n} // namespace openage::datastructure::tests\n\nnamespace std {\n\n/**\n * hash function for the simple heap_elem\n */\ntemplate <>\nstruct hash<openage::datastructure::tests::heap_elem> {\n\tsize_t operator()(const openage::datastructure::tests::heap_elem &elem) const {\n\t\treturn elem.data;\n\t}\n};\n} // namespace std\n"
  },
  {
    "path": "libopenage/engine/CMakeLists.txt",
    "content": "add_sources(libopenage\n    engine.cpp\n)\n"
  },
  {
    "path": "libopenage/engine/engine.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"engine.h\"\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"cvar/cvar.h\"\n#include \"gamestate/simulation.h\"\n#include \"presenter/presenter.h\"\n#include \"time/time_loop.h\"\n\n\nnamespace openage::engine {\n\nEngine::Engine(mode mode,\n               const util::Path &root_dir,\n               const std::vector<std::string> &mods,\n               const renderer::window_settings &window_settings) :\n\trunning{true},\n\trun_mode{mode},\n\troot_dir{root_dir},\n\tthreads{} {\n\tlog::log(INFO\n\t         << \"launching engine with root directory\"\n\t         << root_dir);\n\n\t// read and apply the configuration files\n\tthis->cvar_manager = std::make_shared<cvar::CVarManager>(this->root_dir[\"cfg\"]);\n\tcvar_manager->load_all();\n\n\t// time loop\n\tthis->time_loop = std::make_shared<time::TimeLoop>();\n\n\t// game simulation\n\t// this is run in the main thread\n\tthis->simulation = std::make_shared<gamestate::GameSimulation>(this->root_dir,\n\t                                                               this->cvar_manager,\n\t                                                               this->time_loop);\n\tthis->simulation->set_modpacks(mods);\n\n\t// presenter (optional)\n\tif (this->run_mode == mode::FULL) {\n\t\tthis->presenter = std::make_shared<presenter::Presenter>(this->root_dir,\n\t\t                                                         this->simulation,\n\t\t                                                         this->time_loop);\n\t}\n\n\t// spawn thread to run time loop\n\tthis->threads.emplace_back([&]() {\n\t\tthis->time_loop->run();\n\n\t\tthis->time_loop.reset();\n\t});\n\n\t// if presenter is used, run it in a separate thread\n\tif (this->run_mode == mode::FULL) {\n\t\tthis->threads.emplace_back([&]() {\n\t\t\tthis->presenter->run(window_settings);\n\n\t\t\t// Make sure that the presenter gets destructed in the same thread\n\t\t\t// otherwise OpenGL complains about missing contexts\n\t\t\tthis->presenter.reset();\n\t\t\tthis->running = false;\n\t\t});\n\t}\n\n\tlog::log(INFO << \"Using \" << this->threads.size() + 1 << \" threads \"\n\t              << \"(\" << std::jthread::hardware_concurrency() << \" available)\");\n}\n\nvoid Engine::loop() {\n\t// Run the main game simulation loop:\n\tthis->simulation->run();\n\n\t// After stopping, clean up the simulation\n\tthis->simulation.reset();\n\tif (this->run_mode != mode::FULL) {\n\t\tthis->running = false;\n\t}\n}\n\n} // namespace openage::engine\n"
  },
  {
    "path": "libopenage/engine/engine.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <thread>\n#include <vector>\n\n#include \"renderer/window.h\"\n#include \"util/path.h\"\n\n\n// TODO: Remove custom jthread definition when clang/libc++ finally supports it\n#if __llvm__\n\t#if !__cpp_lib_jthread\nnamespace std {\nclass jthread : public thread {\npublic:\n\tusing thread::thread; // needed constructors\n\tjthread(const jthread &) = delete;\n\tjthread &operator=(const jthread &) = delete;\n\tjthread(jthread &&) = default;\n\tjthread &operator=(jthread &&) = default;\n\t~jthread() {\n\t\tif (this->joinable()) {\n\t\t\tthis->join();\n\t\t}\n\t}\n};\n} // namespace std\n\t#endif\n#else\n\t#include <stop_token>\n#endif\n\n\nnamespace openage {\n\nnamespace cvar {\nclass CVarManager;\n} // namespace cvar\n\nnamespace gamestate {\nclass GameSimulation;\n} // namespace gamestate\n\nnamespace presenter {\nclass Presenter;\n} // namespace presenter\n\nnamespace time {\nclass TimeLoop;\n} // namespace time\n\n\nnamespace engine {\n\n/**\n * Encapsulates all subcomponents needed for a run.\n */\nclass Engine {\npublic:\n\tenum class mode {\n\t\tLEGACY,\n\t\tHEADLESS,\n\t\tFULL,\n\t};\n\n\t/**\n\t * Create the engine instance for this run.\n\t *\n\t * @param mode The run mode to use.\n\t * @param root_dir openage root directory.\n\t * @param mods The mods to load.\n\t * @param window_settings The settings to customize the display window (e.g. size, display mode, vsync).\n\t */\n\tEngine(mode mode,\n\t       const util::Path &root_dir,\n\t       const std::vector<std::string> &mods,\n\t       const renderer::window_settings &window_settings = {});\n\n\t// engine should not be copied or moved\n\tEngine(const Engine &) = delete;\n\tEngine &operator=(const Engine &) = delete;\n\tEngine(Engine &&) = delete;\n\tEngine &operator=(Engine &&) = delete;\n\t~Engine() = default;\n\n\n\t/**\n\t * Run the main loop.\n\t */\n\tvoid loop();\n\n\t/**\n\t * current simulation state variable.\n\t * to be set to false to stop the simulation loop.\n\t */\n\tbool running;\n\nprivate:\n\t/**\n\t * The run mode to use.\n\t */\n\tmode run_mode;\n\n\t/**\n\t * openage root directory.\n\t */\n\tutil::Path root_dir;\n\n\t/**\n\t * The threads used by the engine.\n\t */\n\tstd::vector<std::jthread> threads;\n\n\t/**\n\t * Environment variables.\n\t */\n\tstd::shared_ptr<cvar::CVarManager> cvar_manager;\n\n\t/**\n\t * Controls and update the clock for time-based measurements.\n\t */\n\tstd::shared_ptr<time::TimeLoop> time_loop;\n\n\t/**\n\t * Gameplay simulation.\n\t */\n\tstd::shared_ptr<gamestate::GameSimulation> simulation;\n\n\t/**\n\t * Video/audio/input management. Can be nullptr in headless mode.\n\t */\n\tstd::shared_ptr<presenter::Presenter> presenter;\n};\n\n} // namespace engine\n} // namespace openage\n"
  },
  {
    "path": "libopenage/error/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tbacktrace.cpp\n\tdemo.cpp\n\terror.cpp\n\thandlers.cpp\n\tstackanalyzer.cpp\n)\n\npxdgen(\n\tbacktrace.h\n\terror.h\n\thandlers.h\n)\n"
  },
  {
    "path": "libopenage/error/backtrace.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"backtrace.h\"\n\n#include <iostream>\n\n#include \"config.h\"\n\nnamespace openage {\nnamespace error {\n\n\n/**\n * Prints a backtrace_symbol object.\n */\nstd::ostream &operator <<(std::ostream &os, const backtrace_symbol &bt_sym) {\n\t// imitate the looks of a Python traceback.\n\tos << \"  File \";\n\n\tif (bt_sym.filename.empty()) {\n\t\tos << '?';\n\t} else {\n\t\tos << '\"' << bt_sym.filename << '\"';\n\t}\n\n\tif (bt_sym.lineno) {\n\t\tos << \", line \" << bt_sym.lineno;\n\t}\n\n\tos << \", in \";\n\n\tif (bt_sym.functionname.empty()) {\n\t\tos << '?';\n\t} else {\n\t\tos << bt_sym.functionname;\n\t}\n\n\tif (bt_sym.pc != nullptr) {\n\t\tos << \" [\" << bt_sym.pc << \"]\";\n\t}\n\n\treturn os;\n}\n\n\n/**\n * Prints an entire Backtrace object.\n */\nstd::ostream &operator <<(std::ostream &os, const Backtrace &bt) {\n\t// imitate the looks of a Python traceback.\n\tos << \"Traceback (most recent call last):\" << std::endl;\n\n\tbt.get_symbols([&os](const backtrace_symbol *symbol) {\n\t\tos << *symbol << std::endl;\n\t}, true);\n\n\treturn os;\n}\n\n\n}} // openage::error\n"
  },
  {
    "path": "libopenage/error/backtrace.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool as cppbool\n\n#include <vector>\n// pxd: from libcpp.string cimport string\n#include <iostream>\n#include <string>\n// pxd: from libopenage.pyinterface.functional cimport Func1\n#include <functional>\n\nnamespace openage {\nnamespace error {\n\n\n// Note: This header file describes a fixed frontend for all sorts of backend backtrace-getters.\n\n\n/**\n * A single symbol, as determined from a program counter, and returned by Backtrace::get_symbols().\n *\n * pxd:\n *\n * cppclass backtrace_symbol:\n *     string filename\n *     int lineno\n *     string functionname\n *     void *pc\n *\n * ctypedef const backtrace_symbol *backtrace_symbol_constptr\n */\nstruct backtrace_symbol {\n\tstd::string filename;     // empty if unknown\n\tunsigned int lineno;      // 0 if unknown\n\tstd::string functionname; // empty if unknown\n\tvoid *pc;                 // nullptr if unknown\n};\n\n\nstd::ostream &operator<<(std::ostream &os, const backtrace_symbol &bt_sym);\n\n\n/**\n * Abstract class for objects that provide backtrave information through\n * get_symbols().\n * For a non-abstract implementation, see stackanalyzer.h.\n *\n * pxd:\n *\n * cppclass Backtrace:\n *    void get_symbols(Func1[void, backtrace_symbol *] callback, cppbool reversed) except +\n */\nclass Backtrace {\npublic:\n\t/**\n\t * Returns (via the callback) symbolic names for all stack frames,\n\t * to its best knowledge.\n\t *\n\t * The most recent call is returned last (alike Python).\n\t *\n\t * @param cb\n\t *    is called for every symbol in the backtrace, starting with the top-most\n\t *    frame.\n\t * @param reversed\n\t *    if true, the most recent call is given last.\n\t */\n\tvirtual void get_symbols(std::function<void(const backtrace_symbol *)> cb,\n\t                         bool reversed = true) const = 0;\n\n\t/**\n\t * Removes all the lower frames that are also present in the current stack.\n\t *\n\t * Designed to be used in catch clauses, to simulate stack trace collection\n\t * from throw to catch, instead of from throw to the process entry point.\n\t *\n\t * Defaults to no-op.\n\t */\n\tvirtual void trim_to_current_stack_frame() {\n\t\treturn;\n\t};\n\n\tvirtual ~Backtrace() = default;\n};\n\n\nstd::ostream &operator<<(std::ostream &os, const Backtrace &bt);\n\n\n} // namespace error\n} // namespace openage\n"
  },
  {
    "path": "libopenage/error/demo.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include <memory>\n#include <ostream>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::error {\n\n\n// anonymous namespace to prevent linkage for exception demo helper functions.\nnamespace {\n\n\nvoid foo() {\n\tthrow Error(MSG(err) << \"what an exceptional line of code!\", true);\n}\n\n\nvoid bar(int i) {\n\tif (i % 5 == 3) {\n\t\ttry {\n\t\t\tfoo();\n\t\t}\n\t\tcatch (...) {\n\t\t\t// Note: pokemon exception handling is generally discouraged.\n\t\t\t// This serves as a demo how the Error constructor nevertheless\n\t\t\t// manages to correctly capture the cause exception.\n\t\t\t// Even if you don't want to access the object, `catch (Error &)`.\n\n\t\t\tthrow Error(MSG(crit).fmt(\"exception in foo. i=%d\", i));\n\t\t}\n\t}\n\telse {\n\t\tbar(i + 1);\n\t}\n}\n\n\n} // anonymous namespace\n\n\nvoid demo() {\n\ttry {\n\t\tbar(0);\n\t}\n\tcatch (Error &exc) {\n\t\tif (exc.backtrace) {\n\t\t\texc.trim_backtrace();\n\t\t}\n\n\t\tlog::log(MSG(info) << \"exception_demo: captured the following exception:\" << std::endl\n\t\t                   << exc << std::endl\n\t\t                   << \"exception_demo: end of exception\");\n\t}\n}\n\n\n} // namespace openage::error\n"
  },
  {
    "path": "libopenage/error/error.cpp",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#include \"error.h\"\n\n#include <exception>\n#include <utility>\n\n#include \"util/compiler.h\"\n\n#include \"error/stackanalyzer.h\"\n\nnamespace openage::error {\n\n\nconstexpr const char *runtime_error_message = \"polymorphic openage Error object; catch by reference!\";\n\nstatic bool enable_break_on_create = false;\nvoid Error::debug_break_on_create(bool state) {\n\tenable_break_on_create = state;\n}\n\nError::Error(const log::message &msg, bool generate_backtrace, bool store_cause) :\n\tstd::runtime_error{runtime_error_message},\n\tmsg(msg) {\n\tif (enable_break_on_create) [[unlikely]] {\n\t\tBREAKPOINT;\n\t}\n\n\tif (generate_backtrace) {\n\t\tauto backtrace = std::make_shared<StackAnalyzer>();\n\t\tbacktrace->analyze();\n\t\tthis->backtrace = backtrace;\n\t}\n\n\tif (store_cause) {\n\t\tthis->store_cause();\n\t}\n}\n\n\nvoid Error::store_cause() {\n\t// we could simply do this->cause = std::current_exception(),\n\t// but we need to trim the cause Error's backtrace.\n\n\tif (!std::current_exception()) [[likely]] {\n\t\treturn;\n\t}\n\n\ttry {\n\t\tthrow;\n\t}\n\tcatch (Error &cause) {\n\t\tcause.trim_backtrace();\n\t\tthis->cause = std::current_exception();\n\t}\n\tcatch (...) {\n\t\tthis->cause = std::current_exception();\n\t}\n}\n\n\nvoid Error::trim_backtrace() {\n\tif (this->backtrace) {\n\t\tthis->backtrace->trim_to_current_stack_frame();\n\t}\n}\n\n\nError::Error() :\n\tstd::runtime_error{runtime_error_message} {}\n\n\nconst char *Error::what() const noexcept {\n\treturn this->msg.text.c_str();\n}\n\n\nstd::string Error::type_name() const {\n\treturn util::typestring(*this);\n}\n\n\nvoid Error::rethrow_cause() const {\n\tif (this->cause) {\n\t\tstd::rethrow_exception(this->cause);\n\t}\n}\n\n\nstd::ostream &operator<<(std::ostream &os, const Error &e) {\n\t// output the exception cause\n\tbool had_a_cause = true;\n\ttry {\n\t\te.rethrow_cause();\n\t\thad_a_cause = false;\n\t}\n\tcatch (Error &cause) {\n\t\tos << cause << std::endl;\n\t}\n\tcatch (std::exception &cause) {\n\t\tos << util::typestring(cause) << \": \" << cause.what() << std::endl;\n\t}\n\tcatch (...) {\n\t\tos << \"unknown non std::exception cause!\" << std::endl;\n\t}\n\n\tif (had_a_cause) {\n\t\tos << std::endl\n\t\t   << \"The above exception was the direct cause of the following exception:\" << std::endl\n\t\t   << std::endl;\n\t}\n\n\t// output the exception backtrace\n\tif (e.backtrace) {\n\t\tos << *e.backtrace;\n\t}\n\telse {\n\t\tos << \"origin:\" << std::endl;\n\t}\n\n\t// the exception message metadata also holds some \"backtrace-like\" info\n\tos << backtrace_symbol{\n\t\te.msg.filename,\n\t\te.msg.lineno,\n\t\te.msg.functionname,\n\t\tnullptr}\n\t   << std::endl;\n\n\tos << e.type_name();\n\n\tif (not e.msg.text.empty()) {\n\t\tos << \": \" << e.msg.text;\n\t}\n\n\treturn os;\n}\n\n\n} // namespace openage::error\n"
  },
  {
    "path": "libopenage/error/error.h",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <exception>\n\n// pxd: from libcpp cimport bool\n#include <iostream>\n#include <memory>\n#include <stdexcept>\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"../util/compiler.h\"\n// pxd: from libopenage.log.message cimport message\n#include \"../log/message.h\"\n\n// pxd: from libopenage.error.backtrace cimport Backtrace\nnamespace openage {\nnamespace error {\n// forward-declaration to avoid the header include.\nclass Backtrace;\n} // namespace error\n} // namespace openage\n\nnamespace openage {\nnamespace pyinterface {\n// forward-declaration for use in the 'friend' declaration below.\nclass PyException;\n} // namespace pyinterface\n} // namespace openage\n\n\nnamespace openage {\nnamespace error {\n\n/**\n * Openage base exception type; the constructor usage is analogous to\n * log::log().\n *\n * pxd:\n *\n * cppclass Error:\n *     message msg\n *\n *     string type_name() except +\n *     const char *what() except +\n *     void rethrow_cause() except +\n *     void trim_backtrace() except +\n *     @staticmethod\n *     void debug_break_on_create(bool state) except +\n *\n *     Backtrace *backtrace\n */\nclass OAAPI Error : public std::runtime_error {\npublic:\n\t/**\n\t * @param msg\n\t *     As with log::log()\n\t * @param generate_backtrace\n\t *     If true, some platform-specific code is run to collect\n\t *     traceback information (e.g.: backtrace (3))\n\t *     (default true).\n\t *     The performance impacts should be not too bad, as only\n\t *     program counter pointers are collected.\n\t * @param store_cause\n\t *     If true, a pointer to the causing exception is\n\t *     collected and stored (default true).\n\t */\n\tError(const log::message &msg, bool generate_backtrace = true, bool store_cause = true);\n\n\n\t/**\n\t * Stores a pointer to the currently-handled exception in this->cause.\n\t */\n\tvoid store_cause();\n\n\n\t/**\n\t * Calls this->backtrace->trim_to_current_stack_frame(),\n\t * if this->backtrace is not nullptr.\n\t *\n\t * Designed to be used in catch clauses, to strip away all those\n\t * unneeded symbols from program init upwards.\n\t *\n\t * Automatically called for cause exceptions, and when storing cause exceptions,\n\t * and by the to_py converter.\n\t */\n\tvoid trim_backtrace();\n\n\n\t/**\n\t * The error message.\n\t */\n\tlog::message msg;\n\n\n\t/**\n\t * The (optional) backtrace.\n\t */\n\tstd::shared_ptr<Backtrace> backtrace;\n\n\n\t/**\n\t * Re-throws the exception cause, if the exception has one.\n\t * Otherwise, does nothing.\n\t *\n\t * Use this when handling the exception, to handle the cause.\n\t */\n\tvoid rethrow_cause() const;\n\n\n\t/**\n\t * The type name of the exception (for pretty-printing in case\n\t * the exception hierarchy is used).\n\t * Uses typeid internally.\n\t */\n\tvirtual std::string type_name() const;\n\n\n\t/**\n\t * Returns the message's content.\n\t */\n\tconst char *what() const noexcept override;\n\n\t/**\n\t * Turn on debug breaks in the constructor\n\t */\n\tstatic void debug_break_on_create(bool state);\n\nprivate:\n\tfriend pyinterface::PyException;\n\n\n\t/**\n\t * Constructs an empty error.\n\t * For use by our friend, pyinterface::PyException.\n\t */\n\tError();\n\n\n\t/**\n\t * Re-throw this with rethrow_cause().\n\t */\n\tstd::exception_ptr cause;\n};\n\n\nstd::ostream &operator<<(std::ostream &os, const Error &e);\n\n\n[[deprecated(\"add message to the ENSURE before pushing, please\")]] inline std::string no_ensuring_message() {\n\treturn std::string{};\n}\n\n// ENSURE(condition, errormessage << variable << etcetc)\n#define ENSURE(...) \\\n\tdo { \\\n\t\tif (not OPENAGE_ENS_FIRST(__VA_ARGS__)) [[unlikely]] { \\\n\t\t\tthrow ::openage::error::Error(MSG(err) OPENAGE_ENS_REST(__VA_ARGS__)); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n/*\n *  expands to the first argument\n * Modified for MSVC using the technique by Jeff Walden\n * https://stackoverflow.com/a/9338429\n */\n#define OPENAGE_PP_GLUE(macro, args) macro args\n#define OPENAGE_ENS_FIRST(...) OPENAGE_PP_GLUE(OPENAGE_ENS_FIRST_HELPER, (__VA_ARGS__, throwaway))\n#define OPENAGE_ENS_FIRST_HELPER(first, ...) (first)\n\n/*\n * Standard alternative to GCC's ##__VA_ARGS__ trick (Richard Hansen)\n * http://stackoverflow.com/a/11172679/4742108\n *\n * If there's only one argument, expands to << ::openage::error::no_ensuring_message()\n * If there is more than one argument, expands to a '<<' followed by everything but\n * the first argument. Only supports up to 2 arguments but can be trivially expanded.\n */\n#define OPENAGE_ENS_REST(...) OPENAGE_PP_GLUE(OPENAGE_ENS_REST_HELPER(OPENAGE_ENS_NUM(__VA_ARGS__)), (__VA_ARGS__))\n#define OPENAGE_ENS_REST_HELPER(qty) OPENAGE_ENS_REST_HELPER1(qty)\n#define OPENAGE_ENS_REST_HELPER1(qty) OPENAGE_ENS_REST_HELPER2(qty)\n#define OPENAGE_ENS_REST_HELPER2(qty) OPENAGE_ENS_REST_HELPER_##qty\n#define OPENAGE_ENS_REST_HELPER_ONE(first) << ::openage::error::no_ensuring_message()\n#define OPENAGE_ENS_REST_HELPER_TWOORMORE(first, ...) << __VA_ARGS__\n#define OPENAGE_ENS_NUM(...) OPENAGE_ENS_NUM_IMPL((__VA_ARGS__, TWOORMORE, ONE, throwaway))\n#define OPENAGE_ENS_NUM_IMPL(args) OPENAGE_ENS_SELECT_2ND args\n#define OPENAGE_ENS_SELECT_2ND(a1, a2, a3, ...) a3\n\n} // namespace error\n\nusing error::Error;\n\n} // namespace openage\n"
  },
  {
    "path": "libopenage/error/handlers.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n/*\n * This file holds handlers for std::terminate and SIGSEGV.\n *\n * The handlers print stack trace and (for terminate) exception information,\n * before allowing the program to exit.\n *\n * The handlers are installed when loading the library, and uninstalled\n * when unloading it.\n */\n\n#include \"handlers.h\"\n\n#include <cstring>\n#include <exception>\n#include <iostream>\n\n#ifdef _MSC_VER\n\t#include <io.h>\n#else\n\t#include <unistd.h>\n#endif\n\n#include \"util/init.h\"\n#include \"util/language.h\"\n#include \"util/signal.h\"\n\n#include \"error/error.h\"\n#include \"error/stackanalyzer.h\"\n\n\nnamespace openage {\nnamespace error {\n\n\n[[noreturn]] void terminate_handler() noexcept;\nvoid sigsegv_handler(int /* unused */);\nvoid exit_handler();\n\n// The global state has internal linkage only.\nnamespace {\n\nbool exit_ok;\n\nstd::terminate_handler old_terminate_handler;\n#ifdef __FreeBSD__\ntypedef sig_t sighandler_t;\n#endif\nsighandler_t old_sigsegv_handler;\n\nutil::OnInit install_handlers([]() {\n\told_sigsegv_handler = signal(SIGSEGV, sigsegv_handler);\n\told_terminate_handler = std::set_terminate(terminate_handler);\n\texit_ok = true;\n\tatexit(exit_handler);\n});\n\n\nutil::OnDeInit restore_handlers([]() {\n\tstd::set_terminate(old_terminate_handler);\n\tsignal(SIGSEGV, old_sigsegv_handler);\n});\n\n\n} // anonymous namespace\n\n\n[[noreturn]] void terminate_handler() noexcept {\n\t// immediately unset this handler, to avoid endless recursions if\n\t// terminate() is accidentially triggered from here.\n\tstd::set_terminate(old_terminate_handler);\n\n\tstd::cout << \"\\n\\x1b[31;1mFATAL: terminate has been called\\x1b[m\" << std::endl;\n\n\tif (std::exception_ptr e_ptr = std::current_exception()) {\n\t\tstd::cout << \"\\n\\x1b[33muncaught exception\\x1b[m\\n\"\n\t\t\t\t  << std::endl;\n\n\t\ttry {\n\t\t\tstd::rethrow_exception(e_ptr);\n\t\t}\n\t\tcatch (Error &exc) {\n\t\t\tstd::cout << exc << std::endl;\n\t\t}\n\t\tcatch (std::exception &exc) {\n\t\t\tstd::cout << \"std::exception of type \" << util::typestring(exc) << \": \" << exc.what() << std::endl;\n\t\t}\n\t\tcatch (...) {\n\t\t\tstd::cout << \"non-standard exception object\" << std::endl;\n\t\t}\n\t}\n\n\tstd::cout << \"\\n\\x1b[33mcurrent stack:\\x1b[m\\n\"\n\t\t\t  << std::endl;\n\n\tStackAnalyzer backtrace;\n\tbacktrace.analyze();\n\tstd::cout << backtrace << std::endl;\n\n\t// die again to enable debugger functionality.\n\t// that maybe print some additional useful info that we forgot about.\n\t// TODO: we maybe want to prevent that for end-users.\n\tstd::cout << \"\\x1b[33mhanding over to the system...\\x1b[m\\n\"\n\t\t\t  << std::endl;\n\tstd::terminate();\n}\n\n\nvoid sigsegv_handler(int /* unused */) {\n\t// In theory, this handler may only call async-signal-safe functions,\n\t// such as write().\n\tconst char *message = \"\\n\\x1b[31;1mSIGSEGV\\x1b[m\\n\";\n\tutil::ignore_result(write(1, message, strlen(message)));\n\n\t// however, everything is broken anyways. can't hurt to try to print\n\t// more useful info. fuck the police! wheeee!\n\tstd::terminate();\n}\n\n\nvoid exit_handler() {\n\t// This handler is registered to run atexit().\n\t// It is used to catch calls to exit() that occur somewhere inside\n\t// the running game (while exit_ok == false).\n\t// exit() should never be invoked directly while the game is running,\n\t// but some libraries such as libepoxy might do it anyway.\n\t// The actual proper way of exiting the running game is via throwing\n\t// an exception or similar action.\n\n\tif (exit_ok) {\n\t\treturn;\n\t}\n\n\tstd::cout << \"\\x1b[31;1mexit() was called in an illegal place\\x1b[m\\n\"\n\t\t\t  << std::endl;\n}\n\n\nvoid set_exit_ok(bool value) {\n\texit_ok = value;\n}\n\n\n} // namespace error\n} // namespace openage\n"
  },
  {
    "path": "libopenage/error/handlers.h",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool as cppbool\n#include \"../util/compiler.h\"\n\nnamespace openage {\nnamespace error {\n\n\n/**\n * Call this to set or unset the atexit error handler.\n *\n * In order to capture any stray calls to exit() from any\n * place in the game (including libraries), set this to true\n * on startup, and back to false right before termination.\n *\n * pxd: void set_exit_ok(cppbool value) except +\n */\nOAAPI void set_exit_ok(bool value);\n\n\n} // namespace error\n} // namespace openage\n"
  },
  {
    "path": "libopenage/error/stackanalyzer.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"stackanalyzer.h\"\n\n#include <cstdint>\n#include <string>\n\n#include \"config.h\"\n#include \"log/log.h\"\n#include \"util/compiler.h\"\n#include \"util/init.h\"\n\n\nnamespace openage::error {\n\n/**\n * Skip this many frames at the beginning of the trace.\n * Can trim away various libc calls.\n */\nconstexpr uint64_t skip_entry_frames = 1;\n\n/**\n * Skip this many stack frames,\n * this drops the stackanalyzer function call itself.\n */\nconstexpr uint64_t base_skip_frames = 1;\n\n} // namespace openage::error\n\n\n#if WITH_BACKTRACE\n\n\t// use modern <backtrace.h>\n\t#include <backtrace.h>\n\nnamespace openage {\nnamespace error {\n\n\n// those functions are for internal usage only.\nnamespace {\n\n\nstruct info_cb_data_t {\n\tstd::vector<backtrace_symbol> symbols;\n\tuintptr_t pc;\n};\n\n\n// called in case of internal errors of libbacktrace\nvoid backtrace_error_callback(void * /*unused*/, const char *msg, int errorno) {\n\tlog::log(ERR << \"libbacktrace: \" << msg << \" (errno: \" << errorno << \")\");\n}\n\n\nstruct backtrace_state *bt_state;\n\n\nutil::OnInit init_backtrace_state([]() {\n\tbt_state = backtrace_create_state(\n\t\tnullptr, // auto-determine filename\n\t\t1,       // threaded\n\t\tbacktrace_error_callback,\n\t\tnullptr // passed to the callback\n\t);\n\n\t// There's no documentaton on how to free the state.\n});\n\n\n// called by backtrace_simple in StackAnalyzer::collect()\nint backtrace_simple_callback(void *data, uintptr_t pc) {\n\t// data is a pointer to the StackAnalyzer object\n\tStackAnalyzer *bt = reinterpret_cast<StackAnalyzer *>(data);\n\tbt->stack_addrs.push_back(reinterpret_cast<void *>(pc));\n\treturn 0;\n}\n\n\nvoid backtrace_syminfo_callback(\n\tvoid *data,\n\tuintptr_t pc,\n\tconst char *symname,\n\tuintptr_t /*unused symval*/,\n\tuintptr_t /*unused symsize*/) {\n\t// in this fallback case, we can't get filename or line info, but at least we\n\t// can get and demangle the symbol name... hopefully.\n\n\tauto symbol_vector = &(reinterpret_cast<info_cb_data_t *>(data)->symbols);\n\n\tsymbol_vector->emplace_back(backtrace_symbol{\n\t\t\"\",\n\t\t0,\n\t\t(symname == nullptr) ? \"\" : util::demangle(symname),\n\t\treinterpret_cast<void *>(pc)});\n}\n\n\n// called by backtrace_pcinfo in StackAnalyzer::get_symbols\nint backtrace_pcinfo_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) {\n\tbacktrace_symbol result;\n\n\tauto symbol_vector = &(reinterpret_cast<info_cb_data_t *>(data)->symbols);\n\n\tif (function == nullptr) {\n\t\t// we didn't get very useful info. fall back to dlsym.\n\t\tsymbol_vector->emplace_back(backtrace_symbol{\n\t\t\t\"\",\n\t\t\t0,\n\t\t\tutil::symbol_name(reinterpret_cast<void *>(pc), false, true),\n\t\t\treinterpret_cast<void *>(pc)});\n\t}\n\telse {\n\t\tsymbol_vector->emplace_back(backtrace_symbol{\n\t\t\t(filename == nullptr) ? \"\" : filename,\n\t\t\tstatic_cast<unsigned int>(lineno),\n\t\t\tutil::demangle(function),\n\t\t\treinterpret_cast<void *>(pc)});\n\t}\n\n\treturn 0;\n}\n\n\nvoid backtrace_pcinfo_error_callback(void *data, const char *msg, int errorno) {\n\tif (errorno == -1) {\n\t\tauto info_cb_data = reinterpret_cast<info_cb_data_t *>(data);\n\n\t\t// no debug info in ELF file.\n\t\t// try backtrace_syminfo instead.\n\t\tbacktrace_syminfo(\n\t\t\tbt_state,\n\t\t\tinfo_cb_data->pc,\n\t\t\tbacktrace_syminfo_callback,\n\t\t\tbacktrace_error_callback,\n\t\t\tinfo_cb_data);\n\t}\n\telse {\n\t\t// invoke the general error callback, which prints the message.\n\t\tbacktrace_error_callback(nullptr, msg, errorno);\n\t}\n}\n\n} // anonymous namespace\n\n\nvoid StackAnalyzer::analyze() {\n\tbacktrace_simple(\n\t\tbt_state,\n\t\tbase_skip_frames, // skip some frames at \"most recent call\"\n\t\tbacktrace_simple_callback,\n\t\tbacktrace_error_callback,\n\t\treinterpret_cast<void *>(this));\n}\n\n\nvoid StackAnalyzer::get_symbols(std::function<void(const backtrace_symbol *)> cb, bool reversed) const {\n\tinfo_cb_data_t info_cb_data;\n\n\tfor (void *pc : this->stack_addrs) {\n\t\tinfo_cb_data.pc = reinterpret_cast<uintptr_t>(pc);\n\n\t\t// note: a call to backtrace_pcinfo may, in semi-rare cases, push back\n\t\t// multiple symbols to result. That's nothing to worry about, though.\n\t\t// If you decide you don't like it, make pcinfo_callback return 1.\n\t\tbacktrace_pcinfo(\n\t\t\tbt_state,\n\t\t\tinfo_cb_data.pc,\n\t\t\tbacktrace_pcinfo_callback,\n\t\t\tbacktrace_pcinfo_error_callback,\n\t\t\treinterpret_cast<void *>(&info_cb_data));\n\t}\n\n\tif (reversed) {\n\t\t// this entire vector thing would only be needed for reversed=true...\n\t\tfor (size_t idx = info_cb_data.symbols.size(); idx-- > 0;) {\n\t\t\tcb(&info_cb_data.symbols[idx]);\n\t\t}\n\t}\n\telse {\n\t\tfor (backtrace_symbol &symbol : info_cb_data.symbols) {\n\t\t\tcb(&symbol);\n\t\t}\n\t}\n}\n\n} // namespace error\n} // namespace openage\n\n#else // WITHOUT_BACKTRACE\n\n\t#ifdef _WIN32\n\t\t#include <windows.h>\n\nnamespace openage {\nnamespace error {\n\n\nvoid StackAnalyzer::analyze() {\n\tstd::vector<void *> buffer{64};\n\tauto count = RtlCaptureStackBackTrace(base_skip_frames, buffer.size(), buffer.data(), NULL);\n\tif (count < buffer.size()) {\n\t\tbuffer.resize(count);\n\t}\n\tthis->stack_addrs = std::move(buffer);\n}\n\n} // namespace error\n} // namespace openage\n\n\t#else // not _MSC_VER\n\n\t\t// use GNU's <execinfo.h>\n\t\t#include <execinfo.h>\n\nnamespace openage::error {\n\n\nvoid StackAnalyzer::analyze() {\n\t// unfortunately, backtrace won't tell us how big our buffer\n\t// needs to be, so we have no choice but to try until it\n\t// reports success.\n\tstd::vector<void *> buffer{64};\n\n\twhile (true) {\n\t\tint elements = backtrace(buffer.data(), buffer.size());\n\n\t\t// the buffer was large enough, so stop resizing.\n\t\tif (elements < static_cast<ssize_t>(buffer.size())) {\n\t\t\tbuffer.resize(elements);\n\t\t\tbreak;\n\t\t}\n\t\telse {\n\t\t\tbuffer.resize(buffer.size() * 2);\n\t\t}\n\t}\n\n\t// now store the result, cut off at the front and back.\n\n\tsize_t idx = 0;\n\tfor (void *element : buffer) {\n\t\t// start storing after skipping skip the first few frames\n\t\t// so that e.g. this function does not show up in the trace.\n\t\t// (most-recent-call)\n\t\tif (idx >= base_skip_frames) {\n\t\t\tthis->stack_addrs.push_back(element);\n\t\t}\n\t\tidx += 1;\n\t}\n\n\t// remove some libc-garbage-frames (least-recent-call)\n\tfor (uint64_t i = 0; i < skip_entry_frames; i++) {\n\t\tthis->stack_addrs.pop_back();\n\t}\n}\n\n} // namespace openage::error\n\n\t#endif // for _MSC_VER or GNU execinfo\n\nnamespace openage::error {\n\n\nvoid StackAnalyzer::get_symbols(std::function<void(const backtrace_symbol *)> cb,\n                                bool reversed) const {\n\tbacktrace_symbol symbol;\n\tsymbol.filename = \"\";\n\tsymbol.lineno = 0;\n\n\tif (reversed) {\n\t\t// `for (auto pc : this->stack_addrs | <ranges-ns>::view::reverse)` after Ranges-TS\n\t\tfor (size_t idx = this->stack_addrs.size(); idx-- > 0;) {\n\t\t\tvoid *pc = this->stack_addrs[idx];\n\n\t\t\tsymbol.functionname = util::symbol_name(pc, false, true);\n\t\t\tsymbol.pc = pc;\n\n\t\t\tcb(&symbol);\n\t\t}\n\t}\n\telse {\n\t\tfor (void *pc : this->stack_addrs) {\n\t\t\tsymbol.functionname = util::symbol_name(pc, false, true);\n\t\t\tsymbol.pc = pc;\n\n\t\t\tcb(&symbol);\n\t\t}\n\t}\n}\n\n\n} // namespace openage::error\n\n#endif // WITHOUT_BACKTRACE\n\n\nnamespace openage::error {\n\n\nvoid StackAnalyzer::trim_to_current_stack_frame() {\n\tStackAnalyzer current;\n\tcurrent.analyze();\n\n\twhile (not current.stack_addrs.empty() and not this->stack_addrs.empty()) {\n\t\tif (this->stack_addrs.back() != current.stack_addrs.back()) {\n\t\t\tbreak;\n\t\t}\n\n\t\tthis->stack_addrs.pop_back();\n\t\tcurrent.stack_addrs.pop_back();\n\t}\n}\n\n\n} // namespace openage::error\n"
  },
  {
    "path": "libopenage/error/stackanalyzer.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <vector>\n\n#include \"error/backtrace.h\"\n\n\nnamespace openage {\nnamespace error {\n\n/**\n * Implementation of the Backtrace interface that analyzes the current C++\n * stack.\n *\n * The Usage is:\n *\n * StackAnalyzer sa;\n * sa.analyze();\n *\n * Integrating analyze() into the constructor would be a bad idea because then,\n * all sorts of allocators and other gory constructor internals might be\n * visible in the Backtrace.\n *\n * The implementation  of analyze() and get_symbols() may use all sorts of\n * analyzers, depending on what's available on the platform.\n * The quality of the resolved symbol names may vary accordingly.\n */\nclass StackAnalyzer : public Backtrace {\npublic:\n\t/**\n\t * Creates an empty StackAnalyzer object.\n\t */\n\tStackAnalyzer() = default;\n\n\t/*\n\t * Analyzes the current stack, and stores the program counter values in\n\t * this->stack_addrs.\n\t */\n\tvoid analyze();\n\n\t/**\n\t * All program counters of this backtrace.\n\t */\n\tstd::vector<void *> stack_addrs;\n\n\t// Looks up symbol names for the program counter values.\n\tvoid get_symbols(std::function<void(const backtrace_symbol *)> cb, bool reversed) const override;\n\n\tvoid trim_to_current_stack_frame() override;\n};\n\n} // namespace error\n} // namespace openage\n"
  },
  {
    "path": "libopenage/event/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tevent_loop.cpp\n\tevent.cpp\n\tevententity.cpp\n\teventhandler.cpp\n\teventqueue.cpp\n\teventstore.cpp\n\tstate.cpp\n\ttests.cpp\n)\n\nadd_subdirectory(\"demo\")\n"
  },
  {
    "path": "libopenage/event/demo/CMakeLists.txt",
    "content": "add_sources(libopenage\n\taicontroller.cpp\n\tgamestate.cpp\n\tgui.cpp\n\tmain.cpp\n\tphysics.cpp\n)\n\n\npxdgen(\n\tmain.h\n)\n"
  },
  {
    "path": "libopenage/event/demo/aicontroller.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"aicontroller.h\"\n\nnamespace openage::event::demo {\n\nstd::vector<PongEvent> get_ai_inputs(const std::shared_ptr<PongPlayer> &player,\n                                     const std::shared_ptr<PongBall> &ball,\n                                     const time::time_t &now) {\n\tstd::vector<PongEvent> ret;\n\n\tauto position = player->position->get(now);\n\n\t// Yes i know, there is /3 used - instead of the logical /2 - this is to\n\t// create a small safety boundary of 1/3 for enhanced fancyness\n\n\t// Ball is below position\n\tif (ball->position->get(now)[1] > position + player->size->get(now) / 3) {\n\t\tret.emplace_back(player->id(), PongEvent::DOWN);\n\t}\n\t// Ball is above position\n\telse if (ball->position->get(now)[1] < position - player->size->get(now) / 3) {\n\t\tret.emplace_back(player->id(), PongEvent::UP);\n\t}\n\telse {\n\t\tret.emplace_back(player->id(), PongEvent::IDLE);\n\t}\n\n\treturn ret;\n}\n\n} // openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/aicontroller.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate.h\"\n\n#include <vector>\n\nnamespace openage::event::demo {\n\nstd::vector<PongEvent> get_ai_inputs(const std::shared_ptr<PongPlayer> &player,\n                                     const std::shared_ptr<PongBall> &ball,\n                                     const time::time_t &now);\n\n} // namespace openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/gamestate.cpp",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#include \"gamestate.h\"\n\nnamespace openage::event::demo {\n\n// This file is intentionally left empty\n\n} // openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/gamestate.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <functional>\n#include <list>\n#include <memory>\n#include <sstream>\n#include <string>\n\n#include \"config.h\"\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"event/evententity.h\"\n#include \"event/state.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n#include \"util/strings.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::event {\nclass EventLoop;\n\nnamespace demo {\n\n#if WITH_NCURSES\nclass Gui;\n#endif\n\n\nclass PongEvent {\npublic:\n\tenum state_e {\n\t\tUP,\n\t\tDOWN,\n\t\tSTART,\n\t\tIDLE,\n\t\tLOST\n\t};\n\n\tPongEvent(size_t id, state_e s) :\n\t\tplayer(id), state(s) {}\n\tPongEvent() :\n\t\tplayer(0), state(IDLE) {}\n\n\tsize_t player;\n\tstate_e state;\n};\n\n\nusing namespace std::placeholders;\nclass PongPlayer : public EventEntity {\npublic:\n\tPongPlayer(const std::shared_ptr<EventLoop> &mgr, size_t id) :\n\t\tEventEntity{mgr},\n\t\tspeed(std::make_shared<curve::Discrete<float>>(\n\t\t\tmgr,\n\t\t\t(id << 4) + 1,\n\t\t\tutil::sformat(\"PongPlayer(%zd).speed\", id),\n\t\t\tstd::bind(&PongPlayer::child_changes, this, _1))),\n\t\tposition(std::make_shared<curve::Continuous<float>>(\n\t\t\tmgr,\n\t\t\t(id << 4) + 2,\n\t\t\tutil::sformat(\"PongPlayer(%zd).position\", id),\n\t\t\tstd::bind(&PongPlayer::child_changes, this, _1))),\n\t\tlives(std::make_shared<curve::Discrete<int>>(\n\t\t\tmgr,\n\t\t\t(id << 4) + 3,\n\t\t\tutil::sformat(\"PongPlayer(%zd).lives\", id),\n\t\t\tstd::bind(&PongPlayer::child_changes, this, _1))),\n\t\tstate(std::make_shared<curve::Discrete<PongEvent>>(\n\t\t\tmgr,\n\t\t\t(id << 4) + 4,\n\t\t\tutil::sformat(\"PongPlayer(%zd).state\", id),\n\t\t\tstd::bind(&PongPlayer::child_changes, this, _1))),\n\t\tsize(std::make_shared<curve::Discrete<float>>(\n\t\t\tmgr,\n\t\t\t(id << 4) + 5,\n\t\t\tutil::sformat(\"PongPlayer(%zd).size\", id),\n\t\t\tstd::bind(&PongPlayer::child_changes, this, _1))),\n\t\t_id{id},\n\t\tpaddle_x{0} {}\n\n\tstd::shared_ptr<curve::Discrete<float>> speed;\n\tstd::shared_ptr<curve::Continuous<float>> position;\n\tstd::shared_ptr<curve::Discrete<int>> lives;\n\tstd::shared_ptr<curve::Discrete<PongEvent>> state;\n\tstd::shared_ptr<curve::Discrete<float>> size;\n\n\tsize_t _id;\n\tfloat paddle_x;\n\n\tsize_t id() const override {\n\t\treturn _id;\n\t}\n\n\tstd::string idstr() const override {\n\t\tstd::stringstream ss;\n\t\tss << \"PongPlayer[\" << this->id() << \"]\";\n\t\treturn ss.str();\n\t}\n\nprivate:\n\tvoid child_changes(const time::time_t &time) {\n\t\tthis->changes(time);\n\t}\n};\n\n\nclass PongBall : public EventEntity {\npublic:\n\tPongBall(const std::shared_ptr<EventLoop> &mgr, size_t id) :\n\t\tEventEntity{mgr},\n\t\tspeed(std::make_shared<curve::Discrete<util::Vector2d>>(\n\t\t\tmgr,\n\t\t\t(id << 2) + 1,\n\t\t\tutil::sformat(\"PongBall(%zd).speed\", id),\n\t\t\tstd::bind(&PongBall::child_changes, this, _1))),\n\t\tposition(std::make_shared<curve::Continuous<util::Vector2d>>(\n\t\t\tmgr,\n\t\t\t(id << 2) + 2,\n\t\t\tutil::sformat(\"PongBall(%zd).position\", id),\n\t\t\tstd::bind(&PongBall::child_changes, this, _1))),\n\t\t_id{id} {}\n\n\tstd::shared_ptr<curve::Discrete<util::Vector2d>> speed;\n\tstd::shared_ptr<curve::Continuous<util::Vector2d>> position;\n\n\tsize_t id() const override {\n\t\treturn _id;\n\t}\n\n\tstd::string idstr() const override {\n\t\tstd::stringstream ss;\n\t\tss << \"PongBall[\" << this->id() << \"]\";\n\t\treturn ss.str();\n\t}\n\nprivate:\n\tvoid child_changes(const time::time_t &time) {\n\t\tthis->changes(time);\n\t}\n\tsize_t _id;\n};\n\n\nclass PongState : public State {\npublic:\n\tPongState(const std::shared_ptr<EventLoop> &mgr, bool enable_gui\n#if WITH_NCURSES\n\t          ,\n\t          const std::shared_ptr<Gui> &gui\n#endif\n\t          ) :\n\t\tState{mgr},\n\t\tp1(std::make_shared<PongPlayer>(mgr, 0)),\n\t\tp2(std::make_shared<PongPlayer>(mgr, 1)),\n\t\tball(std::make_shared<PongBall>(mgr, 2)),\n\t\tenable_gui{enable_gui}\n#if WITH_NCURSES\n\t\t,\n\t\tgui{gui}\n#endif\n\t{\n\t}\n\n\tstd::shared_ptr<PongPlayer> p1;\n\tstd::shared_ptr<PongPlayer> p2;\n\tstd::shared_ptr<PongBall> ball;\n\tutil::Vector2d display_boundary;\n\n\tbool enable_gui;\n\n#if WITH_NCURSES\n\tstd::shared_ptr<Gui> gui;\n#endif\n};\n\n} // namespace demo\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/demo/gui.cpp",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#include \"gui.h\"\n\n// the gui requires ncurses.\n#if WITH_NCURSES\n\n\t#include <algorithm>\n\t#include <array>\n\t#include <cstdlib>\n\t#include <cstring>\n\t#ifdef __MINGW32__\n\t\t#include <ncurses/ncurses.h>\n\t#else\n\t\t#include <ncurses.h>\n\t#endif // __MINGW32__\n\t#include <vector>\n\n\t#include \"curve/continuous.h\"\n\t#include \"curve/discrete.h\"\n\t#include \"event/demo/gamestate.h\"\n\t#include \"util/fixed_point.h\"\n\nnamespace openage::event::demo {\n\nconst std::vector<PongEvent> &Gui::get_inputs(const std::shared_ptr<PongPlayer> &player) {\n\tthis->input_cache.clear();\n\n\tPongEvent evnt;\n\tevnt.player = player->id();\n\tevnt.state = PongEvent::IDLE;\n\n\ttimeout(0);\n\n\tstd::vector<int> inputs;\n\tint c;\n\twhile ((c = getch()) != ERR) {\n\t\tif (std::find(inputs.begin(), inputs.end(), c) == inputs.end()) {\n\t\t\tinputs.push_back(c);\n\t\t}\n\t}\n\n\tfor (auto input : inputs) {\n\t\tswitch (input) {\n\t\tcase KEY_DOWN:\n\t\t\tevnt.state = PongEvent::DOWN;\n\t\t\tmvprintw(1, 40, \"DOWN\");\n\t\t\tbreak;\n\n\t\tcase KEY_UP:\n\t\t\tevnt.state = PongEvent::UP;\n\t\t\tmvprintw(1, 40, \"UP\");\n\t\t\tbreak;\n\n\t\tcase 27: // esc or alt\n\t\t\terase();\n\t\t\trefresh();\n\t\t\tendwin();\n\t\t\texit(0);\n\t\t\tbreak;\n\n\t\tcase 'r':\n\t\tcase ' ':\n\t\t\tevnt.state = PongEvent::START;\n\t\t\tbreak;\n\n\t\tdefault:\n\t\t\tevnt.state = PongEvent::IDLE;\n\t\t\tbreak;\n\t\t}\n\n\t\tthis->input_cache.push_back(evnt);\n\t}\n\n\tif (this->input_cache.empty()) {\n\t\t// store 'idle' input to cancel movement\n\t\tthis->input_cache.push_back(evnt);\n\t}\n\n\treturn this->input_cache;\n}\n\n\nenum {\n\tCOLOR_PLAYER1 = 1,\n\tCOLOR_PLAYER2 = 2,\n\tCOLOR_BALL = 3,\n\tCOLOR_DEBUG = 4,\n\n\tCOLOR_0 = 5,\n\tCOLOR_1 = 6,\n\tCOLOR_2 = 7,\n\tCOLOR_3 = 8,\n\tCOLOR_4 = 9,\n};\n\n\nconstexpr const int max_log_msgs = 10;\n\nGui::Gui() = default;\n\n\nvoid Gui::init() {\n\tinitscr();\n\tstart_color();\n\n\tinit_pair(COLOR_PLAYER1, COLOR_BLUE, COLOR_BLUE);\n\tinit_pair(COLOR_PLAYER2, COLOR_RED, COLOR_RED);\n\tinit_pair(COLOR_BALL, COLOR_BLUE, COLOR_WHITE);\n\tinit_pair(COLOR_DEBUG, COLOR_WHITE, COLOR_BLACK);\n\tinit_pair(COLOR_0, COLOR_RED, COLOR_BLACK);\n\tinit_pair(COLOR_1, COLOR_GREEN, COLOR_BLACK);\n\n\tkeypad(stdscr, true);\n\tnoecho();\n\tcbreak();\n\tcurs_set(0);\n\twnoutrefresh(curscr);\n\n\tint x, y;\n\tgetmaxyx(stdscr, y, x);\n\n\tattron(COLOR_PAIR(COLOR_DEBUG));\n\n\tstd::vector<const char *> buffer{\n\t\t\"oooooooooo                                   \",\n\t\t\" 888    888  ooooooo    ooooooo    oooooooo8 \",\n\t\t\" 888oooo88 888     888 888   888  888    88o \",\n\t\t\" 888       888     888 888   888   888oo888o \",\n\t\t\"o888o        88ooo88  o888o o888o     88 888 \",\n\t\t\"                                    888ooo888\",\n\t};\n\n\tsize_t colwidth = 0;\n\tfor (const auto &c : buffer) {\n\t\tcolwidth = std::max(colwidth, strlen(c));\n\t}\n\n\tint row = (y - buffer.size()) / 2;\n\tint col = (x - colwidth) / 2;\n\tfor (const auto &c : buffer) {\n\t\tmvprintw(row++, col, \"%c\", *c);\n\t}\n\n\tattroff(COLOR_PAIR(COLOR_DEBUG));\n\n\trefresh();\n\n\t// wait for a keypress to begin game\n\tgetch();\n}\n\n\nvoid Gui::clear() {\n\terase();\n}\n\n\nvoid Gui::get_display_size(const std::shared_ptr<PongState> &state,\n                           const time::time_t & /*now*/) {\n\t// record the screen dimensions in the game state\n\n\t// TODO: make the display_boundary a curve as well.\n\tgetmaxyx(stdscr, state->display_boundary[1], state->display_boundary[0]);\n\tstate->display_boundary[1] -= 1;\n}\n\n\nvoid Gui::draw(const std::shared_ptr<PongState> &state, const time::time_t &now) {\n\tattron(COLOR_PAIR(COLOR_DEBUG));\n\n\t// print the score\n\tattron(COLOR_PAIR(COLOR_DEBUG));\n\tmvprintw(2,\n\t         state->display_boundary[0] / 2 - 5,\n\t         \"P1 %i | P2 %i\",\n\t         state->p1->lives->get(now),\n\t         state->p2->lives->get(now));\n\n\t// draw the middle line\n\tmvvline(0, state->display_boundary[0] / 2, ACS_VLINE, state->display_boundary[1]);\n\n\t// debug information\n\tmvprintw(0, 1, \"NOW:  %f\", now.to_double());\n\tmvprintw(1, 1, \"SCR:  %f | %f\", state->display_boundary[0], state->display_boundary[1]);\n\tmvprintw(2, 1, \"P1:   %f, %f, %i\", state->p1->position->get(now), state->p1->paddle_x, state->p1->state->get(now).state);\n\tmvprintw(3, 1, \"P2:   %f, %f, %i\", state->p2->position->get(now), state->p2->paddle_x, state->p2->state->get(now).state);\n\n\t// ball position predictions, 10s into the future\n\tfor (int i = 0; i < 10; i++) {\n\t\tauto i_as_ctt = time::time_t::from_int(i);\n\t\tmvprintw((5 + i), 1, \"BALL in %03f: %f | %f; SPEED: %f | %f | PLpos: %f, PRpos: %f\", i_as_ctt.to_double(), state->ball->position->get(now + i_as_ctt)[0], state->ball->position->get(now + i_as_ctt)[1], state->ball->speed->get(now + i_as_ctt)[0], state->ball->speed->get(now + i_as_ctt)[1], state->p1->position->get(now + i_as_ctt), state->p2->position->get(now + i_as_ctt));\n\t}\n\n\t// show log\n\tint msg_i = 0;\n\tfor (auto &msg : this->log_msgs) {\n\t\tmvprintw((6 + msg_i), state->display_boundary[0] / 2 + 10, \"%s\", msg.c_str());\n\t\tmsg_i += 1;\n\t}\n\n\n\t// exit hint message\n\tmvprintw(state->display_boundary[1] - 1, 1, \"Press ESC to exit\");\n\tattroff(COLOR_PAIR(COLOR_DEBUG));\n\n\t// draw player 1 paddle\n\tattron(COLOR_PAIR(COLOR_PLAYER1));\n\tfor (int i = -state->p1->size->get(now) / 2; i < state->p1->size->get(now) / 2; i++) {\n\t\tmvprintw(state->p1->position->get(now) + i, state->p1->paddle_x, \"|\");\n\t}\n\tattroff(COLOR_PAIR(COLOR_PLAYER1));\n\n\t// draw player 2 paddle\n\tattron(COLOR_PAIR(COLOR_PLAYER2));\n\tfor (int i = -state->p2->size->get(now) / 2; i < state->p2->size->get(now) / 2; i++) {\n\t\tmvprintw(state->p2->position->get(now) + i, state->p2->paddle_x, \"|\");\n\t}\n\tattroff(COLOR_PAIR(COLOR_PLAYER2));\n\n\t// ball position prediction 10s into the future\n\tattron(COLOR_PAIR(COLOR_1));\n\tfor (int i = 1; i < 100; ++i) {\n\t\tauto i_as_ctt = time::time_t::from_double(i / 10.0);\n\t\tdraw_ball(state->ball->position->get(now + i_as_ctt), \"x\");\n\t}\n\n\t// draw the ball\n\tattron(COLOR_PAIR(COLOR_BALL));\n\tthis->draw_ball(state->ball->position->get(now), \"M\"); // TODO: use \"⚽\"\n\tattroff(COLOR_PAIR(COLOR_BALL));\n}\n\n\nvoid Gui::update_screen() {\n\t// actually show screen output\n\trefresh();\n}\n\n\nvoid Gui::draw_ball(util::Vector2d pos, const char *str) {\n\tmvprintw(static_cast<int>(pos[1]), static_cast<int>(pos[0]), \"%s\", str);\n\tstandend();\n}\n\n\nvoid Gui::log(const std::string &msg) {\n\tif (this->log_msgs.size() >= max_log_msgs) {\n\t\tthis->log_msgs.pop_back();\n\t}\n\tthis->log_msgs.push_front(msg);\n}\n\n} // namespace openage::event::demo\n\n#endif\n"
  },
  {
    "path": "libopenage/event/demo/gui.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"config.h\"\n\n#if WITH_NCURSES\n\t#include <deque>\n\t#include <memory>\n\t#include <string>\n\t#include <vector>\n\n\t#include \"event/demo/gamestate.h\"\n\t#include \"time/time.h\"\n\t#include \"util/vector.h\"\n\n\nnamespace openage::event::demo {\n\n\nclass Gui {\npublic:\n\tGui();\n\tvoid init();\n\tvoid clear();\n\tvoid update_screen();\n\n\tconst std::vector<PongEvent> &get_inputs(const std::shared_ptr<PongPlayer> &player);\n\tvoid get_display_size(const std::shared_ptr<PongState> &state,\n\t                      const time::time_t &now);\n\n\tvoid draw(const std::shared_ptr<PongState> &state, const time::time_t &now);\n\tvoid draw_ball(util::Vector2d ball, const char *str);\n\n\tvoid log(const std::string &msg);\n\nprivate:\n\tstd::vector<PongEvent> input_cache;\n\tstd::deque<std::string> log_msgs;\n};\n\n} // namespace openage::event::demo\n\n#endif\n"
  },
  {
    "path": "libopenage/event/demo/main.cpp",
    "content": "// Copyright 2016-2024 the openage authors. See copying.md for legal info.\n\n#include \"main.h\"\n\n#include <chrono>\n#include <ratio>\n#include <thread>\n\n#include \"config.h\"\n#include \"event/demo/aicontroller.h\"\n#include \"event/demo/gamestate.h\"\n#include \"event/demo/gui.h\"\n#include \"event/demo/physics.h\"\n#include \"event/event.h\"\n#include \"event/event_loop.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n\n#if WITH_NCURSES\n\t#ifdef __MINGW32__\n\t\t#include <ncurses/ncurses.h>\n\t#else\n\t\t#include <ncurses.h>\n\t#endif // __MINGW32__\n#endif\n\n\nusing Clock = std::chrono::high_resolution_clock;\nusing namespace std::literals::chrono_literals;\n\n\nnamespace openage::event::demo {\n\n/**\n * Simulation speeds\n */\nenum class timescale {\n\tNOSLEEP,\n\tREALTIME,\n\tSLOW,\n\tFAST,\n};\n\n\nvoid curvepong(bool disable_gui, bool no_human) {\n\trenderer::gui::GuiApplicationWithLogger gui_app{};\n\tbool enable_gui = not disable_gui;\n\tbool human_player = not no_human;\n\n\ttimescale speed = timescale::REALTIME;\n\n\n#if WITH_NCURSES\n\tstd::shared_ptr<Gui> gui = std::make_shared<Gui>();\n\n\tif (enable_gui) {\n\t\tgui->init();\n\t}\n#endif\n\n\tbool running = true;\n\n\twhile (running) {\n\t\tauto loop = std::make_shared<EventLoop>();\n\t\ttime::time_t now = 1;\n\t\tPhysics phys;\n\n\t\tauto state = std::make_shared<PongState>(loop, enable_gui\n#if WITH_NCURSES\n\t\t                                         ,\n\t\t                                         gui\n#endif\n\t\t);\n\t\tPhysics::init(state, loop, now);\n\n\t\tstate->p1->lives->set_last(now, 3);\n\t\tstate->p1->size->set_last(now, 4);\n\n\t\tstate->p2->lives->set_last(now, 3);\n\t\tstate->p2->size->set_last(now, 4);\n\n#if WITH_NCURSES\n\t\tif (state->enable_gui) {\n\t\t\tgui->clear();\n\t\t\tgui->get_display_size(state, now); // update gui related parameters\n\t\t}\n\t\telse {\n#endif\n\t\t\tstate->display_boundary = {100, 40};\n#if WITH_NCURSES\n\t\t}\n#endif\n\n\t\t// initialize the game by resetting physics\n\t\tstd::vector<PongEvent> start_event{PongEvent{0, PongEvent::START}};\n\t\tphys.process_input(state, state->p1, start_event, loop, now);\n\n\n\t\tstd::vector<PongEvent> inputs;\n\n\t\t// this is the game loop, running while both players live!\n\t\twhile (state->p1->lives->get(now) > 0 and state->p2->lives->get(now) > 0) {\n\t\t\tauto loop_start = Clock::now();\n\n\t\t\tgui_app.process_events();\n\n#if WITH_NCURSES\n\t\t\tif (enable_gui) {\n\t\t\t\tgui->clear();\n\t\t\t\tgui->get_display_size(state, now);\n\t\t\t}\n#endif\n\n\t\t\t// process the input for both players\n\t\t\t// player 1 can be AI or human.\n\n\t\t\tif (human_player) {\n\t\t\t\tphys.process_input(state, state->p1, inputs, loop, now);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tphys.process_input(state, state->p1, get_ai_inputs(state->p1, state->ball, now), loop, now);\n\t\t\t}\n\n\t\t\tphys.process_input(state, state->p2, get_ai_inputs(state->p2, state->ball, now), loop, now);\n\n\t\t\t// paddle x positions\n\t\t\tstate->p1->paddle_x = 0;\n\t\t\tstate->p2->paddle_x = state->display_boundary[0] - 1;\n\n\t\t\t// evaluate the event queue to reach the desired game time!\n\t\t\tloop->reach_time(now, state);\n\n#if WITH_NCURSES\n\t\t\tif (state->enable_gui) {\n\t\t\t\tgui->draw(state, now);\n\n\t\t\t\tint pos = 1;\n\t\t\t\tmvprintw(pos++, state->display_boundary[0] / 2 + 10, \"Enqueued events:\");\n\t\t\t\tfor (const auto &e : loop->get_queue().get_event_queue().get_sorted_events()) {\n\t\t\t\t\tmvprintw(pos++, state->display_boundary[0] / 2 + 10, \"%f: %s\", e->get_time().to_double(), e->get_eventhandler()->id().c_str());\n\t\t\t\t}\n\n\t\t\t\tgui->update_screen();\n\n\t\t\t\t// get the input after the current frame, because the get_inputs\n\t\t\t\t// implicitly updates the screen. if this is done too early,\n\t\t\t\t// the screen will be updated to be empty -> flickering.\n\t\t\t\t// TODO: take terminal input without ncurses, if on tty?\n\t\t\t\t//       e.g. 'q' to close it?\n\t\t\t\tinputs = gui->get_inputs(state->p1);\n\t\t\t}\n#endif\n\n\t\t\t// handle timing for screen refresh and simulation advancement\n\t\t\tusing dt_s_t = std::chrono::duration<double, std::ratio<1>>;\n\t\t\tusing dt_ms_t = std::chrono::duration<double, std::milli>;\n\n\t\t\t// microseconds per frame\n\t\t\t// 30fps = 1s/30 = 1000000us/30 per frame\n\t\t\tconstexpr static std::chrono::microseconds per_frame = 33333us;\n\t\t\tconstexpr static time::time_t per_frame_s = std::chrono::duration_cast<dt_s_t>(per_frame).count();\n\n\t\t\tif (speed == timescale::NOSLEEP) {\n\t\t\t\t// increase the simulation loop time a bit\n\t\t\t\tstd::this_thread::sleep_for(std::chrono::milliseconds(5));\n\t\t\t}\n\n\t\t\tdt_ms_t dt_us = Clock::now() - loop_start;\n\n\t\t\tif (speed != timescale::NOSLEEP) {\n\t\t\t\tdt_ms_t wait_time = per_frame - dt_us;\n\n\t\t\t\tif (wait_time > dt_ms_t::zero()) {\n\t\t\t\t\tstd::this_thread::sleep_for(wait_time);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch (speed) {\n\t\t\tcase timescale::NOSLEEP:\n\t\t\t\t// advance the frame calculation time only,\n\t\t\t\t// because we didn't sleep\n\t\t\t\tnow += std::chrono::duration_cast<dt_s_t>(dt_us).count();\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::REALTIME:\n\t\t\t\t// advance the game time in seconds\n\t\t\t\tnow += per_frame_s;\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::SLOW:\n\t\t\t\tnow += per_frame_s / 10;\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::FAST:\n\t\t\t\tnow += per_frame_s * 4;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n} // namespace openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/main.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../../util/compiler.h\"\n// pxd: from libcpp cimport bool\n\n\nnamespace openage::event::demo {\n\n// pxd: void curvepong(bool disable_gui, bool no_human) except +\nOAAPI void curvepong(bool disable_gui, bool no_human);\n\n\n} // namespace openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/physics.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"physics.h\"\n\n#include <array>\n#include <cstring>\n#include <optional>\n#include <sstream>\n#include <string>\n\n#if WITH_NCURSES\n\t#ifdef __MINGW32__\n\t\t#include <ncurses/ncurses.h>\n\t#else\n\t\t#include <ncurses.h>\n\t#endif // __MINGW32__\n\n\t#include \"gui.h\"\n#endif\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"config.h\"\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"error/error.h\"\n#include \"event/demo/gamestate.h\"\n#include \"event/event.h\"\n#include \"event/event_loop.h\"\n#include \"event/eventhandler.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n#include \"rng/global_rng.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n#include \"util/stringformatter.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::event::demo {\n\n\nclass BallReflectWall : public DependencyEventHandler {\npublic:\n\tBallReflectWall() :\n\t\tDependencyEventHandler(\"demo.ball.reflect_wall\") {}\n\n\tvoid setup_event(const std::shared_ptr<Event> &evnt,\n\t                 const std::shared_ptr<State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\t// FIXME dependency to a full ball object so that any curve\n\t\t// triggers a change\n\t\tevnt->depend_on(state->ball->position);\n\t\tevnt->depend_on(state->ball->speed);\n\t\t// TODO add dependency to size of game area\n\n\t\t// FIXME: warn if it's not a dependency eventhandler\n\t}\n\n\t// FIXME we REALLY need dependencies to objects i.e. Ball : public EventEntity()\n\tvoid invoke(EventLoop &,\n\t            const std::shared_ptr<EventEntity> &target,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &now,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Continuous<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\t\tauto speedcurve = state->ball->speed;\n\n\t\t// get speed and position to insert new movement keyframe\n\t\tauto speed = speedcurve->get(now);\n\t\tauto pos = positioncurve->get(now);\n\t\tspeed[1] *= -1.0;\n\t\tstate->ball->speed->set_last(now, speed);\n\t\tstate->ball->position->set_last(now, pos);\n\n\t\tif (speed[1] == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[1] > 0) {\n\t\t\tty = time::time_t::from_double(\n\t\t\t\t(state->display_boundary[1] - pos[1]) / speed[1]);\n\t\t}\n\t\telse if (speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(\n\t\t\t\tpos[1] / -speed[1]);\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, pos + (speed * ty.to_double()));\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> &target,\n\t                                 const std::shared_ptr<State> &gstate,\n\t                                 const time::time_t &now) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Continuous<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto speed = state->ball->speed->get(now);\n\t\tauto pos = positioncurve->get(now);\n\n\t\tif (speed[1] == 0) {\n\t\t\treturn time::TIME_MAX;\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[1] > 0) {\n\t\t\tty = time::time_t::from_double((state->display_boundary[1] - pos[1]) / speed[1]);\n\t\t}\n\t\telse if (speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(pos[1] / -speed[1]);\n\t\t}\n\n#if WITH_NCURSES\n\t\tif (state->enable_gui) {\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"WALL TY %f NOW %f, NOWTY %f \",\n\t\t\t        ty.to_double(),\n\t\t\t        now.to_double(),\n\t\t\t        (now + ty).to_double());\n\t\t\tstate->gui->log(str);\n\t\t};\n#endif\n\t\treturn now + ty;\n\t}\n};\n\n\nclass BallReflectPanel : public DependencyEventHandler {\npublic:\n\tBallReflectPanel() :\n\t\tDependencyEventHandler(\"demo.ball.reflect_panel\") {}\n\n\tvoid setup_event(const std::shared_ptr<Event> &target,\n\t                 const std::shared_ptr<State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\t// FIXME dependency to a full ball object\n\t\t// then a change to any of the curves triggers the update.\n\t\ttarget->depend_on(state->ball->position);\n\t\ttarget->depend_on(state->ball->speed);\n\t\ttarget->depend_on(state->p1->position);\n\t\ttarget->depend_on(state->p2->position);\n\t\t// TODO add dependency to size of game area\n\t}\n\n\t// FIXME we REALLY need dependencies to objects\n\tvoid invoke(EventLoop &mgr,\n\t            const std::shared_ptr<EventEntity> & /*target*/,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &now,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto pos = state->ball->position->get(now);\n\t\tauto speed = state->ball->speed->get(now);\n\n#if WITH_NCURSES\n\t\tif (state->enable_gui) {\n\t\t\tstatic int cnt = 0;\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"Panel hit [%i]\", ++cnt);\n\t\t\tstate->gui->log(str);\n\t\t}\n\t\telse {\n#endif\n\t\t\tlog::log(INFO << \"Panel hit at \" << now);\n#if WITH_NCURSES\n\t\t}\n#endif\n\n\t\tif (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)) {\n\t\t\t// ball missed the paddle of player 1\n\t\t\tauto l = state->p1->lives->get(now);\n\t\t\tl--;\n\n\t\t\tstate->p1->lives->set_last(now, l);\n\t\t\tstate->ball->position->set_last(now, pos);\n\n\t\t\tPhysics::reset(state, mgr, now);\n\t\t}\n\t\telse if (pos[0] >= state->display_boundary[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)) {\n\t\t\t// ball missed the paddel of player 2\n\t\t\tauto l = state->p2->lives->get(now);\n\t\t\tl--;\n\t\t\tstate->p2->lives->set_last(now, l);\n\t\t\tstate->ball->position->set_last(now, pos);\n\n\t\t\tPhysics::reset(state, mgr, now);\n\t\t}\n\t\telse if (pos[0] >= state->display_boundary[0] - 1 || pos[0] <= 1) {\n\t\t\tspeed[0] *= -1;\n\t\t\tstate->ball->speed->set_last(now, speed);\n\t\t\tstate->ball->position->set_last(now, pos);\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[0] > 0) {\n\t\t\tty = time::time_t::from_double((state->display_boundary[0] - pos[0]) / speed[0]);\n\t\t}\n\t\telse if (speed[0] < 0) {\n\t\t\tty = time::time_t::from_double(pos[0] / -speed[0]);\n\t\t}\n\n\t\tauto hit_pos = pos + speed * ty.to_double();\n\t\tif (speed[0] > 0) {\n\t\t\thit_pos[0] = state->display_boundary[0];\n\t\t}\n\t\telse {\n\t\t\thit_pos[0] = 0;\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, hit_pos);\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> &target,\n\t                                 const std::shared_ptr<State> &gstate,\n\t                                 const time::time_t &now) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Continuous<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto speed = state->ball->speed->get(now);\n\t\tauto pos = positioncurve->get(now);\n\n\t\tif (speed[0] == 0)\n\t\t\treturn time::TIME_MAX;\n\n\t\ttime::time_t ty = 0;\n\n\t\tif (speed[0] > 0) {\n\t\t\tty = time::time_t::from_double((state->display_boundary[0] - pos[0]) / speed[0]);\n\t\t}\n\t\telse if (speed[0] < 0) {\n\t\t\tty = time::time_t::from_double(pos[0] / -speed[0]);\n\t\t}\n\n#if WITH_NCURSES\n\t\tif (state->enable_gui) {\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"PANEL REFLECT AT %f NEXT %f\", now.to_double(), (now + ty).to_double());\n\t\t\tstate->gui->log(str);\n\t\t}\n\t\telse {\n#endif\n\t\t\tlog::log(INFO << \"predicting panel reflection at t=\" << now\n\t\t\t              << \", next reflection at t=\" << (now + ty));\n#if WITH_NCURSES\n\t\t}\n#endif\n\n\t\tauto hit_pos = pos + speed * ty.to_double();\n\t\tif (speed[0] > 0) {\n\t\t\thit_pos[0] = state->display_boundary[0];\n\t\t}\n\t\telse {\n\t\t\thit_pos[0] = 0;\n\t\t}\n\n\t\tENSURE(hit_pos[0] >= 0, \"hit position x must be positive\");\n\n\t\treturn now + ty;\n\t}\n};\n\n\nclass ResetGame : public OnceEventHandler {\npublic:\n\tResetGame() :\n\t\tOnceEventHandler(\"demo.reset\") {}\n\n\tvoid setup_event(const std::shared_ptr<Event> & /*target*/,\n\t                 const std::shared_ptr<State> & /*state*/) override {}\n\n\tvoid invoke(EventLoop & /*mgr*/,\n\t            const std::shared_ptr<EventEntity> & /*target*/,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &now,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\t// Check if the condition still applies\n\t\t{\n\t\t\tauto pos = state->ball->position->get(now);\n\t\t\tif (pos[0] > 0 && pos[0] < state->display_boundary[0]) {\n\t\t\t\t// the gamestate is still valid - there is no need to reset\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// move ball to the center\n\t\tstate->ball->position->set_last(now - time::time_t::from_double(0.1),\n\t\t                                state->ball->position->get(now));\n\t\tstate->ball->position->set_last(now, state->display_boundary * 0.5);\n\n\t\t// move paddles to center\n\t\tstate->p1->position->set_last(now, state->display_boundary[1] / 2);\n\t\tstate->p2->position->set_last(now, state->display_boundary[1] / 2);\n\n\t\tfloat dirx = (rng::random() % 2) ? 1 : -1;\n\t\tfloat diry = (rng::random() % 2) ? 1 : -1;\n\t\tauto init_speed = util::Vector2d(\n\t\t\tdirx * (10 + (rng::random() % 100) / 4.f),\n\t\t\tdiry * (0.3 + (rng::random() % 100) / 18.f));\n\n\t\tstate->ball->speed->set_last(now, init_speed);\n\t\tauto pos = state->ball->position->get(now);\n\n#if WITH_NCURSES\n\t\tif (state->enable_gui) {\n\t\t\tstatic int cnt = 0;\n\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"Reset. Speed %f | %f POS %f | %f [%i]\",\n\t\t\t        init_speed[0],\n\t\t\t        init_speed[1],\n\t\t\t        pos[0],\n\t\t\t        pos[1],\n\t\t\t        ++cnt);\n\t\t\tstate->gui->log(str);\n\t\t}\n\t\telse {\n#endif\n\t\t\tlog::log(INFO << \"Game was reset.\");\n#if WITH_NCURSES\n\t\t}\n#endif\n\n\t\ttime::time_t ty = 0;\n\n\t\t// calculate the wall-hit-times\n\t\tif (init_speed[1] > 0) {\n\t\t\tty = time::time_t::from_double((state->display_boundary[1] - pos[1]) / init_speed[1]);\n\t\t}\n\t\telse if (init_speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(pos[1] / -init_speed[1]);\n\t\t}\n\t\telse {\n\t\t\t// currently never happens, but this would be a non-vertically-moving ball\n\t\t\t// fallback to calculating panel-hit-times\n\t\t\tif (init_speed[0] > 0) {\n\t\t\t\tty = time::time_t::from_double((state->display_boundary[0] - pos[0]) / init_speed[0]);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tty = time::time_t::from_double(pos[0] / -init_speed[0]);\n\t\t\t}\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, pos + init_speed * ty.to_double());\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> & /*target*/,\n\t                                 const std::shared_ptr<State> & /*state*/,\n\t                                 const time::time_t &old_time) override {\n\t\treturn old_time;\n\t}\n};\n\n\nvoid Physics::init(const std::shared_ptr<PongState> &gstate,\n                   const std::shared_ptr<EventLoop> &loop,\n                   const time::time_t &now) {\n\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\tif (not state->enable_gui) {\n\t\tlog::log(INFO << \"Physics initialization\");\n\t}\n\n\tloop->add_event_handler(std::make_shared<BallReflectPanel>());\n\tloop->add_event_handler(std::make_shared<BallReflectWall>());\n\tloop->add_event_handler(std::make_shared<ResetGame>());\n\n\tloop->create_event(\"demo.ball.reflect_wall\", state->ball->position, state, now);\n\tloop->create_event(\"demo.ball.reflect_panel\", state->ball->position, state, now);\n\n\t// FIXME once \"reset\": deregister\n\t// reset(state, mgr, now);\n}\n\nvoid Physics::process_input(const std::shared_ptr<PongState> &state,\n                            const std::shared_ptr<PongPlayer> &player,\n                            const std::vector<PongEvent> &events,\n                            const std::shared_ptr<EventLoop> &mgr,\n                            const time::time_t &now) {\n\t// seconds into the future\n\tconstexpr static auto predicted_movement_time = time::time_t::from_double(5.0);\n\n\t// lines per second\n\tconstexpr static double movement_speed = 8.0;\n\n\tfor (auto &evnt : events) {\n\t\t// Process only if the future has changed\n\t\tif (player->state->get(now).state != evnt.state) {\n\t\t\t// log the new input in the state curve\n\t\t\tplayer->state->set_last(now, evnt);\n\n\t\t\t// if the state is active longer than predicted,\n\t\t\t// we have to extend the prediction!\n\t\t\tbool extend_previous_prediction = false;\n\t\t\tauto prev_state = player->state->get_previous(now);\n\t\t\tif (prev_state and prev_state->second.state == evnt.state) {\n\t\t\t\textend_previous_prediction = true;\n\t\t\t}\n\n\t\t\tswitch (evnt.state) {\n\t\t\tcase PongEvent::UP:\n\t\t\tcase PongEvent::DOWN: {\n\t\t\t\tfloat current_pos = player->position->get(now);\n\n\t\t\t\tif (not extend_previous_prediction) {\n\t\t\t\t\t// create a keyframe for the new movement\n\t\t\t\t\tplayer->position->set_last(now, current_pos);\n\t\t\t\t\t// TODO: drop the intermediate keyframes\n\t\t\t\t\t//       for position and speed.\n\t\t\t\t}\n\n\t\t\t\tswitch (evnt.state) {\n\t\t\t\tcase PongEvent::UP:\n\t\t\t\t\tplayer->speed->set_last(now, -movement_speed);\n\t\t\t\t\tbreak;\n\t\t\t\tcase PongEvent::DOWN:\n\t\t\t\t\tplayer->speed->set_last(now, movement_speed);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t// never reached.\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\ttime::time_t move_stop_guess = now + predicted_movement_time;\n\n\t\t\t\t// change the position by integrating the speed curve.\n\t\t\t\t// TODO: add native integral to curves.\n\t\t\t\tfloat new_pos = current_pos + (((player->speed->get(move_stop_guess) + player->speed->get(now)) / 2.0) * predicted_movement_time.to_double());\n\n\t\t\t\t// if paddle will get out-of-bounds, calculate the bound hit time\n\n\t\t\t\t// TODO: add native key/value finding in range to curves\n\t\t\t\tif (new_pos < 0) {\n\t\t\t\t\tmove_stop_guess = now + (current_pos / movement_speed);\n\t\t\t\t\tnew_pos = 0;\n\t\t\t\t}\n\n\t\t\t\tif (new_pos > state->display_boundary[1]) {\n\t\t\t\t\tmove_stop_guess = now + ((state->display_boundary[1] - current_pos) / movement_speed);\n\t\t\t\t\tnew_pos = state->display_boundary[1];\n\t\t\t\t}\n\n\t\t\t\tPongEvent set_idle{evnt.player, PongEvent::IDLE};\n\t\t\t\tplayer->state->set_last(move_stop_guess, set_idle);\n\t\t\t\tplayer->speed->set_last(move_stop_guess, 0);\n\t\t\t\tplayer->position->set_last(move_stop_guess, new_pos);\n\n\t\t\t\t// TODO: predict_reflect_panel(state, queue, now);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase PongEvent::IDLE:\n\t\t\t\tplayer->position->set_last(now, player->position->get(now));\n\t\t\t\tplayer->speed->set_last(now, 0);\n\t\t\t\tbreak;\n\n\t\t\tcase PongEvent::START:\n\t\t\t\tPhysics::reset(state, *mgr, now);\n\t\t\t\tbreak;\n\n\t\t\t\t// if (player->state->get(now).state == PongEvent::LOST) {\n\t\t\t\t//\tstate->ball.position->set_last(now, state.display_boundary * 0.5);\n\t\t\t\t// }\n\t\t\t\t// update_ball(state, now, init_recursion_limit);\n\t\t\t\t// break;\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n\nvoid Physics::reset(const std::shared_ptr<State> &gstate,\n                    EventLoop &mgr,\n                    const time::time_t &now) {\n\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\tmgr.create_event(\"demo.reset\", state->ball->position, state, now);\n}\n\n} // namespace openage::event::demo\n"
  },
  {
    "path": "libopenage/event/demo/physics.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include \"time/time.h\"\n\n\nnamespace openage::event {\n\nclass EventLoop;\nclass State;\n\nnamespace demo {\n\nclass PongEvent;\nclass PongPlayer;\nclass PongState;\n\nclass Physics {\npublic:\n\tstatic void init(const std::shared_ptr<PongState> &,\n\t                 const std::shared_ptr<EventLoop> &mgr,\n\t                 const time::time_t &);\n\n\tvoid process_input(const std::shared_ptr<PongState> &,\n\t                   const std::shared_ptr<PongPlayer> &,\n\t                   const std::vector<PongEvent> &input,\n\t                   const std::shared_ptr<EventLoop> &mgr,\n\t                   const time::time_t &now);\n\n\tstatic void reset(const std::shared_ptr<State> &,\n\t                  EventLoop &mgr,\n\t                  const time::time_t &);\n};\n\n} // namespace demo\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/event.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"event.h\"\n\n#include <compare>\n#include <functional>\n#include <string>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n#include \"util/hash.h\"\n\n\nnamespace openage::event {\n\nEvent::Event(const std::shared_ptr<EventEntity> &entity,\n             const std::shared_ptr<EventHandler> &eventhandler,\n             const EventHandler::param_map &params) :\n\tparams(params),\n\tentity{entity},\n\teventhandler{eventhandler},\n\tmyhash{\n\t\tutil::hash_combine(std::hash<size_t>()(entity->id()),\n                           std::hash<std::string>()(eventhandler->id()))} {}\n\n\nvoid Event::depend_on(const std::shared_ptr<EventEntity> &dependency) {\n\t// TODO: do REPEAT and TRIGGER listen to changes (i.e. have dependents)?\n\t// if not, exclude them here and return early.\n\n\tlog::log(DBG << \"Registering dependency event from EventHandler \"\n\t             << this->get_eventhandler()->id()\n\t             << \" to EventEntity \" << dependency->idstr());\n\n\tdependency->add_dependent(this->shared_from_this());\n}\n\n\nvoid Event::cancel(const time::time_t reference_time) {\n\tlog::log(DBG << \"Canceling event from EventHandler \"\n\t             << this->get_eventhandler()->id()\n\t             << \" for time t=\" << this->get_time());\n\n\t// remove the target which releases the event in the next loop iteration\n\tthis->entity.reset();\n\tthis->set_last_changed(reference_time);\n}\n\n\nbool Event::operator<(const Event &other) const {\n\treturn this->time < other.time;\n}\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/event.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::event {\nclass EventEntity;\n\nusing event_hash_t = size_t;\n\n/**\n * The actual one event that may be called - it is used to manage the event itself.\n * It does not need to be stored.\n */\nclass Event : public std::enable_shared_from_this<Event> {\npublic:\n\tEvent(const std::shared_ptr<EventEntity> &trgt,\n\t      const std::shared_ptr<EventHandler> &eventhandler,\n\t      const EventHandler::param_map &params);\n\n\tconst std::weak_ptr<EventEntity> &get_entity() const {\n\t\treturn this->entity;\n\t}\n\n\tconst std::shared_ptr<EventHandler> &get_eventhandler() const {\n\t\treturn this->eventhandler;\n\t}\n\n\t/**\n\t * Reschedule will call the predict_invoke_time method to initiate a reschedule\n\t * for the event it uses the reference_time as base for its calculation\n\t */\n\tvoid reschedule(const time::time_t reference_time);\n\n\tevent_hash_t hash() const {\n\t\treturn this->myhash;\n\t}\n\n\tconst time::time_t &get_time() const {\n\t\treturn this->time;\n\t}\n\n\tvoid set_time(const time::time_t &t) {\n\t\tthis->time = t;\n\t}\n\n\tconst EventHandler::param_map &get_params() const {\n\t\treturn this->params;\n\t}\n\n\t/**\n\t * Let this event depend on another an event entity.\n\t * When this entity is changes, the event is reevaluated.\n\t *\n\t * To be called in the EventHandler::setup function.\n\t */\n\tvoid depend_on(const std::shared_ptr<EventEntity> &dependency);\n\n\t/**\n\t * Cancel the event.\n\t */\n\tvoid cancel(const time::time_t reference_time);\n\n\t/**\n\t * For sorting events by their trigger time.\n\t */\n\tbool operator<(const Event &other) const;\n\n\t/**\n\t * When a change happens on an EventEntity (this->entity),\n\t * it needs to be processed and all depending events need reevaluation as well.\n\t * This registers the time so we know the point in time that we\n\t * need to go back to and handle the change.\n\t * When changes happen after `last_change_time` in the same time-reaching-round,\n\t * they can be ignored since the earlies point in time determines all implications.\n\t */\n\tvoid set_last_changed(const time::time_t &t) {\n\t\tthis->last_change_time = t;\n\t}\n\n\t/**\n\t * Get the time the  event was changed the last time.\n\t */\n\tconst time::time_t &get_last_changed() const {\n\t\treturn this->last_change_time;\n\t}\n\nprivate:\n\t/**\n\t * Parameters for the event (determined by its EventHandler)\n\t */\n\tEventHandler::param_map params;\n\n\t/** The actor that this event refers to. */\n\tstd::weak_ptr<EventEntity> entity;\n\n\t/** Type of this event. */\n\tstd::shared_ptr<EventHandler> eventhandler;\n\n\t/**\n\t * Time this event occurs/occured.\n\t * It establishes the order of events in the EventQueue.\n\t */\n\ttime::time_t time;\n\n\t/** Time this event was registered to be changed last. */\n\ttime::time_t last_change_time = time::time_t::min_value();\n\n\t/** Precalculated std::hash for the event */\n\tevent_hash_t myhash;\n};\n\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/event_loop.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"event_loop.h\"\n\n#include <cstddef>\n#include <type_traits>\n#include <utility>\n#include <vector>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"error/error.h\"\n#include \"event/event.h\"\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"event/eventqueue.h\"\n#include \"event/eventstore.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::event {\n\n\nvoid EventLoop::add_event_handler(const std::shared_ptr<EventHandler> eventhandler) {\n\tstd::unique_lock lock{this->mutex};\n\n\tclassstore.insert(std::make_pair(eventhandler->id(), eventhandler));\n}\n\n\nstd::shared_ptr<Event> EventLoop::create_event(const std::string name,\n                                               const std::shared_ptr<EventEntity> target,\n                                               const std::shared_ptr<State> state,\n                                               const time::time_t reference_time,\n                                               const EventHandler::param_map params) {\n\tstd::unique_lock lock{this->mutex};\n\n\tauto it = classstore.find(name);\n\tif (it == classstore.end()) {\n\t\tthrow Error{MSG(err) << \"Trying to subscribe to eventhandler \"\n\t\t                     << name << \", which does not exist.\"};\n\t}\n\n\treturn this->queue.create_event(target, it->second, state, reference_time, params);\n}\n\n\nstd::shared_ptr<Event> EventLoop::create_event(const std::shared_ptr<EventHandler> eventhandler,\n                                               const std::shared_ptr<EventEntity> target,\n                                               const std::shared_ptr<State> state,\n                                               const time::time_t reference_time,\n                                               const EventHandler::param_map params) {\n\tstd::unique_lock lock{this->mutex};\n\n\tauto it = this->classstore.find(eventhandler->id());\n\tif (it == this->classstore.end()) {\n\t\tauto res = this->classstore.insert(std::make_pair(eventhandler->id(), eventhandler));\n\t\tif (res.second) {\n\t\t\tit = res.first;\n\t\t}\n\t\telse {\n\t\t\tthrow Error{ERR << \"could not insert eventhandler into class store\"};\n\t\t}\n\t}\n\n\treturn this->queue.create_event(target, it->second, state, reference_time, params);\n}\n\n\nvoid EventLoop::reach_time(const time::time_t &time_until,\n                           const std::shared_ptr<State> &state) {\n\tstd::unique_lock lock{this->mutex};\n\n\t// TODO detect infinite loops (is this a halting problem?)\n\t// this happens when the events don't settle:\n\t// at least one processed event adds another event so\n\t// the queue never stops adding changes\n\t// simple \"solution\": abort after over 9000 attempts.\n\tint max_attempts = 10;\n\n\tint cnt = 0;\n\tint attempts = 0;\n\n\tdo {\n\t\tif (attempts > max_attempts) {\n\t\t\tthrow Error(ERR << \"Loop: reached event settling threshold of \"\n\t\t\t                << max_attempts << \", giving up.\");\n\t\t\tbreak;\n\t\t}\n\n\t\tlog::log(SPAM << \"Loop: Attempt \" << attempts << \" to reach t=\" << time_until);\n\t\tthis->update_changes(state);\n\t\tcnt = this->execute_events(time_until, state);\n\n\t\tlog::log(SPAM << \"Loop: to reach t=\" << time_until\n\t\t              << \", n=\" << cnt << \" events were executed\");\n\n\t\tattempts += 1;\n\t}\n\twhile (cnt != 0);\n\n\t// Swap in the end of the execution, else we might skip changes that happen\n\t// in the main loop for one frame - which is bad btw.\n\tthis->queue.swap_changesets();\n\tlog::log(SPAM << \"Loop: t=\" << time_until << \" was reached! ========\");\n}\n\n\nint EventLoop::execute_events(const time::time_t &time_until,\n                              const std::shared_ptr<State> &state) {\n\tlog::log(SPAM << \"Loop: Pending events in the queue (# = \"\n\t              << this->queue.get_event_queue().size() << \"):\");\n\n\t{\n\t\tsize_t i = 0;\n\t\tfor (const auto &e : this->queue.get_event_queue().get_sorted_events()) {\n\t\t\tlog::log(SPAM << \"  event \"\n\t\t\t              << i << \": t=\" << e->get_time() << \": \" << e->get_eventhandler()->id());\n\t\t\ti++;\n\t\t}\n\t}\n\n\tint cnt = 0;\n\n\twhile (true) {\n\t\t// fetch an event from the queue that happens before <= time_until\n\t\tstd::shared_ptr<Event> event = this->queue.take_event(time_until);\n\t\tif (event == nullptr) {\n\t\t\tbreak;\n\t\t}\n\n\t\tauto target = event->get_entity().lock();\n\n\t\tif (target) {\n\t\t\tlog::log(DBG << \"Loop: invoking event \\\"\" << event->get_eventhandler()->id()\n\t\t\t             << \"\\\" on target \\\"\" << target->idstr()\n\t\t\t             << \"\\\" for time t=\" << event->get_time());\n\n\t\t\tthis->active_event = event;\n\n\t\t\t// apply the event effects\n\t\t\tevent->get_eventhandler()->invoke(\n\t\t\t\t*this, target, state, event->get_time(), event->get_params());\n\n\t\t\tthis->active_event = nullptr;\n\t\t\tcnt += 1;\n\n\t\t\t// if the event is REPEAT, readd the event.\n\t\t\tif (event->get_eventhandler()->type == EventHandler::trigger_type::REPEAT) {\n\t\t\t\ttime::time_t new_time = event->get_eventhandler()->predict_invoke_time(\n\t\t\t\t\ttarget, state, event->get_time());\n\n\t\t\t\tif (new_time != time::TIME_MIN) {\n\t\t\t\t\tevent->set_time(new_time);\n\n\t\t\t\t\tlog::log(DBG << \"Loop: repeating event \\\"\" << event->get_eventhandler()->id()\n\t\t\t\t\t             << \"\\\" on target \\\"\" << target->idstr()\n\t\t\t\t\t             << \"\\\" will be reenqueued for time t=\" << event->get_time());\n\n\t\t\t\t\tthis->queue.reenqueue(event);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// The element was already removed from the queue, so we can safely\n\t\t\t// kill it by ignoring it.\n\t\t\tlog::log(DBG << \"Loop: event \\\"\" << event->get_eventhandler()->id()\n\t\t\t             << \"\\\" ignored because its target does not exist anymore \"\n\t\t\t             << \"\\\" for time t=\" << event->get_time());\n\t\t}\n\t}\n\treturn cnt;\n}\n\n\nvoid EventLoop::create_change(const std::shared_ptr<Event> evnt,\n                              const time::time_t changes_at) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->queue.add_change(evnt, changes_at);\n}\n\n\nvoid EventLoop::update_changes(const std::shared_ptr<State> &state) {\n\tlog::log(SPAM << \"Loop: \" << this->queue.get_changes().size()\n\t              << \" target changes have to be processed\");\n\n\tsize_t i = 0;\n\n\t// Some EventEntity has changed, so all depending events were\n\t// added to the EventQueue as changes.\n\t// These changes need to be reevaluated.\n\tfor (const auto &change : this->queue.get_changes()) {\n\t\tauto evnt = change.evnt.lock();\n\t\tif (evnt) {\n\t\t\tlog::log(DBG << \"  change \" << i++ << \": \" << evnt->get_eventhandler()->id());\n\t\t\tswitch (evnt->get_eventhandler()->type) {\n\t\t\tcase EventHandler::trigger_type::ONCE:\n\t\t\tcase EventHandler::trigger_type::DEPENDENCY: {\n\t\t\t\tauto entity = evnt->get_entity().lock();\n\n\t\t\t\tif (entity) {\n\t\t\t\t\ttime::time_t new_time = evnt->get_eventhandler()\n\t\t\t\t\t                            ->predict_invoke_time(entity, state, change.time);\n\n\t\t\t\t\tif (new_time != time::TIME_MIN) {\n\t\t\t\t\t\tlog::log(DBG << \"Loop: due to a change, rescheduling event of '\"\n\t\t\t\t\t\t             << evnt->get_eventhandler()->id()\n\t\t\t\t\t\t             << \"' on entity '\" << entity->idstr()\n\t\t\t\t\t\t             << \"' at time t=\" << change.time\n\t\t\t\t\t\t             << \" to NEW TIME t=\" << new_time);\n\n\t\t\t\t\t\tevnt->set_time(new_time);\n\n\t\t\t\t\t\tthis->queue.enqueue(evnt);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tlog::log(DBG << \"Loop: due to a change, canceled execution of '\"\n\t\t\t\t\t\t             << evnt->get_eventhandler()->id()\n\t\t\t\t\t\t             << \"' on entity '\" << entity->idstr()\n\t\t\t\t\t\t             << \"' at time t=\" << change.time);\n\n\t\t\t\t\t\tthis->queue.remove(evnt);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// the event is for a no-longer-existing entity,\n\t\t\t\t\t// so we can remove it from the queue.\n\t\t\t\t\tthis->queue.remove(evnt);\n\t\t\t\t}\n\t\t\t} break;\n\n\t\t\tcase EventHandler::trigger_type::TRIGGER:\n\t\t\tcase EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY:\n\t\t\t\tevnt->set_time(change.time);\n\t\t\t\tthis->queue.enqueue(evnt);\n\t\t\t\tbreak;\n\n\t\t\tcase EventHandler::trigger_type::REPEAT:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tthis->queue.clear_changes();\n}\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/event_loop.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <mutex>\n#include <string>\n#include <unordered_map>\n\n#include \"event/eventhandler.h\"\n#include \"event/eventqueue.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::event {\n\n// The demo wants to display internal details\nnamespace demo {\nint curvepong();\n}\n\nclass Event;\nclass EventEntity;\nclass State;\n\n\n/**\n * The core class to manage event handler and targets.\n */\nclass EventLoop {\n\t// because the demo function displays internal info.\n\tfriend int demo::curvepong();\n\npublic:\n\t/**\n\t * Create a new event loop.\n\t */\n\tEventLoop() = default;\n\t~EventLoop() = default;\n\n\t/**\n\t * Register a new event handler.\n\t *\n\t * Created event can reference the event handler ID to invoke it on\n\t * execution.\n\t *\n\t * @param eventhandler Event handler.\n\t */\n\tvoid add_event_handler(const std::shared_ptr<EventHandler> eventhandler);\n\n\t/**\n\t * Add a new event to the queue using a registered event handler.\n\t *\n\t * @param eventhandler Event handler ID. The handler must already be registered on the loop.\n\t * @param target Target entity. Can be \\p nullptr.\n\t * @param state Global state.\n\t * @param reference_time Reference time to calculate the event execution time. The actual\n\t *                       depends execution time on the type of event and may be changed\n\t *                       by other events.\n\t * @param params Event parameters map (default = {}). Passed to the event handler on event execution.\n\t */\n\tstd::shared_ptr<Event> create_event(const std::string eventhandler,\n\t                                    const std::shared_ptr<EventEntity> target,\n\t                                    const std::shared_ptr<State> state,\n\t                                    const time::time_t reference_time,\n\t                                    const EventHandler::param_map params = EventHandler::param_map({}));\n\n\t/**\n\t * Add a new event to the queue using an arbritary event handler. If an event handler\n\t * with the same ID is already registered, the registered event handler will be used\n\t * instead.\n\t *\n\t * TODO: Why use this function when one can simply add the event handler and use the other\n\t *      create_event function?\n\t *\n\t * @param eventhandler Event handler.\n\t * @param target Target entity. Can be \\p nullptr.\n\t * @param state Global state.\n\t * @param reference_time Reference time to calculate the event execution time. The actual\n\t *                       depends execution time on the type of event and may be changed\n\t *                       by other events.\n\t * @param params Event parameters map (default = {}). Passed to the event handler on event execution.\n\t */\n\tstd::shared_ptr<Event> create_event(const std::shared_ptr<EventHandler> eventhandler,\n\t                                    const std::shared_ptr<EventEntity> target,\n\t                                    const std::shared_ptr<State> state,\n\t                                    const time::time_t reference_time,\n\t                                    const EventHandler::param_map params = EventHandler::param_map({}));\n\n\t/**\n\t * Execute events in the queue with execution time <= a given point in time.\n\t *\n\t * @param time_until Maximum time until which events are executed.\n\t * @param state Global state.\n\t */\n\tvoid reach_time(const time::time_t &time_until,\n\t                const std::shared_ptr<State> &state);\n\n\t/**\n\t * Initiate a reevaluation of a given event at a given time.\n\t *\n\t * This usually happens because this event depended on an event entity\n\t * that got changed at this time.\n\t *\n\t * This inserts the event into the changes queue\n\t * so it will be evaluated in the next loop iteration.\n\t *\n\t * @param event Event to reevaluate.\n\t * @param changes_at Time at which the event should be reevaluated.\n\t */\n\tvoid create_change(const std::shared_ptr<Event> event,\n\t                   const time::time_t changes_at);\n\n\t/**\n\t * Get the event queue.\n\t *\n\t * @return Event queue.\n\t */\n\tconst EventQueue &get_queue() const {\n\t\treturn this->queue;\n\t}\n\nprivate:\n\t/**\n\t *  Execute events in the queue with execution time <= a given point in time.\n\t *\n\t * @param time_until Maximum time until which events are executed.\n\t * @param state Global state.\n\t *\n\t * @returns number of events processed\n\t */\n\tint execute_events(const time::time_t &time_until,\n\t                   const std::shared_ptr<State> &state);\n\n\t/**\n\t * Call all the time change functions. This is constant on the state!\n\t *\n\t * @param state Global state.\n\t */\n\tvoid update_changes(const std::shared_ptr<State> &state);\n\n\t/**\n\t * Here we do the bookkeeping of registered event handleres.\n\t */\n\tstd::unordered_map<std::string, std::shared_ptr<EventHandler>> classstore;\n\n\t/**\n\t * All events are enqueued here.\n\t */\n\tEventQueue queue;\n\n\t/**\n\t * The currently processed event.\n\t * This is useful for event cancelations (so one can't cancel itself).\n\t */\n\tstd::shared_ptr<Event> active_event;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::recursive_mutex mutex;\n};\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/evententity.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"evententity.h\"\n\n#include <compare>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"event/event.h\"\n#include \"event/event_loop.h\"\n#include \"event/eventhandler.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::event {\n\n\nvoid EventEntity::changes(const time::time_t &time) {\n\t// This target has some change, so we have to notify all dependents\n\t// that subscribed on this entity.\n\n\tif (this->parent_notifier or this->dependents.size()) {\n\t\tlog::log(DBG << \"Target: processing change request at t=\" << time\n\t\t             << \" for EventEntity \" << this->idstr() << \"...\");\n\t}\n\n\tif (this->parent_notifier != nullptr) {\n\t\tthis->parent_notifier(time);\n\t}\n\n\t// This is a maybe-erase construct so obsolete dependents are cleaned up.\n\tfor (auto it = this->dependents.begin(); it != this->dependents.end();) {\n\t\tauto dependent = it->lock();\n\t\tif (dependent and not dependent->get_entity().expired()) {\n\t\t\tswitch (dependent->get_eventhandler()->type) {\n\t\t\tcase EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY:\n\t\t\tcase EventHandler::trigger_type::DEPENDENCY:\n\t\t\t\t// Enqueue a change so that change events,\n\t\t\t\t// which depend on this target, will be retriggered\n\n\t\t\t\tlog::log(DBG << \"Target: change at t=\" << time\n\t\t\t\t             << \" for EventEntity \" << this->idstr() << \" registered\");\n\t\t\t\tthis->loop->create_change(dependent, time);\n\t\t\t\t++it;\n\t\t\t\tbreak;\n\n\t\t\tcase EventHandler::trigger_type::ONCE:\n\t\t\t\t// If the dependent is a ONCE-event\n\t\t\t\t// forget the change if the once event has been notified already.\n\t\t\t\tif (dependent->get_last_changed() > time::time_t::min_value()) {\n\t\t\t\t\tit = this->dependents.erase(it);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis->loop->create_change(dependent, time);\n\t\t\t\t\t++it;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase EventHandler::trigger_type::TRIGGER:\n\t\t\tcase EventHandler::trigger_type::REPEAT:\n\t\t\t\t// Ignore announced changes for triggered or repeated events\n\t\t\t\t// for that there is the 'DEPENDENCY' events.\n\n\t\t\t\t// TRIGGER events are only triggered when this entity's\n\t\t\t\t// trigger() function is called\n\t\t\t\t++it;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// The dependent is no more, so we can safely forget him\n\t\t\tit = this->dependents.erase(it);\n\t\t}\n\t}\n}\n\n\nvoid EventEntity::trigger(const time::time_t &last_valid_time) {\n\t// notify all dependent events that are triggered `on_keyframe`\n\t// that the this target changed.\n\t// the only events that is \"notified\" by are TRIGGER.\n\n\tfor (auto it = this->dependents.begin(); it != this->dependents.end();) {\n\t\tauto dependent = it->lock();\n\t\tif (dependent) {\n\t\t\tif (dependent->get_eventhandler()->type == EventHandler::trigger_type::TRIGGER) {\n\t\t\t\tlog::log(DBG << \"Target: trigger creates a change for \"\n\t\t\t\t             << dependent->get_eventhandler()->id()\n\t\t\t\t             << \" at t=\" << last_valid_time);\n\n\t\t\t\tloop->create_change(dependent, last_valid_time);\n\t\t\t}\n\n\t\t\t++it;\n\t\t}\n\t\telse {\n\t\t\tit = this->dependents.erase(it);\n\t\t}\n\t}\n}\n\n\nvoid EventEntity::add_dependent(const std::shared_ptr<Event> &event) {\n\tthis->dependents.emplace_back(event);\n}\n\nvoid EventEntity::show_dependents() const {\n\tlog::log(DBG << \"Dependent list:\");\n\tfor (auto &dep : this->dependents) {\n\t\tauto dependent = dep.lock();\n\t\tif (dependent) {\n\t\t\tlog::log(DBG << \" - \" << dependent->get_eventhandler()->id());\n\t\t}\n\t\telse {\n\t\t\tlog::log(DBG << \" - ** outdated old reference **\");\n\t\t}\n\t}\n}\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/evententity.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <functional>\n#include <list>\n#include <memory>\n#include <string>\n\n#include \"time/time.h\"\n\nnamespace openage::event {\n\nclass Event;\nclass EventLoop;\n\n/**\n * Every Object in the gameworld that wants to be targeted by events or as\n * dependency for events, has to implement this class.\n */\nclass EventEntity {\npublic:\n\t/** Give a unique event system identifier for the entity */\n\tvirtual size_t id() const = 0;\n\n\t/** Give a human-readable identifier for this target */\n\tvirtual std::string idstr() const = 0;\n\n\tusing single_change_notifier = std::function<void(const time::time_t &)>;\n\nprotected:\n\t/**\n\t * For children to be able to initialize us.\n\t *\n\t * The notifier is used by hierarchical structures to be able to propagate a\n\t * change up in the tree, this is necessary to make containers with event\n\t * targets inside and listen to any changes on the full.\n\t */\n\tEventEntity(const std::shared_ptr<EventLoop> &loop,\n\t            single_change_notifier parent_notifier = nullptr) :\n\t\tloop{loop},\n\t\tparent_notifier{parent_notifier} {}\n\npublic:\n\tvirtual ~EventEntity() = default;\n\n\t/**\n\t * Add a dependent event that is notified whenever this entity changes.\n\t * Does not support TRIGGER and REPEAT event types.\n\t */\n\tvoid add_dependent(const std::shared_ptr<Event> &event);\n\n\t/**\n\t * For debugging: print the dependent eventhandler ids as log messages.\n\t */\n\tvoid show_dependents() const;\n\nprotected:\n\t/**\n\t * Call this whenever some data in the target changes.\n\t * This triggers the reevaluation of dependent events.\n\t */\n\tvoid changes(const time::time_t &change_time);\n\n\t/**\n\t * Call this when depending TriggerEventHandleres should be invoked.\n\t */\n\tvoid trigger(const time::time_t &invoke_time);\n\nprivate:\n\t/** Event loop this target is registered to */\n\tstd::shared_ptr<EventLoop> loop;\n\n\t/** List of events that depend on this target */\n\tstd::list<std::weak_ptr<Event>> dependents;\n\n\tsingle_change_notifier parent_notifier;\n};\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventhandler.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"eventhandler.h\"\n\nnamespace openage::event {\n\n\nEventHandler::EventHandler(const std::string &name, const EventHandler::trigger_type &type) :\n\ttype{type},\n\t_id{name} {}\n\n\nconst std::string &EventHandler::id() {\n\treturn _id;\n}\n\n\nDependencyEventHandler::DependencyEventHandler(const std::string &name) :\n\tEventHandler(name, EventHandler::trigger_type::DEPENDENCY) {}\n\n\nDependencyImmediatelyEventHandler::DependencyImmediatelyEventHandler(const std::string &name) :\n\tEventHandler(name, EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY) {}\n\n\nTriggerEventHandler::TriggerEventHandler(const std::string &name) :\n\tEventHandler(name, EventHandler::trigger_type::TRIGGER) {}\n\n\nRepeatEventHandler::RepeatEventHandler(const std::string &name) :\n\tEventHandler(name, EventHandler::trigger_type::REPEAT) {}\n\n\nOnceEventHandler::OnceEventHandler(const std::string &name) :\n\tEventHandler(name, EventHandler::trigger_type::ONCE) {}\n\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventhandler.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <any>\n#include <initializer_list>\n#include <memory>\n#include <string>\n#include <typeinfo>\n#include <unordered_map>\n#include <utility>\n\n#include \"time/time.h\"\n\n\nnamespace openage::event {\n\nclass Event;\nclass EventLoop;\nclass EventEntity;\nclass State;\n\n\n/**\n * A eventhandler.has to be implemented for every type of event that exists.\n * It determines what the event means and how it is handled.\n */\nclass EventHandler {\npublic:\n\t/**\n\t * Available types for the event handler:\n\t * These decide when an event of this event handler will be executed.\n\t */\n\tenum class trigger_type {\n\t\t/**\n\t\t * Such events are emitted when a modification is done on a target.\n\t\t * The execution time is calculated from the modification time and custom code.\n\t\t */\n\t\tDEPENDENCY,\n\n\t\t/**\n\t\t * Like DEPENDENCY, but does not use a recalculated time,\n\t\t * instead uses the change-time only.\n\t\t * Behaves exactly like DEPENDENCY, if the DEPENDENCY-event is in the same\n\t\t * execution frame.\n\t\t */\n\t\tDEPENDENCY_IMMEDIATELY,\n\n\t\t/**\n\t\t * Will be executed when the target trigger() function is called.\n\t\t */\n\t\tTRIGGER,\n\n\t\t/**\n\t\t * Will be triggered unconditionally at the set time, \"at\" is the\n\t\t * time that was set as return of predict_invoke_time. This event will\n\t\t * be issued again until predict_invoke_time returns min(). To execute\n\t\t * Something only once (i.E. triggered somewhere from the logic and\n\t\t * not based on time, use ONCE\n\t\t */\n\t\tREPEAT,\n\n\t\t/**\n\t\t * Will be triggered only once, but until it is triggered the time,\n\t\t * when this should happen can be recalculated again and again using\n\t\t * the predict_invoke_time method.\n\t\t */\n\t\tONCE,\n\t};\n\n\t/**\n\t * Storage for parameters for an event handler.\n\t */\n\tclass param_map {\n\tpublic:\n\t\tusing map_t = std::unordered_map<std::string, std::any>;\n\n\t\tparam_map() {}\n\t\tparam_map(std::initializer_list<map_t::value_type> l) :\n\t\t\tmap(l) {}\n\t\tparam_map(const map_t &map) :\n\t\t\tmap{std::move(map)} {}\n\n\t\t/**\n\t\t * Returns the value, if it exists and is the right type.\n\t\t * defaultval if not.\n\t\t */\n\t\ttemplate <typename T>\n\t\tT get(const std::string &key, const T &defaultval = T()) const {\n\t\t\tauto it = this->map.find(key);\n\t\t\tif (it != this->map.end() && this->check_type<T>(it)) {\n\t\t\t\treturn std::any_cast<T>(it->second);\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn defaultval;\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * Check if the map contains the given key.\n\t\t */\n\t\tbool contains(const std::string &key) const {\n\t\t\treturn map.find(key) != map.end();\n\t\t}\n\n\t\t/**\n\t\t * Check if the type of a map entry is correct.\n\t\t */\n\t\ttemplate <typename Type>\n\t\tbool check_type(const std::string &key) const {\n\t\t\tauto it = map.find(key);\n\t\t\tif (it != map.end()) {\n\t\t\t\treturn this->check_type<Type>(it);\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\tprivate:\n\t\ttemplate <typename T>\n\t\tbool check_type(const map_t::const_iterator &it) const {\n\t\t\treturn it->second.type() == typeid(T);\n\t\t}\n\n\t\t/**\n\t\t * Data storage.\n\t\t */\n\t\tconst map_t map;\n\t};\n\n\t/**\n\t * Constructor to be constructed with the unique identifier\n\t */\n\tEventHandler(const std::string &name, const trigger_type &type);\n\n\tvirtual ~EventHandler() = default;\n\n\t/**\n\t * The event type this event handler represents.\n\t */\n\tconst trigger_type type;\n\n\t/**\n\t * Return a unique identifier.\n\t */\n\tconst std::string &id();\n\n\t/**\n\t * Called for each event that is created for this EventHandler.\n\t * The job of the setup function is to add all dependencies with other event\n\t * targets found in state.\n\t */\n\tvirtual void setup_event(const std::shared_ptr<Event> &event,\n\t                         const std::shared_ptr<State> &state) = 0;\n\n\t/**\n\t * This method implements the effects of the event.\n\t * It will be called at the time that was determined by `predict_invoke_time`.\n\t *\n\t * Called from the Loop.\n\t */\n\tvirtual void invoke(EventLoop &loop,\n\t                    const std::shared_ptr<EventEntity> &target,\n\t                    const std::shared_ptr<State> &state,\n\t                    const time::time_t &time,\n\t                    const param_map &params) = 0;\n\n\t/**\n\t * Is called to calculate the execution time for an event of this eventhandler.\n\t * This is called whenever one of the set up dependencies was changed,\n\t * or when a REPEAT event was executed.\n\t *\n\t * @param target: the target the event was created for\n\t * @param state: the state this shall work on\n\t * @param at: the time when the change happened, from there on it shall be\n\t *            calculated onwards\n\t *\n\t * If the event is obsolete, return <time_t>::min().\n\t *\n\t * If the time is lower than the previous time,\n\t * then dependencies may not be resolved perfectly anymore\n\t * (if other events have already been calculated before that).\n\t */\n\tvirtual time::time_t predict_invoke_time(const std::shared_ptr<EventEntity> &target,\n\t                                         const std::shared_ptr<State> &state,\n\t                                         const time::time_t &at) = 0;\n\nprivate:\n\t/**\n\t * String identifier for this event handler.\n\t */\n\tstd::string _id;\n};\n\n\n// helper classes\n\nclass DependencyEventHandler : public EventHandler {\npublic:\n\tDependencyEventHandler(const std::string &name);\n};\n\nclass DependencyImmediatelyEventHandler : public EventHandler {\npublic:\n\tDependencyImmediatelyEventHandler(const std::string &name);\n};\n\nclass TriggerEventHandler : public EventHandler {\npublic:\n\tTriggerEventHandler(const std::string &name);\n};\n\nclass RepeatEventHandler : public EventHandler {\npublic:\n\tRepeatEventHandler(const std::string &name);\n};\n\nclass OnceEventHandler : public EventHandler {\npublic:\n\tOnceEventHandler(const std::string &name);\n};\n\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventqueue.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"eventqueue.h\"\n\n#include <compare>\n#include <string>\n#include <utility>\n\n#include \"log/message.h\"\n\n#include \"event/event.h\"\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"event/eventstore.h\"\n#include \"log/log.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::event {\n\nstd::shared_ptr<Event> EventQueue::create_event(const std::shared_ptr<EventEntity> &trgt,\n                                                const std::shared_ptr<EventHandler> &cls,\n                                                const std::shared_ptr<State> &state,\n                                                const time::time_t &reference_time,\n                                                const EventHandler::param_map &params) {\n\tauto event = std::make_shared<Event>(trgt, cls, params);\n\n\tcls->setup_event(event, state);\n\n\tswitch (cls->type) {\n\tcase EventHandler::trigger_type::DEPENDENCY:\n\tcase EventHandler::trigger_type::REPEAT:\n\tcase EventHandler::trigger_type::ONCE:\n\t\tevent->set_time(event->get_eventhandler()\n\t\t                    ->predict_invoke_time(trgt, state, reference_time));\n\n\t\tif (event->get_time() == time::TIME_MIN) {\n\t\t\tlog::log(DBG << \"Queue: ignoring insertion of event \"\n\t\t\t             << event->get_eventhandler()->id() << \" because no execution was scheduled.\");\n\n\t\t\treturn {};\n\t\t}\n\t\tbreak;\n\n\tcase EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY:\n\tcase EventHandler::trigger_type::TRIGGER:\n\t\tevent->set_time(reference_time);\n\t\tbreak;\n\t}\n\n\tlog::log(DBG << \"Queue: inserting event \" << event->get_eventhandler()->id() << \" into queue to be executed at t=\" << event->get_time());\n\n\t// store the event\n\t// or enqueue it for execution\n\tswitch (event->get_eventhandler()->type) {\n\tcase EventHandler::trigger_type::DEPENDENCY:\n\t\tthis->dependency_events.insert(event);\n\t\tbreak;\n\n\tcase EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY:\n\t\tthis->dependency_immediately_events.insert(event);\n\t\tbreak;\n\n\tcase EventHandler::trigger_type::TRIGGER:\n\t\tthis->trigger_events.insert(event);\n\t\tbreak;\n\n\tcase EventHandler::trigger_type::REPEAT:\n\tcase EventHandler::trigger_type::ONCE:\n\tdefault:\n\t\tthis->event_queue.push(event);\n\t}\n\n\treturn event;\n}\n\n\nEventQueue::EventQueue() :\n\tchanges(&changeset_A),\n\tfuture_changes(&changeset_B) {}\n\n\nvoid EventQueue::add_change(const std::shared_ptr<Event> &event,\n                            const time::time_t &changed_at) {\n\tconst time::time_t event_previous_changed = event->get_last_changed();\n\n\t// Has the event already been fired in this round?\n\tif (event_previous_changed < changed_at) {\n\t\tauto it = this->changes->find(Change{event, changed_at});\n\n\t\t// Is the change already in the queue?\n\t\tif (it != changes->end()) {\n\t\t\t// Is the new change dated _before_ the old one?\n\t\t\tif (changed_at < it->time) {\n\t\t\t\tlog::log(DBG << \"Queue: adjusting time in change queue: moving event of \"\n\t\t\t\t             << event->get_eventhandler()->id()\n\t\t\t\t             << \" to earlier time\");\n\n\t\t\t\t// Save the element\n\t\t\t\tChange change = *it;\n\t\t\t\tchange.time = changed_at;\n\n\t\t\t\t// delete it from the container and readd it\n\t\t\t\t// with the updated time\n\t\t\t\tit = this->changes->erase(it);\n\t\t\t\tit = this->changes->insert(it, change);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// this change is to be ignored\n\t\t\t\tlog::log(DBG << \"Queue: skipping change for \" << event->get_eventhandler()->id()\n\t\t\t\t             << \" at \" << changed_at\n\t\t\t\t             << \" because there was already an earlier one at t=\" << it->time);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// the change was not in the to be changed list\n\t\t\tthis->changes->emplace(event, changed_at);\n\t\t\tlog::log(DBG << \"Queue: inserting change for event from \"\n\t\t\t             << event->get_eventhandler()->id()\n\t\t\t             << \" to be applied at t=\" << changed_at);\n\t\t}\n\t}\n\telse {\n\t\t// the event has been triggered in this round already, so skip it this time\n\t\tthis->future_changes->emplace(event, changed_at);\n\t\tlog::log(DBG << \"Queue: ignoring change at t=\" << changed_at\n\t\t             << \" for event for handler \" << event->get_eventhandler()->id()\n\t\t             << \" because it's already processed as change at t=\" << event_previous_changed);\n\t}\n\n\tevent->set_last_changed(changed_at);\n}\n\n\nvoid EventQueue::remove(const std::shared_ptr<Event> &evnt) {\n\t// TODO: remove the event from the other storages.\n\t//       this would require changes to dependent events and triggers.\n\t//       (to stop being a dependent event or allow being triggered)\n\tthis->event_queue.erase(evnt);\n}\n\n\nvoid EventQueue::enqueue(const std::shared_ptr<Event> &evnt) {\n\tif (this->event_queue.contains(evnt)) {\n\t\tthis->event_queue.update(evnt);\n\t}\n\telse {\n\t\tthis->event_queue.push(evnt);\n\t}\n}\n\n\nvoid EventQueue::reenqueue(const std::shared_ptr<Event> &evnt) {\n\tthis->event_queue.push(evnt);\n}\n\n\nconst EventStore &EventQueue::get_event_queue() const {\n\treturn this->event_queue;\n}\n\n\nstd::shared_ptr<Event> EventQueue::take_event(const time::time_t &max_time) {\n\tif (this->event_queue.size() == 0) {\n\t\treturn nullptr;\n\t}\n\n\tstd::shared_ptr<Event> event = this->event_queue.top();\n\n\t// check if this event should be processed\n\t// we take any event that happens <= max_time\n\tif (event->get_time() <= max_time) {\n\t\t// remove the event from the queue\n\t\tthis->event_queue.pop();\n\n\t\treturn event;\n\t}\n\telse {\n\t\treturn nullptr;\n\t}\n}\n\n\nconst EventQueue::change_set &EventQueue::get_changes() const {\n\treturn *this->changes;\n}\n\n\nvoid EventQueue::clear_changes() {\n\tthis->changes->clear();\n}\n\n\nvoid EventQueue::swap_changesets() {\n\tstd::swap(this->changes, this->future_changes);\n}\n\n\nEventQueue::Change::Change(const std::shared_ptr<Event> &evnt,\n                           time::time_t time) :\n\ttime{std::move(time)},\n\tevnt{evnt},\n\thash{evnt->hash()} {}\n\n\nsize_t EventQueue::Change::Equal::operator()(const Change &left,\n                                             const Change &right) const {\n\tauto left_evnt = left.evnt.lock();\n\tauto right_evnt = right.evnt.lock();\n\tif (left_evnt && right_evnt) {\n\t\tif (left_evnt->get_eventhandler()->id() == right_evnt->get_eventhandler()->id()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\telse {\n\t\treturn false;\n\t}\n\n\tauto left_entity = left_evnt->get_entity().lock();\n\tauto right_entity = right_evnt->get_entity().lock();\n\n\tif (left_entity && right_entity) {\n\t\tif (left_entity->id() == right_entity->id()) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventqueue.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <unordered_set>\n\n#include \"event/eventhandler.h\"\n#include \"event/eventstore.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::event {\n\nclass Event;\nclass EventEntity;\nclass State;\n\n/**\n * The core event handler for execution and execution dependencies.\n */\nclass EventQueue final {\npublic:\n\tclass Change {\n\tpublic:\n\t\tChange(const std::shared_ptr<Event> &evnt,\n\t\t       time::time_t time);\n\n\t\ttime::time_t time;\n\t\tstd::weak_ptr<Event> evnt;\n\t\tconst size_t hash;\n\n\t\tclass Hasher {\n\t\tpublic:\n\t\t\tsize_t operator()(const Change &e) const {\n\t\t\t\treturn e.hash;\n\t\t\t}\n\t\t};\n\n\t\tclass Equal {\n\t\tpublic:\n\t\t\tsize_t operator()(const Change &left,\n\t\t\t                  const Change &right) const;\n\t\t};\n\t};\n\n\t/**\n\t * Type for the set to store changes to track.\n\t */\n\tusing change_set = std::unordered_set<Change,\n\t                                      Change::Hasher,\n\t                                      Change::Equal>;\n\n\n\tEventQueue();\n\n\t/**\n\t * Add an event for a specified target\n\t *\n\t * A target is every single unit in the game world - so best add these events\n\t * in the constructor of the game objects.\n\t *\n\t * The `reference_time` is the time used to calculate when\n\t * the actual event time will happen by calling `eventhandler->predict_invoke_time()`!\n\t */\n\tstd::shared_ptr<Event> create_event(const std::shared_ptr<EventEntity> &evententity,\n\t                                    const std::shared_ptr<EventHandler> &eventhandler,\n\t                                    const std::shared_ptr<State> &state,\n\t                                    const time::time_t &reference_time,\n\t                                    const EventHandler::param_map &params);\n\n\t/**\n\t * Remove the given event from the queue.\n\t */\n\tvoid remove(const std::shared_ptr<Event> &evnt);\n\n\t/**\n\t * An update to existing events has to be applied.\n\t * The execution time of this event may have changed or it\n\t * is newly created. This updates/inserts the given event\n\t * in the main queue.\n\t */\n\tvoid enqueue(const std::shared_ptr<Event> &event);\n\n\t/**\n\t * The event was just removed, add it again.\n\t * This is used for REPEAT events so that they are repeated.\n\t */\n\tvoid reenqueue(const std::shared_ptr<Event> &event);\n\n\t/**\n\t * An event target has changed, and the event shall be retriggered\n\t */\n\tvoid add_change(const std::shared_ptr<Event> &event,\n\t                const time::time_t &changed_at);\n\n\t/**\n\t * Get an accessor to the running queue for state output purpose.\n\t */\n\tconst EventStore &get_event_queue() const;\n\n\t/**\n\t * Obtain the next event from the `event_queue` that happens before `<= max_time`.\n\t */\n\tstd::shared_ptr<Event> take_event(const time::time_t &max_time);\n\n\t/**\n\t * Get the change_set to process changes.\n\t */\n\tconst change_set &get_changes() const;\n\n\t/**\n\t * All changes (fetched with `get_changes`) have been processed,\n\t * so we can clear the change_set.\n\t */\n\tvoid clear_changes();\n\n\t/**\n\t * Swap the `changes` and `future_changes`.\n\t */\n\tvoid swap_changesets();\n\nprivate:\n\t// Implement double buffering around changesets, that we do not run into deadlocks\n\t// those point to the `changeset_A` and `changeset_B`.\n\tchange_set *changes;\n\tchange_set *future_changes;\n\n\t// storage for the double buffer in `changes` and `future_changes`.\n\tchange_set changeset_A;\n\tchange_set changeset_B;\n\n\t/**\n\t * Stores events that sleep until their dependency changes.\n\t */\n\tstd::unordered_set<std::shared_ptr<Event>> dependency_events;\n\n\t/**\n\t * Stores events that sleep until their dependency changes, but they trigger\n\t * instantly when their dependency changes.\n\t */\n\tstd::unordered_set<std::shared_ptr<Event>> dependency_immediately_events;\n\n\t/**\n\t * Events that fire only when triggered.\n\t */\n\tstd::unordered_set<std::shared_ptr<Event>> trigger_events;\n\n\t/**\n\t * The universe timeline processes through this queue.\n\t */\n\tEventStore event_queue;\n};\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventstore.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"eventstore.h\"\n\n#include <algorithm>\n#include <iterator>\n#include <utility>\n\n#include \"log/message.h\"\n\n#include \"error/error.h\"\n\n\nnamespace openage::event {\n\nvoid EventStore::push(const std::shared_ptr<Event> &event) {\n\tif (event == nullptr) [[unlikely]] {\n\t\tthrow Error{ERR << \"inserting nullptr event to queue\"};\n\t}\n\n\theap_t::element_t order = this->heap.push(event);\n\tthis->events.emplace(event, order);\n\n\tENSURE(this->heap.size() == this->events.size(),\n\t       \"heap and event set are inconsistent\");\n}\n\n\nstd::shared_ptr<Event> EventStore::pop() {\n\tENSURE(this->heap.size() == this->events.size(),\n\t       \"heap and event set are inconsistent 0\");\n\n\tsize_t heap_s = this->heap.size();\n\tsize_t evnt_s = this->events.size();\n\n\tstd::shared_ptr<Event> event = this->heap.pop();\n\tthis->events.erase(event);\n\n\tif (this->heap.size() != this->events.size()) {\n\t\tthrow Error{ERR << \"inconsistent: prev_heap=\" << heap_s\n\t\t                << \" prev_map=\" << evnt_s};\n\t}\n\t//ENSURE(this->heap.size() == this->events.size(),\n\t//   \"heap and event set are inconsistent 1\");\n\n\treturn event;\n}\n\n\nconst std::shared_ptr<Event> &EventStore::top() {\n\treturn this->heap.top();\n}\n\n\nbool EventStore::erase(const std::shared_ptr<Event> &event) {\n\tbool erased = false;\n\tauto it = this->events.find(event);\n\tif (it != std::end(this->events)) {\n\t\tthis->heap.remove_node(it->second);\n\t\tthis->events.erase(it);\n\t\terased = true;\n\t}\n\n\treturn erased;\n}\n\n\nvoid EventStore::update(const std::shared_ptr<Event> &event) {\n\tauto it = this->events.find(event);\n\tif (it != std::end(this->events)) [[unlikely]] {\n\t\tthis->heap.update(it->second);\n\t}\n\telse {\n\t\tthrow Error{ERR << \"event to update not found in store\"};\n\t}\n}\n\n\nbool EventStore::contains(const std::shared_ptr<Event> &event) const {\n\treturn (this->events.find(event) != std::end(this->events));\n}\n\n\nvoid EventStore::clear() {\n\tthis->heap.clear();\n\tthis->events.clear();\n}\n\n\nsize_t EventStore::size() const {\n\treturn this->events.size();\n}\n\n\nbool EventStore::empty() const {\n\treturn this->events.empty();\n}\n\n\nstd::vector<std::shared_ptr<Event>> EventStore::get_sorted_events() const {\n\tstd::vector<std::shared_ptr<Event>> ret;\n\n\tret.reserve(this->events.size());\n\n\tstd::transform(\n\t\tstd::begin(this->events),\n\t\tstd::end(this->events),\n\t\tstd::back_inserter(ret),\n\t\t[](const auto &elem) {\n\t\t\treturn elem.first;\n\t\t});\n\n\tstd::sort(\n\t\tstd::begin(ret),\n\t\tstd::end(ret),\n\t\t[](const auto &a, const auto &b) {\n\t\t\treturn *a < *b;\n\t\t});\n\n\treturn ret;\n}\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/eventstore.h",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <unordered_map>\n#include <vector>\n\n#include \"datastructure/pairing_heap.h\"\n#include \"event/event.h\"\n#include \"util/misc.h\"\n\n\nnamespace openage::event {\n\n/**\n * Sorted storage for events.\n * Implemented through a heap that automatically provides the newest event.\n */\nclass EventStore {\npublic:\n\t// TODO: don't store a double-sharedpointer.\n\t//       instead, use the event-sharedpointer directly.\n\tusing heap_t = datastructure::PairingHeap<std::shared_ptr<Event>,\n\t                                          util::SharedPtrLess<Event>>;\n\tusing elemmap_t = std::unordered_map<std::shared_ptr<Event>, heap_t::element_t>;\n\n\tvoid push(const std::shared_ptr<Event> &event);\n\tstd::shared_ptr<Event> pop();\n\tconst std::shared_ptr<Event> &top();\n\tbool erase(const std::shared_ptr<Event> &event);\n\tvoid update(const std::shared_ptr<Event> &event);\n\tbool contains(const std::shared_ptr<Event> &event) const;\n\tvoid clear();\n\tsize_t size() const;\n\tbool empty() const;\n\n\t/**\n\t * Helper function that should not be called 'in production'.\n\t * It returns the events in the store sorted by time.\n\t * Use the pop() method instead (but with that you can't iterate).\n\t */\n\tstd::vector<std::shared_ptr<Event>> get_sorted_events() const;\n\n\theap_t heap;\n\telemmap_t events;\n};\n\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/state.cpp",
    "content": "// Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n#include \"state.h\"\n\nnamespace openage::event {\n\n} // openage::event\n"
  },
  {
    "path": "libopenage/event/state.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n\nnamespace openage::event {\n\nclass EventLoop;\n\n\nclass State {\npublic:\n\tState(const std::shared_ptr<EventLoop> & /*mgr*/) {}\n\tvirtual ~State() = default;\n};\n\n} // namespace openage::event\n"
  },
  {
    "path": "libopenage/event/tests.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include <compare>\n#include <cstring>\n#include <iostream>\n#include <list>\n#include <memory>\n#include <string>\n#include <string_view>\n#include <utility>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n#include \"testing/testing.h\"\n\n#include \"event/event.h\"\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"event/state.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::event::tests {\n\n// We have to create a temporary State due to the magic of C++\nclass TestState : public State {\npublic:\n\tclass TestObject : public EventEntity {\n\t\tconst int _id;\n\n\tpublic:\n\t\tTestObject(const std::shared_ptr<EventLoop> &loop, int id) :\n\t\t\tEventEntity(loop),\n\t\t\t_id{id},\n\t\t\tnumber(0) {}\n\n\t\tvoid set_number(int number, const time::time_t &time) {\n\t\t\tthis->number = number;\n\t\t\tthis->changes(time + time::time_t::from_double(1));\n\t\t}\n\n\t\t[[nodiscard]] size_t id() const override {\n\t\t\treturn _id;\n\t\t}\n\n\t\tstd::string idstr() const override {\n\t\t\tstd::stringstream ss;\n\t\t\tss << \"TestObject[\" << this->id() << \"]\";\n\t\t\treturn ss.str();\n\t\t}\n\n\t\tvoid test_trigger(const time::time_t &time) {\n\t\t\tthis->trigger(time);\n\t\t}\n\n\t\tint number;\n\t};\n\n\texplicit TestState(const std::shared_ptr<EventLoop> &loop) :\n\t\tState(loop),\n\t\tobjectA(std::make_shared<TestObject>(loop, 0)),\n\t\tobjectB(std::make_shared<TestObject>(loop, 1)) {}\n\n\tstd::shared_ptr<TestObject> objectA;\n\tstd::shared_ptr<TestObject> objectB;\n\n\tstruct traceelement {\n\t\ttraceelement(std::string event, time::time_t time) :\n\t\t\ttime{std::move(time)},\n\t\t\tname{std::move(event)} {}\n\n\t\ttime::time_t time;\n\t\tstd::string name;\n\t};\n\n\tstd::list<traceelement> trace;\n\n\tvoid log_dbg() {\n\t\tlog::log(DBG << \"Trace: \");\n\t\tfor (const auto &e : this->trace) {\n\t\t\tlog::log(DBG << \"T: \" << e.time << \": \" << e.name);\n\t\t}\n\t}\n};\n\n\nclass TestEventHandler : public EventHandler {\n\tint idx;\n\npublic:\n\tTestEventHandler(const std::string &name, int idx) :\n\t\tEventHandler(name, EventHandler::trigger_type::DEPENDENCY),\n\t\tidx{idx} {}\n\n\tvoid setup_event(const std::shared_ptr<Event> &event,\n\t                 const std::shared_ptr<State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\n\t\tswitch (this->idx) {\n\t\tcase 0:\n\t\t\t// let the modification of objectA depend on objectB\n\t\t\tevent->depend_on(state->objectB);\n\t\t\tbreak;\n\t\tcase 1:\n\t\t\t// let the modification of objectB depend on objectA\n\t\t\tevent->depend_on(state->objectA);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tvoid invoke(EventLoop & /*loop*/,\n\t            const std::shared_ptr<EventEntity> &target,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &time,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\n\t\tswitch (this->idx) {\n\t\tcase 0: {\n\t\t\tauto t = std::dynamic_pointer_cast<TestState::TestObject>(target);\n\t\t\tstate->objectA->set_number(t->number + 1, time);\n\t\t\tlog::log(DBG << \"I am Event A. Setting number to \" << state->objectA->number);\n\t\t\tstate->trace.emplace_back(\"A\", time);\n\t\t} break;\n\n\t\tcase 1: {\n\t\t\tauto t = std::dynamic_pointer_cast<TestState::TestObject>(target);\n\t\t\tstate->objectB->set_number(t->number + 1, time);\n\t\t\tlog::log(DBG << \"I am Event B. Setting number to \" << state->objectB->number);\n\t\t\tstate->trace.emplace_back(\"B\", time);\n\t\t} break;\n\t\t}\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> & /*target*/,\n\t                                 const std::shared_ptr<State> & /*state*/,\n\t                                 const time::time_t &at) override {\n\t\treturn at + time::time_t::from_double(2);\n\t}\n};\n\n\nclass TestEventHandlerTwo : public EventHandler {\npublic:\n\texplicit TestEventHandlerTwo(const std::string &name) :\n\t\tEventHandler(name, EventHandler::trigger_type::DEPENDENCY) {}\n\n\tvoid setup_event(const std::shared_ptr<Event> &target,\n\t                 const std::shared_ptr<State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\t\ttarget->depend_on(state->objectA);\n\t}\n\n\tvoid invoke(EventLoop & /*loop*/,\n\t            const std::shared_ptr<EventEntity> &gtarget,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &time,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\t\tauto target = std::dynamic_pointer_cast<TestState::TestObject>(gtarget);\n\t\tstate->objectB->set_number(target->number + 1, time);\n\n\t\tlog::log(DBG << \"I am EventHandlerTwo. Setting B.number to \" << state->objectB->number);\n\t\tstate->trace.emplace_back(\"B\", time);\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> & /*target*/,\n\t                                 const std::shared_ptr<State> & /*state*/,\n\t                                 const time::time_t &at) override {\n\t\t// TODO recalculate a hit time\n\t\treturn at + time::time_t::from_double(1);\n\t}\n};\n\n\nclass EventTypeTestClass : public EventHandler {\npublic:\n\tEventTypeTestClass(const std::string &name, EventHandler::trigger_type type) :\n\t\tEventHandler(name, type) {}\n\n\tvoid setup_event(const std::shared_ptr<Event> &event,\n\t                 const std::shared_ptr<State> &gstate) override {\n\t\tlog::log(DBG << \"EventTypeTestClass-\" << this->id() << \" setting up new event\");\n\n\t\t// let all events depend on objectA\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\t\tevent->depend_on(state->objectA);\n\t}\n\n\tvoid invoke(EventLoop & /*loop*/,\n\t            const std::shared_ptr<EventEntity> &target,\n\t            const std::shared_ptr<State> &gstate,\n\t            const time::time_t &time,\n\t            const EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<TestState>(gstate);\n\n\t\tauto t = std::dynamic_pointer_cast<TestState::TestObject>(target);\n\t\tlog::log(DBG << \"EventTypeTestClass-\" << this->id() << \" got called on \" << t->id()\n\t\t             << \" with number \" << t->number << \" at t=\" << time);\n\n\t\tstate->trace.emplace_back(this->id(), time);\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> & /*target*/,\n\t                                 const std::shared_ptr<State> & /*state*/,\n\t                                 const time::time_t &at) override {\n\t\tswitch (this->type) {\n\t\tcase EventHandler::trigger_type::DEPENDENCY:\n\t\t\t// Execute 1 after the change (usually it is neccessary to recalculate a collision\n\t\t\treturn at + time::time_t::from_double(1);\n\n\t\tcase EventHandler::trigger_type::DEPENDENCY_IMMEDIATELY:\n\t\t\tTESTFAILMSG(\"DEPENDENCY_IMMEDIATELY does not recalculate time!\");\n\t\t\treturn 0;\n\n\t\tcase EventHandler::trigger_type::TRIGGER:\n\t\t\tTESTFAILMSG(\"TRIGGER does not recalculate time!\");\n\t\t\treturn 0;\n\n\t\tcase EventHandler::trigger_type::REPEAT:\n\t\t\t// This will force the execution every 5ms\n\t\t\treturn at + time::time_t::from_double(5);\n\n\t\tcase EventHandler::trigger_type::ONCE:\n\t\t\treturn 10; // even if data changed it will happen at the given time!\n\t\t}\n\t\treturn at;\n\t}\n};\n\n\nvoid eventtrigger() {\n\tlog::log(DBG << \"------------- [ Starting Test: Basic Ping Pong ] ------------\");\n\n\t// test destruction\n\tstd::weak_ptr<TestState> destruction_test_state;\n\n\t{\n\t\t// Test with one event handler\n\t\tauto loop = std::make_shared<EventLoop>();\n\n\t\tloop->add_event_handler(std::make_shared<TestEventHandler>(\"test_on_A\", 0));\n\t\tloop->add_event_handler(std::make_shared<TestEventHandler>(\"test_on_B\", 1));\n\n\t\tauto state = std::make_shared<TestState>(loop);\n\t\tauto gstate = std::static_pointer_cast<State>(state);\n\n\t\tdestruction_test_state = state;\n\n\t\t// One must not start the game at 0 - this leads to randomness in execution\n\t\tloop->create_event(\"test_on_B\", state->objectB, gstate, 1);\n\t\tloop->create_event(\"test_on_A\", state->objectA, gstate, 1);\n\n\t\t// It is expected, that A and B hand over the \"changed\" events between each other\n\t\tstate->objectA->set_number(0, 0);\n\n\t\t// run 10 iterations, for times 2, 4, ... 20\n\t\tfor (int i = 0; i < 10; ++i) {\n\t\t\tloop->reach_time((i + 1) * 2, gstate);\n\t\t}\n\n\t\tstate->log_dbg();\n\n\t\tif (state->trace.size() < 6) {\n\t\t\tTESTFAILMSG(\"not enough items collected\");\n\t\t}\n\n\t\tint i = 0;\n\n\t\ttime::time_t last_time = 0;\n\t\tfor (const auto &e : state->trace) {\n\t\t\tif (last_time > e.time) {\n\t\t\t\tTESTFAILMSG(\"You broke the time continuum: one shall not execute randomly!\");\n\t\t\t}\n\n\t\t\tlast_time = e.time;\n\t\t\tswitch (i) {\n\t\t\tcase 0:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 3);\n\t\t\t\tbreak;\n\t\t\tcase 1:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 6);\n\t\t\t\tbreak;\n\t\t\tcase 2:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 9);\n\t\t\t\tbreak;\n\t\t\tcase 3:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 12);\n\t\t\t\tbreak;\n\t\t\tcase 4:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 15);\n\t\t\t\tbreak;\n\t\t\tcase 5:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 18);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tTESTFAILMSG(\"Too many elements in stack trace\");\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\tif (!destruction_test_state.expired()) {\n\t\tTESTFAILMSG(\"Test Failed because TestState was not automatically destructed\");\n\t}\n\n\tlog::log(DBG << \"------------- [ Starting Test: Two Event Ping Pong ] ------------\");\n\t{\n\t\t// Test with two event handleres to check interplay\n\t\tauto loop = std::make_shared<EventLoop>();\n\n\t\tloop->add_event_handler(std::make_shared<TestEventHandler>(\"test_on_A\", 0));\n\t\tloop->add_event_handler(std::make_shared<TestEventHandlerTwo>(\"test_on_B\"));\n\n\t\tauto state = std::make_shared<TestState>(loop);\n\t\tauto gstate = std::static_pointer_cast<State>(state);\n\n\t\t// One must not start the game at 0 - this leads to randomness in execution\n\t\tloop->create_event(\"test_on_B\", state->objectB, gstate, 1);\n\t\tloop->create_event(\"test_on_A\", state->objectA, gstate, 1);\n\n\t\t// It is expected, that A and B hand over the \"changed\" events between each other\n\t\tstate->objectA->set_number(0, 1);\n\n\t\t// run 10 iterations, for times 2, 4, ... 20\n\t\tfor (int i = 0; i < 10; ++i) {\n\t\t\tloop->reach_time((i + 1) * 2, gstate);\n\t\t}\n\n\t\tstate->log_dbg();\n\n\t\tif (state->trace.size() < 7) {\n\t\t\tTESTFAILMSG(\"not enough items collected\");\n\t\t}\n\n\t\tint i = 0;\n\t\ttime::time_t last_time = 0;\n\n\t\tfor (const auto &e : state->trace) {\n\t\t\tif (last_time > e.time) {\n\t\t\t\tTESTFAILMSG(\"You broke the time continuum: one shall not execute randomly!\");\n\t\t\t}\n\n\t\t\tlast_time = e.time;\n\t\t\tswitch (i) {\n\t\t\tcase 0:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 3);\n\t\t\t\tbreak;\n\t\t\tcase 1:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 6);\n\t\t\t\tbreak;\n\t\t\tcase 2:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 8);\n\t\t\t\tbreak;\n\t\t\tcase 3:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 11);\n\t\t\t\tbreak;\n\t\t\tcase 4:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 13);\n\t\t\t\tbreak;\n\t\t\tcase 5:\n\t\t\t\tTESTEQUALS(e.name, \"A\");\n\t\t\t\tTESTEQUALS(e.time, 16);\n\t\t\t\tbreak;\n\t\t\tcase 6:\n\t\t\t\tTESTEQUALS(e.name, \"B\");\n\t\t\t\tTESTEQUALS(e.time, 18);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tTESTFAILMSG(\"Too many elements in stack trace\");\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\ti += 1;\n\t\t}\n\t}\n\n\tlog::log(DBG << \"------------- [ Starting Test: Complex Event Types ] ------------\");\n\t// Now set up a more complex test to test the different event types\n\t{\n\t\tauto loop = std::make_shared<EventLoop>();\n\n\t\tloop->add_event_handler(std::make_shared<EventTypeTestClass>(\n\t\t\t\"object_modify\",\n\t\t\tEventHandler::trigger_type::DEPENDENCY));\n\t\tloop->add_event_handler(std::make_shared<EventTypeTestClass>(\n\t\t\t\"object_modify_immediately\",\n\t\t\tEventHandler::trigger_type::DEPENDENCY_IMMEDIATELY));\n\t\tloop->add_event_handler(std::make_shared<EventTypeTestClass>(\n\t\t\t\"object_trigger\",\n\t\t\tEventHandler::trigger_type::TRIGGER));\n\t\tloop->add_event_handler(std::make_shared<EventTypeTestClass>(\n\t\t\t\"repeat_exec\",\n\t\t\tEventHandler::trigger_type::REPEAT));\n\t\tloop->add_event_handler(std::make_shared<EventTypeTestClass>(\n\t\t\t\"once\",\n\t\t\tEventHandler::trigger_type::ONCE));\n\n\t\tauto state = std::make_shared<TestState>(loop);\n\t\tauto gstate = std::static_pointer_cast<State>(state);\n\n\t\t// One must not start the game at 0 - this leads to randomness in execution\n\n\t\t// -------------------------------------\n\t\t// Add some events to be invoked:\n\n\t\t// execs whenever objectA is changed + 1\n\t\tloop->create_event(\"object_modify\", state->objectA, gstate, 4);\n\n\t\t// execs whenever objectA is changed + 1,\n\t\t// does not use a newly predicted time as object_modify_immediately\n\t\tloop->create_event(\"object_modify_immediately\", state->objectA, gstate, 1);\n\n\t\t// execs at t=x when test_trigger(x) is invoked on objectA\n\t\tloop->create_event(\"object_trigger\", state->objectA, gstate, 1);\n\n\t\t// execs periodically at t=n*5\n\t\tloop->create_event(\"repeat_exec\", state->objectA, gstate, 0);\n\n\t\t// executes just once at t=10\n\t\tloop->create_event(\"once\", state->objectA, gstate, 10);\n\n\t\tlog::log(DBG << \"##### SETUP DONE \");\n\n\t\t// Without anything happening and until time 0, nothing will happen\n\t\t{\n\t\t\t// Expected: object_modify_immediately(t=1) [ we changed data at t=1 ]\n\t\t\tloop->reach_time(0, gstate);\n\t\t\tTESTEQUALS(state->trace.empty(), true);\n\t\t}\n\n\t\tstate->objectA->set_number(0, 1);\n\t\t{\n\t\t\t// Evaluate the state until t=2\n\t\t\t// Expected: object_modify_immediately(t=(1+2)),\n\t\t\t// because we set the number at t=1,\n\t\t\t// which then creates a invoke time of t+1 == 2\n\t\t\tloop->reach_time(2, gstate);\n\t\t\tTESTEQUALS(state->trace.size(), 1);\n\t\t\tTESTEQUALS(state->trace.front().name, \"object_modify_immediately\");\n\t\t\tTESTEQUALS(state->trace.front().time, 2);\n\t\t\tstate->trace.clear();\n\t\t}\n\n\t\t{\n\t\t\t// executing to t=2 again should yield nothing,\n\t\t\t// we reached t=2 in the step before already\n\t\t\tloop->reach_time(2, gstate);\n\t\t\tTESTEQUALS(state->trace.empty(), true);\n\t\t}\n\n\t\tlog::log(DBG << \"##### INIT DONE \");\n\t\tlog::log(DBG << \"Triggering Keyframe at 1\");\n\n\t\tstate->objectA->test_trigger(1);\n\t\t{\n\t\t\t// Expected: object_trigger(1)\n\t\t\tloop->reach_time(2, gstate);\n\t\t\tstate->log_dbg();\n\t\t\tTESTEQUALS(state->trace.size(), 1);\n\t\t\tif (state->trace.front().name != \"object_trigger\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << state->trace.front().name\n\t\t\t\t                                 << \", expected object_trigger\");\n\n\t\t\tTESTEQUALS(state->trace.front().time, 1);\n\t\t\tstate->trace.clear();\n\t\t}\n\t\t{\n\t\t\t// Expected: object_modify(1+1+1=3) and repeat_exec(5)\n\t\t\tloop->reach_time(5, gstate);\n\n\t\t\tTESTEQUALS(state->trace.size(), 2);\n\t\t\tauto it = state->trace.begin();\n\t\t\tif (it->name != \"object_modify\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected object_modify\");\n\t\t\tTESTEQUALS(it->time, 3);\n\t\t\tit++;\n\t\t\tif (it->name != \"repeat_exec\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected repeat_exec\");\n\t\t\tTESTEQUALS(it->time, 5);\n\t\t\tstate->trace.clear();\n\t\t}\n\t\t{\n\t\t\t// Expected: repeat_exec(10), once(10)\n\t\t\tloop->reach_time(11, gstate);\n\t\t\tTESTEQUALS(state->trace.size(), 2);\n\t\t\tauto it = state->trace.begin();\n\t\t\tif (it->name != \"repeat_exec\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected repeat_exec\");\n\t\t\tTESTEQUALS(it->time, 10);\n\t\t\tit++;\n\t\t\tif (it->name != \"once\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected once\");\n\t\t\tTESTEQUALS(it->time, 10);\n\t\t\tstate->trace.clear();\n\t\t}\n\n\t\tlog::log(DBG << \"changing the value at t=12\");\n\t\tstate->objectA->set_number(1, 12);\n\t\t{\n\t\t\t// Expected: object_modify_immediately(12+1=13), object_modify(12+1+1=14)\n\t\t\t// object_modify_immediately is executed at the time of change (12+1)\n\t\t\t// whereas object_modify is executed with a newly predicted time (that is +1)\n\t\t\tloop->reach_time(15, gstate);\n\t\t\tTESTEQUALS(state->trace.size(), 3);\n\t\t\tauto it = state->trace.begin();\n\t\t\tif (it->name != \"object_modify_immediately\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected object_modify_immediately\");\n\t\t\tTESTEQUALS(it->time, 13);\n\t\t\tit++;\n\t\t\tif (it->name != \"object_modify\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected object_modify\");\n\t\t\tTESTEQUALS(it->time, 14);\n\t\t\tit++;\n\t\t\tif (it->name != \"repeat_exec\")\n\t\t\t\tTESTFAILMSG(\"Unexpected Event: \" << it->name << \" expected repeat_exec\");\n\t\t\tTESTEQUALS(it->time, 15);\n\t\t\tstate->trace.clear();\n\t\t}\n\t}\n\n\tlog::log(DBG << \"------------- [ Starting Test: Event parameter Mapping ] ------------\");\n\t{\n\t\tclass EventParameterMapTestClass : public EventHandler {\n\t\tpublic:\n\t\t\tEventParameterMapTestClass() :\n\t\t\t\tEventHandler(\"EventParameterMap\", EventHandler::trigger_type::ONCE) {}\n\n\t\t\tvoid setup_event(const std::shared_ptr<Event> & /*target*/,\n\t\t\t                 const std::shared_ptr<State> & /*state*/) override {}\n\n\t\t\tvoid invoke(EventLoop & /*loop*/,\n\t\t\t            const std::shared_ptr<EventEntity> & /*target*/,\n\t\t\t            const std::shared_ptr<State> & /*state*/,\n\t\t\t            const time::time_t & /*time*/,\n\t\t\t            const EventHandler::param_map &param) override {\n\t\t\t\tlog::log(DBG << \"Testing unknown parameter\");\n\t\t\t\tTESTEQUALS(param.contains(\"tomato\"), false);\n\t\t\t\tTESTEQUALS(param.check_type<int>(\"tomato\"), false);\n\t\t\t\tTESTEQUALS(param.get<int>(\"tomato\", 1), 1);\n\t\t\t\tTESTEQUALS(param.get<int>(\"tomato\"), 0);\n\t\t\t\tTESTEQUALS(param.get<std::string>(\"tomato\", \"test\"), \"test\");\n\t\t\t\tTESTEQUALS(param.get<std::string>(\"tomato\"), \"\");\n\n\t\t\t\tlog::log(DBG << \"Testing Integer parameter\");\n\t\t\t\tTESTEQUALS(param.contains(\"testInt\"), true);\n\t\t\t\tTESTEQUALS(param.check_type<int>(\"testInt\"), true);\n\t\t\t\tTESTEQUALS(param.get<int>(\"testInt\"), 1);\n\t\t\t\tTESTEQUALS(param.get<std::string>(\"testInt\", \"int\"), \"int\");\n\n\t\t\t\tlog::log(DBG << \"Testing char* parameter\");\n\t\t\t\tTESTEQUALS(param.contains(\"testString\"), true);\n\t\t\t\tTESTEQUALS(param.check_type<const char *>(\"testString\"), true);\n\t\t\t\tTESTEQUALS(strcmp(param.get<const char *>(\"testString\"), \"string\"), 0);\n\n\t\t\t\tlog::log(DBG << \"Testing std::string parameter\");\n\t\t\t\tTESTEQUALS(param.contains(\"testStdString\"), true);\n\t\t\t\tTESTEQUALS(param.check_type<std::string>(\"testStdString\"), true);\n\t\t\t\tTESTEQUALS(param.get<std::string>(\"testStdString\"), \"stdstring\");\n\t\t\t}\n\n\t\t\ttime::time_t predict_invoke_time(const std::shared_ptr<EventEntity> & /*target*/,\n\t\t\t                                 const std::shared_ptr<State> & /*state*/,\n\t\t\t                                 const time::time_t &at) override {\n\t\t\t\treturn at;\n\t\t\t}\n\t\t};\n\n\t\tusing namespace std::literals;\n\n\t\tauto loop = std::make_shared<EventLoop>();\n\t\tloop->add_event_handler(std::make_shared<EventParameterMapTestClass>());\n\t\tauto state = std::make_shared<TestState>(loop);\n\t\tauto gstate = std::dynamic_pointer_cast<State>(state);\n\n\t\tloop->create_event(\"EventParameterMap\", state->objectA, gstate, 1, {{\"testInt\", 1}, {\"testStdString\", \"stdstring\"s}, {\"testString\", \"string\"}});\n\t\tloop->reach_time(10, gstate);\n\t}\n}\n\n} // namespace openage::event::tests\n"
  },
  {
    "path": "libopenage/gamestate/CMakeLists.txt",
    "content": "add_sources(libopenage\n    definitions.cpp\n    entity_factory.cpp\n\tgame_entity.cpp\n    game_state.cpp\n\tgame.cpp\n\tmanager.cpp\n    map.cpp\n\tplayer.cpp\n    simulation.cpp\n\tterrain_chunk.cpp\n    terrain_factory.cpp\n    terrain_tile.cpp\n\tterrain.cpp\n    types.cpp\n\tworld.cpp\n\tuniverse.cpp\n)\n\nadd_subdirectory(activity/)\nadd_subdirectory(api/)\nadd_subdirectory(component/)\nadd_subdirectory(demo/)\nadd_subdirectory(event/)\nadd_subdirectory(system/)\n"
  },
  {
    "path": "libopenage/gamestate/activity/CMakeLists.txt",
    "content": "add_sources(libopenage\n    activity.cpp\n    end_node.cpp\n    node.cpp\n    start_node.cpp\n    task_node.cpp\n    task_system_node.cpp\n    tests.cpp\n    types.cpp\n    xor_event_gate.cpp\n    xor_gate.cpp\n)\n\nadd_subdirectory(\"event\")\nadd_subdirectory(\"condition\")\n"
  },
  {
    "path": "libopenage/gamestate/activity/activity.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"activity.h\"\n\n\nnamespace openage::gamestate::activity {\n\nActivity::Activity(activity_id id,\n                   const std::shared_ptr<Node> &start,\n                   activity_label label) :\n\tid{id},\n\tlabel{label},\n\tstart{start} {\n}\n\nactivity_id Activity::get_id() const {\n\treturn this->id;\n}\n\nconst activity_label Activity::get_label() const {\n\treturn this->label;\n}\n\nconst std::shared_ptr<Node> &Activity::get_start() const {\n\treturn this->start;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/activity.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n\n\nnamespace openage::gamestate::activity {\nclass Node;\n\nusing activity_id = size_t;\nusing activity_label = std::string;\n\n/**\n * Encapsules a self-contained control flow as a node graph.\n */\nclass Activity {\npublic:\n\t/**\n\t * Create a new activity.\n\t *\n\t * @param id Unique ID.\n\t * @param start Start node in the graph.\n\t * @param label Human-readable label (optional).\n\t */\n\tActivity(activity_id id,\n\t         const std::shared_ptr<Node> &start,\n\t         activity_label label = \"\");\n\n\t/**\n\t * Get the unique ID of this activity.\n\t *\n\t * @return Unique ID.\n\t */\n\tactivity_id get_id() const;\n\n\t/**\n\t * Get the human-readable label of this activity.\n\t *\n\t * @return Human-readable label.\n\t */\n\tconst activity_label get_label() const;\n\n\t/**\n\t * Get the start node of this activity.\n\t *\n\t * @return Start node.\n\t */\n\tconst std::shared_ptr<Node> &get_start() const;\n\nprivate:\n\t/**\n\t * Unique ID.\n\t */\n\tconst activity_id id;\n\n\t/**\n\t * Human-readable label.\n\t */\n\tconst activity_label label;\n\n\t/**\n\t * Start node.\n\t */\n\tstd::shared_ptr<Node> start;\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/condition/CMakeLists.txt",
    "content": "add_sources(libopenage\n    command_in_queue.cpp\n    next_command.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/activity/condition/command_in_queue.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"next_command.h\"\n\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/game_entity.h\"\n\n\nnamespace openage::gamestate::activity {\n\nbool command_in_queue(const time::time_t &time,\n                      const std::shared_ptr<gamestate::GameEntity> &entity) {\n\tauto command_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\n\treturn not command_queue->get_queue().empty(time);\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/condition/command_in_queue.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\n\nnamespace activity {\n\n/**\n * Condition for command in queue check in the activity system.\n *\n * @param time Time when the condition is checked.\n * @param entity Game entity.\n *\n * @return true if there is at least one command in the entity's command queue, false otherwise.\n */\nbool command_in_queue(const time::time_t &time,\n                      const std::shared_ptr<gamestate::GameEntity> &entity);\n\n} // namespace activity\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/activity/condition/next_command.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"next_command.h\"\n\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/game_entity.h\"\n\n\nnamespace openage::gamestate::activity {\n\nbool next_command_idle(const time::time_t &time,\n                       const std::shared_ptr<gamestate::GameEntity> &entity) {\n\tauto command_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\n\tif (command_queue->get_queue().empty(time)) {\n\t\treturn false;\n\t}\n\n\tauto command = command_queue->get_queue().front(time);\n\treturn command->get_type() == component::command::command_t::MOVE;\n}\n\nbool next_command_move(const time::time_t &time,\n                       const std::shared_ptr<gamestate::GameEntity> &entity) {\n\tauto command_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\n\tif (command_queue->get_queue().empty(time)) {\n\t\treturn false;\n\t}\n\n\tauto command = command_queue->get_queue().front(time);\n\treturn command->get_type() == component::command::command_t::MOVE;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/condition/next_command.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\n\nnamespace activity {\n\n/**\n * Condition for next command check in the activity system.\n *\n * @param time Time when the condition is checked.\n * @param entity Game entity.\n *\n * @return true if the entity has a idle command next in the queue, false otherwise.\n */\nbool next_command_idle(const time::time_t &time,\n                       const std::shared_ptr<gamestate::GameEntity> &entity);\n\n/**\n * Condition for next command check in the activity system.\n *\n * @param time Time when the condition is checked.\n * @param entity Game entity.\n *\n * @return true if the entity has a move command next in the queue, false otherwise.\n */\nbool next_command_move(const time::time_t &time,\n                       const std::shared_ptr<gamestate::GameEntity> &entity);\n\n} // namespace activity\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/activity/end_node.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"end_node.h\"\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::gamestate::activity {\n\nEndNode::EndNode(node_id_t id,\n                 node_label_t label) :\n\tNode{id, label, {}} {\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/end_node.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n\n\nnamespace openage::gamestate::activity {\n\n/**\n * End node of an activity. This is where the control flow for an activity\n * ends. When the activity is a subactivity, the control flow returns to the\n * parent activity.\n *\n * inputs: 1\n * outputs: none\n */\nclass EndNode : public Node {\npublic:\n\t/**\n\t * Create a new end node.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t */\n\tEndNode(node_id_t id,\n\t        node_label_t label = \"End\");\n\tvirtual ~EndNode() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::END;\n\t}\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/event/CMakeLists.txt",
    "content": "add_sources(libopenage\n    command_in_queue.cpp\n    wait.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/activity/event/command_in_queue.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"command_in_queue.h\"\n\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/manager.h\"\n\n\nnamespace openage::gamestate::activity {\n\nstd::shared_ptr<openage::event::Event> primer_command_in_queue(const time::time_t &,\n                                                               const std::shared_ptr<gamestate::GameEntity> &entity,\n                                                               const std::shared_ptr<openage::event::EventLoop> &loop,\n                                                               const std::shared_ptr<gamestate::GameState> &state,\n                                                               size_t next_id) {\n\topenage::event::EventHandler::param_map::map_t params{{\"next\", next_id}}; // move->get_id();\n\tauto ev = loop->create_event(\"game.process_command\",\n\t                             entity->get_manager(),\n\t                             state,\n\t                             // event is not executed until a command is available\n\t                             time::TIME_MAX,\n\t                             params);\n\tauto entity_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\tauto &queue = entity_queue->get_queue();\n\tqueue.add_dependent(ev);\n\n\treturn ev;\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/event/command_in_queue.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass Event;\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace activity {\n\n/**\n * Primer for command in queue events in the activity system.\n *\n * @param time Current simulation time.\n * @param entity Game entity.\n * @param loop Event loop that the event is registered on.\n * @param state Game state.\n * @param next_id ID of the next node in the activity graph.\n *\n * @return Scheduled event.\n */\nstd::shared_ptr<openage::event::Event> primer_command_in_queue(const time::time_t &,\n                                                               const std::shared_ptr<gamestate::GameEntity> &entity,\n                                                               const std::shared_ptr<openage::event::EventLoop> &loop,\n                                                               const std::shared_ptr<gamestate::GameState> &state,\n                                                               size_t next_id);\n\n} // namespace activity\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/activity/event/wait.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"wait.h\"\n\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/manager.h\"\n\n\nnamespace openage::gamestate::activity {\n\nstd::shared_ptr<openage::event::Event> primer_wait(const time::time_t &time,\n                                                   const std::shared_ptr<GameEntity> &entity,\n                                                   const std::shared_ptr<openage::event::EventLoop> &loop,\n                                                   const std::shared_ptr<gamestate::GameState> &state,\n                                                   size_t next_id) {\n\topenage::event::EventHandler::param_map::map_t params{{\"next\", next_id}};\n\tauto ev = loop->create_event(\"game.wait\",\n\t                             entity->get_manager(),\n\t                             state,\n\t                             time,\n\t                             params);\n\n\treturn ev;\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/event/wait.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass Event;\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace activity {\n\n\n/**\n * Primer for wait events in the activity system.\n *\n * @param time Wait until this time. If the time is in the past, the event is executed immediately.\n * @param entity Game entity.\n * @param loop Event loop that the event is registered on.\n * @param state Game state.\n * @param next_id ID of the next node in the activity graph.\n *\n * @return Scheduled event.\n */\nstd::shared_ptr<openage::event::Event> primer_wait(const time::time_t &time,\n                                                   const std::shared_ptr<GameEntity> &entity,\n                                                   const std::shared_ptr<openage::event::EventLoop> &loop,\n                                                   const std::shared_ptr<gamestate::GameState> &state,\n                                                   size_t next_id);\n\n\n} // namespace activity\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/activity/node.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"node.h\"\n\n#include <ostream>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::gamestate::activity {\n\nNode::Node(node_id_t id,\n           node_label_t label,\n           const std::vector<std::shared_ptr<Node>> &outputs) :\n\toutputs{},\n\tid{id},\n\tlabel{label} {\n\tthis->outputs.reserve(outputs.size());\n\tfor (const auto &output : outputs) {\n\t\tthis->outputs.emplace(output->get_id(), output);\n\t}\n}\n\nnode_id_t Node::get_id() const {\n\treturn this->id;\n}\n\nconst node_label_t Node::get_label() const {\n\treturn this->label;\n}\n\nstd::string Node::str() const {\n\tstd::stringstream ret;\n\tif (this->label.empty()) {\n\t\tret << this->get_id();\n\t}\n\telse {\n\t\tret << this->get_label()\n\t\t\t<< \" (id=\" << this->get_id() << \")\";\n\t}\n\treturn ret.str();\n}\n\nconst std::shared_ptr<Node> &Node::next(node_id_t id) const {\n\tif (not this->outputs.contains(id)) [[unlikely]] {\n\t\tthrow Error{MSG(err) << \"Node \" << this->str() << \" has no output with id \" << id};\n\t}\n\n\treturn this->outputs.at(id);\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/node.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"gamestate/activity/types.h\"\n\n\nnamespace openage::gamestate::activity {\n\nusing node_id_t = size_t;\nusing node_label_t = std::string;\n\n/**\n * Node in the flow graph describing the activity.\n */\nclass Node {\npublic:\n\t/**\n\t * Create a new node.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t * @param outputs Output nodes.\n\t */\n\tNode(node_id_t id,\n\t     node_label_t label = \"\",\n\t     const std::vector<std::shared_ptr<Node>> &outputs = {});\n\tvirtual ~Node() = default;\n\n\t/**\n\t * Get the type of this node.\n\t *\n\t * @return Node type.\n\t */\n\tvirtual node_t get_type() const = 0;\n\n\t/**\n\t * Get the unique identifier for this node.\n\t *\n\t * @return The unique identifier.\n\t */\n\tnode_id_t get_id() const;\n\n\t/**\n\t * Get the human-readable label for this node.\n\t *\n\t * @return Human-readable label.\n\t */\n\tconst node_label_t get_label() const;\n\n\t/**\n\t * Get a human-readable string representation of this node.\n\t *\n\t * @return Human-readable representation.\n\t */\n\tstd::string str() const;\n\n\t/**\n\t * Get the output node with the given identifier.\n\t *\n\t * @param id Unique identifier of the output node.\n\t * @return Output node.\n\t */\n\tconst std::shared_ptr<Node> &next(node_id_t id) const;\n\nprotected:\n\t/**\n\t * Output nodes.\n\t */\n\tstd::unordered_map<node_id_t, std::shared_ptr<Node>> outputs;\n\nprivate:\n\t/**\n\t * Unique identifier for this node.\n\t */\n\tconst node_id_t id;\n\n\t/**\n\t * Human-readable label.\n\t */\n\tconst node_label_t label;\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/start_node.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"start_node.h\"\n\n#include <unordered_map>\n#include <utility>\n\n\nnamespace openage::gamestate::activity {\n\nStartNode::StartNode(node_id_t id,\n                     node_label_t label,\n                     const std::shared_ptr<Node> &output) :\n\tNode{id, label} {\n\tif (output) {\n\t\tthis->add_output(output);\n\t}\n}\n\nvoid StartNode::add_output(const std::shared_ptr<Node> &output) {\n\tthis->outputs.clear();\n\tthis->outputs.emplace(output->get_id(), output);\n}\n\nnode_id_t StartNode::get_next() const {\n\treturn (*this->outputs.begin()).first;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/start_node.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n\n\nnamespace openage::gamestate::activity {\n\n/**\n * Start node of an activity. This is where the control flow begins.\n *\n * inputs: none\n * outputs: 1\n */\nclass StartNode : public Node {\npublic:\n\t/**\n\t * Create a new start node.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t * @param output Next node to visit (can be set later).\n\t */\n\tStartNode(node_id_t id,\n\t          node_label_t label = \"Start\",\n\t          const std::shared_ptr<Node> &output = nullptr);\n\tvirtual ~StartNode() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::START;\n\t}\n\n\t/**\n\t * Set the current output node.\n\t *\n\t * Replace any output node that has been set before.\n\t *\n\t * @param output Output node.\n\t */\n\tvoid add_output(const std::shared_ptr<Node> &output);\n\n\t/**\n\t * Get the next node to visit.\n\t *\n\t * @param time Current time.\n\t * @return Next node to visit.\n\t */\n\tnode_id_t get_next() const;\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/task_node.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"task_node.h\"\n\n#include <unordered_map>\n#include <utility>\n\n\nnamespace openage::gamestate::activity {\n\nTaskCustom::TaskCustom(node_id_t id,\n                   node_label_t label,\n                   const std::shared_ptr<Node> &output,\n                   task_func_t task_func) :\n\tNode{id, label},\n\ttask_func{task_func} {\n\tif (output) {\n\t\tthis->add_output(output);\n\t}\n}\n\nvoid TaskCustom::add_output(const std::shared_ptr<Node> &output) {\n\tthis->outputs.clear();\n\tthis->outputs.emplace(output->get_id(), output);\n}\n\nvoid TaskCustom::set_task_func(task_func_t task_func) {\n\tthis->task_func = task_func;\n}\n\ntask_func_t TaskCustom::get_task_func() const {\n\treturn this->task_func;\n}\n\nnode_id_t TaskCustom::get_next() const {\n\treturn (*this->outputs.begin()).first;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/task_node.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\n\nnamespace activity {\n\nusing task_func_t = std::function<void(const time::time_t &,\n                                       const std::shared_ptr<gamestate::GameEntity> &)>;\n\nstatic const task_func_t no_task = [](const time::time_t &,\n                                      const std::shared_ptr<gamestate::GameEntity> &) {\n\tthrow Error{ERR << \"No task defined for this node.\"};\n};\n\n/**\n * Executes a function when visited.\n */\nclass TaskCustom : public Node {\npublic:\n\t/**\n\t * Create a new task node.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t * @param task_func Action to perform when visiting this node (can be set later).\n\t * @param output Next node to visit (optional).\n\t */\n\tTaskCustom(node_id_t id,\n\t           node_label_t label = \"TaskCustom\",\n\t           const std::shared_ptr<Node> &output = nullptr,\n\t           task_func_t task_func = no_task);\n\tvirtual ~TaskCustom() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::TASK_CUSTOM;\n\t}\n\n\t/**\n\t * Set the current output node.\n\t *\n\t * @param output Output node.\n\t */\n\tvoid add_output(const std::shared_ptr<Node> &output);\n\n\t/**\n\t * Set the task function.\n\t *\n\t * @param task_func Action to perform when visiting this node.\n\t */\n\tvoid set_task_func(task_func_t task_func);\n\n\t/**\n\t * Get the task function.\n\t *\n\t * @return Action to perform when visiting this node.\n\t */\n\ttask_func_t get_task_func() const;\n\n\t/**\n\t * Get the next node to visit.\n\t *\n\t * @param time Current time.\n\t * @return Next node to visit.\n\t */\n\tnode_id_t get_next() const;\n\nprivate:\n\t/**\n\t * Action to perform when visiting this node.\n\t */\n\ttask_func_t task_func;\n};\n\n} // namespace activity\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/activity/task_system_node.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"task_system_node.h\"\n\n#include <unordered_map>\n#include <utility>\n\n\nnamespace openage::gamestate::activity {\n\nTaskSystemNode::TaskSystemNode(node_id_t id,\n                               node_label_t label,\n                               const std::shared_ptr<Node> &output,\n                               system::system_id_t system_id) :\n\tNode{id, label},\n\tsystem_id{system_id} {\n\tif (output) {\n\t\tthis->add_output(output);\n\t}\n}\n\nvoid TaskSystemNode::add_output(const std::shared_ptr<Node> &output) {\n\tthis->outputs.clear();\n\tthis->outputs.emplace(output->get_id(), output);\n}\n\nvoid TaskSystemNode::set_system_id(system::system_id_t system_id) {\n\tthis->system_id = system_id;\n}\n\nsystem::system_id_t TaskSystemNode::get_system_id() const {\n\treturn this->system_id;\n}\n\nnode_id_t TaskSystemNode::get_next() const {\n\treturn (*this->outputs.begin()).first;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/task_system_node.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"gamestate/system/types.h\"\n\n\nnamespace openage::gamestate::activity {\n\n/**\n * Similar to the custom task node, but runs a built-in system function\n * when visited.\n */\nclass TaskSystemNode : public Node {\npublic:\n\t/**\n\t * Create a new custom task system node.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t * @param output Next node to visit (optional).\n\t * @param system_id System to run when visiting this node (can be set later).\n\t */\n\tTaskSystemNode(node_id_t id,\n\t               node_label_t label = \"TaskSystem\",\n\t               const std::shared_ptr<Node> &output = nullptr,\n\t               system::system_id_t system_id = system::system_id_t::NONE);\n\tvirtual ~TaskSystemNode() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::TASK_SYSTEM;\n\t}\n\n\t/**\n\t * Set the current output node.\n\t *\n\t * @param output Output node.\n\t */\n\tvoid add_output(const std::shared_ptr<Node> &output);\n\n\t/**\n\t * Set the system id.\n\t *\n\t * @param system_id System to run when visiting this node.\n\t */\n\tvoid set_system_id(system::system_id_t system_id);\n\n\t/**\n\t * Get the system id.\n\t *\n\t * @return System to run when visiting this node.\n\t */\n\tsystem::system_id_t get_system_id() const;\n\n\t/**\n\t * Get the next node to visit.\n\t *\n\t * @param time Current time.\n\t * @return Next node to visit.\n\t */\n\tnode_id_t get_next() const;\n\nprivate:\n\t/**\n\t * System to run when visiting this node.\n\t */\n\tsystem::system_id_t system_id;\n};\n\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/tests.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include <cstddef>\n#include <functional>\n#include <memory>\n#include <string>\n\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"event/state.h\"\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/activity/end_node.h\"\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/start_node.h\"\n#include \"gamestate/activity/task_node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"gamestate/activity/xor_event_gate.h\"\n#include \"gamestate/activity/xor_gate.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate::tests {\n\n\n/**\n * Main control flow that navigates through the activity node graph.\n *\n * @param current_node Node where the control flow starts from.\n * @return Node where the control flow should continue.\n */\nconst std::shared_ptr<activity::Node> activity_flow(const std::shared_ptr<activity::Node> &current_node,\n                                                    const std::optional<event::EventHandler::param_map> ev_params = std::nullopt);\n\n\n/**\n * Notified by event loop when the activity flow starts/resumes.\n *\n * It keeps track of the current node in the activity flow graph.\n */\nclass TestActivityManager : public event::EventEntity {\npublic:\n\tTestActivityManager(const std::shared_ptr<event::EventLoop> &loop,\n\t                    const std::shared_ptr<activity::Node> &start_node) :\n\t\tEventEntity{loop},\n\t\tcurrent_node{start_node} {\n\t}\n\n\tsize_t id() const override {\n\t\treturn 0;\n\t}\n\n\tstd::string idstr() const override {\n\t\treturn \"TestActivityManager\";\n\t}\n\n\tvoid run(const std::optional<event::EventHandler::param_map> ev_params = std::nullopt) {\n\t\tif (not current_node) {\n\t\t\tthrow Error{ERR << \"No current node given\"};\n\t\t}\n\t\tthis->current_node = activity_flow(this->current_node, ev_params);\n\t}\n\n\tstd::shared_ptr<activity::Node> current_node;\n};\n\n/**\n * Event state. Unused but required by the event system.\n */\nclass TestActivityState : public event::State {\npublic:\n\tusing event::State::State;\n};\n\n\n/**\n * Receives the event when the activity flow and notifies the activity manager\n * to continue the flow.\n */\nclass TestActivityHandler : public event::OnceEventHandler {\npublic:\n\tusing event::OnceEventHandler::OnceEventHandler;\n\n\tvoid setup_event(const std::shared_ptr<event::Event> & /* event */,\n\t                 const std::shared_ptr<event::State> & /* state */) override {\n\t\treturn;\n\t}\n\n\tvoid invoke(event::EventLoop & /* loop */,\n\t            const std::shared_ptr<event::EventEntity> &target,\n\t            const std::shared_ptr<event::State> & /* state */,\n\t            const time::time_t & /* time */,\n\t            const param_map &params) override {\n\t\tauto mgr_target = std::dynamic_pointer_cast<TestActivityManager>(target);\n\t\tmgr_target->run(params);\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<event::EventEntity> & /* target */,\n\t                                 const std::shared_ptr<event::State> & /* state */,\n\t                                 const time::time_t &at) override {\n\t\treturn at;\n\t}\n};\n\n\nconst std::shared_ptr<activity::Node> activity_flow(const std::shared_ptr<activity::Node> &current_node,\n                                                    const std::optional<event::EventHandler::param_map> ev_params) {\n\t// events that are currently being listened for\n\t// in the gamestate these are stored in the activity component\n\tstatic std::vector<std::shared_ptr<event::Event>> events;\n\n\tauto current = current_node;\n\n\tif (current->get_type() == activity::node_t::XOR_EVENT_GATE) {\n\t\tlog::log(INFO << \"Continuing from event node\");\n\t\tif (not ev_params.has_value()) {\n\t\t\tthrow Error{ERR << \"XorEventGate: No event parameters given on continue\"};\n\t\t}\n\n\t\tauto next_id = ev_params.value().get<size_t>(\"next\");\n\t\tcurrent = current->next(next_id);\n\n\t\t// cancel all other events that the manager may have been waiting for\n\t\tfor (auto &event : events) {\n\t\t\tevent->cancel(0);\n\t\t}\n\t}\n\n\twhile (current->get_type() != activity::node_t::END) {\n\t\tlog::log(INFO << \"Visiting node: \" << current->str());\n\n\t\tswitch (current->get_type()) {\n\t\tcase activity::node_t::START: {\n\t\t\tauto node = std::static_pointer_cast<activity::StartNode>(current);\n\t\t\tauto next_id = node->get_next();\n\t\t\tcurrent = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::END: {\n\t\t\t// TODO\n\t\t\treturn current;\n\t\t} break;\n\t\tcase activity::node_t::TASK_CUSTOM: {\n\t\t\tauto node = std::static_pointer_cast<activity::TaskCustom>(current);\n\t\t\tauto task = node->get_task_func();\n\t\t\ttask(0, nullptr);\n\t\t\tauto next_id = node->get_next();\n\t\t\tcurrent = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::XOR_GATE: {\n\t\t\tauto node = std::static_pointer_cast<activity::XorGate>(current);\n\t\t\tauto next_id = node->get_default()->get_id();\n\t\t\tfor (auto &condition : node->get_conditions()) {\n\t\t\t\tauto condition_func = condition.second;\n\t\t\t\tif (condition_func(0, nullptr)) {\n\t\t\t\t\tnext_id = condition.first;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcurrent = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::XOR_EVENT_GATE: {\n\t\t\tauto node = std::static_pointer_cast<activity::XorEventGate>(current);\n\t\t\tauto event_primers = node->get_primers();\n\t\t\tfor (auto &primer : event_primers) {\n\t\t\t\tauto ev = primer.second(0,\n\t\t\t\t                        nullptr,\n\t\t\t\t                        nullptr,\n\t\t\t\t                        nullptr,\n\t\t\t\t                        primer.first);\n\t\t\t\tevents.push_back(ev);\n\t\t\t}\n\n\t\t\t// wait for event\n\t\t\tlog::log(INFO << \"Waiting for event\");\n\t\t\treturn current;\n\t\t} break;\n\t\tdefault:\n\t\t\tthrow Error{ERR << \"Unhandled node type for node \" << current->str()};\n\t\t}\n\n\t\tlog::log(INFO << \"Next node: \" << current->str());\n\t}\n\n\tlog::log(INFO << \"Reached end node: \" << current->str());\n\n\treturn current;\n}\n\n\n/**\n * Main test. Sets up the activity graph.\n *\n *\n * Graph:\n * Start -> Task 1 -> XOR -> Event -> Task 2 -> End\n *            ^--------|\n */\nvoid activity_demo() {\n\t// create an event loop for events in the graph\n\tauto loop = std::make_shared<event::EventLoop>();\n\tauto handler = std::make_shared<TestActivityHandler>(\"test.activity\");\n\tloop->add_event_handler(handler);\n\tauto state = std::make_shared<TestActivityState>(loop);\n\n\t// create the nodes in the graph\n\t// connections are created further below\n\tauto start = std::make_shared<activity::StartNode>(0);\n\tauto task1 = std::make_shared<activity::TaskCustom>(1);\n\tauto xor_node = std::make_shared<activity::XorGate>(2);\n\tauto event_node = std::make_shared<activity::XorEventGate>(3);\n\tauto task2 = std::make_shared<activity::TaskCustom>(4);\n\tauto end = std::make_shared<activity::EndNode>(5);\n\n\t// create an activity manager that controls the flow\n\tauto mgr = std::make_shared<TestActivityManager>(loop, start);\n\n\t// connect the nodes\n\n\t// start node\n\tstart->add_output(task1);\n\n\t// task 1\n\ttask1->add_output(xor_node);\n\ttask1->set_task_func([](const time::time_t & /* time */,\n\t                        const std::shared_ptr<gamestate::GameEntity> & /* entity */) {\n\t\tlog::log(INFO << \"Running task 1\");\n\t});\n\n\t// Conditional branch\n\tstatic size_t counter = 0;\n\tactivity::condition_t branch_task1 = [&](const time::time_t & /* time */,\n\t                                         const std::shared_ptr<gamestate::GameEntity> & /* entity */) {\n\t\tlog::log(INFO << \"Checking condition (counter < 4): counter=\" << counter);\n\t\tif (counter < 4) {\n\t\t\tlog::log(INFO << \"Selecting path 1 (back to task node \" << task1->get_id() << \")\");\n\n\t\t\tcounter++;\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t};\n\txor_node->add_output(task1, branch_task1);\n\tactivity::condition_t branch_event = [&](const time::time_t & /* time */,\n\t                                         const std::shared_ptr<gamestate::GameEntity> & /* entity */) {\n\t\t// No check needed here, the event node is always selected\n\t\tlog::log(INFO << \"Selecting path 2 (to event node \" << event_node->get_id() << \")\");\n\t\treturn true;\n\t};\n\txor_node->add_output(event_node, branch_event);\n\txor_node->set_default(event_node);\n\n\t// event node\n\tactivity::event_primer_t primer = [&](const time::time_t & /* time */,\n\t                                      const std::shared_ptr<gamestate::GameEntity> & /* entity */,\n\t                                      const std::shared_ptr<event::EventLoop> & /* loop */,\n\t                                      const std::shared_ptr<gamestate::GameState> & /* state */,\n\t                                      size_t next_id) {\n\t\tlog::log(INFO << \"Setting up event\");\n\t\tevent::EventHandler::param_map::map_t params{{\"next\", next_id}};\n\t\tauto ev = loop->create_event(\"test.activity\",\n\t\t                             mgr,\n\t\t                             state,\n\t\t                             0,\n\t\t                             params);\n\t\treturn ev;\n\t};\n\tevent_node->add_output(task2, primer);\n\n\t// task 2\n\ttask2->add_output(end);\n\ttask2->set_task_func([](const time::time_t & /* time */,\n\t                        const std::shared_ptr<gamestate::GameEntity> & /* entity */) {\n\t\tlog::log(INFO << \"Running task 2\");\n\t});\n\n\t// run the manager to start from the start node\n\t// this will run until the event node is reached\n\tmgr->run();\n\n\t// process events\n\t// this will call back the activity manager to continue the flow\n\tloop->reach_time(0, state);\n}\n\n} // namespace openage::gamestate::tests\n"
  },
  {
    "path": "libopenage/gamestate/activity/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::gamestate::activity {\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/types.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage::gamestate::activity {\n\n/**\n * Node types in the flow graph.\n */\nenum class node_t {\n\tSTART,\n\tEND,\n\tXOR_EVENT_GATE,\n\tXOR_GATE,\n\tTASK_CUSTOM,\n\tTASK_SYSTEM,\n};\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/xor_event_gate.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"xor_event_gate.h\"\n\n#include <unordered_map>\n\n\nnamespace openage::gamestate::activity {\n\nXorEventGate::XorEventGate(node_id_t id,\n                           node_label_t label) :\n\tNode{id, label},\n\tprimers{} {\n}\n\nXorEventGate::XorEventGate(node_id_t id,\n                           node_label_t label,\n                           const std::vector<std::shared_ptr<Node>> &outputs,\n                           const std::map<node_id_t, event_primer_t> &primers) :\n\tNode{id, label, outputs},\n\tprimers{} {\n\tif (primers.size() != outputs.size()) {\n\t\tthrow Error{MSG(err) << \"XorEventGate \" << this->str() << \" has \" << outputs.size()\n\t\t                     << \" outputs but \" << primers.size() << \" primers\"};\n\t}\n\n\tfor (const auto &[id, primer] : primers) {\n\t\tthis->primers.emplace(id, primer);\n\t}\n}\n\nvoid XorEventGate::add_output(const std::shared_ptr<Node> &output,\n                              const event_primer_t &primer) {\n\tthis->outputs.emplace(output->get_id(), output);\n\tthis->primers.emplace(output->get_id(), primer);\n}\n\nconst std::map<node_id_t, event_primer_t> &XorEventGate::get_primers() const {\n\treturn this->primers;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/xor_event_gate.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <map>\n#include <memory>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass Event;\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace activity {\n\n\n/**\n * Create and register an event on the event loop.\n *\n * When the event is executed, the control flow continues on the branch\n * associated with the event.\n *\n * @param time Time at which the primer function is executed.\n * @param entity Game entity that the activity is assigned to.\n * @param loop Event loop that events are registered on.\n * @param state Game state.\n * @param next_id ID of the next node to visit. This is passed as an event parameter.\n *\n * @return Event registered on the event loop.\n */\nusing event_primer_t = std::function<std::shared_ptr<openage::event::Event>(const time::time_t &,\n                                                                            const std::shared_ptr<gamestate::GameEntity> &,\n                                                                            const std::shared_ptr<event::EventLoop> &,\n                                                                            const std::shared_ptr<gamestate::GameState> &,\n                                                                            size_t next_id)>;\n\n\n/**\n * Waits for an event to be executed before continuing the control flow.\n */\nclass XorEventGate : public Node {\npublic:\n\t/**\n\t * Create a new exclusive event gateway.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label (optional).\n\t */\n\tXorEventGate(node_id_t id,\n\t             node_label_t label = \"EventGateWay\");\n\n\t/**\n\t * Create a new exclusive event gateway.\n\t *\n\t * @param id Unique identifier for this node.\n\t * @param label Human-readable label.\n\t * @param outputs Output nodes.\n\t * @param primers Event primers for each output node.\n\t */\n\tXorEventGate(node_id_t id,\n\t             node_label_t label,\n\t             const std::vector<std::shared_ptr<Node>> &outputs,\n\t             const std::map<node_id_t, event_primer_t> &primers);\n\n\tvirtual ~XorEventGate() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::XOR_EVENT_GATE;\n\t}\n\n\t/**\n\t * Add an output node.\n\t *\n\t * @param output Output node.\n\t * @param primer Creation function for the event associated with the output node.\n\t */\n\tvoid add_output(const std::shared_ptr<Node> &output,\n\t                const event_primer_t &primer);\n\n\t/**\n\t * Get the output->event primer mappings.\n\t *\n\t * @return Event primer functions for each output node.\n\t */\n\tconst std::map<node_id_t, event_primer_t> &get_primers() const;\n\nprivate:\n\t/**\n\t * Maps output node IDs to event primer functions.\n\t *\n\t * Events are created and registered on the event loop when the node is visited.\n\t */\n\tstd::map<node_id_t, event_primer_t> primers;\n};\n\n} // namespace activity\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/activity/xor_gate.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"xor_gate.h\"\n\n#include <unordered_map>\n\n\nnamespace openage::gamestate::activity {\n\nXorGate::XorGate(node_id_t id,\n                 node_label_t label) :\n\tNode{id, label, {}},\n\tconditions{},\n\tdefault_node{nullptr} {\n}\n\nXorGate::XorGate(node_id_t id,\n                 node_label_t label,\n                 const std::vector<std::shared_ptr<Node>> &outputs,\n                 const std::vector<condition_t> &conditions,\n                 const std::shared_ptr<Node> &default_node) :\n\tNode{id, label, outputs},\n\tconditions{},\n\tdefault_node{default_node} {\n\tif (conditions.size() != outputs.size()) [[unlikely]] {\n\t\tthrow Error{MSG(err) << \"XorGate \" << this->str() << \" has \" << outputs.size()\n\t\t                     << \" outputs but \" << conditions.size() << \" conditions\"};\n\t}\n\n\tfor (size_t i = 0; i < conditions.size(); ++i) {\n\t\tthis->conditions.emplace(outputs[i]->get_id(), conditions[i]);\n\t}\n}\n\nvoid XorGate::add_output(const std::shared_ptr<Node> &output,\n                         const condition_t condition_func) {\n\tthis->outputs.emplace(output->get_id(), output);\n\tthis->conditions.emplace(output->get_id(), condition_func);\n}\n\nconst std::map<node_id_t, condition_t> &XorGate::get_conditions() const {\n\treturn this->conditions;\n}\n\nconst std::shared_ptr<Node> &XorGate::get_default() const {\n\treturn this->default_node;\n}\n\nvoid XorGate::set_default(const std::shared_ptr<Node> &node) {\n\tif (this->default_node != nullptr) {\n\t\tthrow Error{MSG(err) << \"XorGate \" << this->str() << \" already has a default node\"};\n\t}\n\n\tthis->outputs.emplace(node->get_id(), node);\n\tthis->default_node = node;\n}\n\n} // namespace openage::gamestate::activity\n"
  },
  {
    "path": "libopenage/gamestate/activity/xor_gate.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <map>\n#include <memory>\n#include <optional>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\n\nnamespace activity {\n\n/**\n * Function that determines if an output node is chosen.\n *\n * @param time Current game time.\n * @param entity Entity that is executing the activity.\n *\n * @return true if the output node is chosen, false otherwise.\n */\nusing condition_t = std::function<bool(const time::time_t &,\n                                       const std::shared_ptr<gamestate::GameEntity> &)>;\n\n\n/**\n * Chooses one of its output nodes based on conditions.\n */\nclass XorGate : public Node {\npublic:\n\t/**\n\t * Creates a new condition node.\n\t *\n\t * @param id Unique identifier of the node.\n\t * @param label Label of the node (optional).\n\t */\n\tXorGate(node_id_t id,\n\t        node_label_t label = \"ExclusiveGateway\");\n\n\t/**\n\t * Creates a new condition node.\n\t *\n\t * @param id Unique identifier of the node.\n\t * @param label Label of the node.\n\t * @param outputs Output nodes.\n\t * @param conditions Conditions for each output node.\n\t * @param default_node Default output node. Chosen if no condition is true.\n\t */\n\tXorGate(node_id_t id,\n\t        node_label_t label,\n\t        const std::vector<std::shared_ptr<Node>> &outputs,\n\t        const std::vector<condition_t> &conditions,\n\t        const std::shared_ptr<Node> &default_node);\n\n\tvirtual ~XorGate() = default;\n\n\tinline node_t get_type() const override {\n\t\treturn node_t::XOR_GATE;\n\t}\n\n\t/**\n\t * Add an output node.\n\t *\n\t * @param output Output node.\n\t * @param condition_func Function that determines whether this output node is chosen.\n\t *                       This must be a valid node ID of one of the output nodes.\n\t */\n\tvoid add_output(const std::shared_ptr<Node> &output,\n\t                const condition_t condition_func);\n\n\t/**\n\t * Get the output->condition mappings.\n\t *\n\t * @return Conditions for each output node.\n\t */\n\tconst std::map<node_id_t, condition_t> &get_conditions() const;\n\n\t/**\n\t * Get the default output node.\n\t *\n\t * @return Default output node.\n\t */\n\tconst std::shared_ptr<Node> &get_default() const;\n\n\t/**\n\t * Set the the default output node.\n\t *\n\t * This node is chosen if no condition is true.\n\t *\n\t * @param node Default output node.\n\t */\n\tvoid set_default(const std::shared_ptr<Node> &node);\n\nprivate:\n\t/**\n\t * Maps output node IDs to condition functions.\n\t *\n\t * Conditions are checked in order they appear in the map.\n\t */\n\tstd::map<node_id_t, condition_t> conditions;\n\n\t/**\n\t * Default output node. Chosen if no condition is true.\n\t */\n\tstd::shared_ptr<Node> default_node;\n};\n\n} // namespace activity\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/api/CMakeLists.txt",
    "content": "add_sources(libopenage\n    ability.cpp\n    activity.cpp\n    animation.cpp\n    definitions.cpp\n    patch.cpp\n    player_setup.cpp\n    property.cpp\n    sound.cpp\n    terrain.cpp\n    types.cpp\n    util.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/api/ability.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"ability.h\"\n\n#include <deque>\n#include <memory>\n#include <string>\n#include <unordered_map>\n\n#include <nyan/nyan.h>\n\n#include \"datastructure/constexpr_map.h\"\n#include \"gamestate/api/definitions.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APIAbility::is_ability(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.ability.Ability\";\n}\n\nbool APIAbility::check_property(const nyan::Object &ability, const ability_property_t &property) {\n\tstd::shared_ptr<nyan::Dict> properties = ability.get<nyan::Dict>(\"Ability.properties\");\n\tnyan::ValueHolder property_type = ABILITY_PROPERTY_DEFS.get(property);\n\n\tif (properties->contains(property_type)) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n\nconst nyan::Object APIAbility::get_property(const nyan::Object &ability, const ability_property_t &property) {\n\tstd::shared_ptr<nyan::Dict> properties = ability.get<nyan::Dict>(\"Ability.properties\");\n\tnyan::ValueHolder property_type = ABILITY_PROPERTY_DEFS.get(property);\n\n\tstd::shared_ptr<nyan::View> db_view = ability.get_view();\n\tstd::shared_ptr<nyan::ObjectValue> property_val = std::dynamic_pointer_cast<nyan::ObjectValue>(\n\t\tproperties->get().at(property_type).get_ptr());\n\n\treturn db_view->get_object(property_val->get_name());\n}\n\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/ability.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/api/types.h\"\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for extracting values from Ability objects in the nyan API.\n */\nclass APIAbility {\npublic:\n\t/**\n\t * Check if a nyan object is an Ability (type == \\p engine.ability.Ability).\n\t *\n\t * @param obj nyan object.\n\t *\n\t * @return true if the object is an ability, else false.\n\t */\n\tstatic bool is_ability(const nyan::Object &obj);\n\n\t/**\n\t * Check if an ability has a given property.\n\t *\n\t * @param ability \\p Ability nyan object (type == \\p engine.ability.Ability).\n\t * @param property Property type.\n\t *\n\t * @return true if the ability has the property, else false.\n\t */\n\tstatic bool check_property(const nyan::Object &ability,\n\t                           const ability_property_t &property);\n\n\t/**\n\t * Get the nyan object for a property from an ability.\n\t *\n\t * @param ability \\p Ability nyan object (type == \\p engine.ability.Ability).\n\t * @param property Property type.\n\t *\n\t * @return \\p Property nyan object (type == \\p engine.ability.property.Property).\n\t */\n\tstatic const nyan::Object get_property(const nyan::Object &ability,\n\t                                       const ability_property_t &property);\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/activity.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"activity.h\"\n\n#include \"gamestate/api/definitions.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APIActivity::is_activity(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.activity.Activity\";\n}\n\nnyan::Object APIActivity::get_start(const nyan::Object &activity) {\n\tauto obj_value = activity.get<nyan::ObjectValue>(\"Activity.start\");\n\n\tstd::shared_ptr<nyan::View> db_view = activity.get_view();\n\treturn db_view->get_object(obj_value->get_name());\n}\n\n\nbool APIActivityNode::is_node(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.activity.node.Node\";\n}\n\nactivity::node_t APIActivityNode::get_type(const nyan::Object &node) {\n\tnyan::fqon_t immediate_parent = node.get_parents()[0];\n\treturn ACTIVITY_NODE_DEFS.get(immediate_parent);\n}\n\nstd::vector<nyan::Object> APIActivityNode::get_next(const nyan::Object &node) {\n\tswitch (APIActivityNode::get_type(node)) {\n\t// 0 next nodes\n\tcase activity::node_t::END: {\n\t\treturn {};\n\t}\n\t// 1 next node\n\tcase activity::node_t::TASK_SYSTEM: {\n\t\tauto next = node.get<nyan::ObjectValue>(\"Ability.next\");\n\t\tstd::shared_ptr<nyan::View> db_view = node.get_view();\n\t\treturn {db_view->get_object(next->get_name())};\n\t}\n\tcase activity::node_t::START: {\n\t\tauto next = node.get<nyan::ObjectValue>(\"Start.next\");\n\t\tstd::shared_ptr<nyan::View> db_view = node.get_view();\n\t\treturn {db_view->get_object(next->get_name())};\n\t}\n\t// 1+ next nodes\n\tcase activity::node_t::XOR_GATE: {\n\t\tauto conditions = node.get<nyan::OrderedSet>(\"XORGate.next\");\n\t\tstd::shared_ptr<nyan::View> db_view = node.get_view();\n\n\t\tstd::vector<nyan::Object> next_nodes;\n\t\tfor (auto &condition : conditions->get()) {\n\t\t\tauto condition_value = std::dynamic_pointer_cast<nyan::ObjectValue>(condition.get_ptr());\n\t\t\tauto condition_obj = db_view->get_object(condition_value->get_name());\n\n\t\t\tauto next_node_value = condition_obj.get<nyan::ObjectValue>(\"Condition.next\");\n\t\t\tnext_nodes.push_back(db_view->get_object(next_node_value->get_name()));\n\t\t}\n\n\t\tauto default_next = node.get<nyan::ObjectValue>(\"XORGate.default\");\n\t\tnext_nodes.push_back(db_view->get_object(default_next->get_name()));\n\n\t\treturn next_nodes;\n\t}\n\tcase activity::node_t::XOR_EVENT_GATE: {\n\t\tauto next = node.get<nyan::Dict>(\"XOREventGate.next\");\n\t\tstd::shared_ptr<nyan::View> db_view = node.get_view();\n\n\t\tstd::vector<nyan::Object> next_nodes;\n\t\tfor (auto &next_node : next->get()) {\n\t\t\tauto next_node_value = std::dynamic_pointer_cast<nyan::ObjectValue>(next_node.second.get_ptr());\n\t\t\tnext_nodes.push_back(db_view->get_object(next_node_value->get_name()));\n\t\t}\n\n\t\treturn next_nodes;\n\t}\n\tdefault:\n\t\tthrow Error(MSG(err) << \"Unknown activity node type.\");\n\t}\n}\n\nsystem::system_id_t APIActivityNode::get_system_id(const nyan::Object &ability_node) {\n\tauto ability = ability_node.get<nyan::ObjectValue>(\"Ability.ability\");\n\n\tif (not ACTIVITY_TASK_SYSTEM_DEFS.contains(ability->get_name())) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Ability '\" << ability->get_name() << \"' has no associated system defined.\");\n\t}\n\n\treturn ACTIVITY_TASK_SYSTEM_DEFS.get(ability->get_name());\n}\n\nbool APIActivityCondition::is_condition(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.activity.condition.Condition\";\n}\n\nactivity::condition_t APIActivityCondition::get_condition(const nyan::Object &condition) {\n\tnyan::fqon_t immediate_parent = condition.get_parents()[0];\n\treturn ACTIVITY_CONDITIONS.get(immediate_parent);\n}\n\nbool APIActivityEvent::is_event(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.activity.event.Event\";\n}\n\nactivity::event_primer_t APIActivityEvent::get_primer(const nyan::Object &event) {\n\treturn ACTIVITY_EVENT_PRIMERS.get(event.get_name());\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/activity.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/activity/types.h\"\n#include \"gamestate/activity/xor_event_gate.h\"\n#include \"gamestate/activity/xor_gate.h\"\n#include \"gamestate/system/types.h\"\n\n\nnamespace openage::gamestate {\n\nnamespace api {\n\n/**\n * Helper class for creating Activity objects from the nyan API.\n */\nclass APIActivity {\npublic:\n\t/**\n\t * Check if a nyan object is an Activity (type == \\p engine.util.activity.Activity).\n\t *\n\t * @param obj nyan object.\n\t *\n\t * @return true if the object is an activity, else false.\n\t */\n\tstatic bool is_activity(const nyan::Object &obj);\n\n\t/**\n\t * Get the start node of an activity.\n\t *\n\t * @param activity nyan object.\n\t *\n\t * @return nyan object handle of the start node.\n\t */\n\tstatic nyan::Object get_start(const nyan::Object &activity);\n};\n\n/**\n * Helper class for creating Activity node objects from the nyan API.\n */\nclass APIActivityNode {\npublic:\n\t/**\n\t * Check if a nyan object is a node (type == \\p engine.util.activity.node.Node).\n\t *\n\t * @param obj nyan object.\n\t *\n\t * @return true if the object is a node, else false.\n\t */\n\tstatic bool is_node(const nyan::Object &obj);\n\n\t/**\n\t * Get the type of a node.\n\t *\n\t * @param node nyan object.\n\t *\n\t * @return Type of the node.\n\t */\n\tstatic activity::node_t get_type(const nyan::Object &node);\n\n\t/**\n\t * Get the next nodes of a node.\n\t *\n\t * The number of next nodes depends on the type of the node and can range\n\t * from 0 (end nodes) to n (gateways).\n\t *\n\t * @param node nyan object.\n\t *\n\t * @return nyan object handles of the next nodes.\n\t */\n\tstatic std::vector<nyan::Object> get_next(const nyan::Object &node);\n\n\t/**\n\t * Get the system id of an Ability node.\n\t *\n\t * @param node nyan object.\n\t *\n\t * @return System ID of the node.\n\t */\n\tstatic system::system_id_t get_system_id(const nyan::Object &ability_node);\n};\n\n/**\n * Helper class for creating Activity condition objects from the nyan API.\n */\nclass APIActivityCondition {\npublic:\n\t/**\n\t * Check if a nyan object is a condition (type == \\p engine.util.activity.condition.Condition).\n\t *\n\t * @param obj nyan object.\n\t *\n\t * @return true if the object is a condition, else false.\n\t */\n\tstatic bool is_condition(const nyan::Object &obj);\n\n\t/**\n\t * Get the condition function for a condition.\n\t *\n\t * @param condition nyan object.\n\t *\n\t * @return Condition function.\n\t */\n\tstatic activity::condition_t get_condition(const nyan::Object &condition);\n};\n\n/**\n * Helper class for creating Activity event objects from the nyan API.\n */\nclass APIActivityEvent {\npublic:\n\t/**\n\t * Check if a nyan object is an event (type == \\p engine.util.activity.event.Event).\n\t *\n\t * @param obj nyan object.\n\t *\n\t * @return true if the object is an event, else false.\n\t */\n\tstatic bool is_event(const nyan::Object &obj);\n\n\t/**\n\t * Get the primer function for an event type.\n\t *\n\t * @param event nyan object.\n\t *\n\t * @return Event primer function.\n\t */\n\tstatic activity::event_primer_t get_primer(const nyan::Object &event);\n};\n\n\n} // namespace api\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/api/animation.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"animation.h\"\n\n#include <deque>\n\n#include \"gamestate/api/util.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APIAnimation::is_animation(nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.ability.property.Property\";\n}\n\nconst std::string APIAnimation::get_animation_path(const nyan::Object &animation) {\n\treturn resolve_file_path(animation, animation.get_file(\"Animation.sprite\"));\n}\n\nconst std::vector<std::string> APIAnimation::get_animation_paths(const std::vector<nyan::Object> &animations) {\n\tstd::vector<std::string> result;\n\n\tfor (auto &animation : animations) {\n\t\tresult.push_back(get_animation_path(animation));\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/animation.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <vector>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for API animations.\n */\nclass APIAnimation {\npublic:\n\t/**\n\t * Check if a nyan object is an animation (type == \\p engine.util.animation.Animation).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is an animation, else false.\n\t */\n\tstatic bool is_animation(nyan::Object &obj);\n\n\t/**\n\t * Get the sprite path of an animation.\n\t *\n\t * The path is relative to the directory the modpack is mounted in.\n\t *\n\t * @param animation \\p Animation nyan object (type == \\p engine.util.animation.Animation).\n\t *\n\t * @return Relative path to the animation sprite file.\n\t */\n\tstatic const std::string get_animation_path(const nyan::Object &animation);\n\n\t/**\n\t * Get the sprite paths for a collection of animations.\n\t *\n\t * Paths are relative to the directory the modpack is mounted in.\n\t *\n\t * @param animations \\p Animation nyan objects (type == \\p engine.util.animation.Animation).\n\t *\n\t * @return Relative paths to the animation sprite files.\n\t */\n\tstatic const std::vector<std::string> get_animation_paths(const std::vector<nyan::Object> &animations);\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/definitions.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"definitions.h\"\n\nnamespace openage::gamestate::api {\n\n// this file is intended to be empty\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/definitions.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <utility>\n\n#include <nyan/nyan.h>\n\n#include \"datastructure/constexpr_map.h\"\n#include \"gamestate/activity/condition/command_in_queue.h\"\n#include \"gamestate/activity/condition/next_command.h\"\n#include \"gamestate/activity/event/command_in_queue.h\"\n#include \"gamestate/activity/event/wait.h\"\n#include \"gamestate/activity/types.h\"\n#include \"gamestate/activity/xor_event_gate.h\"\n#include \"gamestate/activity/xor_gate.h\"\n#include \"gamestate/api/types.h\"\n#include \"gamestate/system/types.h\"\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Maps internal ability types to nyan API values.\n */\nstatic const auto ABILITY_DEFS = datastructure::create_const_map<ability_t, nyan::ValueHolder>(\n\tstd::pair(ability_t::IDLE,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.type.Idle\"))),\n\tstd::pair(ability_t::MOVE,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.type.Move\"))),\n\tstd::pair(ability_t::LIVE,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.type.Live\"))),\n\tstd::pair(ability_t::TURN,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.type.Turn\"))));\n\n/**\n * Maps internal property types to nyan API values.\n */\nstatic const auto ABILITY_PROPERTY_DEFS = datastructure::create_const_map<ability_property_t, nyan::ValueHolder>(\n\tstd::pair(ability_property_t::ANIMATED,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.Animated\"))),\n\tstd::pair(ability_property_t::ANIMATION_OVERRIDE,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.AnimationOverride\"))),\n\tstd::pair(ability_property_t::COMMAND_SOUND,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.CommandSound\"))),\n\tstd::pair(ability_property_t::EXECUTION_SOUND,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.ExecutionSound\"))),\n\tstd::pair(ability_property_t::DIPLOMATIC,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.Diplomatic\"))),\n\tstd::pair(ability_property_t::LOCK,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.ability.property.type.Lock\"))));\n\n/**\n * Maps API activity node types to engine node types.\n */\nstatic const auto ACTIVITY_NODE_DEFS = datastructure::create_const_map<std::string, activity::node_t>(\n\tstd::pair(\"engine.util.activity.node.type.Start\",\n              activity::node_t::START),\n\tstd::pair(\"engine.util.activity.node.type.End\",\n              activity::node_t::END),\n\tstd::pair(\"engine.util.activity.node.type.Ability\",\n              activity::node_t::TASK_SYSTEM),\n\tstd::pair(\"engine.util.activity.node.type.XORGate\",\n              activity::node_t::XOR_GATE),\n\tstd::pair(\"engine.util.activity.node.type.XOREventGate\",\n              activity::node_t::XOR_EVENT_GATE));\n\n/**\n * Maps API activity task system types to engine system types.\n *\n * TODO: Expand this to include all systems.\n */\nstatic const auto ACTIVITY_TASK_SYSTEM_DEFS = datastructure::create_const_map<std::string, system::system_id_t>(\n\tstd::pair(\"engine.ability.type.Idle\",\n              system::system_id_t::IDLE),\n\tstd::pair(\"engine.ability.type.Move\",\n              system::system_id_t::MOVE_COMMAND));\n\n/**\n * Maps API activity condition types to engine condition types.\n */\nstatic const auto ACTIVITY_CONDITIONS = datastructure::create_const_map<std::string, activity::condition_t>(\n\tstd::pair(\"engine.util.activity.condition.type.CommandInQueue\",\n              std::function(gamestate::activity::command_in_queue)),\n\tstd::pair(\"engine.util.activity.condition.type.NextCommandIdle\",\n              std::function(gamestate::activity::next_command_idle)),\n\tstd::pair(\"engine.util.activity.condition.type.NextCommandMove\",\n              std::function(gamestate::activity::next_command_move)));\n\nstatic const auto ACTIVITY_EVENT_PRIMERS = datastructure::create_const_map<std::string, activity::event_primer_t>(\n\tstd::pair(\"engine.util.activity.event.type.CommandInQueue\",\n              std::function(gamestate::activity::primer_command_in_queue)),\n\tstd::pair(\"engine.util.activity.event.type.Wait\",\n              std::function(gamestate::activity::primer_wait)),\n\tstd::pair(\"engine.util.activity.event.type.WaitAbility\",\n              std::function(gamestate::activity::primer_wait)));\n\n/**\n * Maps internal patch property types to nyan API values.\n */\nstatic const auto PATCH_PROPERTY_DEFS = datastructure::create_const_map<patch_property_t, nyan::ValueHolder>(\n\tstd::pair(patch_property_t::DIPLOMATIC,\n              nyan::ValueHolder(std::make_shared<nyan::ObjectValue>(\"engine.patch.property.type.Diplomatic\"))));\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/patch.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"patch.h\"\n\n#include <deque>\n#include <memory>\n#include <string>\n#include <unordered_map>\n\n#include <nyan/nyan.h>\n\n#include \"datastructure/constexpr_map.h\"\n#include \"gamestate/api/definitions.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APIPatch::is_patch(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.patch.Patch\";\n}\n\nbool APIPatch::check_property(const nyan::Object &patch,\n                              const patch_property_t &property) {\n\tstd::shared_ptr<nyan::Dict> properties = patch.get<nyan::Dict>(\"Patch.properties\");\n\tnyan::ValueHolder property_type = PATCH_PROPERTY_DEFS.get(property);\n\n\tif (properties->contains(property_type)) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nconst nyan::Object APIPatch::get_property(const nyan::Object &patch,\n                                          const patch_property_t &property) {\n\tstd::shared_ptr<nyan::Dict> properties = patch.get<nyan::Dict>(\"Patch.properties\");\n\tnyan::ValueHolder property_type = PATCH_PROPERTY_DEFS.get(property);\n\n\tstd::shared_ptr<nyan::View> db_view = patch.get_view();\n\tstd::shared_ptr<nyan::ObjectValue> property_val = std::dynamic_pointer_cast<nyan::ObjectValue>(\n\t\tproperties->get().at(property_type).get_ptr());\n\n\treturn db_view->get_object(property_val->get_name());\n}\n\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/patch.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/api/types.h\"\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for API patches.\n */\nclass APIPatch {\npublic:\n\t/**\n\t * Check if a nyan object is a patch (type == \\p engine.util.patch.Patch).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is a patch, else false.\n\t */\n\tstatic bool is_patch(const nyan::Object &obj);\n\n\t/**\n\t * Check if a patch has a given property.\n\t *\n\t * @param patch \\p Patch nyan object (type == \\p engine.util.patch.Patch).\n\t * @param property Property type.\n\t *\n\t * @return true if the patch has the property, else false.\n\t */\n\tstatic bool check_property(const nyan::Object &patch,\n\t                           const patch_property_t &property);\n\n\t/**\n\t * Get the nyan object for a property from a patch.\n\t *\n\t * @param patch \\p Patch nyan object (type == \\p engine.util.patch.Patch).\n\t * @param property Property type.\n\t *\n\t * @return \\p Property nyan object (type == \\p engine.util.patch.property.PatchProperty).\n\t */\n\tstatic const nyan::Object get_property(const nyan::Object &patch,\n\t                                       const patch_property_t &property);\n};\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/player_setup.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"player_setup.h\"\n\n#include <deque>\n#include <memory>\n#include <string>\n#include <unordered_set>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\nbool APIPlayerSetup::is_player_setup(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.setup.PlayerSetup\";\n}\n\nconst std::vector<nyan::Object> APIPlayerSetup::get_modifiers(const nyan::Object &player_setup) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = player_setup.get_view();\n\tauto modifiers = player_setup.get_set(\"PlayerSetup.modifiers\");\n\tfor (auto &modifier_val : modifiers) {\n\t\tauto modifier_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(modifier_val.get_ptr());\n\t\tauto modifier_obj = db_view->get_object(modifier_obj_val->get_name());\n\t\tresult.push_back(modifier_obj);\n\t}\n\n\treturn result;\n}\n\nconst std::vector<nyan::Object> APIPlayerSetup::get_start_resources(const nyan::Object &player_setup) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = player_setup.get_view();\n\tauto start_resources = player_setup.get_set(\"PlayerSetup.starting_resources\");\n\tfor (auto &resource_val : start_resources) {\n\t\tauto resource_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(resource_val.get_ptr());\n\t\tauto resource_obj = db_view->get_object(resource_obj_val->get_name());\n\t\tresult.push_back(resource_obj);\n\t}\n\n\treturn result;\n}\n\nconst std::vector<nyan::Object> APIPlayerSetup::get_patches(const nyan::Object &player_setup) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = player_setup.get_view();\n\tauto patches = player_setup.get_set(\"PlayerSetup.game_setup\");\n\tfor (auto &patch_val : patches) {\n\t\tauto patch_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(patch_val.get_ptr());\n\t\tauto patch_obj = db_view->get_object(patch_obj_val->get_name());\n\t\tresult.push_back(patch_obj);\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/player_setup.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for API player setups (== civs from AoE).\n */\nclass APIPlayerSetup {\npublic:\n\t/**\n\t * Check if a nyan object is a player setup (type == \\p engine.util.setup.PlayerSetup).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is a player setup, else false.\n\t */\n\tstatic bool is_player_setup(const nyan::Object &obj);\n\n\t/**\n\t * Get the modifiers of a player setup.\n\t *\n\t * @param player_setup nyan object (type == \\p engine.util.setup.PlayerSetup).\n\t *\n\t * @return \\p Modifier nyan objects (type == \\p engine.modifier.Modifier).\n\t */\n\tstatic const std::vector<nyan::Object> get_modifiers(const nyan::Object &player_setup);\n\n\t/**\n\t * Get the starting resources of a player setup.\n\t *\n\t * @param player_setup nyan object (type == \\p engine.util.setup.PlayerSetup).\n\t *\n\t * @return \\p ResourceAmount nyan objects (type == \\p engine.util.resource.ResourceAmount).\n\t */\n\tstatic const std::vector<nyan::Object> get_start_resources(const nyan::Object &player_setup);\n\n\t/**\n\t * Get the initial patches of a player setup.\n\t *\n\t * @param player_setup nyan object (type == \\p engine.util.setup.PlayerSetup).\n\t *\n\t * @return \\p Patch nyan objects (type == \\p engine.util.patch.Patch).\n\t */\n\tstatic const std::vector<nyan::Object> get_patches(const nyan::Object &player_setup);\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/property.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"property.h\"\n\n#include <deque>\n#include <memory>\n#include <string>\n#include <unordered_set>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\nbool APIAbilityProperty::is_property(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.ability.property.Property\";\n}\n\nconst std::vector<nyan::Object> APIAbilityProperty::get_animations(const nyan::Object &property) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = property.get_view();\n\tauto animations = property.get_set(\"Animated.animations\");\n\tfor (auto &anim_val : animations) {\n\t\tauto anim_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(anim_val.get_ptr());\n\t\tauto anim_obj = db_view->get_object(anim_obj_val->get_name());\n\t\tresult.push_back(anim_obj);\n\t}\n\n\treturn result;\n}\n\nconst std::vector<nyan::Object> APIAbilityProperty::get_command_sounds(const nyan::Object &property) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = property.get_view();\n\tauto command_sounds = property.get_set(\"CommandSound.sounds\");\n\tfor (auto &sound_val : command_sounds) {\n\t\tauto sound_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(sound_val.get_ptr());\n\t\tauto sound_obj = db_view->get_object(sound_obj_val->get_name());\n\t\tresult.push_back(sound_obj);\n\t}\n\n\treturn result;\n}\n\nconst std::vector<nyan::Object> APIAbilityProperty::get_execution_sounds(const nyan::Object &property) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = property.get_view();\n\tauto execution_sounds = property.get_set(\"ExecutionSound.sounds\");\n\tfor (auto &sound_val : execution_sounds) {\n\t\tauto sound_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(sound_val.get_ptr());\n\t\tauto sound_obj = db_view->get_object(sound_obj_val->get_name());\n\t\tresult.push_back(sound_obj);\n\t}\n\n\treturn result;\n}\n\nconst std::vector<nyan::Object> APIAbilityProperty::get_diplo_stances(const nyan::Object &property) {\n\tstd::vector<nyan::Object> result;\n\n\tauto db_view = property.get_view();\n\tauto diplo_stances = property.get_set(\"Diplomatic.stances\");\n\tfor (auto &diplo_stance_val : diplo_stances) {\n\t\tauto diplo_stance_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(diplo_stance_val.get_ptr());\n\t\tauto diplo_stance_obj = db_view->get_object(diplo_stance_obj_val->get_name());\n\t\tresult.push_back(diplo_stance_obj);\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/property.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for API properties.\n */\nclass APIAbilityProperty {\npublic:\n\t/**\n\t * Check if a nyan object is a property (type == \\p engine.ability.property.Property).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is a property, else false.\n\t */\n\tstatic bool is_property(const nyan::Object &obj);\n\n\t/**\n\t * Get the animations of an \\p Animated property (type == \\p engine.ability.property.type.Animated).\n\t *\n\t * @param property \\p Property nyan object (type == \\p engine.ability.property.Property).\n\t *\n\t * @return \\p Animation nyan objects (type == \\p engine.util.animation.Animation).\n\t */\n\tstatic const std::vector<nyan::Object> get_animations(const nyan::Object &property);\n\n\t/**\n\t * Get the sounds of a \\p CommandSound property (type == \\p engine.ability.property.type.CommandSound).\n\t *\n\t * @param property \\p Property nyan object (type == \\p engine.ability.property.Property).\n\t *\n\t * @return \\p Sound nyan objects (type == \\p engine.util.sound.Sound).\n\t */\n\tstatic const std::vector<nyan::Object> get_command_sounds(const nyan::Object &property);\n\n\t/**\n\t * Get the sounds of an \\p ExecutionSound property (type == \\p engine.ability.property.type.ExecutionSound).\n\t *\n\t * @param property \\p Property nyan object (type == \\p engine.ability.property.Property).\n\t *\n\t * @return \\p Sound nyan objects (type == \\p engine.util.sound.Sound).\n\t */\n\tstatic const std::vector<nyan::Object> get_execution_sounds(const nyan::Object &property);\n\n\t/**\n\t * Get the sounds of a \\p Diplomatic property (type == \\p engine.ability.property.type.Diplomatic).\n\t *\n\t * @param property \\p Property nyan object (type == \\p engine.ability.property.Property).\n\t *\n\t * @return \\p DiplomaticStance nyan objects (type == \\p engine.util.diplomatic_stance.DiplomaticStance).\n\t */\n\tstatic const std::vector<nyan::Object> get_diplo_stances(const nyan::Object &property);\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/sound.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"sound.h\"\n\n#include <deque>\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/api/util.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APISound::is_sound(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.sound.Sound\";\n}\n\nconst std::string APISound::get_sound_path(const nyan::Object &sound) {\n\treturn resolve_file_path(sound, sound.get_file(\"Sound.sound\"));\n}\n\nconst std::vector<std::string> APISound::get_sound_paths(const std::vector<nyan::Object> &sounds) {\n\tstd::vector<std::string> result;\n\n\tfor (auto &sound : sounds) {\n\t\tresult.push_back(get_sound_path(sound));\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/sound.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <vector>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Helper class for API sounds.\n */\nclass APISound {\npublic:\n\t/**\n\t * Check if a nyan object is a sound (type == \\p engine.util.sound.Sound).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is a sound, else false.\n\t */\n\tstatic bool is_sound(const nyan::Object &obj);\n\n\t/**\n\t * Get the sound path of a sound.\n\t *\n\t * The path is relative to the directory the modpack is mounted in.\n\t *\n\t * @param sound \\p Sound nyan object (type == \\p engine.util.sound.Sound).\n\t *\n\t * @return Relative path to the sound file.\n\t */\n\tstatic const std::string get_sound_path(const nyan::Object &sound);\n\n\t/**\n\t * Get the sound paths for a collection of sounds.\n\t *\n\t * Paths are relative to the directory the modpack is mounted in.\n\t *\n\t * @param sounds \\p Sound nyan objects (type == \\p engine.util.sound.Sound).\n\t *\n\t * @return Relative paths to the sound files.\n\t */\n\tstatic const std::vector<std::string> get_sound_paths(const std::vector<nyan::Object> &sounds);\n};\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/terrain.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"terrain.h\"\n\n#include <memory>\n\n#include \"gamestate/api/util.h\"\n\n\nnamespace openage::gamestate::api {\n\nbool APITerrain::is_terrain(const nyan::Object &obj) {\n\tnyan::fqon_t immediate_parent = obj.get_parents()[0];\n\treturn immediate_parent == \"engine.util.terrain.Terrain\";\n}\n\nconst std::string APITerrain::get_terrain_path(const nyan::Object &terrain) {\n\tnyan::Object terrain_texture_obj = terrain.get_object(\"Terrain.terrain_graphic\");\n\tstd::string terrain_path = terrain_texture_obj.get_file(\"Terrain.sprite\");\n\n\treturn resolve_file_path(terrain, terrain_path);\n}\n\nconst std::unordered_map<nyan::fqon_t, int> APITerrain::get_path_costs(const nyan::Object &terrain) {\n\tstd::unordered_map<nyan::fqon_t, int> result;\n\n\tnyan::dict_t path_costs = terrain.get_dict(\"Terrain.path_costs\");\n\tfor (const auto &pair : path_costs) {\n\t\tauto key = std::dynamic_pointer_cast<nyan::ObjectValue>(pair.first.get_ptr());\n\t\tauto value = std::dynamic_pointer_cast<nyan::Int>(pair.second.get_ptr());\n\n\t\tresult.emplace(key->get_name(), value->get());\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/terrain.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\nclass APITerrain {\npublic:\n\t/**\n\t * Check if a nyan object is a terrain (type == \\p engine.util.terrain.Terrain).\n\t *\n\t * @param obj nyan object handle.\n\t *\n\t * @return true if the object is a terrain, else false.\n\t */\n\tstatic bool is_terrain(const nyan::Object &obj);\n\n\t/**\n\t * Get the terrain path of a terrain.\n\t *\n\t * The path is relative to the directory the modpack is mounted in.\n\t *\n\t * @param terrain \\p Terrain nyan object (type == \\p engine.util.terrain.Terrain).\n\t *\n\t * @return Relative path to the terrain file.\n\t */\n\tstatic const std::string get_terrain_path(const nyan::Object &terrain);\n\n\t/**\n\t * Get the path costs of a terrain.\n\t *\n\t * @param terrain \\p Terrain nyan object (type == \\p engine.util.terrain.Terrain).\n\t *\n\t * @return Path costs for the cost fields of the pathfinder.\n\t */\n\tstatic const std::unordered_map<nyan::fqon_t, int> get_path_costs(const nyan::Object &terrain);\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\nnamespace openage::gamestate::api {\n\n// this file is intended to be empty\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/types.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Types of abilities for API objects.\n */\nenum class ability_t {\n\tIDLE,\n\tLIVE,\n\tMOVE,\n\tTURN,\n\n\t// TODO\n};\n\n/**\n * Types of properties for API abilities.\n */\nenum class ability_property_t {\n\tANIMATED,\n\tANIMATION_OVERRIDE,\n\tCOMMAND_SOUND,\n\tEXECUTION_SOUND,\n\tDIPLOMATIC,\n\tLOCK,\n};\n\n/**\n * Types of properties for API patches.\n */\nenum class patch_property_t {\n\tDIPLOMATIC,\n};\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/util.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"util.h\"\n\n#include <regex>\n#include <vector>\n\n#include <nyan/nyan.h>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::gamestate::api {\n\nconst std::string resolve_file_path(const nyan::Object &obj, const std::string &path) {\n\tif (path.empty()) {\n\t\treturn path;\n\t}\n\n\tif (path[0] == '/') {\n\t\t// absolute path\n\t\tstd::regex modpack_id_regex(\"{[A-Za-z0-9_]+@[A-Za-z0-9_]+}\");\n\t\tstd::smatch match;\n\t\tif (std::regex_match(path, match, modpack_id_regex)) {\n\t\t\tauto modpack_id = match[0].str().substr(1, match[0].str().size() - 2);\n\n\t\t\t// TODO: get mount point of modpack\n\n\t\t\tauto path_end = match.suffix().str();\n\n\t\t\treturn modpack_id + path_end;\n\t\t}\n\t\telse {\n\t\t\tthrow Error(MSG(err) << \"Absolute file path '\" << path << \"' does not contain a modpack ID.\");\n\t\t}\n\t}\n\telse {\n\t\t// relative path\n\t\tstd::string obj_path = obj.get_info().get_namespace().to_dirpath();\n\t\treturn obj_path + \"/\" + path;\n\t}\n}\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/api/util.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include <nyan/nyan.h>\n\n\nnamespace openage::gamestate::api {\n\n/**\n * Get the file path from a reference in \\p file member of a nyan object.\n *\n * File references in \\p file members can be relative (to the nyan object)\n * or absolute (relative to a modpack mount folder). See\n * https://github.com/SFTtech/openage/blob/master/doc/media/openage/file_referencing.md\n * for more info.\n *\n * @param obj nyan object where the file reference is stored.\n * @param path File reference (relative or absolute).\n *\n * @return Relative file path from the modpack root folder.\n */\nconst std::string resolve_file_path(const nyan::Object &obj, const std::string &path);\n\n} // namespace openage::gamestate::api\n"
  },
  {
    "path": "libopenage/gamestate/component/CMakeLists.txt",
    "content": "add_sources(libopenage\n    api_component.cpp\n    base_component.cpp\n    internal_component.cpp\n    types.cpp\n)\n\nadd_subdirectory(api/)\nadd_subdirectory(internal/)\n"
  },
  {
    "path": "libopenage/gamestate/component/api/CMakeLists.txt",
    "content": "add_sources(libopenage\n    idle.cpp\n    live.cpp\n    move.cpp\n    selectable.cpp\n    turn.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/component/api/idle.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"idle.h\"\n\n#include \"gamestate/component/types.h\"\n\nnamespace openage::gamestate::component {\n\ncomponent_t Idle::get_type() const {\n\treturn component_t::IDLE;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/idle.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/api_component.h\"\n#include \"gamestate/component/types.h\"\n\nnamespace openage::gamestate::component {\n\nclass Idle final : public APIComponent {\npublic:\n\tusing APIComponent::APIComponent;\n\n\tcomponent_t get_type() const override;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/live.cpp",
    "content": "// Copyright 2021-2025 the openage authors. See copying.md for legal info.\n\n#include \"live.h\"\n\n#include <optional>\n\n#include \"curve/container/iterator.h\"\n#include \"curve/container/map_filter_iterator.h\"\n#include \"curve/discrete.h\"\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\ncomponent_t Live::get_type() const {\n\treturn component_t::LIVE;\n}\n\nvoid Live::add_attribute(const time::time_t &time,\n                         const nyan::fqon_t &attribute,\n                         std::shared_ptr<curve::Discrete<int64_t>> starting_values) {\n\tthis->attribute_values.insert(time, attribute, starting_values);\n}\n\nvoid Live::set_attribute(const time::time_t &time,\n                         const nyan::fqon_t &attribute,\n                         int64_t value) {\n\tauto attribute_value = this->attribute_values.at(time, attribute);\n\n\tif (attribute_value) {\n\t\t(**attribute_value)->set_last(time, value);\n\t}\n\telse {\n\t\t// TODO: fail here\n\t}\n}\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/live.h",
    "content": "// Copyright 2021-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <memory>\n\n#include <nyan/nyan.h>\n\n#include \"curve/container/map.h\"\n#include \"gamestate/component/api_component.h\"\n#include \"gamestate/component/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate::component {\nclass Live final : public APIComponent {\npublic:\n\tusing APIComponent::APIComponent;\n\n\tcomponent_t get_type() const override;\n\n\t/**\n\t * Add a new attribute to the component attributes.\n\t *\n\t * @param time The time at which the attribute is added.\n\t * @param attribute Attribute identifier (fqon of the nyan object).\n\t * @param starting_values Attribute values at the time of addition.\n\t */\n\tvoid add_attribute(const time::time_t &time,\n\t                   const nyan::fqon_t &attribute,\n\t                   std::shared_ptr<curve::Discrete<int64_t>> starting_values);\n\n\t/**\n\t * Set the value of an attribute at a given time.\n\t *\n\t * @param time The time at which the attribute is set.\n\t * @param attribute Attribute identifier (fqon of the nyan object).\n\t * @param value New attribute value.\n\t */\n\tvoid set_attribute(const time::time_t &time,\n\t                   const nyan::fqon_t &attribute,\n\t                   int64_t value);\n\nprivate:\n\tusing attribute_storage_t = curve::UnorderedMap<nyan::fqon_t,\n\t                                                std::shared_ptr<curve::Discrete<int64_t>>>;\n\n\t/**\n\t * Map of attribute values by attribute type.\n\t */\n\tattribute_storage_t attribute_values;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/move.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"move.h\"\n\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\ncomponent_t Move::get_type() const {\n\treturn component_t::MOVE;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/move.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/api_component.h\"\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\nclass Move final : public APIComponent {\npublic:\n\tusing APIComponent::APIComponent;\n\n\tcomponent_t get_type() const override;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/selectable.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"selectable.h\"\n\n\nnamespace openage::gamestate::component {\n\ncomponent_t Selectable::get_type() const {\n\treturn component_t::SELECTABLE;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/selectable.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/component/api_component.h\"\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\nclass Selectable final : public APIComponent {\npublic:\n\tusing APIComponent::APIComponent;\n\n\tcomponent_t get_type() const override;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/turn.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"turn.h\"\n\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\ncomponent_t Turn::get_type() const {\n\treturn component_t::TURN;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api/turn.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/component/api_component.h\"\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\nclass Turn final : public APIComponent {\npublic:\n\tusing APIComponent::APIComponent;\n\n\tcomponent_t get_type() const override;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api_component.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"api_component.h\"\n\n\nnamespace openage::gamestate::component {\n\nAPIComponent::APIComponent(const std::shared_ptr<event::EventLoop> &loop,\n                           nyan::Object &ability,\n                           const time::time_t &creation_time,\n                           const bool enabled) :\n\tability{ability},\n\tenabled(loop, 0) {\n\tthis->enabled.set_insert(creation_time, enabled);\n}\n\nAPIComponent::APIComponent(const std::shared_ptr<event::EventLoop> &loop,\n                           nyan::Object &ability,\n                           bool enabled) :\n\tability{ability},\n\tenabled(loop, 0, \"\", nullptr, enabled) {\n}\n\nconst nyan::Object &APIComponent::get_ability() const {\n\treturn this->ability;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/api_component.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <nyan/nyan.h>\n\n#include \"curve/discrete.h\"\n#include \"gamestate/component/base_component.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\n}\n\nnamespace gamestate::component {\n\n/**\n * Interface for componenta that are represented by a nyan object from\n * the engine.ability namespace in the openage modding API.\n */\nclass APIComponent : public Component {\npublic:\n\t/**\n\t * Creates an APIComponent.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t * @param ability nyan ability object for the component.\n\t * @param creation_time Ingame creation time of the component.\n\t * @param enabled If true, enable the component at creation time.\n\t */\n\tAPIComponent(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t             nyan::Object &ability,\n\t             const time::time_t &creation_time,\n\t             bool enabled = true);\n\n\t/**\n\t * Creates an APIComponent.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t * @param ability nyan ability object for the component.\n\t * @param enabled If true, enable the component at creation time.\n\t */\n\tAPIComponent(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t             nyan::Object &ability,\n\t             bool enabled = true);\n\n\t/**\n\t * Get the ability object from the nyan dataset.\n\t *\n\t * @return Ability object.\n\t */\n\tconst nyan::Object &get_ability() const;\n\nprivate:\n\t/**\n\t * nyan object holding the data for the component.\n\t */\n\tnyan::Object ability;\n\n\t/**\n\t * Determines if the component is available to its game entity.\n\t */\n\tcurve::Discrete<bool> enabled;\n};\n\n} // namespace gamestate::component\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/component/base_component.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"base_component.h\"\n\nnamespace openage::gamestate::component {\n\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/base_component.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/types.h\"\n\nnamespace openage::gamestate::component {\n\n/**\n * Interface for components.\n */\nclass Component {\npublic:\n\tvirtual ~Component() = default;\n\n\t/**\n\t * Get the component type of the component.\n\t *\n\t * @return Component type of the component.\n\t */\n\tvirtual component_t get_type() const = 0;\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/CMakeLists.txt",
    "content": "add_sources(libopenage\n    activity.cpp\n    command_queue.cpp\n    ownership.cpp\n    position.cpp\n)\n\nadd_subdirectory(commands/)\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/activity.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"activity.h\"\n\n#include \"event/event.h\"\n#include \"gamestate/activity/activity.h\"\n#include \"gamestate/component/internal/activity.h\"\n\n\nnamespace openage::gamestate::component {\n\nActivity::Activity(const std::shared_ptr<openage::event::EventLoop> &loop,\n                   const std::shared_ptr<activity::Activity> &start_activity) :\n\tstart_activity{start_activity},\n\tnode{loop, 0} {\n}\n\ncomponent_t Activity::get_type() const {\n\treturn component_t::ACTIVITY;\n}\n\nconst std::shared_ptr<activity::Activity> &Activity::get_start_activity() const {\n\treturn this->start_activity;\n}\n\nconst std::shared_ptr<activity::Node> Activity::get_node(const time::time_t &time) const {\n\treturn this->node.get(time);\n}\n\nvoid Activity::set_node(const time::time_t &time,\n                        const std::shared_ptr<activity::Node> &node) {\n\tthis->node.set_last(time, node);\n}\n\nvoid Activity::init(const time::time_t &time) {\n\tthis->set_node(time, this->start_activity->get_start());\n}\n\nvoid Activity::add_event(const std::shared_ptr<event::Event> &event) {\n\tthis->scheduled_events.push_back(event);\n}\n\nvoid Activity::cancel_events(const time::time_t &time) {\n\tfor (auto &event : this->scheduled_events) {\n\t\tevent->cancel(time);\n\t}\n\tthis->scheduled_events.clear();\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/activity.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include \"curve/discrete.h\"\n#include \"gamestate/component/internal_component.h\"\n#include \"gamestate/component/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass Event;\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate {\n\nnamespace activity {\nclass Activity;\nclass Node;\n} // namespace activity\n\nnamespace component {\n\nclass Activity final : public InternalComponent {\npublic:\n\t/**\n\t * Creates a new activity component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t * @param start_activity Initial activity flow graph.\n\t */\n\tActivity(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t         const std::shared_ptr<activity::Activity> &start_activity);\n\n\tcomponent_t get_type() const override;\n\n\t/**\n\t * Get the initial activity.\n\t *\n\t * @return Initial activity.\n\t */\n\tconst std::shared_ptr<activity::Activity> &get_start_activity() const;\n\n\t/**\n\t * Get the node in the activity flow graph at a given time.\n\t *\n\t * @param time Time at which the node is requested.\n\t * @return Current node in the flow graph.\n\t */\n\tconst std::shared_ptr<activity::Node> get_node(const time::time_t &time) const;\n\n\t/**\n\t * Sets the current node in the activity flow graph at a given time.\n\t *\n\t * @param time Time at which the node is set.\n\t * @param node Current node in the flow graph.\n\t */\n\tvoid set_node(const time::time_t &time,\n\t              const std::shared_ptr<activity::Node> &node);\n\n\t/**\n\t * Set the current node to the start node of the start activity.\n\t *\n\t * @param time Time at which the node is set.\n\t */\n\tvoid init(const time::time_t &time);\n\n\t/**\n\t * Add a scheduled event that is waited for to progress in the node graph.\n\t *\n\t * @param event Event to add.\n\t */\n\tvoid add_event(const std::shared_ptr<openage::event::Event> &event);\n\n\t/**\n\t * Cancel all scheduled events.\n\t *\n\t * @param time Time at which the events are cancelled.\n\t */\n\tvoid cancel_events(const time::time_t &time);\n\nprivate:\n\t/**\n\t * Initial activity that encapsulates the entity's control flow graph.\n\t *\n\t * When a game entity is spawned, the activity system should advance in\n\t * this activity flow graph to initialize the entity's action state.\n\t *\n\t * TODO: Define as curve, so it's changeable?\n\t */\n\tstd::shared_ptr<activity::Activity> start_activity;\n\n\t/**\n\t * Current node in the activity flow graph.\n\t */\n\tcurve::Discrete<std::shared_ptr<activity::Node>> node;\n\n\t/**\n\t * Scheduled events that are waited for to progress in the node graph.\n\t */\n\tstd::vector<std::shared_ptr<openage::event::Event>> scheduled_events;\n};\n\n} // namespace component\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/command_queue.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"command_queue.h\"\n\n#include <deque>\n\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\nCommandQueue::CommandQueue(const std::shared_ptr<openage::event::EventLoop> &loop) :\n\tcommand_queue{loop, 0} {\n}\n\ninline component_t CommandQueue::get_type() const {\n\treturn component_t::COMMANDQUEUE;\n}\n\nvoid CommandQueue::add_command(const time::time_t &time,\n                               const std::shared_ptr<command::Command> &command) {\n\tthis->command_queue.insert(time, command);\n}\n\ncurve::Queue<std::shared_ptr<command::Command>> &CommandQueue::get_queue() {\n\treturn this->command_queue;\n}\n\nconst std::shared_ptr<command::Command> CommandQueue::pop_command(const time::time_t &time) {\n\treturn this->command_queue.pop_front(time);\n}\n\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/command_queue.h",
    "content": "// Copyright 2021-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"curve/container/queue.h\"\n#include \"gamestate/component/internal/commands/base_command.h\"\n#include \"gamestate/component/internal_component.h\"\n#include \"gamestate/component/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate::component {\n\nclass CommandQueue final : public InternalComponent {\npublic:\n\t/**\n\t * Creates an Ownership component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t */\n\tCommandQueue(const std::shared_ptr<openage::event::EventLoop> &loop);\n\n\tcomponent_t get_type() const override;\n\n\t/**\n\t * Adds a command to the queue.\n\t *\n\t * @param time Time at which the command is added.\n\t * @param command New command.\n\t */\n\tvoid add_command(const time::time_t &time,\n\t                 const std::shared_ptr<command::Command> &command);\n\n\t/**\n\t * Get the command queue.\n\t *\n\t * @return Command queue.\n\t */\n\tcurve::Queue<std::shared_ptr<command::Command>> &get_queue();\n\n\t/**\n\t * Get the command in the front of the queue.\n\t *\n\t * @param time Time at which the command is retrieved.\n\t *\n\t * @return Command in the front of the queue or nullptr if the queue is empty.\n\t */\n\tconst std::shared_ptr<command::Command> pop_command(const time::time_t &time);\n\nprivate:\n\t/**\n\t * Command queue.\n\t */\n\tcurve::Queue<std::shared_ptr<command::Command>> command_queue;\n};\n\n} // namespace gamestate::component\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/CMakeLists.txt",
    "content": "add_sources(libopenage\n    base_command.cpp\n    custom.cpp\n    idle.cpp\n    move.cpp\n    types.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/base_command.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"base_command.h\"\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/base_command.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/internal/commands/types.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n/**\n * Base interface for commands.\n */\nclass Command {\npublic:\n\tvirtual ~Command() = default;\n\n\t/**\n\t * Get the type of the command.\n\t *\n\t * @return Command type.\n\t */\n\tvirtual command_t get_type() const = 0;\n};\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/custom.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"custom.h\"\n\n\nnamespace openage::gamestate::component::command {\n\nCustomCommand::CustomCommand(const std::string &id) :\n\tid{id} {}\n\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/custom.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include \"gamestate/component/internal/commands/base_command.h\"\n#include \"gamestate/component/internal/commands/types.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n/**\n * Custom command for everything that is not covered by the other commands.\n */\nclass CustomCommand : public Command {\npublic:\n\t/**\n\t * Create a new custom command.\n\t *\n\t * @param id Command identifier.\n\t */\n\tCustomCommand(const std::string &id);\n\tvirtual ~CustomCommand() = default;\n\n\tinline command_t get_type() const override {\n\t\treturn command_t::CUSTOM;\n\t}\n\n\t/**\n\t * Get the command identifier.\n\t *\n\t * @return Command identifier.\n\t */\n\tconst std::string &get_id() const;\n\nprivate:\n\t/**\n\t * Command identifier.\n\t */\n\tconst std::string id;\n\n\t// TODO: Payload\n};\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/idle.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"idle.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n// this file is intended to be empty\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/idle.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/internal/commands/base_command.h\"\n#include \"gamestate/component/internal/commands/types.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n/**\n * Command for idling (TODO: rename to Stop?).\n */\nclass IdleCommand : public Command {\npublic:\n\tIdleCommand() = default;\n\tvirtual ~IdleCommand() = default;\n\n\tinline command_t get_type() const override {\n\t\treturn command_t::IDLE;\n\t}\n};\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/move.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"move.h\"\n\n\nnamespace openage::gamestate::component::command {\n\nMoveCommand::MoveCommand(const coord::phys3 &target) :\n\ttarget{target} {}\n\nconst coord::phys3 &MoveCommand::get_target() const {\n\treturn this->target;\n}\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/move.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"coord/phys.h\"\n#include \"gamestate/component/internal/commands/base_command.h\"\n#include \"gamestate/component/internal/commands/types.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n/**\n * Command for moving to a target position.\n */\nclass MoveCommand : public Command {\npublic:\n\t/**\n\t * Creates a new move command.\n\t *\n\t * @param target Target position coordinates.\n\t */\n\tMoveCommand(const coord::phys3 &target);\n\tvirtual ~MoveCommand() = default;\n\n\tinline command_t get_type() const override {\n\t\treturn command_t::MOVE;\n\t}\n\n\t/**\n\t * Get the target position.\n\t *\n\t * @return Target position coordinates.\n\t */\n\tconst coord::phys3 &get_target() const;\n\nprivate:\n\t/**\n\t * Target position.\n\t */\n\tconst coord::phys3 target;\n};\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::gamestate::component::command {\n\n// this file is intended to be empty\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/commands/types.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage::gamestate::component::command {\n\n/**\n * Command types.\n */\nenum class command_t {\n\tNONE,\n\n\tCUSTOM,\n\tIDLE,\n\tMOVE,\n};\n\n} // namespace openage::gamestate::component::command\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/ownership.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"ownership.h\"\n\n#include \"gamestate/component/types.h\"\n\n\nnamespace openage::gamestate::component {\n\nOwnership::Ownership(const std::shared_ptr<openage::event::EventLoop> &loop,\n                     const player_id_t owner_id,\n                     const time::time_t &creation_time) :\n\towner(loop, 0) {\n\tthis->owner.set_last(creation_time, owner_id);\n}\n\nOwnership::Ownership(const std::shared_ptr<openage::event::EventLoop> &loop) :\n\towner(loop, 0) {\n}\n\ninline component_t Ownership::get_type() const {\n\treturn component_t::OWNERSHIP;\n}\n\nvoid Ownership::set_owner(const time::time_t &time, const player_id_t owner_id) {\n\tthis->owner.set_last(time, owner_id);\n}\n\nconst curve::Discrete<player_id_t> &Ownership::get_owners() const {\n\treturn this->owner;\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/ownership.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <memory>\n\n#include \"curve/discrete.h\"\n#include \"gamestate/component/internal_component.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass EventLoop;\n}\n\nnamespace gamestate::component {\n\nclass Ownership final : public InternalComponent {\npublic:\n\t/**\n\t * Creates an Ownership component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t * @param owner_id Initial owner ID at creation time.\n\t * @param creation_time Ingame creation time of the component.\n\t */\n\tOwnership(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t          const player_id_t owner_id,\n\t          const time::time_t &creation_time);\n\n\t/**\n\t * Creates an Ownership component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t */\n\tOwnership(const std::shared_ptr<openage::event::EventLoop> &loop);\n\n\tcomponent_t get_type() const override;\n\n\t/**\n\t * Set the owner ID at a given time.\n\t *\n\t * @param time Time at which the owner ID is set.\n\t * @param owner_id New owner ID.\n\t */\n\tvoid set_owner(const time::time_t &time, const player_id_t owner_id);\n\n\t/**\n\t * Get the owner IDs over time.\n\t *\n\t * @return Owner ID curve.\n\t */\n\tconst curve::Discrete<player_id_t> &get_owners() const;\n\nprivate:\n\t/**\n\t * Owner ID storage over time.\n\t */\n\tcurve::Discrete<player_id_t> owner;\n};\n\n} // namespace gamestate::component\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/position.cpp",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#include \"position.h\"\n\n#include \"gamestate/component/types.h\"\n#include \"gamestate/definitions.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::gamestate::component {\nPosition::Position(const std::shared_ptr<openage::event::EventLoop> &loop,\n                   const coord::phys3 &initial_pos,\n                   const time::time_t &creation_time) :\n\tposition(loop, 0, \"\", nullptr, WORLD_ORIGIN),\n\tangle(loop, 0) {\n\tthis->position.set_insert(creation_time, initial_pos);\n\n\t// TODO: testing values\n\tthis->position.set_insert(creation_time + 1, initial_pos + coord::phys3_delta{0, 1, 0});\n\tthis->position.set_insert(creation_time + 2, initial_pos + coord::phys3_delta{1, 2, 0});\n\tthis->position.set_insert(creation_time + 3, initial_pos + coord::phys3_delta{2, 2, 0});\n\tthis->position.set_insert(creation_time + 4, initial_pos + coord::phys3_delta{3, 1, 0});\n\tthis->position.set_insert(creation_time + 5, initial_pos + coord::phys3_delta{3, 0, 0});\n\tthis->position.set_insert(creation_time + 6, initial_pos + coord::phys3_delta{2, -1, 0});\n\tthis->position.set_insert(creation_time + 7, initial_pos + coord::phys3_delta{1, -1, 0});\n\tthis->position.set_insert(creation_time + 8, initial_pos);\n\n\tthis->angle.set_insert(creation_time, coord::phys_angle_t::from_int(315));\n\tthis->angle.set_insert_jump(creation_time + 1, coord::phys_angle_t::from_int(315), coord::phys_angle_t::from_int(270));\n\tthis->angle.set_insert_jump(creation_time + 2, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(225));\n\tthis->angle.set_insert_jump(creation_time + 3, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(180));\n\tthis->angle.set_insert_jump(creation_time + 4, coord::phys_angle_t::from_int(180), coord::phys_angle_t::from_int(135));\n\tthis->angle.set_insert_jump(creation_time + 5, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(90));\n\tthis->angle.set_insert_jump(creation_time + 6, coord::phys_angle_t::from_int(90), coord::phys_angle_t::from_int(45));\n\tthis->angle.set_insert_jump(creation_time + 7, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(0));\n\tthis->angle.set_insert_jump(creation_time + 8, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(315));\n}\n\n\nPosition::Position(const std::shared_ptr<openage::event::EventLoop> &loop) :\n\tposition(loop, 0, \"\", nullptr, WORLD_ORIGIN),\n\tangle(loop, 0) {\n}\n\ninline component_t Position::get_type() const {\n\treturn component_t::POSITION;\n}\n\nconst curve::Continuous<coord::phys3> &Position::get_positions() const {\n\treturn this->position;\n}\n\nvoid Position::set_position(const time::time_t &time, const coord::phys3 &pos) {\n\tthis->position.set_last(time, pos);\n}\n\nconst curve::Segmented<coord::phys_angle_t> &Position::get_angles() const {\n\treturn this->angle;\n}\n\nvoid Position::set_angle(const time::time_t &time, const coord::phys_angle_t &angle) {\n\tauto old_angle = this->angle.get(time);\n\tthis->angle.set_last_jump(time, old_angle, angle);\n}\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal/position.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <list>\n#include <memory>\n\n#include \"coord/phys.h\"\n#include \"curve/continuous.h\"\n#include \"curve/segmented.h\"\n#include \"gamestate/component/internal_component.h\"\n#include \"gamestate/component/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass EventLoop;\n}\n\nnamespace gamestate::component {\n\nclass Position final : public InternalComponent {\npublic:\n\t/**\n\t * Create a Position component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t * @param initial_pos Initial position at creation time.\n\t * @param creation_time Ingame creation time of the component.\n\t */\n\tPosition(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t         const coord::phys3 &initial_pos,\n\t         const time::time_t &creation_time);\n\n\t/**\n\t * Create a Position component.\n\t *\n\t * @param loop Event loop that all events from the component are registered on.\n\t */\n\tPosition(const std::shared_ptr<openage::event::EventLoop> &loop);\n\n\tcomponent_t get_type() const override;\n\n\t/**\n\t * Get the positions in the world coordinate system over time.\n\t *\n\t * @return Position curve.\n\t */\n\tconst curve::Continuous<coord::phys3> &get_positions() const;\n\n\t/**\n\t * Set the position at a given time.\n\t *\n\t * This adds a new keyframe to the position curve.\n\t *\n\t * @param time Time at which the position is set.\n\t * @param pos New position.\n\t */\n\tvoid set_position(const time::time_t &time, const coord::phys3 &pos);\n\n\t/**\n\t * Get the directions in degrees over time.\n\t *\n\t * @return Direction curve.\n\t */\n\tconst curve::Segmented<coord::phys_angle_t> &get_angles() const;\n\n\t/**\n\t * Set the angle at a given time.\n\t *\n\t * This adds a new keyframe to the angle curve.\n\t *\n\t * @param time Time at which the angle is set.\n\t * @param angle New angle.\n\t */\n\tvoid set_angle(const time::time_t &time, const coord::phys_angle_t &angle);\n\nprivate:\n\t/**\n\t * Position storage over time.\n\t */\n\tcurve::Continuous<coord::phys3> position;\n\n\t/**\n\t * Angle the entity is facing over time.\n\t *\n\t * Represents degrees in the range [0, 360). At angle 0, the entity is facing\n\t * towards the camera (direction vector {x, y} = {-1, 1}).\n\t *\n\t * Rotation is clockwise, so at 90 degrees the entity is facing left.\n\t */\n\tcurve::Segmented<coord::phys_angle_t> angle;\n};\n\n} // namespace gamestate::component\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/component/internal_component.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"internal_component.h\"\n\nnamespace openage::gamestate::component {\n\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/internal_component.h",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"gamestate/component/base_component.h\"\n\nnamespace openage::gamestate::component {\n\n/**\n * Interface for components that track ingame information about\n * a game entity, but don't use information from the nyan API.\n */\nclass InternalComponent : public Component {};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/types.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\nnamespace openage::gamestate::component {\n\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/component/types.h",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage::gamestate::component {\n\n/**\n * Types of components.\n */\nenum class component_t {\n\t// Internal\n\tPOSITION,\n\tCOMMANDQUEUE,\n\tOWNERSHIP,\n\tACTIVITY,\n\n\t// API\n\tIDLE,\n\tTURN,\n\tMOVE,\n\tSELECTABLE,\n\tLIVE\n};\n\n} // namespace openage::gamestate::component\n"
  },
  {
    "path": "libopenage/gamestate/definitions.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"definitions.h\"\n\nnamespace openage::gamestate {\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/definitions.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"coord/phys.h\"\n\n/**\n * Hardcoded definitions for parameters used in the gamestate.\n *\n * May be moved to configuration files in the future.\n */\nnamespace openage::gamestate {\n\n/**\n * Origin point of the game world.\n */\nconstexpr coord::phys3 WORLD_ORIGIN = coord::phys3{0, 0, 0};\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/demo/CMakeLists.txt",
    "content": "add_sources(libopenage\n    demo_0.cpp\n\ttests.cpp\n)\n\npxdgen(\n\ttests.h\n)\n"
  },
  {
    "path": "libopenage/gamestate/demo/demo_0.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"demo_0.h\"\n\n#include \"cvar/cvar.h\"\n#include \"gamestate/simulation.h\"\n#include \"time/time_loop.h\"\n\n\nnamespace openage::gamestate::tests {\n\nvoid simulation_demo_0(const util::Path &path) {\n\tauto cvar = std::make_shared<cvar::CVarManager>(path);\n\tauto time_loop = std::make_shared<time::TimeLoop>();\n\tauto simulation = std::make_shared<GameSimulation>(path, cvar, time_loop);\n\n\tsimulation->set_modpacks({\"engine\"});\n\n\tsimulation->start();\n}\n\n} // namespace openage::gamestate::tests\n"
  },
  {
    "path": "libopenage/gamestate/demo/demo_0.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\n\nnamespace openage::gamestate::tests {\n\n/**\n * Show the initialization of a game instance.\n */\nvoid simulation_demo_0(const util::Path &path);\n\n} // namespace openage::gamestate::tests\n"
  },
  {
    "path": "libopenage/gamestate/demo/tests.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"tests.h\"\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/demo/demo_0.h\"\n\n\nnamespace openage::gamestate::tests {\n\nvoid simulation_demo(int demo_id, const util::Path &path) {\n\tswitch (demo_id) {\n\tcase 0:\n\t\tsimulation_demo_0(path);\n\t\tbreak;\n\n\tdefault:\n\t\tlog::log(MSG(err) << \"Unknown renderer demo requested: \" << demo_id << \".\");\n\t\tbreak;\n\t}\n}\n\n} // namespace openage::gamestate::tests\n"
  },
  {
    "path": "libopenage/gamestate/demo/tests.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../../util/compiler.h\"\n// pxd: from libopenage.util.path cimport Path\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n} // namespace util\n\nnamespace gamestate::tests {\n\n// pxd: void simulation_demo(int demo_id, Path path) except +\nOAAPI void simulation_demo(int demo_id, const util::Path &path);\n\n} // namespace gamestate::tests\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/entity_factory.cpp",
    "content": "// Copyright 2023-2025 the openage authors. See copying.md for legal info.\n\n#include \"entity_factory.h\"\n\n#include <cstdint>\n#include <deque>\n#include <mutex>\n#include <string>\n#include <unordered_set>\n\n#include \"error/error.h\"\n\n#include \"curve/container/queue.h\"\n#include \"curve/discrete.h\"\n#include \"event/event_loop.h\"\n#include \"gamestate/activity/activity.h\"\n#include \"gamestate/activity/condition/command_in_queue.h\"\n#include \"gamestate/activity/end_node.h\"\n#include \"gamestate/activity/event/command_in_queue.h\"\n#include \"gamestate/activity/event/wait.h\"\n#include \"gamestate/activity/start_node.h\"\n#include \"gamestate/activity/task_system_node.h\"\n#include \"gamestate/activity/xor_event_gate.h\"\n#include \"gamestate/activity/xor_gate.h\"\n#include \"gamestate/api/activity.h\"\n#include \"gamestate/component/api/idle.h\"\n#include \"gamestate/component/api/live.h\"\n#include \"gamestate/component/api/move.h\"\n#include \"gamestate/component/api/selectable.h\"\n#include \"gamestate/component/api/turn.h\"\n#include \"gamestate/component/internal/activity.h\"\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/component/internal/ownership.h\"\n#include \"gamestate/component/internal/position.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/manager.h\"\n#include \"gamestate/player.h\"\n#include \"gamestate/system/types.h\"\n#include \"log/message.h\"\n#include \"renderer/render_factory.h\"\n#include \"time/time.h\"\n#include \"util/fixed_point.h\"\n\nnamespace openage::gamestate {\n\n/**\n * Create a simple test activity for the game entity.\n *\n * The activity is as follows:\n *                      |------------------------------------------------------|\n *                      |                                                      v\n * Start -> Idle -> Condition -> Condition -> Wait for command -> Move -> Wait for move -> End\n *            ^                                                                |\n *            |----------------------------------------------------------------|\n *\n * TODO: Replace with config\n */\nstd::shared_ptr<activity::Activity> create_test_activity() {\n\tauto start = std::make_shared<activity::StartNode>(0);\n\tauto idle = std::make_shared<activity::TaskSystemNode>(1, \"Idle\");\n\tauto condition_moveable = std::make_shared<activity::XorGate>(2);\n\tauto condition_command = std::make_shared<activity::XorGate>(3);\n\tauto wait_for_command = std::make_shared<activity::XorEventGate>(4);\n\tauto move = std::make_shared<activity::TaskSystemNode>(5, \"Move\");\n\tauto wait_for_move = std::make_shared<activity::XorEventGate>(6);\n\tauto end = std::make_shared<activity::EndNode>(7);\n\n\tstart->add_output(idle);\n\n\t// idle after start\n\tidle->add_output(condition_moveable);\n\tidle->set_system_id(system::system_id_t::IDLE);\n\n\t// branch 1: check if the entity is moveable\n\tactivity::condition_t command_branch = [&](const time::time_t & /* time */,\n\t                                           const std::shared_ptr<gamestate::GameEntity> &entity) {\n\t\treturn entity->has_component(component::component_t::MOVE);\n\t};\n\tcondition_moveable->add_output(condition_command, command_branch);\n\n\t// default: if it's not moveable, go straight to the end\n\tcondition_moveable->set_default(end);\n\n\t// branch 1: check if there is already a command in the queue\n\tcondition_command->add_output(move, gamestate::activity::command_in_queue);\n\n\t// default: if there is no command, wait for a command\n\tcondition_command->set_default(wait_for_command);\n\n\t// wait for a command event\n\twait_for_command->add_output(move, gamestate::activity::primer_command_in_queue);\n\n\t// move\n\tmove->add_output(wait_for_move);\n\tmove->set_system_id(system::system_id_t::MOVE_COMMAND);\n\n\t// branch 1: wait for move event to finish\n\twait_for_move->add_output(idle, gamestate::activity::primer_wait);\n\n\t// branch 2: wait for a new command event\n\twait_for_move->add_output(move, gamestate::activity::primer_command_in_queue);\n\n\treturn std::make_shared<activity::Activity>(0, start, \"test\");\n}\n\nEntityFactory::EntityFactory() :\n\tnext_entity_id{0},\n\tnext_player_id{0},\n\trender_factory{nullptr} {\n}\n\nstd::shared_ptr<GameEntity> EntityFactory::add_game_entity(const std::shared_ptr<openage::event::EventLoop> &loop,\n                                                           const std::shared_ptr<GameState> &state,\n                                                           player_id_t owner_id,\n                                                           const nyan::fqon_t &nyan_entity) {\n\tauto entity = std::make_shared<GameEntity>(this->get_next_entity_id());\n\tentity->set_manager(std::make_shared<GameEntityManager>(loop, state, entity));\n\n\t// use the owner's data to initialize the entity\n\t// this ensures that only the owner's tech upgrades apply\n\tauto db_view = state->get_player(owner_id)->get_db_view();\n\tinit_components(loop, db_view, entity, nyan_entity);\n\n\tif (this->render_factory) {\n\t\tentity->set_render_entity(this->render_factory->add_world_render_entity());\n\t}\n\n\treturn entity;\n}\n\nstd::shared_ptr<Player> EntityFactory::add_player(const std::shared_ptr<openage::event::EventLoop> & /* loop */,\n                                                  const std::shared_ptr<GameState> &state,\n                                                  const nyan::fqon_t & /* player_setup */) {\n\tauto player = std::make_shared<Player>(this->get_next_player_id(),\n\t                                       state->get_db_view()->new_child());\n\n\t// TODO: Components (for resources, diplomacy, etc.)\n\n\treturn player;\n}\n\nvoid EntityFactory::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->render_factory = render_factory;\n}\n\nvoid EntityFactory::init_components(const std::shared_ptr<openage::event::EventLoop> &loop,\n                                    const std::shared_ptr<nyan::View> &owner_db_view,\n                                    const std::shared_ptr<GameEntity> &entity,\n                                    const nyan::fqon_t &nyan_entity) {\n\tauto position = std::make_shared<component::Position>(loop);\n\tentity->add_component(position);\n\n\tauto ownership = std::make_shared<component::Ownership>(loop);\n\tentity->add_component(ownership);\n\n\tauto command_queue = std::make_shared<component::CommandQueue>(loop);\n\tentity->add_component(command_queue);\n\n\tauto nyan_obj = owner_db_view->get_object(nyan_entity);\n\tnyan::set_t abilities = nyan_obj.get_set(\"GameEntity.abilities\");\n\n\tstd::optional<nyan::Object> activity_ability;\n\tfor (const auto &ability_val : abilities) {\n\t\tauto ability_fqon = std::dynamic_pointer_cast<nyan::ObjectValue>(ability_val.get_ptr())->get_name();\n\t\tauto ability_obj = owner_db_view->get_object(ability_fqon);\n\n\t\tauto ability_parent = ability_obj.get_parents()[0];\n\t\tif (ability_parent == \"engine.ability.type.Move\") {\n\t\t\tauto move = std::make_shared<component::Move>(loop, ability_obj);\n\t\t\tentity->add_component(move);\n\t\t}\n\t\telse if (ability_parent == \"engine.ability.type.Turn\") {\n\t\t\tauto turn = std::make_shared<component::Turn>(loop, ability_obj);\n\t\t\tentity->add_component(turn);\n\t\t}\n\t\telse if (ability_parent == \"engine.ability.type.Idle\") {\n\t\t\tauto idle = std::make_shared<component::Idle>(loop, ability_obj);\n\t\t\tentity->add_component(idle);\n\t\t}\n\t\telse if (ability_parent == \"engine.ability.type.Live\") {\n\t\t\tauto live = std::make_shared<component::Live>(loop, ability_obj);\n\t\t\tentity->add_component(live);\n\n\t\t\tauto attr_settings = ability_obj.get_set(\"Live.attributes\");\n\t\t\tfor (auto &setting : attr_settings) {\n\t\t\t\tauto setting_obj_val = std::dynamic_pointer_cast<nyan::ObjectValue>(setting.get_ptr());\n\t\t\t\tauto setting_obj = owner_db_view->get_object(setting_obj_val->get_name());\n\t\t\t\tauto attribute = setting_obj.get_object(\"AttributeSetting.attribute\");\n\t\t\t\tauto start_value = setting_obj.get_int(\"AttributeSetting.starting_value\");\n\n\t\t\t\tlive->add_attribute(time::TIME_MIN,\n\t\t\t\t                    attribute.get_name(),\n\t\t\t\t                    std::make_shared<curve::Discrete<int64_t>>(loop,\n\t\t\t\t                                                               0,\n\t\t\t\t                                                               \"\",\n\t\t\t\t                                                               nullptr,\n\t\t\t\t                                                               start_value));\n\t\t\t}\n\t\t}\n\t\telse if (ability_parent == \"engine.ability.type.Activity\") {\n\t\t\tactivity_ability = ability_obj;\n\t\t}\n\t\telse if (ability_parent == \"engine.ability.type.Selectable\") {\n\t\t\tauto selectable = std::make_shared<component::Selectable>(loop, ability_obj);\n\t\t\tentity->add_component(selectable);\n\t\t}\n\t}\n\n\tif (activity_ability) {\n\t\tinit_activity(loop, owner_db_view, entity, activity_ability.value());\n\t}\n\telse {\n\t\tauto activity = std::make_shared<component::Activity>(loop, create_test_activity());\n\t\tentity->add_component(activity);\n\t}\n}\n\nvoid EntityFactory::init_activity(const std::shared_ptr<openage::event::EventLoop> &loop,\n                                  const std::shared_ptr<nyan::View> &owner_db_view,\n                                  const std::shared_ptr<GameEntity> &entity,\n                                  const nyan::Object &ability) {\n\tnyan::Object graph = ability.get_object(\"Activity.graph\");\n\n\t// Check if the activity is already exists in the cache\n\tif (this->activity_cache.contains(graph.get_name())) {\n\t\tauto activity = this->activity_cache.at(graph.get_name());\n\t\tauto component = std::make_shared<component::Activity>(loop, activity);\n\t\tentity->add_component(component);\n\n\t\treturn;\n\t}\n\n\tauto start_obj = api::APIActivity::get_start(graph);\n\n\tsize_t node_id = 0;\n\n\tstd::deque<nyan::Object> nyan_nodes;\n\tstd::unordered_map<size_t, std::shared_ptr<activity::Node>> node_id_map{};\n\tstd::unordered_map<nyan::fqon_t, size_t> visited{};\n\tstd::shared_ptr<activity::Node> start_node;\n\n\t// First pass: create all nodes using breadth-first search\n\tnyan_nodes.push_back(start_obj);\n\twhile (!nyan_nodes.empty()) {\n\t\tauto node = nyan_nodes.front();\n\t\tnyan_nodes.pop_front();\n\n\t\tif (visited.contains(node.get_name())) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Create the node\n\t\tswitch (api::APIActivityNode::get_type(node)) {\n\t\tcase activity::node_t::END:\n\t\t\tbreak;\n\t\tcase activity::node_t::START:\n\t\t\tstart_node = std::make_shared<activity::StartNode>(node_id);\n\t\t\tnode_id_map[node_id] = start_node;\n\t\t\tbreak;\n\t\tcase activity::node_t::TASK_SYSTEM: {\n\t\t\tauto task_node = std::make_shared<activity::TaskSystemNode>(node_id);\n\t\t\ttask_node->set_system_id(api::APIActivityNode::get_system_id(node));\n\t\t\tnode_id_map[node_id] = task_node;\n\t\t\tbreak;\n\t\t}\n\t\tcase activity::node_t::XOR_GATE:\n\t\t\tnode_id_map[node_id] = std::make_shared<activity::XorGate>(node_id);\n\t\t\tbreak;\n\t\tcase activity::node_t::XOR_EVENT_GATE:\n\t\t\tnode_id_map[node_id] = std::make_shared<activity::XorEventGate>(node_id);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow Error{ERR << \"Unknown activity node type of node: \" << node.get_name()};\n\t\t}\n\n\t\t// Get the node's outputs\n\t\tauto next_nodes = api::APIActivityNode::get_next(node);\n\t\tnyan_nodes.insert(nyan_nodes.end(), next_nodes.begin(), next_nodes.end());\n\n\t\tvisited.insert({node.get_name(), node_id});\n\t\tnode_id++;\n\t}\n\n\t// Second pass: connect the nodes\n\tfor (const auto &current_node : visited) {\n\t\tauto nyan_node = owner_db_view->get_object(current_node.first);\n\t\tauto activity_node = node_id_map[current_node.second];\n\n\t\tswitch (activity_node->get_type()) {\n\t\tcase activity::node_t::END:\n\t\t\tbreak;\n\t\tcase activity::node_t::START: {\n\t\t\tauto start = std::static_pointer_cast<activity::StartNode>(activity_node);\n\t\t\tauto output_fqon = nyan_node.get<nyan::ObjectValue>(\"Start.next\")->get_name();\n\t\t\tauto output_id = visited[output_fqon];\n\t\t\tauto output_node = node_id_map[output_id];\n\t\t\tstart->add_output(output_node);\n\t\t\tbreak;\n\t\t}\n\t\tcase activity::node_t::TASK_SYSTEM: {\n\t\t\tauto task_system = std::static_pointer_cast<activity::TaskSystemNode>(activity_node);\n\t\t\tauto output_fqon = nyan_node.get<nyan::ObjectValue>(\"Ability.next\")->get_name();\n\t\t\tauto output_id = visited[output_fqon];\n\t\t\tauto output_node = node_id_map[output_id];\n\t\t\ttask_system->add_output(output_node);\n\t\t\tbreak;\n\t\t}\n\t\tcase activity::node_t::XOR_GATE: {\n\t\t\tauto xor_gate = std::static_pointer_cast<activity::XorGate>(activity_node);\n\t\t\tauto conditions = nyan_node.get<nyan::OrderedSet>(\"XORGate.next\");\n\t\t\tfor (auto &condition : conditions->get()) {\n\t\t\t\tauto condition_value = std::dynamic_pointer_cast<nyan::ObjectValue>(condition.get_ptr());\n\t\t\t\tauto condition_obj = owner_db_view->get_object(condition_value->get_name());\n\n\t\t\t\tauto output_value = condition_obj.get<nyan::ObjectValue>(\"Condition.next\")->get_name();\n\t\t\t\tauto output_id = visited[output_value];\n\t\t\t\tauto output_node = node_id_map[output_id];\n\n\t\t\t\txor_gate->add_output(output_node, api::APIActivityCondition::get_condition(condition_obj));\n\t\t\t}\n\n\t\t\tauto default_fqon = nyan_node.get<nyan::ObjectValue>(\"XORGate.default\")->get_name();\n\t\t\tauto default_id = visited[default_fqon];\n\t\t\tauto default_node = node_id_map[default_id];\n\t\t\txor_gate->set_default(default_node);\n\t\t\tbreak;\n\t\t}\n\t\tcase activity::node_t::XOR_EVENT_GATE: {\n\t\t\tauto xor_event_gate = std::static_pointer_cast<activity::XorEventGate>(activity_node);\n\t\t\tauto next = nyan_node.get<nyan::Dict>(\"XOREventGate.next\");\n\t\t\tfor (auto &next_node : next->get()) {\n\t\t\t\tauto event_value = std::dynamic_pointer_cast<nyan::ObjectValue>(next_node.first.get_ptr());\n\t\t\t\tauto event_obj = owner_db_view->get_object(event_value->get_name());\n\n\t\t\t\tauto next_node_value = std::dynamic_pointer_cast<nyan::ObjectValue>(next_node.second.get_ptr());\n\t\t\t\tauto next_node_obj = owner_db_view->get_object(next_node_value->get_name());\n\n\t\t\t\tauto output_id = visited[next_node_obj.get_name()];\n\t\t\t\tauto output_node = node_id_map[output_id];\n\n\t\t\t\txor_event_gate->add_output(output_node, api::APIActivityEvent::get_primer(event_obj));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tthrow Error{ERR << \"Unknown activity node type of node: \" << current_node.first};\n\t\t}\n\t}\n\n\tauto activity = std::make_shared<activity::Activity>(0, start_node, graph.get_name());\n\tthis->activity_cache.insert({graph.get_name(), activity});\n\n\tauto component = std::make_shared<component::Activity>(loop, activity);\n\tentity->add_component(component);\n}\n\nentity_id_t EntityFactory::get_next_entity_id() {\n\tauto new_id = this->next_entity_id;\n\tthis->next_entity_id++;\n\n\treturn new_id;\n}\n\nplayer_id_t EntityFactory::get_next_player_id() {\n\tauto new_id = this->next_player_id;\n\tthis->next_player_id++;\n\n\treturn new_id;\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/entity_factory.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/types.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\n} // namespace event\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace gamestate {\n\nnamespace activity {\nclass Activity;\n} // namespace activity\n\nclass GameEntity;\nclass GameState;\nclass Player;\n\n/**\n * Creates game entities that contain data of objects inside the game world.\n */\nclass EntityFactory {\npublic:\n\t/**\n\t * Create a new entity factory for game entities.\n\t */\n\tEntityFactory();\n\t~EntityFactory() = default;\n\n\t/**\n\t * Create a new game entity.\n\t *\n\t * This just creates the entity. The caller is responsible for initializing\n\t * its components and placing it into the game.\n\t *\n\t * @param loop Event loop for the gamestate.\n\t * @param state State of the game.\n\t * @param owner_id ID of the player owning the entity.\n\t * @param nyan_entity fqon of the GameEntity data in the nyan database.\n\t *\n\t * @return New game entity.\n\t */\n\tstd::shared_ptr<GameEntity> add_game_entity(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                                            const std::shared_ptr<GameState> &state,\n\t                                            player_id_t owner_id,\n\t                                            const nyan::fqon_t &nyan_entity);\n\n\t/**\n\t * Create a new player.\n\t *\n\t * This just creates the player. The caller is responsible for initializing\n\t * its components and placing it into the game.\n\t *\n\t * @param loop Event loop for the gamestate.\n\t * @param state State of the game.\n\t * @param player_setup fqon of the player setup in the nyan database.\n\t *\n\t * @return New player.\n\t */\n\tstd::shared_ptr<Player> add_player(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                                   const std::shared_ptr<GameState> &state,\n\t                                   const nyan::fqon_t &player_setup);\n\n\t/**\n\t * Attach a renderer which enables graphical display options for all ingame entities.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * Initialize components of a game entity.\n\t *\n\t * @param loop Event loop for the gamestate.\n\t * @param owner_db_view View of the nyan database of the player owning the entity.\n\t * @param entity Game entity.\n\t * @param nyan_entity fqon of the GameEntity data in the nyan database.\n\t */\n\tvoid init_components(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                     const std::shared_ptr<nyan::View> &owner_db_view,\n\t                     const std::shared_ptr<GameEntity> &entity,\n\t                     const nyan::fqon_t &nyan_entity);\n\n\tvoid init_activity(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                   const std::shared_ptr<nyan::View> &owner_db_view,\n\t                   const std::shared_ptr<GameEntity> &entity,\n\t                   const nyan::Object &ability);\n\n\t/**\n\t * Get a unique ID for creating a game entity.\n\t *\n\t * @return Unique ID for a game entity.\n\t */\n\tentity_id_t get_next_entity_id();\n\n\t/**\n\t * Get a unique ID for creating a player.\n\t *\n\t * @return Unique ID for a player.\n\t */\n\tplayer_id_t get_next_player_id();\n\n\t/**\n\t * ID of the next game entity to be created.\n\t */\n\tentity_id_t next_entity_id;\n\n\t/**\n\t * ID of the next player to be created.\n\t */\n\tplayer_id_t next_player_id;\n\n\t/**\n\t * Factory for creating connector objects to the renderer which make game entities displayable.\n\t */\n\tstd::shared_ptr<renderer::RenderFactory> render_factory;\n\n\t// TODO: Cache created game entities.\n\n\t/**\n\t * Cache for activities.\n\t */\n\tstd::unordered_map<nyan::fqon_t, std::shared_ptr<activity::Activity>> activity_cache;\n\n\t/**\n\t * Mutex for thread safety.\n\t */\n\tstd::shared_mutex mutex;\n};\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/event/CMakeLists.txt",
    "content": "add_sources(libopenage\n    drag_select.cpp\n    process_command.cpp\n    send_command.cpp\n    spawn_entity.cpp\n    wait.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/event/drag_select.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"drag_select.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/phys.h\"\n#include \"coord/pixel.h\"\n#include \"coord/scene.h\"\n#include \"curve/discrete.h\"\n#include \"gamestate/component/internal/ownership.h\"\n#include \"gamestate/component/internal/position.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/types.h\"\n\n\nnamespace openage::gamestate::event {\n\nDragSelectHandler::DragSelectHandler() :\n\tOnceEventHandler{\"game.drag_select\"} {}\n\nvoid DragSelectHandler::setup_event(const std::shared_ptr<openage::event::Event> & /* event */,\n                                    const std::shared_ptr<openage::event::State> & /* state */) {\n\t// TODO\n}\n\nvoid DragSelectHandler::invoke(openage::event::EventLoop & /* loop */,\n                               const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                               const std::shared_ptr<openage::event::State> &state,\n                               const time::time_t &time,\n                               const param_map &params) {\n\tauto gstate = std::dynamic_pointer_cast<openage::gamestate::GameState>(state);\n\n\tsize_t controlled_id = params.get(\"controlled\", 0);\n\n\tEigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();\n\tEigen::Matrix4f cam_matrix = params.get(\"camera_matrix\", id_matrix);\n\tEigen::Vector2f drag_start = params.get(\"drag_start\", Eigen::Vector2f{0, 0});\n\tEigen::Vector2f drag_end = params.get(\"drag_end\", Eigen::Vector2f{0, 0});\n\n\t// Boundaries of the rectangle\n\tfloat top = std::max(drag_start.y(), drag_end.y());\n\tfloat bottom = std::min(drag_start.y(), drag_end.y());\n\tfloat left = std::min(drag_start.x(), drag_end.x());\n\tfloat right = std::max(drag_start.x(), drag_end.x());\n\n\tlog::log(SPAM << \"Drag select rectangle (NDC):\");\n\tlog::log(SPAM << \"\\tTop: \" << top);\n\tlog::log(SPAM << \"\\tBottom: \" << bottom);\n\tlog::log(SPAM << \"\\tLeft: \" << left);\n\tlog::log(SPAM << \"\\tRight: \" << right);\n\n\tstd::vector<entity_id_t> selected;\n\tfor (auto &entity : gstate->get_game_entities()) {\n\t\tif (not entity.second->has_component(component::component_t::SELECTABLE)) {\n\t\t\t// skip entities that are not selectable\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if the entity is owned by the controlled player\n\t\t// TODO: Check this using Selectable diplomatic property\n\t\tauto owner = std::dynamic_pointer_cast<component::Ownership>(\n\t\t\tentity.second->get_component(component::component_t::OWNERSHIP));\n\t\tif (owner->get_owners().get(time) != controlled_id) {\n\t\t\t// only select entities of the controlled player\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Get the position of the entity in the viewport\n\t\tauto pos = std::dynamic_pointer_cast<component::Position>(\n\t\t\tentity.second->get_component(component::component_t::POSITION));\n\t\tauto current_pos = pos->get_positions().get(time);\n\t\tauto world_pos = current_pos.to_scene3().to_world_space();\n\t\tEigen::Vector4f clip_pos = cam_matrix * Eigen::Vector4f{world_pos.x(), world_pos.y(), world_pos.z(), 1};\n\n\t\t// Check if the entity is in the rectangle\n\t\tif (clip_pos.x() > left\n\t\t    and clip_pos.x() < right\n\t\t    and clip_pos.y() > bottom\n\t\t    and clip_pos.y() < top) {\n\t\t\tselected.push_back(entity.first);\n\t\t}\n\t}\n\n\t// Select the units\n\tauto select_cb = params.get(\"select_cb\",\n\t                            std::function<void(const std::vector<entity_id_t> ids)>{\n\t\t\t\t\t\t\t\t\t[](const std::vector<entity_id_t> /* ids */) {}});\n\tselect_cb(selected);\n}\n\ntime::time_t DragSelectHandler::predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                                    const std::shared_ptr<openage::event::State> & /* state */,\n                                                    const time::time_t &at) {\n\treturn at;\n}\n\n\n} // namespace openage::gamestate::event\n"
  },
  {
    "path": "libopenage/gamestate/event/drag_select.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\nclass Event;\nclass State;\n} // namespace event\n\nnamespace gamestate::event {\n\n/**\n * Drag select game entities.\n */\nclass DragSelectHandler : public openage::event::OnceEventHandler {\npublic:\n\tDragSelectHandler();\n\t~DragSelectHandler() = default;\n\n\tvoid setup_event(const std::shared_ptr<openage::event::Event> &event,\n\t                 const std::shared_ptr<openage::event::State> &state) override;\n\n\tvoid invoke(openage::event::EventLoop &loop,\n\t            const std::shared_ptr<openage::event::EventEntity> &target,\n\t            const std::shared_ptr<openage::event::State> &state,\n\t            const time::time_t &time,\n\t            const param_map &params) override;\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> &target,\n\t                                 const std::shared_ptr<openage::event::State> &state,\n\t                                 const time::time_t &at) override;\n};\n\n} // namespace gamestate::event\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/event/process_command.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"process_command.h\"\n\n#include \"gamestate/manager.h\"\n\n\nnamespace openage::gamestate::event {\n\nProcessCommandHandler::ProcessCommandHandler() :\n\tOnceEventHandler{\"game.process_command\"} {}\n\nvoid ProcessCommandHandler::setup_event(const std::shared_ptr<openage::event::Event> & /* event */,\n                                        const std::shared_ptr<openage::event::State> & /* state */) {\n\t// TODO\n}\n\nvoid ProcessCommandHandler::invoke(openage::event::EventLoop & /* loop */,\n                                   const std::shared_ptr<openage::event::EventEntity> &target,\n                                   const std::shared_ptr<openage::event::State> & /* state */,\n                                   const time::time_t &time,\n                                   const param_map &params) {\n\tauto mgr = std::dynamic_pointer_cast<openage::gamestate::GameEntityManager>(target);\n\tmgr->run_activity_system(time, params);\n}\n\ntime::time_t ProcessCommandHandler::predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                                        const std::shared_ptr<openage::event::State> & /* state */,\n                                                        const time::time_t &at) {\n\treturn at;\n}\n\n} // namespace openage::gamestate::event\n"
  },
  {
    "path": "libopenage/gamestate/event/process_command.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\nclass Event;\nclass EventEntity;\nclass State;\n} // namespace event\n\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace event {\n/**\n * Process a command from a game entity command queue.\n */\nclass ProcessCommandHandler : public openage::event::OnceEventHandler {\npublic:\n\tProcessCommandHandler();\n\t~ProcessCommandHandler() = default;\n\n\tvoid setup_event(const std::shared_ptr<openage::event::Event> &event,\n\t                 const std::shared_ptr<openage::event::State> &state) override;\n\n\tvoid invoke(openage::event::EventLoop &loop,\n\t            const std::shared_ptr<openage::event::EventEntity> &target,\n\t            const std::shared_ptr<openage::event::State> &state,\n\t            const time::time_t &time,\n\t            const param_map &params) override;\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> &target,\n\t                                 const std::shared_ptr<openage::event::State> &state,\n\t                                 const time::time_t &at) override;\n};\n\n\n} // namespace event\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/event/send_command.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"send_command.h\"\n\n#include <vector>\n\n#include \"coord/phys.h\"\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/component/internal/commands/idle.h\"\n#include \"gamestate/component/internal/commands/move.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/types.h\"\n\n\nnamespace openage::gamestate {\nnamespace component {\nclass CommandQueue;\n\nnamespace command {\nclass IdleCommand;\nclass MoveCommand;\n} // namespace command\n} // namespace component\n\nnamespace event {\n\nCommander::Commander(const std::shared_ptr<openage::event::EventLoop> &loop) :\n\topenage::event::EventEntity{loop} {\n}\n\nsize_t Commander::id() const {\n\treturn 0;\n}\n\nstd::string Commander::idstr() const {\n\treturn \"command_target\";\n}\n\n\nSendCommandHandler::SendCommandHandler() :\n\topenage::event::OnceEventHandler{\"game.send_command\"} {\n}\n\nvoid SendCommandHandler::setup_event(const std::shared_ptr<openage::event::Event> & /* event */,\n                                     const std::shared_ptr<openage::event::State> & /* state */) {\n\t// TODO\n}\n\nvoid SendCommandHandler::invoke(openage::event::EventLoop & /* loop */,\n                                const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                const std::shared_ptr<openage::event::State> &state,\n                                const time::time_t &time,\n                                const param_map &params) {\n\tauto gstate = std::dynamic_pointer_cast<openage::gamestate::GameState>(state);\n\n\tauto command_type = params.get(\"type\", component::command::command_t::NONE);\n\tstd::vector<gamestate::entity_id_t> ids = params.get(\"entity_ids\",\n\t                                                     std::vector<gamestate::entity_id_t>{});\n\tfor (auto id : ids) {\n\t\tauto entity = gstate->get_game_entity(id);\n\t\tauto command_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\n\t\tswitch (command_type) {\n\t\tcase component::command::command_t::IDLE:\n\t\t\tcommand_queue->add_command(time, std::make_shared<component::command::IdleCommand>());\n\t\t\tbreak;\n\t\tcase component::command::command_t::MOVE:\n\t\t\tcommand_queue->add_command(\n\t\t\t\ttime,\n\t\t\t\tstd::make_shared<component::command::MoveCommand>(\n\t\t\t\t\tparams.get(\"target\",\n\t\t\t                   coord::phys3{0, 0, 0})));\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\ntime::time_t SendCommandHandler::predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                                     const std::shared_ptr<openage::event::State> & /* state */,\n                                                     const time::time_t &at) {\n\treturn at;\n}\n\n} // namespace event\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/event/send_command.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\nclass Event;\nclass State;\n} // namespace event\n\nnamespace gamestate::event {\n\n// TODO: This is only for testing\nclass Commander : public openage::event::EventEntity {\npublic:\n\tCommander(const std::shared_ptr<openage::event::EventLoop> &loop);\n\t~Commander() = default;\n\n\tsize_t id() const override;\n\tstd::string idstr() const override;\n};\n\n/**\n * Send commands to game entities.\n */\nclass SendCommandHandler : public openage::event::OnceEventHandler {\npublic:\n\tSendCommandHandler();\n\t~SendCommandHandler() = default;\n\n\tvoid setup_event(const std::shared_ptr<openage::event::Event> &event,\n\t                 const std::shared_ptr<openage::event::State> &state) override;\n\n\tvoid invoke(openage::event::EventLoop &loop,\n\t            const std::shared_ptr<openage::event::EventEntity> &target,\n\t            const std::shared_ptr<openage::event::State> &state,\n\t            const time::time_t &time,\n\t            const param_map &params) override;\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> &target,\n\t                                 const std::shared_ptr<openage::event::State> &state,\n\t                                 const time::time_t &at) override;\n};\n\n} // namespace gamestate::event\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/event/spawn_entity.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"spawn_entity.h\"\n\n#include <cstdint>\n#include <functional>\n#include <vector>\n\n#include <nyan/nyan.h>\n\n#include \"coord/phys.h\"\n#include \"gamestate/component/internal/activity.h\"\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/component/internal/ownership.h\"\n#include \"gamestate/component/internal/position.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/definitions.h\"\n#include \"gamestate/entity_factory.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/manager.h\"\n#include \"gamestate/map.h\"\n#include \"gamestate/types.h\"\n\n// TODO: Testing\n#include \"assets/mod_manager.h\"\n\nnamespace openage::gamestate::event {\n\nstatic const std::vector<nyan::fqon_t> aoe1_test_entities = {\n\t\"aoe1_base.data.game_entity.generic.chariot_archer.chariot_archer.ChariotArcher\",\n\t\"aoe1_base.data.game_entity.generic.bowman.bowman.Bowman\",\n\t\"aoe1_base.data.game_entity.generic.hoplite.hoplite.Hoplite\",\n\t\"aoe1_base.data.game_entity.generic.temple.temple.Temple\",\n\t\"aoe1_base.data.game_entity.generic.academy.academy.Academy\",\n};\nstatic const std::vector<nyan::fqon_t> de1_test_entities = {\n\t\"de1_base.data.game_entity.generic.chariot_archer.chariot_archer.ChariotArcher\",\n\t\"de1_base.data.game_entity.generic.bowman.bowman.Bowman\",\n\t\"de1_base.data.game_entity.generic.hoplite.hoplite.Hoplite\",\n\t\"de1_base.data.game_entity.generic.temple.temple.Temple\",\n\t\"de1_base.data.game_entity.generic.academy.academy.Academy\",\n};\nstatic const std::vector<nyan::fqon_t> aoe2_test_entities = {\n\t\"aoe2_base.data.game_entity.generic.knight.knight.Knight\",\n\t\"aoe2_base.data.game_entity.generic.monk.monk.Monk\",\n\t\"aoe2_base.data.game_entity.generic.archer.archer.Archer\",\n\t\"aoe2_base.data.game_entity.generic.castle.castle.Castle\",\n\t\"aoe2_base.data.game_entity.generic.barracks.barracks.Barracks\",\n};\nstatic const std::vector<nyan::fqon_t> de2_test_entities = {\n\t\"de2_base.data.game_entity.generic.knight.knight.Knight\",\n\t\"de2_base.data.game_entity.generic.monk.monk.Monk\",\n\t\"de2_base.data.game_entity.generic.archer.archer.Archer\",\n\t\"de2_base.data.game_entity.generic.castle.castle.Castle\",\n\t\"de2_base.data.game_entity.generic.barracks.barracks.Barracks\",\n};\nstatic const std::vector<nyan::fqon_t> hd_test_entities = {\n\t\"hd_base.data.game_entity.generic.knight.knight.Knight\",\n\t\"hd_base.data.game_entity.generic.monk.monk.Monk\",\n\t\"hd_base.data.game_entity.generic.archer.archer.Archer\",\n\t\"hd_base.data.game_entity.generic.castle.castle.Castle\",\n\t\"hd_base.data.game_entity.generic.barracks.barracks.Barracks\",\n};\nstatic const std::vector<nyan::fqon_t> swgb_test_entities = {\n\t\"swgb_base.data.game_entity.generic.trooper.trooper.Trooper\",\n\t\"swgb_base.data.game_entity.generic.force_knight.force_knight.ForceKnight\",\n\t\"swgb_base.data.game_entity.generic.bounty_hunter.bounty_hunter.BountyHunter\",\n\t\"swgb_base.data.game_entity.generic.command_center.command_center.CommandCenter\",\n\t\"swgb_base.data.game_entity.generic.heavy_weapons_factory.heavy_weapons_factory.HeavyWeaponsFactory\",\n};\nstatic const std::vector<nyan::fqon_t> trial_test_entities = {\n\t\"trial_base.data.game_entity.generic.eagle_warrior.eagle_warrior.EagleWarrior\",\n\t\"trial_base.data.game_entity.generic.jaguar_warrior.jaguar_warrior.JaguarWarrior\",\n\t\"trial_base.data.game_entity.generic.plumed_archer.plumed_archer.PlumedArcher\",\n\t\"trial_base.data.game_entity.generic.jungle_tree.jungle_tree.JungleTree\",\n\t\"trial_base.data.game_entity.generic.barracks.barracks.Barracks\",\n};\n\n// TODO: Remove hardcoded test entity references\n// declared static so we only have to build the vector once\nstatic std::vector<nyan::fqon_t> test_entities;\n\n\nvoid build_test_entities(const std::shared_ptr<GameState> &gstate) {\n\tauto modpack_ids = gstate->get_mod_manager()->get_load_order();\n\tfor (auto &modpack_id : modpack_ids) {\n\t\tif (modpack_id == \"aoe1_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     aoe1_test_entities.begin(),\n\t\t\t                     aoe1_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"de1_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     de1_test_entities.begin(),\n\t\t\t                     de1_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"aoe2_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     aoe2_test_entities.begin(),\n\t\t\t                     aoe2_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"de2_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     de2_test_entities.begin(),\n\t\t\t                     de2_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"hd_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     hd_test_entities.begin(),\n\t\t\t                     hd_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"swgb_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     swgb_test_entities.begin(),\n\t\t\t                     swgb_test_entities.end());\n\t\t}\n\t\telse if (modpack_id == \"trial_base\") {\n\t\t\ttest_entities.insert(test_entities.end(),\n\t\t\t                     trial_test_entities.begin(),\n\t\t\t                     trial_test_entities.end());\n\t\t}\n\t}\n}\n\n\nSpawner::Spawner(const std::shared_ptr<openage::event::EventLoop> &loop) :\n\tEventEntity(loop) {\n}\n\nsize_t Spawner::id() const {\n\treturn 0;\n}\n\nstd::string Spawner::idstr() const {\n\treturn \"spawner\";\n}\n\n\nSpawnEntityHandler::SpawnEntityHandler(const std::shared_ptr<openage::event::EventLoop> &loop,\n                                       const std::shared_ptr<gamestate::EntityFactory> &factory) :\n\tOnceEventHandler(\"game.spawn_entity\"),\n\tloop{loop},\n\tfactory{factory} {\n}\n\nvoid SpawnEntityHandler::setup_event(const std::shared_ptr<openage::event::Event> & /* event */,\n                                     const std::shared_ptr<openage::event::State> & /* state */) {\n\t// TODO\n}\n\nvoid SpawnEntityHandler::invoke(openage::event::EventLoop & /* loop */,\n                                const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                const std::shared_ptr<openage::event::State> &state,\n                                const time::time_t &time,\n                                const param_map &params) {\n\tauto gstate = std::dynamic_pointer_cast<gamestate::GameState>(state);\n\n\t// Check if spawn position is on the map\n\tauto pos = params.get(\"position\", gamestate::WORLD_ORIGIN);\n\tauto map_size = gstate->get_map()->get_size();\n\tif (not(pos.ne >= 0\n\t        and pos.ne < map_size[0]\n\t        and pos.se >= 0\n\t        and pos.se < map_size[1])) {\n\t\t// Do nothing if the spawn position is not on the map\n\t\tlog::log(DBG << \"Entity spawn failed: \"\n\t\t             << \"Spawn position \" << pos\n\t\t             << \" is not inside the map area \"\n\t\t             << \"(map size: \" << map_size << \")\");\n\t\treturn;\n\t}\n\n\tauto nyan_db = gstate->get_db_view();\n\n\tauto game_entities = nyan_db->get_obj_children_all(\"engine.util.game_entity.GameEntity\");\n\n\tif (test_entities.empty()) {\n\t\tbuild_test_entities(gstate);\n\n\t\t// Do nothing if there are no test entities\n\t\tif (test_entities.empty()) {\n\t\t\treturn;\n\t\t}\n\t}\n\n\tstatic uint8_t index = 0;\n\tnyan::fqon_t nyan_entity = test_entities.at(index);\n\t++index;\n\tif (index >= test_entities.size()) {\n\t\tindex = 0;\n\t}\n\n\t// Create entity\n\tplayer_id_t owner_id = params.get(\"owner\", 0);\n\tauto entity = this->factory->add_game_entity(this->loop, gstate, owner_id, nyan_entity);\n\n\t// Setup components\n\tauto entity_pos = std::dynamic_pointer_cast<component::Position>(\n\t\tentity->get_component(component::component_t::POSITION));\n\n\tentity_pos->set_position(time, pos);\n\tentity_pos->set_angle(time, coord::phys_angle_t::from_int(315));\n\n\tauto entity_owner = std::dynamic_pointer_cast<component::Ownership>(\n\t\tentity->get_component(component::component_t::OWNERSHIP));\n\tentity_owner->set_owner(time, owner_id);\n\n\tauto activity = std::dynamic_pointer_cast<component::Activity>(\n\t\tentity->get_component(component::component_t::ACTIVITY));\n\tactivity->init(time);\n\tentity->get_manager()->run_activity_system(time);\n\n\tgstate->add_game_entity(entity);\n}\n\ntime::time_t SpawnEntityHandler::predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                                     const std::shared_ptr<openage::event::State> & /* state */,\n                                                     const time::time_t &at) {\n\treturn at;\n}\n\n} // namespace openage::gamestate::event\n"
  },
  {
    "path": "libopenage/gamestate/event/spawn_entity.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\nclass Event;\nclass State;\n} // namespace event\n\nnamespace gamestate {\n\nclass EntityFactory;\n\nnamespace event {\n\n// TODO: This is only for testing\nclass Spawner : public openage::event::EventEntity {\npublic:\n\tSpawner(const std::shared_ptr<openage::event::EventLoop> &loop);\n\t~Spawner() = default;\n\n\tsize_t id() const override;\n\tstd::string idstr() const override;\n};\n\n/**\n * Handle the creation of entities in the game world\n */\nclass SpawnEntityHandler : public openage::event::OnceEventHandler {\npublic:\n\t/**\n\t * Creates a new SpawnEntityHandler.\n\t *\n\t * @param loop: Event loop that the components register on.\n\t * @param factory: Factory that is used to create the entity.\n\t */\n\tSpawnEntityHandler(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                   const std::shared_ptr<gamestate::EntityFactory> &factory);\n\t~SpawnEntityHandler() = default;\n\n\t/**\n\t * Called for each event that is created for this EventHandler.\n\t * The job of the setup function is to add all dependencies with other event\n\t * targets found in state.\n\t */\n\tvoid setup_event(const std::shared_ptr<openage::event::Event> &event,\n\t                 const std::shared_ptr<openage::event::State> &state) override;\n\n\t/**\n\t * This method implements the effects of the event.\n\t * It will be called at the time that was determined by `predict_invoke_time`.\n\t *\n\t * Called from the Loop.\n\t */\n\tvoid invoke(openage::event::EventLoop &loop,\n\t            const std::shared_ptr<openage::event::EventEntity> &target,\n\t            const std::shared_ptr<openage::event::State> &state,\n\t            const time::time_t &time,\n\t            const param_map &params) override;\n\n\t/**\n\t * Is called to calculate the execution time for an event of this eventhandler.\n\t * This is called whenever one of the set up dependencies was changed,\n\t * or when a REPEAT event was executed.\n\t *\n\t * @param target: the target the event was created for\n\t * @param state: the state this shall work on\n\t * @param at: the time when the change happened, from there on it shall be\n\t *            calculated onwards\n\t *\n\t * If the event is obsolete, return <time_t>::min().\n\t *\n\t * If the time is lower than the previous time,\n\t * then dependencies may not be resolved perfectly anymore\n\t * (if other events have already been calculated before that).\n\t */\n\ttime::time_t predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> &target,\n\t                                 const std::shared_ptr<openage::event::State> &state,\n\t                                 const time::time_t &at) override;\n\nprivate:\n\t/**\n\t * Event loop that the entity components are registered on.\n\t */\n\tstd::shared_ptr<openage::event::EventLoop> loop;\n\n\t/**\n\t * The factory that is used to create the entity.\n\t */\n\tstd::shared_ptr<gamestate::EntityFactory> factory;\n};\n\n} // namespace event\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/event/wait.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"wait.h\"\n\n#include \"gamestate/manager.h\"\n\n\nnamespace openage::gamestate::event {\n\nWaitHandler::WaitHandler() :\n\tOnceEventHandler{\"game.wait\"} {}\n\nvoid WaitHandler::setup_event(const std::shared_ptr<openage::event::Event> & /* event */,\n                              const std::shared_ptr<openage::event::State> & /* state */) {\n\t// TODO\n}\n\nvoid WaitHandler::invoke(openage::event::EventLoop & /* loop */,\n                         const std::shared_ptr<openage::event::EventEntity> &target,\n                         const std::shared_ptr<openage::event::State> & /* state */,\n                         const time::time_t &time,\n                         const param_map &params) {\n\tauto mgr = std::dynamic_pointer_cast<openage::gamestate::GameEntityManager>(target);\n\tmgr->run_activity_system(time, params);\n}\n\ntime::time_t WaitHandler::predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> & /* target */,\n                                              const std::shared_ptr<openage::event::State> & /* state */,\n                                              const time::time_t &at) {\n\treturn at;\n}\n\n\n} // namespace openage::gamestate::event\n"
  },
  {
    "path": "libopenage/gamestate/event/wait.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\nclass Event;\nclass EventEntity;\nclass State;\n} // namespace event\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace event {\n\n/**\n * Waits until the event is handled and calls back the entity manager.\n */\nclass WaitHandler : public openage::event::OnceEventHandler {\npublic:\n\tWaitHandler();\n\t~WaitHandler() = default;\n\n\tvoid setup_event(const std::shared_ptr<openage::event::Event> &event,\n\t                 const std::shared_ptr<openage::event::State> &state) override;\n\n\tvoid invoke(openage::event::EventLoop &loop,\n\t            const std::shared_ptr<openage::event::EventEntity> &target,\n\t            const std::shared_ptr<openage::event::State> &state,\n\t            const time::time_t &time,\n\t            const param_map &params) override;\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<openage::event::EventEntity> &target,\n\t                                 const std::shared_ptr<openage::event::State> &state,\n\t                                 const time::time_t &at) override;\n};\n\n} // namespace event\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/game.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"game.h\"\n\n#include <vector>\n\n#include <nyan/nyan.h>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"assets/mod_manager.h\"\n#include \"assets/modpack.h\"\n#include \"gamestate/entity_factory.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/map.h\"\n#include \"gamestate/terrain.h\"\n#include \"gamestate/terrain_factory.h\"\n#include \"gamestate/universe.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n#include \"coord/tile.h\"\n\nnamespace openage::gamestate {\n\nGame::Game(const std::shared_ptr<openage::event::EventLoop> &event_loop,\n           const std::shared_ptr<assets::ModManager> &mod_manager,\n           const std::shared_ptr<EntityFactory> &entity_factory,\n           const std::shared_ptr<TerrainFactory> &terrain_factory) :\n\tdb{nyan::Database::create()},\n\tstate{std::make_shared<GameState>(this->db, event_loop)},\n\tuniverse{std::make_shared<Universe>(state)} {\n\tthis->load_data(mod_manager);\n\n\t// TODO: Testing player creation\n\tauto player1 = entity_factory->add_player(event_loop, state, \"\");\n\tauto player2 = entity_factory->add_player(event_loop, state, \"\");\n\tstate->add_player(player1);\n\tstate->add_player(player2);\n\n\t// TODO: This lets the spawner event check which modpacks are loaded,\n\t//       so that it can decide which entities it can spawn.\n\t//       This can be removed when we spawn based on game logic rather than\n\t//       hardcoded entity types.\n\tthis->state->set_mod_manager(mod_manager);\n\n\tthis->generate_terrain(terrain_factory);\n}\n\nconst std::shared_ptr<GameState> &Game::get_state() const {\n\treturn this->state;\n}\n\nvoid Game::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tthis->universe->attach_renderer(render_factory);\n\tthis->state->get_map()->get_terrain()->attach_renderer(render_factory);\n}\n\nvoid Game::load_data(const std::shared_ptr<assets::ModManager> &mod_manager) {\n\tauto load_order = mod_manager->get_load_order();\n\n\tfor (auto &mod_id : load_order) {\n\t\tauto mod = mod_manager->get_modpack(mod_id);\n\t\tauto info = mod->get_info();\n\n\t\tauto includes = info.includes;\n\t\tfor (const auto &include : includes) {\n\t\t\t// handle wildcards\n\t\t\tauto parts = util::split(include, '/');\n\t\t\tauto last_part = parts.back();\n\t\t\tbool recursive = false;\n\t\t\tauto search = include;\n\t\t\tif (last_part == \"**\") {\n\t\t\t\trecursive = true;\n\t\t\t\tif (parts.size() == 1) {\n\t\t\t\t\t// include = \"**\"\n\t\t\t\t\t// start in root directory\n\t\t\t\t\tsearch = \"\";\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// include = \"path/to/somewhere/**\"\n\t\t\t\t\t// remove the wildcard '**' and the slash '/'\n\t\t\t\t\tsearch = include.substr(0, include.size() - 3);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis->load_path(info.path.get_parent(), info.path.get_name(), search, recursive);\n\t\t}\n\t}\n}\n\nvoid Game::load_path(const util::Path &base_dir,\n                     const std::string &mod_dir,\n                     const std::string &search,\n                     bool recursive) {\n\tauto base_path = base_dir.resolve_native_path();\n\tauto search_path = base_dir / mod_dir / search;\n\n\tauto fileload_func = [&base_path](const std::string &filename) {\n\t\t// nyan wants a string filepath, so we have to construct it from the\n\t\t// path and subpath parameters\n\t\tlog::log(INFO << \"Loading .nyan file: \" << filename);\n\t\tauto loc = base_path + \"/\" + filename;\n\t\treturn std::make_shared<nyan::File>(loc);\n\t};\n\n\t// file loading\n\tif (search_path.is_file() and search_path.get_suffix() == \".nyan\") {\n\t\tauto loc = mod_dir + \"/\" + search;\n\t\tthis->db->load(loc, fileload_func);\n\t\treturn;\n\t}\n\n\t// directory loading\n\tif (search_path.is_dir()) {\n\t\t// load all files in a directory\n\t\tfor (auto p : search_path.iterdir()) {\n\t\t\tif (p.is_dir() and not recursive) {\n\t\t\t\t// folders are skipped unless we read recursively\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tauto new_search = search + \"/\" + p.get_name();\n\t\t\tthis->load_path(base_dir, mod_dir, new_search, recursive);\n\t\t}\n\t}\n}\n\nvoid Game::generate_terrain(const std::shared_ptr<TerrainFactory> &terrain_factory) {\n\tauto chunk0 = terrain_factory->add_chunk(this->state,\n\t                                         util::Vector2s{10, 10},\n\t                                         coord::tile_delta{0, 0});\n\tauto chunk1 = terrain_factory->add_chunk(this->state,\n\t                                         util::Vector2s{10, 10},\n\t                                         coord::tile_delta{10, 0});\n\tauto chunk2 = terrain_factory->add_chunk(this->state,\n\t                                         util::Vector2s{10, 10},\n\t                                         coord::tile_delta{0, 10});\n\tauto chunk3 = terrain_factory->add_chunk(this->state,\n\t                                         util::Vector2s{10, 10},\n\t                                         coord::tile_delta{10, 10});\n\n\tauto terrain = terrain_factory->add_terrain({20, 20}, {chunk0, chunk1, chunk2, chunk3});\n\n\tauto map = std::make_shared<Map>(this->state, terrain);\n\tthis->state->set_map(map);\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/game.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\nnamespace nyan {\nclass Database;\n}\n\nnamespace openage {\n\nnamespace assets {\nclass ModManager;\n}\n\nnamespace event {\nclass EventLoop;\n}\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace util {\nclass Path;\n}\n\nnamespace gamestate {\nclass GameState;\nclass EntityFactory;\nclass TerrainFactory;\nclass Universe;\n\n/**\n * Manages a game session (settings, win conditions, etc.).\n *\n * TODO: Create sensible structure of gamestate classes. Right now it's\n *\n *       Game->Universe|->World->GameEntity\n *                     |->Terrain\n *\n * \t\twhich can be confusing.\n */\nclass Game {\npublic:\n\t/**\n\t * Create a new game.\n\t *\n\t * @param event_loop Event simulation loop for the gamestate.\n\t * @param mod_manager Mod manager.\n\t * @param entity_factory Factory for creating entities. Used for creating the players.\n\t */\n\tGame(const std::shared_ptr<openage::event::EventLoop> &event_loop,\n\t     const std::shared_ptr<assets::ModManager> &mod_manager,\n\t     const std::shared_ptr<EntityFactory> &entity_factory,\n\t     const std::shared_ptr<TerrainFactory> &terrain_factory);\n\t~Game() = default;\n\n\t/**\n\t * Get the current game state.\n\t */\n\tconst std::shared_ptr<GameState> &get_state() const;\n\n\t/**\n\t * Attach a renderer to the game which enables graphical display options for\n\t * all ingame entities.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * Load game data from the filesystem.\n\t *\n\t * @param mod_manager Mod manager.\n\t */\n\tvoid load_data(const std::shared_ptr<assets::ModManager> &mod_manager);\n\n\t/**\n\t * Load game data from the filesystem recursively.\n\t *\n\t * TODO: Move this into nyan.\n\t *\n\t * @param base_dir Base directory where mods are stored.\n\t * @param mod_dir Name of the mod directory.\n\t * @param search Search path relative to the mod directory.\n\t * @param recursive if true, recursively search subfolders if the the search path is a directory.\n\t */\n\tvoid load_path(const util::Path &base_dir,\n\t               const std::string &mod_dir,\n\t               const std::string &search,\n\t               bool recursive = false);\n\n\t/**\n\t * Generate the terrain for the current game.\n\t *\n\t * TODO: Use a real map generator.\n\t *\n\t * @param terrain_factory Factory for creating terrain objects.\n\t */\n\tvoid generate_terrain(const std::shared_ptr<TerrainFactory> &terrain_factory);\n\n\t/**\n\t * Nyan game data database.\n\t */\n\tstd::shared_ptr<nyan::Database> db;\n\n\t/**\n\t * State of the current game.\n\t */\n\tstd::shared_ptr<GameState> state;\n\n\t/**\n\t * Object that controls entities in the game world.\n\t */\n\tstd::shared_ptr<Universe> universe;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/game_entity.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"game_entity.h\"\n\n#include \"gamestate/api/ability.h\"\n#include \"gamestate/api/animation.h\"\n#include \"gamestate/api/property.h\"\n#include \"gamestate/component/api/idle.h\"\n#include \"gamestate/component/api/move.h\"\n#include \"gamestate/component/base_component.h\"\n#include \"gamestate/component/internal/position.h\"\n#include \"renderer/stages/world/render_entity.h\"\n\nnamespace openage::gamestate {\n\nGameEntity::GameEntity(entity_id_t id) :\n\tid{id},\n\tcomponents{},\n\trender_entity{nullptr} {\n}\n\nstd::shared_ptr<GameEntity> GameEntity::copy(entity_id_t id) {\n\tauto copy = std::shared_ptr<GameEntity>(new GameEntity(*this));\n\tcopy->set_id(id);\n\n\treturn copy;\n}\n\nentity_id_t GameEntity::get_id() const {\n\treturn this->id;\n}\n\nvoid GameEntity::set_render_entity(const std::shared_ptr<renderer::world::RenderEntity> &entity) {\n\t// TODO: Transfer state from old render entity to new one?\n\n\tthis->render_entity = entity;\n}\n\nvoid GameEntity::set_manager(const std::shared_ptr<GameEntityManager> &manager) {\n\tthis->manager = manager;\n}\n\nconst std::shared_ptr<GameEntityManager> &GameEntity::get_manager() const {\n\treturn this->manager;\n}\n\nconst std::shared_ptr<component::Component> &GameEntity::get_component(component::component_t type) {\n\treturn this->components.at(type);\n}\n\nvoid GameEntity::add_component(const std::shared_ptr<component::Component> &component) {\n\tthis->components.insert({component->get_type(), component});\n}\n\nbool GameEntity::has_component(component::component_t type) {\n\treturn this->components.contains(type);\n}\n\nvoid GameEntity::render_update(const time::time_t &time,\n                               const std::string &animation_path) {\n\tif (this->render_entity != nullptr) {\n\t\tconst auto &pos = dynamic_pointer_cast<component::Position>(\n\t\t\t\t\t\t\t  this->components.at(component::component_t::POSITION))\n\t\t                      ->get_positions();\n\t\tconst auto &angle = dynamic_pointer_cast<component::Position>(\n\t\t\t\t\t\t\t\tthis->components.at(component::component_t::POSITION))\n\t\t                        ->get_angles();\n\t\tthis->render_entity->update(this->id, pos, angle, animation_path, time);\n\t}\n}\n\nvoid GameEntity::set_id(entity_id_t id) {\n\tthis->id = id;\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/game_entity.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <unordered_map>\n\n#include \"gamestate/component/types.h\"\n#include \"gamestate/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace renderer::world {\nclass RenderEntity;\n}\n\nnamespace gamestate {\nclass GameEntityManager;\n\nnamespace component {\nclass Component;\n}\n\n/**\n * Entity for a \"physical\" thing (unit, building, etc.) in the game world.\n */\nclass GameEntity {\npublic:\n\t/**\n\t * Create a new game entity.\n\t *\n\t * @param id Unique identifier.\n\t */\n\tGameEntity(entity_id_t id);\n\n\t~GameEntity() = default;\n\n\tGameEntity(GameEntity &&) = default;\n\tGameEntity &operator=(GameEntity &&) = default;\n\n\t/**\n\t * Copy this game entity.\n\t *\n\t * @param id Unique identifier.\n\t *\n\t * @return Copy of this game entity.\n\t */\n\tstd::shared_ptr<GameEntity> copy(entity_id_t id);\n\n\t/**\n\t * Get the unique identifier of this entity.\n\t *\n\t * @return Unique identifier.\n\t */\n\tentity_id_t get_id() const;\n\n\t/**\n\t * Set the current render entity.\n\t *\n\t * @param entity New render entity.\n\t */\n\tvoid set_render_entity(const std::shared_ptr<renderer::world::RenderEntity> &entity);\n\n\t/**\n\t * Set the event manager of this entity.\n\t *\n\t * @param manager Event manager.\n\t */\n\tvoid set_manager(const std::shared_ptr<GameEntityManager> &manager);\n\n\t/**\n\t * Get the event manager of this entity.\n\t *\n\t * @return Event manager.\n\t */\n\tconst std::shared_ptr<GameEntityManager> &get_manager() const;\n\n\t/**\n\t * Get a component of this entity.\n\t *\n\t * @param type Component type.\n\t */\n\tconst std::shared_ptr<component::Component> &get_component(component::component_t type);\n\n\t/**\n\t * Add a component to this entity.\n\t *\n\t * @param component Component to add.\n\t */\n\tvoid add_component(const std::shared_ptr<component::Component> &component);\n\n\t/**\n\t * Check if this entity has a component of the given type.\n\t *\n\t * @param type Component type.\n\t */\n\tbool has_component(component::component_t type);\n\n\t/**\n\t * Update the render entity.\n\t *\n\t * @param time Simulation time of the update.\n\t * @param animation_path Path to the animation definition used at \\p time.\n\t */\n\tvoid render_update(const time::time_t &time,\n\t                   const std::string &animation_path);\n\nprotected:\n\t/**\n\t * A game entity cannot be default copied because of their unique ID.\n\t *\n\t * \\p copy() must be used instead.\n\t */\n\tGameEntity(const GameEntity &) = default;\n\tGameEntity &operator=(const GameEntity &) = default;\n\nprivate:\n\t/**\n\t * Set the unique identifier of this game entity.\n\t *\n\t * Only called by \\p copy().\n\t *\n\t * @param id New ID.\n\t */\n\tvoid set_id(entity_id_t id);\n\n\t/**\n\t * Unique identifier.\n\t */\n\tentity_id_t id;\n\n\t/**\n\t * Data components.\n\t *\n\t * TODO: Multiple components of the same type.\n\t */\n\tstd::unordered_map<component::component_t, std::shared_ptr<component::Component>> components;\n\n\t/**\n\t * Render entity for pushing updates to the renderer. Can be \\p nullptr.\n\t */\n\tstd::shared_ptr<renderer::world::RenderEntity> render_entity;\n\n\t/**\n\t * Event manager.\n\t */\n\tstd::shared_ptr<GameEntityManager> manager;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/game_state.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"game_state.h\"\n\n#include <nyan/nyan.h>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/player.h\"\n\n\nnamespace openage::gamestate {\n\nGameState::GameState(const std::shared_ptr<nyan::Database> &db,\n                     const std::shared_ptr<openage::event::EventLoop> &event_loop) :\n\tevent::State{event_loop},\n\tdb_view{db->new_view()} {\n}\n\nconst std::shared_ptr<nyan::View> &GameState::get_db_view() {\n\treturn this->db_view;\n}\n\nvoid GameState::add_game_entity(const std::shared_ptr<GameEntity> &entity) {\n\tif (this->game_entities.contains(entity->get_id())) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Game entity with ID \" << entity->get_id() << \" already exists\");\n\t}\n\tthis->game_entities[entity->get_id()] = entity;\n}\n\nvoid GameState::add_player(const std::shared_ptr<Player> &player) {\n\tif (this->players.contains(player->get_id())) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Player with ID \" << player->get_id() << \" already exists\");\n\t}\n\tthis->players[player->get_id()] = player;\n}\n\nvoid GameState::set_map(const std::shared_ptr<Map> &map) {\n\tthis->map = map;\n}\n\nconst std::shared_ptr<GameEntity> &GameState::get_game_entity(entity_id_t id) const {\n\tif (!this->game_entities.contains(id)) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Game entity with ID \" << id << \" does not exist\");\n\t}\n\treturn this->game_entities.at(id);\n}\n\nconst std::unordered_map<entity_id_t, std::shared_ptr<GameEntity>> &GameState::get_game_entities() const {\n\treturn this->game_entities;\n}\n\nconst std::shared_ptr<Player> &GameState::get_player(player_id_t id) const {\n\tif (!this->players.contains(id)) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Player with ID \" << id << \" does not exist\");\n\t}\n\treturn this->players.at(id);\n}\n\nconst std::shared_ptr<Map> &GameState::get_map() const {\n\treturn this->map;\n}\n\nconst std::shared_ptr<assets::ModManager> &GameState::get_mod_manager() const {\n\treturn this->mod_manager;\n}\n\nvoid GameState::set_mod_manager(const std::shared_ptr<assets::ModManager> &mod_manager) {\n\tthis->mod_manager = mod_manager;\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/game_state.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <unordered_map>\n\n#include \"event/state.h\"\n#include \"gamestate/types.h\"\n\n\nnamespace nyan {\nclass Database;\nclass View;\n} // namespace nyan\n\nnamespace openage {\n\nnamespace assets {\nclass ModManager;\n}\n\nnamespace event {\nclass EventLoop;\n}\n\nnamespace gamestate {\nclass GameEntity;\nclass Map;\nclass Player;\n\n/**\n * State of the game.\n *\n * Contains index structures for looking up game entities and other\n * information for the current game.\n */\nclass GameState : public openage::event::State {\npublic:\n\t/**\n\t * Create a new game state.\n\t *\n\t * @param db Nyan game data database.\n\t * @param event_loop Event loop for the game state.\n\t */\n\texplicit GameState(const std::shared_ptr<nyan::Database> &db,\n\t                   const std::shared_ptr<openage::event::EventLoop> &event_loop);\n\n\t/**\n\t * Get the nyan database view for the whole game.\n\t *\n\t * Players have individual views for their own data.\n\t *\n\t * @return nyan database view.\n\t */\n\tconst std::shared_ptr<nyan::View> &get_db_view();\n\n\t/**\n\t * Add a new game entity to the index.\n\t *\n\t * @param entity New game entity.\n\t */\n\tvoid add_game_entity(const std::shared_ptr<GameEntity> &entity);\n\n\t/**\n\t * Add a new player to the index.\n\t *\n\t * @param player New player.\n\t */\n\tvoid add_player(const std::shared_ptr<Player> &player);\n\n\t/**\n\t * Set the map of the current game.\n\t *\n\t * @param terrain Map object.\n\t */\n\tvoid set_map(const std::shared_ptr<Map> &map);\n\n\t/**\n\t * Get a game entity by its ID.\n\t *\n\t * @param id ID of the game entity.\n\t *\n\t * @return Game entity with the given ID.\n\t */\n\tconst std::shared_ptr<GameEntity> &get_game_entity(entity_id_t id) const;\n\n\t/**\n\t * Get all game entities in the current game.\n\t *\n\t * @return Map of all game entities in the current game by their ID.\n\t */\n\tconst std::unordered_map<entity_id_t, std::shared_ptr<GameEntity>> &get_game_entities() const;\n\n\t/**\n\t * Get a player by its ID.\n\t *\n\t * @param id ID of the player.\n\t *\n\t * @return Player with the given ID.\n\t */\n\tconst std::shared_ptr<Player> &get_player(player_id_t id) const;\n\n\t/**\n\t * Get the map of the current game.\n\t *\n\t * @return Map object.\n\t */\n\tconst std::shared_ptr<Map> &get_map() const;\n\n\t/**\n\t * TODO: Only for testing.\n\t */\n\tconst std::shared_ptr<assets::ModManager> &get_mod_manager() const;\n\tvoid set_mod_manager(const std::shared_ptr<assets::ModManager> &mod_manager);\n\nprivate:\n\t/**\n\t * View for the nyan game data database.\n\t */\n\tstd::shared_ptr<nyan::View> db_view;\n\n\t/**\n\t * Map of all game entities in the current game by their ID.\n\t */\n\tstd::unordered_map<entity_id_t, std::shared_ptr<GameEntity>> game_entities;\n\n\t/**\n\t * Map of all players in the current game by their ID.\n\t */\n\tstd::unordered_map<player_id_t, std::shared_ptr<Player>> players;\n\n\t/**\n\t * Map of the current game.\n\t */\n\tstd::shared_ptr<Map> map;\n\n\t/**\n\t * TODO: Only for testing\n\t */\n\tstd::shared_ptr<assets::ModManager> mod_manager;\n};\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/manager.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"manager.h\"\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/system/activity.h\"\n\n\nnamespace openage::gamestate {\n\nGameEntityManager::GameEntityManager(const std::shared_ptr<openage::event::EventLoop> &loop,\n                                     const std::shared_ptr<openage::gamestate::GameState> &state,\n                                     const std::shared_ptr<GameEntity> &game_entity) :\n\tevent::EventEntity{loop},\n\tloop{loop},\n\tstate{state},\n\tgame_entity{game_entity} {}\n\nvoid GameEntityManager::run_activity_system(const time::time_t &time,\n                                            const std::optional<openage::event::EventHandler::param_map> &ev_params) {\n\tlog::log(DBG << \"Running activity system for entity \" << this->game_entity->get_id());\n\tsystem::Activity::advance(time, this->game_entity, this->loop, this->state, ev_params);\n}\n\nsize_t GameEntityManager::id() const {\n\t// TODO\n\treturn this->game_entity->get_id();\n}\n\nstd::string GameEntityManager::idstr() const {\n\t// TODO\n\treturn \"manager\";\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/manager.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <optional>\n#include <string>\n\n#include \"event/evententity.h\"\n#include \"event/eventhandler.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace event {\nclass EventLoop;\n} // namespace event\n\nnamespace gamestate {\nclass GameState;\nclass GameEntity;\n\nclass GameEntityManager : public openage::event::EventEntity {\npublic:\n\tGameEntityManager(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                  const std::shared_ptr<openage::gamestate::GameState> &state,\n\t                  const std::shared_ptr<GameEntity> &game_entity);\n\t~GameEntityManager() = default;\n\n\tvoid run_activity_system(const time::time_t &time,\n\t                         const std::optional<openage::event::EventHandler::param_map> &ev_params = std::nullopt);\n\n\tsize_t id() const override;\n\tstd::string idstr() const override;\n\nprivate:\n\tstd::shared_ptr<openage::event::EventLoop> loop;\n\n\tstd::shared_ptr<openage::gamestate::GameState> state;\n\n\tstd::shared_ptr<GameEntity> game_entity;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/map.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"map.h\"\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/api/terrain.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/terrain.h\"\n#include \"gamestate/terrain_chunk.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/grid.h\"\n#include \"pathfinding/pathfinder.h\"\n#include \"pathfinding/sector.h\"\n\n\nnamespace openage::gamestate {\nMap::Map(const std::shared_ptr<GameState> &state,\n         const std::shared_ptr<Terrain> &terrain) :\n\tterrain{terrain},\n\tpathfinder{std::make_shared<path::Pathfinder>()},\n\tgrid_lookup{} {\n\t// Create a grid for each path type\n\t// TODO: This is non-deterministic because of the unordered set. Is this a problem?\n\tauto nyan_db = state->get_db_view();\n\tstd::unordered_set<nyan::fqon_t> path_types = nyan_db->get_obj_children_all(\"engine.util.path_type.PathType\");\n\tsize_t grid_idx = 0;\n\tauto chunk_size = this->terrain->get_chunk(0)->get_size();\n\tauto side_length = std::max(chunk_size[0], chunk_size[1]);\n\tauto grid_size = this->terrain->get_chunks_size();\n\tfor (const auto &path_type : path_types) {\n\t\tauto grid = std::make_shared<path::Grid>(grid_idx, grid_size, side_length);\n\t\tthis->pathfinder->add_grid(grid);\n\n\t\tthis->grid_lookup.emplace(path_type, grid_idx);\n\t\tgrid_idx += 1;\n\t}\n\n\t// Set path costs\n\tfor (size_t chunk_idx = 0; chunk_idx < this->terrain->get_chunks().size(); ++chunk_idx) {\n\t\tauto chunk_terrain = this->terrain->get_chunk(chunk_idx);\n\t\tfor (size_t tile_idx = 0; tile_idx < chunk_terrain->get_tiles().size(); ++tile_idx) {\n\t\t\tauto tile = chunk_terrain->get_tile(tile_idx);\n\t\t\tauto path_costs = api::APITerrain::get_path_costs(tile.terrain);\n\n\t\t\tfor (const auto &path_cost : path_costs) {\n\t\t\t\tauto grid_id = this->grid_lookup.at(path_cost.first);\n\t\t\t\tauto grid = this->pathfinder->get_grid(grid_id);\n\n\t\t\t\tauto sector = grid->get_sector(chunk_idx);\n\t\t\t\tauto cost_field = sector->get_cost_field();\n\t\t\t\tcost_field->set_cost(tile_idx, path_cost.second, time::TIME_ZERO);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Connect sectors with portals\n\tfor (const auto &path_type : this->grid_lookup) {\n\t\tauto grid = this->pathfinder->get_grid(path_type.second);\n\t\tgrid->init_portals();\n\t\tgrid->init_portal_nodes();\n\t}\n}\n\nconst util::Vector2s &Map::get_size() const {\n\treturn this->terrain->get_size();\n}\n\nconst std::shared_ptr<Terrain> &Map::get_terrain() const {\n\treturn this->terrain;\n}\n\nconst std::shared_ptr<path::Pathfinder> &Map::get_pathfinder() const {\n\treturn this->pathfinder;\n}\n\npath::grid_id_t Map::get_grid_id(const nyan::fqon_t &path_grid) const {\n\treturn this->grid_lookup.at(path_grid);\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/map.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <unordered_map>\n\n#include <nyan/nyan.h>\n\n#include \"pathfinding/types.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage {\nnamespace path {\nclass Pathfinder;\n} // namespace path\n\nnamespace gamestate {\nclass GameState;\nclass Terrain;\n\nclass Map {\npublic:\n\t/**\n\t * Create a new map from existing terrain.\n\t *\n\t * Initializes the pathfinder with the terrain path costs.\n\t *\n\t * @param state Game state.\n\t * @param terrain Terrain object.\n\t */\n\tMap(const std::shared_ptr<GameState> &state,\n\t    const std::shared_ptr<Terrain> &terrain);\n\n\t~Map() = default;\n\n\t/**\n\t * Get the size of the map.\n\t *\n\t * @return Map size (width x height).\n\t */\n\tconst util::Vector2s &get_size() const;\n\n\t/**\n\t * Get the terrain of the map.\n\t *\n\t * @return Terrain.\n\t */\n\tconst std::shared_ptr<Terrain> &get_terrain() const;\n\n\t/**\n\t * Get the pathfinder for the map.\n\t *\n\t * @return Pathfinder.\n\t */\n\tconst std::shared_ptr<path::Pathfinder> &get_pathfinder() const;\n\n\t/**\n\t * Get the grid ID associated with a nyan path grid object.\n\t *\n\t * @param path_grid Path grid object fqon.\n\t *\n\t * @return Grid ID.\n\t */\n\tpath::grid_id_t get_grid_id(const nyan::fqon_t &path_grid) const;\n\nprivate:\n\t/**\n\t * Terrain.\n\t */\n\tstd::shared_ptr<Terrain> terrain;\n\n\t/**\n\t * Pathfinder.\n\t */\n\tstd::shared_ptr<path::Pathfinder> pathfinder;\n\n\t/**\n\t * Lookup table for mapping path grid objects in nyan to grid indices.\n\t */\n\tstd::unordered_map<nyan::fqon_t, path::grid_id_t> grid_lookup;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/player.cpp",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#include \"player.h\"\n\n#include \"nyan/nyan.h\"\n\n\nnamespace openage::gamestate {\n\nPlayer::Player(player_id_t id,\n               const std::shared_ptr<nyan::View> &db_view) :\n\tid{id},\n\tdb_view{db_view} {\n}\n\nstd::shared_ptr<Player> Player::copy(entity_id_t id) {\n\tauto copy = std::shared_ptr<Player>(new Player(*this));\n\tcopy->set_id(id);\n\n\treturn copy;\n}\n\nplayer_id_t Player::get_id() const {\n\treturn this->id;\n}\n\nconst std::shared_ptr<nyan::View> &Player::get_db_view() const {\n\treturn this->db_view;\n}\n\nvoid Player::set_id(entity_id_t id) {\n\tthis->id = id;\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/player.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"gamestate/types.h\"\n\n\nnamespace nyan {\nclass View;\n} // namespace nyan\n\nnamespace openage::gamestate {\n\n/**\n * Entity for managing a player inside the game world.\n */\nclass Player {\npublic:\n\t/**\n\t * Create a new player with the given id.\n\t *\n\t * @param id Unique player ID.\n\t * @param db_view View of the nyan database used for the player.\n\t */\n\tPlayer(player_id_t id,\n\t       const std::shared_ptr<nyan::View> &db_view);\n\n\tPlayer(Player &&) = default;\n\tPlayer &operator=(Player &&) = default;\n\n\t~Player() = default;\n\n\t/**\n\t * Copy this player.\n\t *\n\t * @param id Unique identifier.\n\t *\n\t * @return Copy of this player.\n\t */\n\tstd::shared_ptr<Player> copy(entity_id_t id);\n\n\t/**\n\t * Get the unique ID of the player.\n\t *\n\t * @return Unique player ID.\n\t */\n\tplayer_id_t get_id() const;\n\n\t/**\n\t * Get the nyan game database view for the player.\n\t *\n\t * @return nyan database view.\n\t */\n\tconst std::shared_ptr<nyan::View> &get_db_view() const;\n\nprotected:\n\t/**\n\t * A player cannot be default copied because of their unique ID.\n\t *\n\t * \\p copy() must be used instead.\n\t */\n\tPlayer(const Player &) = default;\n\tPlayer &operator=(const Player &) = default;\n\nprivate:\n\t/**\n\t * Set the unique identifier of this player.\n\t *\n\t * Only called by \\p copy().\n\t *\n\t * @param id New ID.\n\t */\n\tvoid set_id(entity_id_t id);\n\n\t/**\n\t * Player ID. Must be unique.\n\t */\n\tplayer_id_t id;\n\n\t/**\n\t * Player view of the nyan game data database.\n\t */\n\tstd::shared_ptr<nyan::View> db_view;\n};\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/simulation.cpp",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#include \"simulation.h\"\n\n#include \"assets/mod_manager.h\"\n#include \"event/event_loop.h\"\n#include \"gamestate/entity_factory.h\"\n#include \"gamestate/event/drag_select.h\"\n#include \"gamestate/event/process_command.h\"\n#include \"gamestate/event/send_command.h\"\n#include \"gamestate/event/spawn_entity.h\"\n#include \"gamestate/event/wait.h\"\n#include \"gamestate/terrain_factory.h\"\n#include \"time/clock.h\"\n#include \"time/time_loop.h\"\n\n// TODO\n#include \"gamestate/game.h\"\n#include \"gamestate/game_state.h\"\n\nnamespace openage::gamestate {\n\nGameSimulation::GameSimulation(const util::Path &root_dir,\n                               const std::shared_ptr<cvar::CVarManager> &cvar_manager,\n                               const std::shared_ptr<openage::time::TimeLoop> time_loop) :\n\trunning{false},\n\troot_dir{root_dir},\n\tcvar_manager{cvar_manager},\n\ttime_loop{time_loop},\n\tevent_loop{std::make_shared<openage::event::EventLoop>()},\n\tentity_factory{std::make_shared<gamestate::EntityFactory>()},\n\tterrain_factory{std::make_shared<gamestate::TerrainFactory>()},\n\tmod_manager{std::make_shared<assets::ModManager>(this->root_dir / \"assets\" / \"converted\")},\n\tspawner{std::make_shared<gamestate::event::Spawner>(this->event_loop)},\n\tcommander{std::make_shared<gamestate::event::Commander>(this->event_loop)} {\n\tauto mods = mod_manager->enumerate_modpacks(root_dir / \"assets\" / \"converted\");\n\tfor (const auto &mod : mods) {\n\t\tthis->mod_manager->register_modpack(mod);\n\t}\n\n\tlog::log(MSG(info) << \"Created game simulation\");\n}\n\n\nvoid GameSimulation::run() {\n\tthis->start();\n\twhile (this->running) {\n\t\ttime::time_t current_time = this->time_loop->get_clock()->get_time();\n\t\tthis->event_loop->reach_time(current_time, this->game->get_state());\n\t}\n\tlog::log(MSG(info) << \"Game simulation loop exited\");\n}\n\n\nvoid GameSimulation::start() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->init_event_handlers();\n\n\t// TODO: wait for presenter to initialize before starting?\n\tthis->game = std::make_shared<gamestate::Game>(event_loop,\n\t                                               this->mod_manager,\n\t                                               this->entity_factory,\n\t                                               this->terrain_factory);\n\n\tthis->running = true;\n\n\tlog::log(MSG(info) << \"Game simulation started\");\n}\n\n\nvoid GameSimulation::stop() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->running = false;\n\n\tlog::log(MSG(info) << \"Game simulation stopped\");\n}\n\n\nconst util::Path &GameSimulation::get_root_dir() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->root_dir;\n}\n\nconst std::shared_ptr<cvar::CVarManager> GameSimulation::get_cvar_manager() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->cvar_manager;\n}\n\nconst std::shared_ptr<gamestate::Game> GameSimulation::get_game() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->game;\n}\n\nconst std::shared_ptr<openage::event::EventLoop> GameSimulation::get_event_loop() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->event_loop;\n}\n\nconst std::shared_ptr<gamestate::event::Spawner> GameSimulation::get_spawner() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->spawner;\n}\n\nconst std::shared_ptr<gamestate::event::Commander> GameSimulation::get_commander() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->commander;\n}\n\nvoid GameSimulation::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->game->attach_renderer(render_factory);\n\tthis->entity_factory->attach_renderer(render_factory);\n\tthis->terrain_factory->attach_renderer(render_factory);\n}\n\nvoid GameSimulation::set_modpacks(const std::vector<std::string> &modpacks) {\n\tstd::unique_lock lock{this->mutex};\n\n\tstd::vector<std::string> mods{\"engine\"};\n\tmods.insert(mods.end(), modpacks.begin(), modpacks.end());\n\n\tthis->mod_manager->activate_modpacks(mods);\n\n\t// TODO: Prevent setting modpacks if a game is already running\n}\n\nvoid GameSimulation::init_event_handlers() {\n\tauto drag_select_handler = std::make_shared<gamestate::event::DragSelectHandler>();\n\tauto spawn_handler = std::make_shared<gamestate::event::SpawnEntityHandler>(this->event_loop,\n\t                                                                            this->entity_factory);\n\tauto command_handler = std::make_shared<gamestate::event::SendCommandHandler>();\n\tauto manager_handler = std::make_shared<gamestate::event::ProcessCommandHandler>();\n\tauto wait_handler = std::make_shared<gamestate::event::WaitHandler>();\n\tthis->event_loop->add_event_handler(drag_select_handler);\n\tthis->event_loop->add_event_handler(spawn_handler);\n\tthis->event_loop->add_event_handler(command_handler);\n\tthis->event_loop->add_event_handler(manager_handler);\n\tthis->event_loop->add_event_handler(wait_handler);\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/simulation.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <shared_mutex>\n\n#include \"util/path.h\"\n\nnamespace openage {\n\nnamespace assets {\nclass ModManager;\n}\n\nnamespace cvar {\nclass CVarManager;\n}\n\nnamespace event {\nclass EventLoop;\n} // namespace event\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace time {\nclass TimeLoop;\n} // namespace time\n\nnamespace gamestate {\nclass EntityFactory;\nclass Game;\nclass TerrainFactory;\n\nnamespace event {\nclass Commander;\nclass Spawner;\n} // namespace event\n\n/**\n * Gameplay subsystem of the engine.\n *\n * Manages the state of the game world and the event loop.\n */\nclass GameSimulation final {\npublic:\n\t/**\n\t * Create the game simulation subsystems depending on the requested run mode.\n\t *\n\t * @param root_dir openage root directory.\n\t * @param cvar_manager Environment variable manager.\n\t * @param time_loop Time management loop.\n\t */\n\tGameSimulation(const util::Path &root_dir,\n\t               const std::shared_ptr<cvar::CVarManager> &cvar_manager,\n\t               const std::shared_ptr<openage::time::TimeLoop> time_loop);\n\n\t// game simulation should not be copied or moved\n\tGameSimulation(const GameSimulation &copy) = delete;\n\tGameSimulation &operator=(const GameSimulation &copy) = delete;\n\tGameSimulation(GameSimulation &&other) = delete;\n\tGameSimulation &operator=(GameSimulation &&other) = delete;\n\t~GameSimulation() = default;\n\n\t/**\n\t * Run the simulation loop.\n\t */\n\tvoid run();\n\n\t/**\n\t * Start the simulation loop.\n\t */\n\tvoid start();\n\n\t/**\n\t * Stop of the simulation loop.\n\t */\n\tvoid stop();\n\n\t/**\n\t * Get the root directory of the simulation executable.\n\t */\n\tconst util::Path &get_root_dir();\n\n\t/**\n\t * Get this simulation's cvar manager.\n\t *\n\t * @return CVarManager instance.\n\t */\n\tconst std::shared_ptr<cvar::CVarManager> get_cvar_manager();\n\n\t/**\n\t * Get the game running in the simulation.\n\t *\n\t * @return Game instance.\n\t */\n\tconst std::shared_ptr<gamestate::Game> get_game();\n\n\t/**\n\t * Get the event loop for the gamestate.\n\t *\n\t * @return Event loop.\n\t */\n\tconst std::shared_ptr<openage::event::EventLoop> get_event_loop();\n\n\t/**\n\t * Get the event entity for spawing game entities.\n\t *\n\t * TODO: Move somewhere else or remove.\n\t *\n\t * @return Spawner for entity creation.\n\t */\n\tconst std::shared_ptr<gamestate::event::Spawner> get_spawner();\n\n\t/**\n\t * Get the event entity for sending commands.\n\t *\n\t * TODO: Move somewhere else or remove.\n\t *\n\t * @return Commander for sending commands.\n\t */\n\tconst std::shared_ptr<gamestate::event::Commander> get_commander();\n\n\t/**\n\t * Attach a renderer to the simulation.\n\t *\n\t * @param factory Factory for creating render entities.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\n\t/**\n\t * Set the modpacks to load for a game.\n\t *\n\t * @param modpacks IDs of the modpacks to load.\n\t */\n\tvoid set_modpacks(const std::vector<std::string> &modpacks);\n\n\t/**\n\t * current simulation state variable.\n\t * to be set to false to stop the simulation loop.\n\t */\n\tbool running;\n\nprivate:\n\t/**\n\t * Initialize event handlers.\n\t */\n\tvoid init_event_handlers();\n\n\t/**\n\t * The simulation root directory.\n\t * Uses the openage fslike path abstraction that can mount paths into one.\n\t *\n\t * This means that this path does simultaneously lead to global assets,\n\t * home-folder-assets, settings, and basically the whole filesystem access.\n\t *\n\t * TODO: move this to a settings class, which then also hosts cvar and the options system.\n\t */\n\tutil::Path root_dir;\n\n\t/**\n\t * the simulation's cvar manager.\n\t */\n\tstd::shared_ptr<cvar::CVarManager> cvar_manager;\n\n\t/**\n\t * Time loop for getting the current simulation time and changing speed.\n\t */\n\tstd::shared_ptr<openage::time::TimeLoop> time_loop;\n\n\t/**\n\t * Event loop for processing events in the game.\n\t */\n\tstd::shared_ptr<openage::event::EventLoop> event_loop;\n\n\t/**\n\t * Factory for creating game entities.\n\t */\n\tstd::shared_ptr<gamestate::EntityFactory> entity_factory;\n\n\t/**\n\t * Factory for creating terrain.\n\t */\n\tstd::shared_ptr<gamestate::TerrainFactory> terrain_factory;\n\n\t/**\n\t * Mod manager.\n\t */\n\tstd::shared_ptr<assets::ModManager> mod_manager;\n\n\t// TODO: move somewhere sensible or remove\n\tstd::shared_ptr<gamestate::event::Spawner> spawner;\n\tstd::shared_ptr<gamestate::event::Commander> commander;\n\n\t// TODO: The game run by the engine\n\tstd::shared_ptr<gamestate::Game> game;\n\n\t/**\n\t * Mutex for thread-safe access to the simulation.\n\t */\n\tstd::shared_mutex mutex;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/system/CMakeLists.txt",
    "content": "add_sources(libopenage\n    activity.cpp\n    idle.cpp\n    move.cpp\n    types.cpp\n)\n"
  },
  {
    "path": "libopenage/gamestate/system/activity.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"activity.h\"\n\n#include <functional>\n#include <string>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/activity/node.h\"\n#include \"gamestate/activity/start_node.h\"\n#include \"gamestate/activity/task_node.h\"\n#include \"gamestate/activity/task_system_node.h\"\n#include \"gamestate/activity/types.h\"\n#include \"gamestate/activity/xor_event_gate.h\"\n#include \"gamestate/activity/xor_gate.h\"\n#include \"gamestate/component/internal/activity.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/system/idle.h\"\n#include \"gamestate/system/move.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::gamestate::system {\n\n\nvoid Activity::advance(const time::time_t &start_time,\n                       const std::shared_ptr<gamestate::GameEntity> &entity,\n                       const std::shared_ptr<openage::event::EventLoop> &loop,\n                       const std::shared_ptr<openage::gamestate::GameState> &state,\n                       const std::optional<openage::event::EventHandler::param_map> &ev_params) {\n\tauto activity_component = std::dynamic_pointer_cast<component::Activity>(\n\t\tentity->get_component(component::component_t::ACTIVITY));\n\tauto current_node = activity_component->get_node(start_time);\n\n\tif (current_node == nullptr) [[unlikely]] {\n\t\tthrow Error{ERR << \"No node defined in activity graph for entity \"\n\t\t                << std::to_string(entity->get_id()) << \" (t=\" << start_time << \")\"};\n\t}\n\n\t// TODO: this check should be moved to a more general pre-processing section\n\tif (current_node->get_type() == activity::node_t::XOR_EVENT_GATE) {\n\t\t// returning to a event gateway means that the event has been triggered\n\t\t// move to the next node here\n\t\tif (not ev_params.has_value()) {\n\t\t\tthrow Error{ERR << \"XorEventGate: No event parameters given on continue\"};\n\t\t}\n\n\t\tauto next_id = ev_params.value().get<size_t>(\"next\");\n\t\tcurrent_node = current_node->next(next_id);\n\n\t\t// cancel all other events that the manager may have been waiting for\n\t\tactivity_component->cancel_events(start_time);\n\t}\n\n\ttime::time_t event_wait_time = 0;\n\tauto stop = false;\n\twhile (not stop) {\n\t\tswitch (current_node->get_type()) {\n\t\tcase activity::node_t::START: {\n\t\t\tauto node = std::static_pointer_cast<activity::StartNode>(current_node);\n\t\t\tauto next_id = node->get_next();\n\t\t\tcurrent_node = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::END: {\n\t\t\t// TODO: if activities are nested, advance to parent activity\n\t\t\tstop = true;\n\t\t} break;\n\t\tcase activity::node_t::TASK_CUSTOM: {\n\t\t\tauto node = std::static_pointer_cast<activity::TaskCustom>(current_node);\n\t\t\tauto task = node->get_task_func();\n\t\t\ttask(start_time, entity);\n\t\t\tauto next_id = node->get_next();\n\t\t\tcurrent_node = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::TASK_SYSTEM: {\n\t\t\tauto node = std::static_pointer_cast<activity::TaskSystemNode>(current_node);\n\t\t\tauto task = node->get_system_id();\n\t\t\tevent_wait_time = Activity::handle_subsystem(start_time, entity, state, task);\n\t\t\tauto next_id = node->get_next();\n\t\t\tcurrent_node = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::XOR_GATE: {\n\t\t\tauto node = std::static_pointer_cast<activity::XorGate>(current_node);\n\t\t\tauto next_id = node->get_default()->get_id();\n\t\t\tfor (auto &condition : node->get_conditions()) {\n\t\t\t\tauto condition_func = condition.second;\n\t\t\t\tif (condition_func(start_time, entity)) {\n\t\t\t\t\tnext_id = condition.first;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tcurrent_node = node->next(next_id);\n\t\t} break;\n\t\tcase activity::node_t::XOR_EVENT_GATE: {\n\t\t\tauto node = std::static_pointer_cast<activity::XorEventGate>(current_node);\n\t\t\tauto event_primers = node->get_primers();\n\t\t\tfor (auto &primer : event_primers) {\n\t\t\t\tauto ev = primer.second(start_time + event_wait_time,\n\t\t\t\t                        entity,\n\t\t\t\t                        loop,\n\t\t\t\t                        state,\n\t\t\t\t                        primer.first);\n\t\t\t\tactivity_component->add_event(ev);\n\t\t\t}\n\n\t\t\t// exit and wait for event\n\t\t\tevent_wait_time = 0;\n\t\t\tstop = true;\n\t\t} break;\n\t\tdefault:\n\t\t\tthrow Error{ERR << \"Unhandled node type for node \" << current_node->str()};\n\t\t}\n\t}\n\n\t// save the current node in the component\n\tactivity_component->set_node(start_time, current_node);\n}\n\nconst time::time_t Activity::handle_subsystem(const time::time_t &start_time,\n                                              const std::shared_ptr<gamestate::GameEntity> &entity,\n                                              const std::shared_ptr<openage::gamestate::GameState> &state,\n                                              system_id_t system_id) {\n\tswitch (system_id) {\n\tcase system_id_t::IDLE:\n\t\treturn Idle::idle(entity, start_time);\n\t\tbreak;\n\tcase system_id_t::MOVE_COMMAND:\n\t\treturn Move::move_command(entity, state, start_time);\n\t\tbreak;\n\tcase system_id_t::MOVE_DEFAULT:\n\t\t// TODO: replace destination value with a parameter\n\t\treturn Move::move_default(entity, state, {1, 1, 1}, start_time);\n\t\tbreak;\n\tdefault:\n\t\tthrow Error{ERR << \"Unhandled subsystem \" << static_cast<int>(system_id)};\n\t}\n\n\treturn time::time_t::from_int(0);\n}\n\n\n} // namespace openage::gamestate::system\n"
  },
  {
    "path": "libopenage/gamestate/system/activity.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <optional>\n\n#include \"event/eventhandler.h\"\n#include \"gamestate/system/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\n\nnamespace event {\nclass EventLoop;\n}\n\nnamespace gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace system {\n\nclass Activity {\npublic:\n\t/**\n\t * Advance in the activity flow graph of the game entity.\n\t *\n\t * @param start_time Start time of change.\n\t * @param entity Game entity.\n\t */\n\tstatic void advance(const time::time_t &start_time,\n\t                    const std::shared_ptr<gamestate::GameEntity> &entity,\n\t                    const std::shared_ptr<openage::event::EventLoop> &loop,\n\t                    const std::shared_ptr<openage::gamestate::GameState> &state,\n\t                    const std::optional<openage::event::EventHandler::param_map> &ev_params = std::nullopt);\n\nprivate:\n\t/**\n\t * Run a built-in engine subsystem.\n\t *\n\t * @param start_time Start time of change.\n\t * @param entity Game entity.\n\t * @param state Game state.\n\t * @param system_id ID of the subsystem to run.\n\t *\n\t * @return Runtime of the change in simulation time.\n\t */\n\tstatic const time::time_t handle_subsystem(const time::time_t &start_time,\n\t                                           const std::shared_ptr<gamestate::GameEntity> &entity,\n\t                                           const std::shared_ptr<openage::gamestate::GameState> &state,\n\t                                           system_id_t system_id);\n};\n\n} // namespace system\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/system/idle.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"idle.h\"\n\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"gamestate/api/ability.h\"\n#include \"gamestate/api/animation.h\"\n#include \"gamestate/api/property.h\"\n#include \"gamestate/api/types.h\"\n#include \"gamestate/component/api/idle.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/game_entity.h\"\n\n\nnamespace openage::gamestate::system {\n\nconst time::time_t Idle::idle(const std::shared_ptr<gamestate::GameEntity> &entity,\n                              const time::time_t &start_time) {\n\tif (not entity->has_component(component::component_t::IDLE)) [[unlikely]] {\n\t\tthrow Error{ERR << \"Entity \" << entity->get_id() << \" has no idle component.\"};\n\t}\n\n\tauto idle_component = std::dynamic_pointer_cast<component::Idle>(\n\t\tentity->get_component(component::component_t::IDLE));\n\tauto ability = idle_component->get_ability();\n\tif (api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED)) {\n\t\tauto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED);\n\t\tauto animations = api::APIAbilityProperty::get_animations(property);\n\t\tauto animation_paths = api::APIAnimation::get_animation_paths(animations);\n\n\t\tif (animation_paths.size() > 0) [[likely]] {\n\t\t\tentity->render_update(start_time, animation_paths[0]);\n\t\t}\n\t}\n\n\t// TODO: play sound\n\n\treturn time::time_t::from_int(0);\n}\n\n} // namespace openage::gamestate::system\n"
  },
  {
    "path": "libopenage/gamestate/system/idle.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\n\nnamespace system {\n\nclass Idle {\npublic:\n\t/**\n\t * Let a game entity idle.\n\t *\n\t * This does not change the state of a unit. It only changes its animation and\n\t * sounds.\n\t *\n\t * @param entity Game entity.\n\t * @param start_time Start time of change.\n\t *\n\t * @return Runtime of the change in simulation time.\n\t */\n\tstatic const time::time_t idle(const std::shared_ptr<gamestate::GameEntity> &entity,\n\t                               const time::time_t &start_time);\n};\n\n} // namespace system\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/system/move.cpp",
    "content": "// Copyright 2023-2025 the openage authors. See copying.md for legal info.\n\n#include \"move.h\"\n\n#include <compare>\n#include <vector>\n\n#include <nyan/nyan.h>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"coord/phys.h\"\n#include \"curve/continuous.h\"\n#include \"curve/segmented.h\"\n#include \"gamestate/api/ability.h\"\n#include \"gamestate/api/animation.h\"\n#include \"gamestate/api/property.h\"\n#include \"gamestate/api/types.h\"\n#include \"gamestate/component/api/move.h\"\n#include \"gamestate/component/api/turn.h\"\n#include \"gamestate/component/internal/command_queue.h\"\n#include \"gamestate/component/internal/commands/move.h\"\n#include \"gamestate/component/internal/position.h\"\n#include \"gamestate/component/types.h\"\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/map.h\"\n#include \"pathfinding/path.h\"\n#include \"pathfinding/pathfinder.h\"\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::gamestate::system {\n\n\nstd::vector<coord::phys3> find_path(const std::shared_ptr<path::Pathfinder> &pathfinder,\n                                    path::grid_id_t grid_id,\n                                    const coord::phys3 &start,\n                                    const coord::phys3 &end,\n                                    const time::time_t &start_time) {\n\tauto start_tile = start.to_tile();\n\tauto end_tile = end.to_tile();\n\n\t// Search for a path between the start and end tiles\n\tpath::PathRequest request{\n\t\tgrid_id,\n\t\tstart_tile,\n\t\tend_tile,\n\t\tstart_time,\n\t};\n\tauto tile_path = pathfinder->get_path(request);\n\n\tif (tile_path.status != path::PathResult::FOUND) {\n\t\t// No path found\n\t\treturn {};\n\t}\n\n\t// Get the waypoints of the path\n\tstd::vector<coord::phys3> path;\n\tpath.reserve(tile_path.waypoints.size());\n\n\t// Start poition is first waypoint\n\tpath.push_back(start);\n\n\t// Pathfinder waypoints contain start and end tile; we can ignore them\n\tfor (size_t i = 1; i < tile_path.waypoints.size() - 1; ++i) {\n\t\tauto tile = tile_path.waypoints.at(i);\n\t\tpath.push_back(tile.to_phys3_center());\n\t}\n\n\t// End position is last waypoint\n\tpath.push_back(end);\n\n\treturn path;\n}\n\nconst time::time_t Move::move_command(const std::shared_ptr<gamestate::GameEntity> &entity,\n                                      const std::shared_ptr<openage::gamestate::GameState> &state,\n                                      const time::time_t &start_time) {\n\tauto command_queue = std::dynamic_pointer_cast<component::CommandQueue>(\n\t\tentity->get_component(component::component_t::COMMANDQUEUE));\n\tauto command = std::dynamic_pointer_cast<component::command::MoveCommand>(\n\t\tcommand_queue->pop_command(start_time));\n\n\tif (not command) [[unlikely]] {\n\t\tlog::log(MSG(warn) << \"Command is not a move command.\");\n\t\treturn time::time_t::from_int(0);\n\t}\n\n\treturn Move::move_default(entity, state, command->get_target(), start_time);\n}\n\n\nconst time::time_t Move::move_default(const std::shared_ptr<gamestate::GameEntity> &entity,\n                                      const std::shared_ptr<openage::gamestate::GameState> &state,\n                                      const coord::phys3 &destination,\n                                      const time::time_t &start_time) {\n\tif (not entity->has_component(component::component_t::MOVE)) [[unlikely]] {\n\t\tlog::log(WARN << \"Entity \" << entity->get_id() << \" has no move component.\");\n\t\treturn time::time_t::from_int(0);\n\t}\n\n\tauto turn_component = std::dynamic_pointer_cast<component::Turn>(\n\t\tentity->get_component(component::component_t::TURN));\n\tauto turn_ability = turn_component->get_ability();\n\tauto turn_speed = turn_ability.get<nyan::Float>(\"Turn.turn_speed\");\n\n\tauto move_component = std::dynamic_pointer_cast<component::Move>(\n\t\tentity->get_component(component::component_t::MOVE));\n\tauto move_ability = move_component->get_ability();\n\tauto move_speed = move_ability.get<nyan::Float>(\"Move.speed\");\n\tauto move_path_grid = move_ability.get<nyan::ObjectValue>(\"Move.path_type\");\n\n\tauto pos_component = std::dynamic_pointer_cast<component::Position>(\n\t\tentity->get_component(component::component_t::POSITION));\n\n\tauto &positions = pos_component->get_positions();\n\tauto &angles = pos_component->get_angles();\n\tauto current_pos = positions.get(start_time);\n\tauto current_angle = angles.get(start_time);\n\n\t// Find path\n\tauto map = state->get_map();\n\tauto pathfinder = map->get_pathfinder();\n\tauto grid_id = map->get_grid_id(move_path_grid->get_name());\n\tauto waypoints = find_path(pathfinder, grid_id, current_pos, destination, start_time);\n\n\t// use waypoints for movement\n\tdouble total_time = 0;\n\tpos_component->set_position(start_time, current_pos);\n\tfor (size_t i = 1; i < waypoints.size(); ++i) {\n\t\tauto prev_waypoint = waypoints[i - 1];\n\t\tauto cur_waypoint = waypoints[i];\n\n\t\tauto path_vector = cur_waypoint - prev_waypoint;\n\t\tauto path_angle = path_vector.to_angle();\n\n\t\t// rotation\n\t\tif (not turn_speed->is_infinite_positive()) {\n\t\t\tauto angle_diff = path_angle - current_angle;\n\t\t\tif (angle_diff < 0) {\n\t\t\t\t// get the positive difference\n\t\t\t\tangle_diff = angle_diff * -1;\n\t\t\t}\n\t\t\tif (angle_diff > 180) {\n\t\t\t\t// always use the smaller angle\n\t\t\t\tangle_diff = angle_diff - 360;\n\t\t\t\tangle_diff = angle_diff * -1;\n\t\t\t}\n\n\t\t\t// Set an intermediate position keyframe to halt the game entity\n\t\t\t// until the rotation is done\n\t\t\tdouble turn_time = angle_diff.to_double() / turn_speed->get();\n\t\t\ttotal_time += turn_time;\n\t\t\tpos_component->set_position(start_time + total_time, prev_waypoint);\n\n\t\t\t// update current angle for next waypoint\n\t\t\tcurrent_angle = path_angle;\n\t\t}\n\t\tpos_component->set_angle(start_time + total_time, path_angle);\n\n\t\t// movement\n\t\tdouble move_time = 0;\n\t\tif (not move_speed->is_infinite_positive()) {\n\t\t\tauto distance = path_vector.length();\n\t\t\tmove_time = distance / move_speed->get();\n\t\t}\n\t\ttotal_time += move_time;\n\n\t\tpos_component->set_position(start_time + total_time, cur_waypoint);\n\t}\n\n\t// properties\n\tauto ability = move_component->get_ability();\n\tif (api::APIAbility::check_property(ability, api::ability_property_t::ANIMATED)) {\n\t\tauto property = api::APIAbility::get_property(ability, api::ability_property_t::ANIMATED);\n\t\tauto animations = api::APIAbilityProperty::get_animations(property);\n\t\tauto animation_paths = api::APIAnimation::get_animation_paths(animations);\n\n\t\tif (animation_paths.size() > 0) [[likely]] {\n\t\t\tentity->render_update(start_time, animation_paths[0]);\n\t\t}\n\t}\n\n\treturn total_time;\n}\n\n} // namespace openage::gamestate::system\n"
  },
  {
    "path": "libopenage/gamestate/system/move.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"coord/phys.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::gamestate {\nclass GameEntity;\nclass GameState;\n\nnamespace system {\n\nclass Move {\npublic:\n\t/**\n\t * Move a game entity to a destination from a move command.\n\t *\n\t * @param entity Game entity.\n\t * @param state Game state.\n\t * @param start_time Start time of change.\n\t *\n\t * @return Runtime of the change in simulation time.\n\t */\n\tstatic const time::time_t move_command(const std::shared_ptr<gamestate::GameEntity> &entity,\n\t                                       const std::shared_ptr<openage::gamestate::GameState> &state,\n\t                                       const time::time_t &start_time);\n\n\t/**\n\t * Move a game entity to a destination.\n\t *\n\t * @param entity Game entity.\n\t * @param state Game state.\n\t * @param destination Destination coordinates.\n\t * @param start_time Start time of change.\n\t *\n\t * @return Runtime of the change in simulation time.\n\t */\n\tstatic const time::time_t move_default(const std::shared_ptr<gamestate::GameEntity> &entity,\n\t                                       const std::shared_ptr<openage::gamestate::GameState> &state,\n\t                                       const coord::phys3 &destination,\n\t                                       const time::time_t &start_time);\n};\n\n} // namespace system\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/system/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::gamestate::system {\n\n// this file is intended to be empty\n\n} // namespace openage::gamestate::system\n"
  },
  {
    "path": "libopenage/gamestate/system/types.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage::gamestate::system {\n\n/**\n * Associates a system function with an identifier.\n */\nenum class system_id_t {\n\tNONE,\n\n\tIDLE,\n\n\tMOVE_COMMAND,\n\tMOVE_DEFAULT,\n\n\tACTIVITY_ADVANCE,\n};\n\n} // namespace openage::gamestate::system\n"
  },
  {
    "path": "libopenage/gamestate/terrain.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"terrain.h\"\n\n#include <algorithm>\n#include <array>\n#include <cstddef>\n\n#include \"error/error.h\"\n\n#include \"gamestate/terrain_chunk.h\"\n#include \"renderer/render_factory.h\"\n\n\nnamespace openage::gamestate {\n\nTerrain::Terrain() :\n\tsize{0, 0},\n\tchunks{} {\n\t// TODO: Get actual size of terrain.\n}\n\nTerrain::Terrain(const util::Vector2s &size,\n                 std::vector<std::shared_ptr<TerrainChunk>> &&chunks) :\n\tsize{size},\n\tchunks{std::move(chunks)} {\n\t// Check if chunk is correct\n\tcoord::tile_delta current_offset{0, 0};\n\tutil::Vector2s all_chunks_size{0, 0};\n\tif (this->chunks.size() > 0) {\n\t\tall_chunks_size = this->chunks[0]->get_size();\n\t}\n\n\tfor (const auto &chunk : this->chunks) {\n\t\tauto chunk_size = chunk->get_size();\n\n\t\t// Check chunk delta\n\t\tauto chunk_offset = chunk->get_offset();\n\t\tif (current_offset != chunk_offset) [[unlikely]] {\n\t\t\tthrow error::Error{ERR << \"Chunk offset of chunk \" << chunk->get_offset()\n\t\t\t                       << \" does not match position in vector: \" << chunk_offset\n\t\t\t                       << \" (expected: \" << current_offset << \")\"};\n\t\t}\n\n\t\tcurrent_offset.ne += chunk_size[0];\n\n\t\t// Wrap around to the next row\n\t\tif (current_offset.ne == static_cast<coord::tile_t>(size[0])) {\n\t\t\tcurrent_offset.ne = 0;\n\t\t\tcurrent_offset.se += chunk_size[1];\n\t\t}\n\t\t// Check if chunk size is correct\n\t\telse if (chunk_size != chunk_size) [[unlikely]] {\n\t\t\tthrow error::Error{ERR << \"Chunk size of chunk \" << chunk->get_offset()\n\t\t\t                       << \" is not equal to the first chunk size: \" << chunk_size\n\t\t\t                       << \" (expected: \" << all_chunks_size << \")\"};\n\t\t}\n\t\telse if (chunk_size[0] != chunk_size[1]) {\n\t\t\tthrow error::Error{ERR << \"Chunk size of chunk \" << chunk->get_offset()\n\t\t\t                       << \" is not square: \" << chunk_size};\n\t\t}\n\n\t\t// Check terrain boundaries\n\t\tif (current_offset.ne > static_cast<coord::tile_t>(size[0])) {\n\t\t\tthrow error::Error{ERR << \"Width of chunk \" << chunk->get_offset()\n\t\t\t                       << \" exceeds terrain width: \" << chunk_size[0]\n\t\t\t                       << \" (max width: \" << size[0] << \")\"};\n\t\t}\n\t\telse if (current_offset.se > static_cast<coord::tile_t>(size[1])) {\n\t\t\tthrow error::Error{ERR << \"Height of chunk \" << chunk->get_offset()\n\t\t\t                       << \" exceeds terrain height: \" << chunk_size[1]\n\t\t\t                       << \" (max height: \" << size[1] << \")\"};\n\t\t}\n\t}\n}\n\nconst util::Vector2s &Terrain::get_size() const {\n\treturn this->size;\n}\n\nconst util::Vector2s Terrain::get_chunks_size() const {\n\tauto chunk_size = this->chunks[0]->get_size();\n\treturn {this->size[0] / chunk_size[0], this->size[1] / chunk_size[1]};\n}\n\nvoid Terrain::add_chunk(const std::shared_ptr<TerrainChunk> &chunk) {\n\tthis->chunks.push_back(chunk);\n}\n\nconst std::vector<std::shared_ptr<TerrainChunk>> &Terrain::get_chunks() const {\n\treturn this->chunks;\n}\n\nconst std::shared_ptr<TerrainChunk> &Terrain::get_chunk(size_t idx) const {\n\treturn this->chunks.at(idx);\n}\n\nconst std::shared_ptr<TerrainChunk> &Terrain::get_chunk(const coord::chunk &pos) const {\n\tsize_t index = pos.ne + pos.se * this->size[0];\n\n\treturn this->get_chunk(index);\n}\n\nvoid Terrain::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tfor (auto &chunk : this->get_chunks()) {\n\t\tauto render_entity = render_factory->add_terrain_render_entity(chunk->get_size(),\n\t\t                                                               chunk->get_offset());\n\t\tchunk->set_render_entity(render_entity);\n\n\t\tchunk->render_update(time::TIME_ZERO);\n\t}\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/terrain.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <vector>\n\n#include \"coord/chunk.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass RenderFactory;\n} // namespace renderer\n\nnamespace gamestate {\nclass TerrainChunk;\n\n/**\n * Entity for managing the map terrain of a game.\n */\nclass Terrain {\npublic:\n\t/**\n\t * Create a new terrain.\n\t */\n\tTerrain();\n\t~Terrain() = default;\n\n\t/**\n\t * Create a new terrain.\n\t *\n\t * Chunks must conform to these constraints:\n\t *  - All chunks that are not last in a row OR columns must have the same size (width x height).\n\t *  - All chunks that are not last in a row OR columns must be square (width == height).\n\t *  - The last chunk in a row may have a different width.\n\t *  - The last chunk in a column may have a different height.\n\t *\n\t * @param size Total size of the terrain in tiles (width x height).\n\t * @param chunks Terrain chunks.\n\t */\n\tTerrain(const util::Vector2s &size,\n\t        std::vector<std::shared_ptr<TerrainChunk>> &&chunks);\n\n\t/**\n\t * Get the size of the terrain (in tiles).\n\t *\n\t * @return Terrain tile size (width x height).\n\t */\n\tconst util::Vector2s &get_size() const;\n\n\t/**\n\t * Get the size of the terrain (in chunks).\n\t *\n\t * @return Terrain chunk size (width x height).\n\t */\n\tconst util::Vector2s get_chunks_size() const;\n\n\t/**\n\t * Add a chunk to the terrain.\n\t *\n\t * @param chunk New chunk.\n\t */\n\tvoid add_chunk(const std::shared_ptr<TerrainChunk> &chunk);\n\n\t/**\n\t * Get the chunks of the terrain.\n\t *\n\t * @return Terrain chunks.\n\t */\n\tconst std::vector<std::shared_ptr<TerrainChunk>> &get_chunks() const;\n\n\t/**\n\t * Get a specific chunk of the terrain.\n\t *\n\t * @param idx Index of the chunk.\n\t *\n\t * @return Terrain chunk.\n\t */\n\tconst std::shared_ptr<TerrainChunk> &get_chunk(size_t idx) const;\n\n\t/**\n\t * Get a specific chunk of the terrain.\n\t *\n\t * @param pos Position of the chunk in the terrain.\n\t *\n\t * @return Terrain chunk.\n\t */\n\tconst std::shared_ptr<TerrainChunk> &get_chunk(const coord::chunk &pos) const;\n\n\t/**\n\t * Attach a renderer which enables graphical display.\n\t *\n\t * TODO: We currently have to do attach this here too in addition to the terrain\n\t *       factory because the renderer gets attached AFTER the terrain is\n\t *       already created. In the future, the game should wait for the renderer\n\t *       before creating the terrain.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * Total size of the terrain in tiles (width x height).\n\t *\n\t * Origin is the top left corner (left corner with camera projection).\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Subdivision of the main terrain entity.\n\t *\n\t * Ordered in rows from left to right, top to bottom.\n\t */\n\tstd::vector<std::shared_ptr<TerrainChunk>> chunks;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/terrain_chunk.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"terrain_chunk.h\"\n\n\nnamespace openage::gamestate {\n\nTerrainChunk::TerrainChunk(const util::Vector2s size,\n                           const coord::tile_delta offset,\n                           const std::vector<TerrainTile> &&tiles) :\n\tsize{size},\n\toffset{offset},\n\ttiles{std::move(tiles)} {\n\tif (this->size[0] > MAX_CHUNK_WIDTH || this->size[1] > MAX_CHUNK_HEIGHT) {\n\t\tthrow Error(MSG(err) << \"Terrain chunk size exceeds maximum size: \"\n\t\t                     << this->size[0] << \"x\" << this->size[1] << \" > \"\n\t\t                     << MAX_CHUNK_WIDTH << \"x\" << MAX_CHUNK_HEIGHT);\n\t}\n}\n\nvoid TerrainChunk::set_render_entity(const std::shared_ptr<renderer::terrain::RenderEntity> &entity) {\n\tthis->render_entity = entity;\n}\n\nconst util::Vector2s &TerrainChunk::get_size() const {\n\treturn this->size;\n}\n\nconst coord::tile_delta &TerrainChunk::get_offset() const {\n\treturn this->offset;\n}\n\nconst std::vector<TerrainTile> &TerrainChunk::get_tiles() const {\n\treturn this->tiles;\n}\n\nconst TerrainTile &TerrainChunk::get_tile(size_t idx) const {\n\treturn this->tiles.at(idx);\n}\n\nconst TerrainTile &TerrainChunk::get_tile(const coord::tile &pos) const {\n\treturn this->tiles.at(pos.ne + pos.se * this->size[0]);\n}\n\nvoid TerrainChunk::render_update(const time::time_t &time) {\n\tif (this->render_entity != nullptr) {\n\t\t// TODO: Update individual tiles instead of the whole chunk\n\t\tstd::vector<std::pair<terrain_elevation_t, std::string>> tiles;\n\t\ttiles.reserve(this->tiles.size());\n\t\tfor (const auto &tile : this->tiles) {\n\t\t\ttiles.emplace_back(tile.elevation, tile.terrain_asset_path);\n\t\t}\n\n\t\tthis->render_entity->update(this->size,\n\t\t                            tiles,\n\t\t                            time);\n\t}\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/terrain_chunk.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"coord/tile.h\"\n#include \"gamestate/terrain_tile.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"time/time.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::gamestate {\n\nconst size_t MAX_CHUNK_WIDTH = 16;\nconst size_t MAX_CHUNK_HEIGHT = 16;\n\n\n/**\n * Subdivision of the main terrain entity.\n */\nclass TerrainChunk {\npublic:\n\tTerrainChunk(const util::Vector2s size,\n\t             const coord::tile_delta offset,\n\t             const std::vector<TerrainTile> &&tiles);\n\t~TerrainChunk() = default;\n\n\t/**\n\t * Set the current render entity of the terrain.\n\t *\n\t * @param entity New render entity.\n\t */\n\tvoid set_render_entity(const std::shared_ptr<renderer::terrain::RenderEntity> &entity);\n\n\t/**\n\t * Get the size of this terrain chunk.\n\t *\n\t * @return Size of the terrain chunk (in tiles).\n\t */\n\tconst util::Vector2s &get_size() const;\n\n\t/**\n\t * Get the offset of this terrain chunk to the terrain origin.\n\t *\n\t * @return Offset of the terrain chunk (in tiles).\n\t */\n\tconst coord::tile_delta &get_offset() const;\n\n\t/**\n\t * Get the tiles of this terrain chunk.\n\t *\n\t * @return Terrain tiles.\n\t */\n\tconst std::vector<TerrainTile> &get_tiles() const;\n\n\t/**\n\t * Get the tile at the given index.\n\t *\n\t * @param idx Index of the tile.\n\t *\n\t * @return Terrain tile.\n\t */\n\tconst TerrainTile &get_tile(size_t idx) const;\n\n\t/**\n\t * Get the tile at the given position.\n\t *\n\t * @param pos Position of the tile.\n\t *\n\t * @return Terrain tile.\n\t */\n\tconst TerrainTile &get_tile(const coord::tile &pos) const;\n\n\t/**\n\t * Update the render entity.\n\t *\n\t * @param time Simulation time of the update.\n\t */\n\tvoid render_update(const time::time_t &time);\n\nprivate:\n\t/**\n\t * Size of the terrain chunk.\n\t * Origin is the left corner.\n\t * x = top left edge; y = top right edge.\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Offset of the terrain chunk to the origin.\n\t */\n\tcoord::tile_delta offset;\n\n\t/**\n\t * Terrain tile info of the terrain chunk.\n\t *\n\t * Layout is row-major.\n\t */\n\tstd::vector<TerrainTile> tiles;\n\n\t/**\n\t * Render entity for pushing updates to the renderer. Can be \\p nullptr.\n\t */\n\tstd::shared_ptr<renderer::terrain::RenderEntity> render_entity;\n};\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/terrain_factory.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"terrain_factory.h\"\n\n#include <mutex>\n\n#include <nyan/nyan.h>\n\n#include \"gamestate/api/terrain.h\"\n#include \"gamestate/terrain.h\"\n#include \"gamestate/terrain_chunk.h\"\n#include \"gamestate/terrain_tile.h\"\n#include \"renderer/render_factory.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"time/time.h\"\n\n#include \"assets/mod_manager.h\"\n#include \"gamestate/game_state.h\"\n\n\nnamespace openage::gamestate {\n\n// TODO: Remove test terrain references\nstatic const std::vector<std::string> test_terrain_paths = {\n\t\"../test/textures/test_terrain.terrain\",\n\t\"../test/textures/test_terrain2.terrain\",\n};\n\nstatic const std::vector<nyan::fqon_t> aoe1_test_terrain = {\n\t\"aoe1_base.data.terrain.forest.forest.Forest\",\n\t\"aoe1_base.data.terrain.grass.grass.Grass\",\n\t\"aoe1_base.data.terrain.dirt.dirt.Dirt\",\n\t\"aoe1_base.data.terrain.water.water.Water\",\n};\nstatic const std::vector<nyan::fqon_t> de1_test_terrain = {\n\t\"aoe1_base.data.terrain.desert.desert.Desert\",\n\t\"aoe1_base.data.terrain.grass.grass.Grass\",\n\t\"aoe1_base.data.terrain.dirt.dirt.Dirt\",\n\t\"aoe1_base.data.terrain.water.water.Water\",\n};\nstatic const std::vector<nyan::fqon_t> aoe2_test_terrain = {\n\t\"aoe2_base.data.terrain.foundation.foundation.Foundation\",\n\t\"aoe2_base.data.terrain.grass.grass.Grass\",\n\t\"aoe2_base.data.terrain.dirt.dirt.Dirt\",\n\t\"aoe2_base.data.terrain.water.water.Water\",\n};\nstatic const std::vector<nyan::fqon_t> de2_test_terrain = {\n\t\"de2_base.data.terrain.foundation.foundation.Foundation\",\n\t\"de2_base.data.terrain.grass.grass.Grass\",\n\t\"de2_base.data.terrain.dirt.dirt.Dirt\",\n\t\"de2_base.data.terrain.water.water.Water\",\n};\nstatic const std::vector<nyan::fqon_t> hd_test_terrain = {\n\t\"hd_base.data.terrain.foundation.foundation.Foundation\",\n\t\"hd_base.data.terrain.grass.grass.Grass\",\n\t\"hd_base.data.terrain.dirt.dirt.Dirt\",\n\t\"hd_base.data.terrain.water.water.Water\",\n};\nstatic const std::vector<nyan::fqon_t> swgb_test_terrain = {\n\t\"swgb_base.data.terrain.foundation.foundation.Foundation\",\n\t\"swgb_base.data.terrain.grass2.grass2.Grass2\",\n\t\"swgb_base.data.terrain.desert0.desert0.Desert0\",\n\t\"swgb_base.data.terrain.water1.water1.Water1\",\n};\nstatic const std::vector<nyan::fqon_t> trial_test_terrain = {\n\t\"trial_base.data.terrain.foundation.foundation.Foundation\",\n\t\"trial_base.data.terrain.grass.grass.Grass\",\n\t\"trial_base.data.terrain.dirt.dirt.Dirt\",\n\t\"trial_base.data.terrain.water.water.Water\",\n};\n\n// TODO: Remove hardcoded test texture references\nstatic std::vector<nyan::fqon_t> test_terrains; // declare static so we only have to do this once\nstatic bool has_graphics = false;\n\nvoid build_test_terrains(const std::shared_ptr<GameState> &gstate) {\n\tauto modpack_ids = gstate->get_mod_manager()->get_load_order();\n\tfor (auto &modpack_id : modpack_ids) {\n\t\tif (modpack_id == \"aoe1_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     aoe1_test_terrain.begin(),\n\t\t\t                     aoe1_test_terrain.end());\n\t\t\thas_graphics = false;\n\t\t}\n\t\telse if (modpack_id == \"de1_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     de1_test_terrain.begin(),\n\t\t\t                     de1_test_terrain.end());\n\t\t\thas_graphics = false;\n\t\t}\n\t\telse if (modpack_id == \"aoe2_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     aoe2_test_terrain.begin(),\n\t\t\t                     aoe2_test_terrain.end());\n\t\t\thas_graphics = true;\n\t\t}\n\t\telse if (modpack_id == \"de2_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     de2_test_terrain.begin(),\n\t\t\t                     de2_test_terrain.end());\n\t\t\thas_graphics = false;\n\t\t}\n\t\telse if (modpack_id == \"hd_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     hd_test_terrain.begin(),\n\t\t\t                     hd_test_terrain.end());\n\t\t\thas_graphics = true;\n\t\t}\n\t\telse if (modpack_id == \"swgb_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     swgb_test_terrain.begin(),\n\t\t\t                     swgb_test_terrain.end());\n\t\t\thas_graphics = true;\n\t\t}\n\t\telse if (modpack_id == \"trial_base\") {\n\t\t\ttest_terrains.insert(test_terrains.end(),\n\t\t\t                     trial_test_terrain.begin(),\n\t\t\t                     trial_test_terrain.end());\n\t\t\thas_graphics = true;\n\t\t}\n\t}\n}\n\n// Layout of terrain tiles on chunk 0\n// values are the terrain index\nstatic const std::array<size_t, 100> layout_chunk0{\n\t// clang-format off\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 1, 1, 1, 1, 1, 1, 1, 1, 0,\n    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,\n    0, 0, 0, 1, 1, 1, 1, 0, 0, 0,\n    0, 0, 0, 0, 1, 1, 0, 0, 0, 2,\n    0, 0, 0, 0, 1, 1, 0, 0, 0, 2,\n    0, 0, 0, 1, 3, 3, 1, 0, 0, 0,\n    0, 0, 1, 1, 3, 3, 1, 1, 0, 0,\n    0, 1, 1, 1, 3, 3, 1, 1, 1, 0,\n    0, 0, 1, 1, 3, 3, 1, 0, 0, 0,\n\t// clang-format on\n};\n\n// Layout of terrain tiles on chunk 1\n// values are the terrain index\nstatic const std::array<size_t, 100> layout_chunk1{\n\t// clang-format off\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n    2, 2, 2, 2, 0, 0, 0, 0, 0, 0,\n    2, 2, 2, 2, 2, 0, 0, 0, 0, 0,\n    0, 0, 0, 2, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 0, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 0, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 0, 2, 2, 0, 0, 0, 0,\n\t// clang-format on\n};\n\n// Layout of terrain tiles on chunk 2\n// values are the terrain index\nstatic const std::array<size_t, 100> layout_chunk2{\n\t// clang-format off\n    1, 1, 1, 1, 3, 3, 1, 0, 0, 0,\n    1, 1, 1, 1, 3, 3, 1, 1, 0, 0,\n    1, 1, 1, 1, 3, 3, 1, 1, 0, 0,\n    1, 1, 1, 3, 3, 3, 3, 1, 0, 0,\n    1, 3, 3, 3, 3, 3, 3, 3, 1, 2,\n    1, 3, 3, 3, 2, 2, 2, 3, 1, 2,\n    3, 3, 3, 3, 2, 2, 2, 3, 1, 2,\n    1, 3, 3, 3, 2, 2, 2, 3, 1, 1,\n    1, 3, 3, 3, 3, 3, 3, 3, 3, 1,\n    1, 3, 3, 3, 3, 3, 3, 3, 1, 0,\n\t// clang-format on\n};\n\n// Layout of terrain tiles on chunk 3\n// values are the terrain index\nstatic const std::array<size_t, 100> layout_chunk3{\n\t// clang-format off\n    0, 0, 0, 0, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 2, 2, 2, 0, 0, 0, 0,\n    0, 0, 2, 2, 2, 2, 1, 1, 2, 0,\n    0, 2, 2, 2, 2, 2, 1, 2, 0, 0,\n    2, 2, 2, 2, 2, 2, 2, 0, 0, 0,\n    2, 2, 2, 2, 2, 2, 2, 0, 0, 0,\n    0, 0, 2, 2, 2, 2, 2, 0, 0, 0,\n    0, 0, 0, 2, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 2, 2, 2, 0, 0, 0, 0,\n    0, 0, 0, 2, 2, 2, 0, 0, 0, 0,\n\t// clang-format on\n};\n\n\nstatic const std::vector<std::array<size_t, 100>> layout_chunks{\n\tlayout_chunk0,\n\tlayout_chunk1,\n\tlayout_chunk2,\n\tlayout_chunk3,\n};\n\n\nstd::shared_ptr<Terrain> TerrainFactory::add_terrain(const util::Vector2s &size,\n                                                     std::vector<std::shared_ptr<TerrainChunk>> &&chunks) {\n\t// TODO: Replace this with a proper terrain generator.\n\tauto terrain = std::make_shared<Terrain>(size, std::move(chunks));\n\n\treturn terrain;\n}\n\nstd::shared_ptr<TerrainChunk> TerrainFactory::add_chunk(const std::shared_ptr<GameState> &gstate,\n                                                        const util::Vector2s size,\n                                                        const coord::tile_delta offset) {\n\tstd::string terrain_info_path;\n\n\t// TODO: Remove test texture references\n\t// ==========\n\tnyan::Object terrain_obj;\n\tif (test_terrains.empty()) {\n\t\tbuild_test_terrains(gstate);\n\t}\n\n\t// fill the chunk with tiles\n\tstd::vector<TerrainTile> tiles{};\n\ttiles.reserve(size[0] * size[1]);\n\n\tstatic size_t test_chunk_index = 0;\n\tif (not test_terrains.empty()) {\n\t\t// use one of the modpack terrain textures\n\t\tif (test_chunk_index >= layout_chunks.size()) {\n\t\t\ttest_chunk_index = 0;\n\t\t}\n\n\t\tfor (size_t i = 0; i < size[0] * size[1]; ++i) {\n\t\t\tsize_t terrain_index = layout_chunks.at(test_chunk_index).at(i);\n\t\t\tterrain_obj = gstate->get_db_view()->get_object(test_terrains.at(terrain_index));\n\n\t\t\tif (has_graphics) {\n\t\t\t\tterrain_info_path = api::APITerrain::get_terrain_path(terrain_obj);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tterrain_info_path = test_terrain_paths.at(test_chunk_index % test_terrain_paths.size());\n\t\t\t}\n\n\t\t\ttiles.push_back({terrain_obj, terrain_info_path, terrain_elevation_t::zero()});\n\t\t}\n\n\t\ttest_chunk_index += 1;\n\t}\n\t// ==========\n\n\tauto chunk = std::make_shared<TerrainChunk>(size, offset, std::move(tiles));\n\n\tif (this->render_factory) {\n\t\tauto render_entity = this->render_factory->add_terrain_render_entity(size, offset);\n\t\tchunk->set_render_entity(render_entity);\n\n\t\tchunk->render_update(time::TIME_ZERO);\n\t}\n\n\treturn chunk;\n}\n\nvoid TerrainFactory::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->render_factory = render_factory;\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/terrain_factory.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n\n#include \"coord/tile.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage {\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace gamestate {\nclass GameState;\nclass Terrain;\nclass TerrainChunk;\n\n/**\n * Creates terrain data (tiles, chunks, etc.) to generate a map.\n */\nclass TerrainFactory {\npublic:\n\t/**\n\t * Create a new terrain factory.\n\t */\n\tTerrainFactory() = default;\n\t~TerrainFactory() = default;\n\n\t/**\n\t * Create a new empty terrain object.\n\t *\n\t * @return New terrain object.\n\t */\n\tstd::shared_ptr<Terrain> add_terrain(const util::Vector2s &size,\n\t                                     std::vector<std::shared_ptr<TerrainChunk>> &&chunks);\n\n\t/**\n\t * Create a new empty terrain chunk.\n\t *\n\t * @param size Size of the chunk.\n\t * @param offset Offset of the chunk.\n\t *\n\t * @return New terrain chunk.\n\t */\n\tstd::shared_ptr<TerrainChunk> add_chunk(const std::shared_ptr<GameState> &gstate,\n\t                                        const util::Vector2s size,\n\t                                        const coord::tile_delta offset);\n\n\t// TODO: Add tiles\n\t// std::shared_ptr<TerrainTile> add_tile(const std::shared_ptr<openage::event::EventLoop> &loop,\n\t//                                       const std::shared_ptr<GameState> &state,\n\t//                                       const nyan::fqon_t &nyan_entity);\n\n\t/**\n\t * Attach a render factory for graphical display.\n\t *\n\t * This enables rendering for all created terrain chunks.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * Factory for creating connector objects to the renderer which make game entities displayable.\n\t */\n\tstd::shared_ptr<renderer::RenderFactory> render_factory;\n\n\t/**\n\t * Mutex for thread safety.\n\t */\n\tstd::shared_mutex mutex;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/terrain_tile.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"terrain_tile.h\"\n\n\nnamespace openage::gamestate {\n\n// This file is intentionally empty.\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/terrain_tile.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <optional>\n\n#include <nyan/nyan.h>\n\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::gamestate {\n\nusing terrain_elevation_t = util::FixedPoint<uint64_t, 16>;\n\n/**\n * A single terrain tile.\n */\nstruct TerrainTile {\n\t/**\n\t * Terrain definition used by this tile.\n\t */\n\tnyan::Object terrain;\n\n\t/**\n\t * Path to the terrain asset used by this tile.\n\t *\n\t * TODO: Remove this and fetch the asset path from the terrain definition.\n\t */\n\tstd::string terrain_asset_path;\n\n\t/**\n\t * Height of this tile on the terrain.\n\t */\n\tterrain_elevation_t elevation;\n};\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\nnamespace openage::gamestate {\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/types.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\nnamespace openage::gamestate {\n\n/**\n * Game entity IDs.\n */\nusing entity_id_t = uint64_t;\n\n/**\n * Player IDs.\n */\nusing player_id_t = uint64_t;\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/universe.cpp",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#include \"universe.h\"\n\n#include \"gamestate/terrain.h\"\n#include \"gamestate/world.h\"\n#include \"renderer/render_factory.h\"\n\nnamespace openage::gamestate {\n\nUniverse::Universe(const std::shared_ptr<GameState> &state) :\n\tworld{std::make_shared<World>(state)} {\n}\n\nstd::shared_ptr<World> Universe::get_world() {\n\treturn this->world;\n}\n\nstd::shared_ptr<Terrain> Universe::get_terrain() {\n\treturn this->terrain;\n}\n\nvoid Universe::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tthis->render_factory = render_factory;\n\n\tthis->world->attach_renderer(render_factory);\n}\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/universe.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\nnamespace openage {\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace gamestate {\nclass GameState;\nclass Terrain;\nclass World;\n\n/**\n * Entity for managing the \"physical\" game world entities (units, buildings, etc.) as well as\n * conceptual entities (players, resources, ...).\n *\n * TODO: Remove Universe and other subclasses.\n */\nclass Universe {\npublic:\n\t/**\n\t * Create a new universe.\n\t *\n\t * @param state State of the game.\n\t */\n\tUniverse(const std::shared_ptr<GameState> &state);\n\t~Universe() = default;\n\n\t/**\n\t * Get the object controlling entities in the game world (i.e. units, buildings, etc.).\n\t *\n\t * @return World object.\n\t */\n\tstd::shared_ptr<World> get_world();\n\n\t/**\n\t * Get the object controlling the terrain.\n\t *\n\t * @return Terrain object.\n\t */\n\tstd::shared_ptr<Terrain> get_terrain();\n\n\t/**\n\t * Attach a renderer which enables graphical display options for all ingame entities.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * Manages \"physical\" entities inside the game world (units, buildings, ambient, ...),\n\t * excluding terrain.\n\t */\n\tstd::shared_ptr<World> world;\n\n\t/**\n\t * Manages the terrain.\n\t */\n\tstd::shared_ptr<Terrain> terrain;\n\n\t/**\n\t * Factory for creating connector objects to the renderer which make game entities displayable.\n\t */\n\tstd::shared_ptr<renderer::RenderFactory> render_factory;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/gamestate/world.cpp",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#include \"world.h\"\n\n#include <unordered_map>\n#include <utility>\n\n#include \"gamestate/game_entity.h\"\n#include \"gamestate/game_state.h\"\n#include \"renderer/render_factory.h\"\n\n\nnamespace openage::gamestate {\n\nWorld::World(const std::shared_ptr<GameState> &state) :\n\tstate{state} {\n\t// TODO\n}\n\nvoid World::attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory) {\n\tthis->render_factory = render_factory;\n\n\tfor (auto entity_ref : state->get_game_entities()) {\n\t\tauto world_render_entity = this->render_factory->add_world_render_entity();\n\t\tentity_ref.second->set_render_entity(world_render_entity);\n\t}\n}\n\n\n} // namespace openage::gamestate\n"
  },
  {
    "path": "libopenage/gamestate/world.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n\nnamespace openage {\n\nnamespace renderer {\nclass RenderFactory;\n}\n\nnamespace gamestate {\nclass GameState;\n\n/**\n * Entity for managing \"physical\" things (units, buildings) inside\n * the game.\n */\nclass World {\npublic:\n\t/**\n\t * Create a new world.\n\t *\n\t * @param state State of the game.\n\t */\n\tWorld(const std::shared_ptr<GameState> &state);\n\t~World() = default;\n\n\t/**\n\t * Attach a renderer which enables graphical display options for all ingame entities.\n\t *\n\t * @param render_factory Factory for creating connector objects for gamestate->renderer\n\t *                       communication.\n\t */\n\tvoid attach_renderer(const std::shared_ptr<renderer::RenderFactory> &render_factory);\n\nprivate:\n\t/**\n\t * State of the current game.\n\t */\n\tstd::shared_ptr<GameState> state;\n\n\t/**\n\t * Factory for creating connector objects to the renderer which make game entities displayable.\n\t */\n\tstd::shared_ptr<renderer::RenderFactory> render_factory;\n};\n\n} // namespace gamestate\n} // namespace openage\n"
  },
  {
    "path": "libopenage/input/CMakeLists.txt",
    "content": "add_sources(libopenage\n    action.cpp\n\tevent.cpp\n\tinput_context.cpp\n\tinput_manager.cpp\n\ttests.cpp\n    text_to_event.cpp\n)\n\nadd_subdirectory(\"controller\")\n"
  },
  {
    "path": "libopenage/input/action.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"input/action.h\"\n\nnamespace openage::input {\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/action.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <unordered_map>\n\n#include \"input/event.h\"\n\nnamespace openage::input {\n\nusing action_flags_t = std::unordered_map<std::string, std::string>;\nusing action_func_t = std::function<void(const event_arguments &args)>;\n\n\n/**\n * Action types.\n *\n * All action types (except \\p input_action_t::CUSTOM) have a default\n * action.\n */\nenum class input_action_t {\n\tPUSH_CONTEXT,\n\tPOP_CONTEXT,\n\tREMOVE_CONTEXT,\n\tGAME,\n\tCAMERA,\n\tHUD,\n\tGUI,\n\tCUSTOM,\n};\n\n/**\n * Action taken by the input manager when receiving an input.\n *\n * @param action_type Action type. May determine a default action if \\p action is not defined.\n * @param action Executes custom code for the action based on the received event (optional).\n * @param flags Additional parameters for the (default) action.\n */\nstruct input_action {\n\tconst input_action_t action_type;\n\tconst std::optional<action_func_t> action = std::nullopt;\n\tconst action_flags_t flags = {};\n};\n\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/controller/CMakeLists.txt",
    "content": "add_subdirectory(\"camera\")\nadd_subdirectory(\"game\")\nadd_subdirectory(\"hud\")\n"
  },
  {
    "path": "libopenage/input/controller/camera/CMakeLists.txt",
    "content": "add_sources(libopenage\n    binding_context.cpp\n    binding.cpp\n    controller.cpp\n)\n"
  },
  {
    "path": "libopenage/input/controller/camera/binding.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding.h\"\n\nnamespace openage::input::camera {\n\n} // namespace openage::input::camera\n"
  },
  {
    "path": "libopenage/input/controller/camera/binding.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <unordered_map>\n\n#include \"event/event.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::camera {\n\nusing binding_flags_t = std::unordered_map<std::string, std::string>;\nusing binding_func_t = std::function<void(const event_arguments &e)>;\n\n\n/**\n * Action taken by the controller when receiving an input.\n *\n * @param action Maps an input event to a camera action.\n * @param flags Additional parameters for the transformation.\n */\nstruct binding_action {\n\tconst binding_func_t action;\n\tconst binding_flags_t flags = {};\n};\n\n} // namespace openage::input::camera\n"
  },
  {
    "path": "libopenage/input/controller/camera/binding_context.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding_context.h\"\n\n\nnamespace openage::input::camera {\n\nBindingContext::BindingContext() :\n\tby_event{} {}\n\nvoid BindingContext::bind(const Event &ev, const binding_action bind) {\n\tthis->by_event.emplace(std::make_pair(ev, bind));\n}\n\nvoid BindingContext::bind(const event_class &cl, const binding_action bind) {\n\tthis->by_class.emplace(std::make_pair(cl, bind));\n}\n\nbool BindingContext::is_bound(const Event &ev) const {\n\treturn this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl);\n}\n\nconst binding_action &BindingContext::lookup(const Event &ev) const {\n\tauto event_lookup = this->by_event.find(ev);\n\tif (event_lookup != std::end(this->by_event)) {\n\t\treturn (*event_lookup).second;\n\t}\n\n\tfor (auto eclass : ev.cc.get_classes()) {\n\t\tauto class_lookup = this->by_class.find(eclass);\n\t\tif (class_lookup != std::end(this->by_class)) {\n\t\t\treturn (*class_lookup).second;\n\t\t}\n\t}\n\n\tthrow Error{MSG(err) << \"Event is not bound in binding_action context.\"};\n}\n\n} // namespace openage::input::camera\n"
  },
  {
    "path": "libopenage/input/controller/camera/binding_context.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n\n#include \"input/controller/camera/binding.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::camera {\n\n/**\n * Maps input events to camera actons.\n */\nclass BindingContext {\npublic:\n\t/**\n\t * Create a new binding context.\n\t */\n\tBindingContext();\n\n\t~BindingContext() = default;\n\n\t/**\n\t * Bind a specific key combination to a binding.\n\t *\n\t * This is the first matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const Event &ev, const binding_action bind);\n\n\t/**\n\t * Bind an event class to an action.\n\t *\n\t * This is the second matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const event_class &cl, const binding_action bind);\n\n\t/**\n\t * Check whether a specific key event is bound in this context.\n\t *\n\t * @param ev Input event.\n\t *\n\t * @return true if event is bound, else false.\n\t */\n\tbool is_bound(const Event &ev) const;\n\n\t/**\n\t * Get the bindings for a specific event.\n\t *\n\t * @param ev Input event mapped to the binding.\n\t */\n\tconst binding_action &lookup(const Event &ev) const;\n\nprivate:\n\t/**\n\t * Maps specific input events to bindings.\n\t */\n\tstd::unordered_map<Event, binding_action, event_hash> by_event;\n\n\t/**\n\t * Maps event classes to bindings.\n\t */\n\tstd::unordered_map<event_class, binding_action, event_class_hash> by_class;\n};\n\n} // namespace openage::input::camera\n"
  },
  {
    "path": "libopenage/input/controller/camera/controller.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"controller.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"input/controller/camera/binding_context.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/stages/camera/manager.h\"\n\nnamespace openage::input::camera {\n\nController::Controller() {}\n\nbool Controller::process(const event_arguments &ev_args,\n                         const std::shared_ptr<BindingContext> &ctx) {\n\tif (not ctx->is_bound(ev_args.e)) {\n\t\treturn false;\n\t}\n\n\tauto bind = ctx->lookup(ev_args.e);\n\tbind.action(ev_args);\n\n\treturn true;\n}\n\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<renderer::camera::Camera> &cam,\n                    const std::shared_ptr<renderer::camera::CameraManager> &cam_manager) {\n\t// arrow movements\n\tbinding_func_t move_left{[&](const event_arguments & /*args*/) {\n\t\tcam_manager->move_frame(renderer::camera::MoveDirection::LEFT, 0.5f);\n\t}};\n\tbinding_func_t move_right{[&](const event_arguments & /*args*/) {\n\t\tcam_manager->move_frame(renderer::camera::MoveDirection::RIGHT, 0.5f);\n\t}};\n\tbinding_func_t move_forward{[&](const event_arguments & /*args*/) {\n\t\tcam_manager->move_frame(renderer::camera::MoveDirection::FORWARD, 0.5f);\n\t}};\n\tbinding_func_t move_backward{[&](const event_arguments & /*args*/) {\n\t\tcam_manager->move_frame(renderer::camera::MoveDirection::BACKWARD, 0.5f);\n\t}};\n\n\tbinding_action move_left_action{move_left};\n\tbinding_action move_right_action{move_right};\n\tbinding_action move_forward_action{move_forward};\n\tbinding_action move_backward_action{move_backward};\n\n\tEvent ev_left{event_class::KEYBOARD, Qt::Key_Left, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_right{event_class::KEYBOARD, Qt::Key_Right, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_up{event_class::KEYBOARD, Qt::Key_Up, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_down{event_class::KEYBOARD, Qt::Key_Down, Qt::NoModifier, QEvent::KeyPress};\n\n\tctx->bind(ev_left, move_left_action);\n\tctx->bind(ev_right, move_right_action);\n\tctx->bind(ev_up, move_forward_action);\n\tctx->bind(ev_down, move_backward_action);\n\n\t// zoom\n\tbinding_func_t zoom_in{[&](const event_arguments & /* args */) {\n\t\t// TODO: Use delta from QWheelEvent?\n\t\tcam_manager->zoom_frame(renderer::camera::ZoomDirection::IN, 0.05f);\n\t}};\n\tbinding_func_t zoom_out{[&](const event_arguments & /* args */) {\n\t\t// TODO: Use delta from QWheelEvent?\n\t\tcam_manager->zoom_frame(renderer::camera::ZoomDirection::OUT, 0.05f);\n\t}};\n\n\tbinding_action zoom_in_action{zoom_in};\n\tbinding_action zoom_out_action{zoom_out};\n\n\tEvent ev_wheel_up{event_class::WHEEL, 1, Qt::NoModifier, QEvent::Wheel};\n\tEvent ev_wheel_down{event_class::WHEEL, -1, Qt::NoModifier, QEvent::Wheel};\n\n\tctx->bind(ev_wheel_up, zoom_in_action);\n\tctx->bind(ev_wheel_down, zoom_out_action);\n\n\t// edge movement\n\tbinding_func_t edge_move{[&](const event_arguments &args) {\n\t\tauto event = std::dynamic_pointer_cast<QMouseEvent>(args.e.get_event());\n\t\tauto pos_x = event->position().x();\n\t\tauto pos_y = event->position().y();\n\n\t\tint move_directions = 0;\n\t\tif (pos_x < 10) {\n\t\t\tmove_directions = move_directions | static_cast<int>(renderer::camera::MoveDirection::LEFT);\n\t\t}\n\t\telse if (pos_x > cam->get_viewport_size()[0] - 10) {\n\t\t\tmove_directions = move_directions | static_cast<int>(renderer::camera::MoveDirection::RIGHT);\n\t\t}\n\n\t\tif (pos_y < 10) {\n\t\t\tmove_directions = move_directions | static_cast<int>(renderer::camera::MoveDirection::FORWARD);\n\t\t}\n\t\telse if (pos_y > cam->get_viewport_size()[1] - 10) {\n\t\t\tmove_directions = move_directions | static_cast<int>(renderer::camera::MoveDirection::BACKWARD);\n\t\t}\n\n\t\tcam_manager->set_move_motion_dirs(move_directions);\n\t}};\n\n\tbinding_action edge_move_action{edge_move};\n\tctx->bind(event_class::MOUSE_MOVE, edge_move_action);\n}\n\n} // namespace openage::input::camera\n"
  },
  {
    "path": "libopenage/input/controller/camera/controller.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"input/event.h\"\n\n\nnamespace openage {\n\nnamespace renderer::camera {\nclass Camera;\nclass CameraManager;\n} // namespace renderer::camera\n\nnamespace input::camera {\n\nclass BindingContext;\n\n/**\n * Control a camera with input from the input manager.\n */\nclass Controller {\npublic:\n\tController();\n\n\t~Controller() = default;\n\n\t/**\n\t * Process an input event from the input manager.\n\t *\n\t * @param ev Input event and arguments.\n\t * @param ctx Binding context that maps input events to camera actions.\n\t *\n\t * @return true if the event is accepted, else false.\n\t */\n\tbool process(const event_arguments &ev_args, const std::shared_ptr<BindingContext> &ctx);\n};\n\n/**\n * Setup default camera action bindings:\n *\n * - Keyboard arrows: move camera.\n * - Mouse movement to window edge: move camera.\n * - Mouse wheel: zoom camera.\n *\n * TODO: Make this configurable.\n *\n * @param ctx Binding context the actions are added to.\n * @param cam Controlled camera.\n * @param cam_manager Camera manager for persistent movements.\n */\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<renderer::camera::Camera> &cam,\n                    const std::shared_ptr<renderer::camera::CameraManager> &cam_manager);\n\n} // namespace input::camera\n} // namespace openage\n"
  },
  {
    "path": "libopenage/input/controller/game/CMakeLists.txt",
    "content": "add_sources(libopenage\n    binding_context.cpp\n    binding.cpp\n    controller.cpp\n)\n"
  },
  {
    "path": "libopenage/input/controller/game/binding.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding.h\"\n\nnamespace openage::input::game {\n\n} // namespace openage::input::game\n"
  },
  {
    "path": "libopenage/input/controller/game/binding.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <unordered_map>\n\n#include \"event/event.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::game {\n\nclass Controller;\n\nusing binding_flags_t = std::unordered_map<std::string, std::string>;\nusing binding_func_t = std::function<const std::shared_ptr<event::Event>(const event_arguments &,\n                                                                         const std::shared_ptr<Controller>)>;\n\n\n/**\n * Action type for the event forwarding.\n *\n * Determines what is done with the created gamestate event\n *     - \\p SEND forward the event (and all queued events) immediately\n *     - \\p QUEUE queue the event\n *     - \\p CLEAR clear the queue\n */\nenum class forward_action_t {\n\tSEND,\n\tQUEUE,\n\tCLEAR,\n};\n\n/**\n * Action taken by the controller when receiving an input.\n *\n * @param action_type Event action type.\n * @param transform Maps an input event to a gamestate event.\n * @param flags Additional parameters for the transformation.\n */\nstruct binding_action {\n\tconst forward_action_t action_type;\n\tconst binding_func_t transform;\n\tconst binding_flags_t flags = {};\n};\n\n} // namespace openage::input::game\n"
  },
  {
    "path": "libopenage/input/controller/game/binding_context.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding_context.h\"\n\n\nnamespace openage::input::game {\n\nBindingContext::BindingContext() :\n\tby_event{} {}\n\nvoid BindingContext::bind(const Event &ev, const binding_action bind) {\n\tthis->by_event.emplace(std::make_pair(ev, bind));\n}\n\nvoid BindingContext::bind(const event_class &cl, const binding_action bind) {\n\tthis->by_class.emplace(std::make_pair(cl, bind));\n}\n\nbool BindingContext::is_bound(const Event &ev) const {\n\treturn this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl);\n}\n\nconst binding_action &BindingContext::lookup(const Event &ev) const {\n\tauto event_lookup = this->by_event.find(ev);\n\tif (event_lookup != std::end(this->by_event)) {\n\t\treturn (*event_lookup).second;\n\t}\n\n\tfor (auto eclass : ev.cc.get_classes()) {\n\t\tauto class_lookup = this->by_class.find(eclass);\n\t\tif (class_lookup != std::end(this->by_class)) {\n\t\t\treturn (*class_lookup).second;\n\t\t}\n\t}\n\n\tthrow Error{MSG(err) << \"Event is not bound in binding_action context.\"};\n}\n\n} // namespace openage::input::game\n"
  },
  {
    "path": "libopenage/input/controller/game/binding_context.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n\n#include \"input/controller/game/binding.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::game {\n\n/**\n * Maps input events to gamestate events.\n */\nclass BindingContext {\npublic:\n\t/**\n\t * Create a new binding context.\n\t */\n\tBindingContext();\n\n\t~BindingContext() = default;\n\n\t/**\n\t * Bind a specific key combination to a binding.\n\t *\n\t * This is the first matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const Event &ev, const binding_action bind);\n\n\t/**\n\t * Bind an event class to an action.\n\t *\n\t * This is the second matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const event_class &cl, const binding_action bind);\n\n\t/**\n\t * Check whether a specific key event is bound in this context.\n\t *\n\t * @param ev Input event.\n\t *\n\t * @return true if event is bound, else false.\n\t */\n\tbool is_bound(const Event &ev) const;\n\n\t/**\n\t * Get the bindings for a specific event.\n\t *\n\t * @param ev Input event mapped to the binding.\n\t */\n\tconst binding_action &lookup(const Event &ev) const;\n\nprivate:\n\t/**\n\t * Maps specific input events to bindings.\n\t */\n\tstd::unordered_map<Event, binding_action, event_hash> by_event;\n\n\t/**\n\t * Maps event classes to bindings.\n\t */\n\tstd::unordered_map<event_class, binding_action, event_class_hash> by_class;\n};\n\n} // namespace openage::input::game\n"
  },
  {
    "path": "libopenage/input/controller/game/controller.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"controller.h\"\n\n#include \"log/log.h\"\n\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"event/state.h\"\n#include \"gamestate/component/internal/commands/types.h\"\n#include \"gamestate/event/send_command.h\"\n#include \"gamestate/event/spawn_entity.h\"\n#include \"gamestate/game.h\"\n#include \"gamestate/game_state.h\"\n#include \"gamestate/simulation.h\"\n#include \"input/controller/game/binding_context.h\"\n#include \"time/clock.h\"\n#include \"time/time_loop.h\"\n\n#include \"coord/phys.h\"\n#include \"renderer/camera/camera.h\"\n\n\nnamespace openage::input::game {\n\nController::Controller(const std::unordered_set<size_t> &controlled_factions,\n                       size_t active_faction_id) :\n\tcontrolled_factions{controlled_factions},\n\tactive_faction_id{active_faction_id},\n\toutqueue{} {}\n\nvoid Controller::set_control(size_t faction_id) {\n\tstd::unique_lock lock{this->mutex};\n\n\tif (this->controlled_factions.find(faction_id) != this->controlled_factions.end()) {\n\t\tthis->active_faction_id = faction_id;\n\t}\n}\n\nsize_t Controller::get_controlled() const {\n\tstd::unique_lock lock{this->mutex};\n\n\treturn this->active_faction_id;\n}\n\nconst std::vector<gamestate::entity_id_t> &Controller::get_selected() const {\n\tstd::unique_lock lock{this->mutex};\n\n\treturn this->selected;\n}\n\nvoid Controller::set_selected(const std::vector<gamestate::entity_id_t> ids) {\n\tstd::unique_lock lock{this->mutex};\n\n\tlog::log(DBG << \"Selected \" << ids.size() << \" entities\");\n\n\tthis->selected = ids;\n}\n\nbool Controller::process(const event_arguments &ev_args, const std::shared_ptr<BindingContext> &ctx) {\n\tstd::unique_lock lock{this->mutex};\n\n\tif (not ctx->is_bound(ev_args.e)) {\n\t\treturn false;\n\t}\n\n\t// TODO: check if action is allowed\n\tauto bind = ctx->lookup(ev_args.e);\n\tauto controller = this->shared_from_this();\n\tauto game_event = bind.transform(ev_args, controller);\n\n\tswitch (bind.action_type) {\n\tcase forward_action_t::SEND:\n\t\tthis->outqueue.push_back(game_event);\n\t\tfor (auto event : this->outqueue) {\n\t\t\t// TODO: Send gamestate event\n\t\t}\n\t\tbreak;\n\n\tcase forward_action_t::QUEUE:\n\t\tthis->outqueue.push_back(game_event);\n\t\tbreak;\n\n\tcase forward_action_t::CLEAR:\n\t\tthis->outqueue.clear();\n\t\tbreak;\n\n\tdefault:\n\t\tthrow Error{MSG(err) << \"Unrecognized action type.\"};\n\t}\n\n\treturn true;\n}\n\nvoid Controller::set_drag_select_start(const coord::input &start) {\n\tstd::unique_lock lock{this->mutex};\n\n\tlog::log(DBG << \"Drag select start at \" << start);\n\tthis->drag_select_start = start;\n}\n\nconst coord::input Controller::get_drag_select_start() const {\n\tstd::unique_lock lock{this->mutex};\n\n\tif (not this->drag_select_start.has_value()) {\n\t\tthrow Error{ERR << \"Drag select start not set.\"};\n\t}\n\n\treturn this->drag_select_start.value();\n}\n\nvoid Controller::reset_drag_select() {\n\tstd::unique_lock lock{this->mutex};\n\n\tlog::log(DBG << \"Drag select start reset\");\n\tthis->drag_select_start = std::nullopt;\n}\n\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<time::TimeLoop> &time_loop,\n                    const std::shared_ptr<openage::gamestate::GameSimulation> &simulation,\n                    const std::shared_ptr<renderer::camera::Camera> &camera) {\n\tbinding_func_t create_entity_event{[&](const event_arguments &args,\n\t                                       const std::shared_ptr<Controller> controller) {\n\t\tauto mouse_pos = args.mouse.to_phys3(camera);\n\t\tevent::EventHandler::param_map::map_t params{\n\t\t\t{\"position\", mouse_pos},\n\t\t\t{\"owner\", controller->get_controlled()},\n\t\t};\n\n\t\tauto event = simulation->get_event_loop()->create_event(\n\t\t\t\"game.spawn_entity\",\n\t\t\tsimulation->get_spawner(),\n\t\t\tsimulation->get_game()->get_state(),\n\t\t\ttime_loop->get_clock()->get_time(),\n\t\t\tparams);\n\t\treturn event;\n\t}};\n\n\tbinding_action create_entity_action{forward_action_t::SEND, create_entity_event};\n\tEvent ev_mouse_lmb_ctrl{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::LeftButton,\n\t\tQt::KeyboardModifier::ControlModifier,\n\t\tQEvent::MouseButtonRelease};\n\n\tctx->bind(ev_mouse_lmb_ctrl, create_entity_action);\n\n\tbinding_func_t move_entity{[&](const event_arguments &args,\n\t                               const std::shared_ptr<Controller> controller) {\n\t\tauto mouse_pos = args.mouse.to_phys3(camera);\n\t\tevent::EventHandler::param_map::map_t params{\n\t\t\t{\"type\", gamestate::component::command::command_t::MOVE},\n\t\t\t{\"target\", mouse_pos},\n\t\t\t{\"entity_ids\", controller->get_selected()},\n\t\t};\n\n\t\tauto event = simulation->get_event_loop()->create_event(\n\t\t\t\"game.send_command\",\n\t\t\tsimulation->get_commander(),\n\t\t\tsimulation->get_game()->get_state(),\n\t\t\ttime_loop->get_clock()->get_time(),\n\t\t\tparams);\n\t\treturn event;\n\t}};\n\n\tbinding_action move_entity_action{forward_action_t::SEND, move_entity};\n\tEvent ev_mouse_rmb{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::RightButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseButtonRelease};\n\n\tctx->bind(ev_mouse_rmb, move_entity_action);\n\n\tbinding_func_t init_drag_selection{[&](const event_arguments &args,\n\t                                       const std::shared_ptr<Controller> controller) {\n\t\tcontroller->set_drag_select_start(args.mouse);\n\t\treturn nullptr;\n\t}};\n\n\tbinding_action init_drag_selection_action{forward_action_t::CLEAR, init_drag_selection};\n\tEvent ev_mouse_lmb_press{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::LeftButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseButtonPress};\n\n\tctx->bind(ev_mouse_lmb_press, init_drag_selection_action);\n\n\tbinding_func_t drag_selection{\n\t\t[&](const event_arguments &args,\n\t        const std::shared_ptr<Controller> controller) {\n\t\t\tEigen::Matrix4f cam_matrix = camera->get_projection_matrix() * camera->get_view_matrix();\n\t\t\tevent::EventHandler::param_map::map_t params{\n\t\t\t\t{\"controlled\", controller->get_controlled()},\n\t\t\t\t{\"drag_start\", controller->get_drag_select_start().to_viewport(camera).to_ndc_space(camera)},\n\t\t\t\t{\"drag_end\", args.mouse.to_viewport(camera).to_ndc_space(camera)},\n\t\t\t\t{\"camera_matrix\", cam_matrix},\n\t\t\t\t{\"select_cb\",\n\t\t         std::function<void(const std::vector<gamestate::entity_id_t> ids)>{\n\t\t\t\t\t [controller](\n\t\t\t\t\t\t const std::vector<gamestate::entity_id_t> ids) {\n\t\t\t\t\t\t controller->set_selected(ids);\n\t\t\t\t\t }}},\n\t\t\t};\n\n\t\t\tauto event = simulation->get_event_loop()->create_event(\n\t\t\t\t\"game.drag_select\",\n\t\t\t\tsimulation->get_commander(),\n\t\t\t\tsimulation->get_game()->get_state(),\n\t\t\t\ttime_loop->get_clock()->get_time(),\n\t\t\t\tparams);\n\n\t\t\t// Reset drag selection start\n\t\t\tcontroller->reset_drag_select();\n\n\t\t\treturn event;\n\t\t}};\n\n\tbinding_action drag_selection_action{forward_action_t::CLEAR, drag_selection};\n\tEvent ev_mouse_lmb_release{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::LeftButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseButtonRelease};\n\n\tctx->bind(ev_mouse_lmb_release, drag_selection_action);\n}\n\n\n} // namespace openage::input::game\n"
  },
  {
    "path": "libopenage/input/controller/game/controller.h",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <mutex>\n#include <optional>\n#include <unordered_set>\n\n#include \"coord/pixel.h\"\n#include \"curve/discrete.h\"\n#include \"gamestate/types.h\"\n#include \"input/event.h\"\n\n\nnamespace openage {\n\nnamespace gamestate {\nclass GameSimulation;\n}\n\nnamespace time {\nclass TimeLoop;\n}\n\nnamespace input::game {\n\nclass BindingContext;\n\n/**\n * Interface for game controllers.\n *\n * Controllers handle inputs from outside of a game (e.g. GUI, AI, scripts, ...)\n * and pass the resulting events to game entities. They also act as a form of\n * access control for using in-game functionality of game entities.\n */\nclass Controller : public std::enable_shared_from_this<Controller> {\npublic:\n\tController(const std::unordered_set<size_t> &controlled_factions,\n\t           size_t active_faction_id);\n\n\t~Controller() = default;\n\n\t/**\n\t * Switch the actively controlled faction by the controller.\n\t * The ID must be in the list of controlled factions.\n\t *\n\t * @param faction_id ID of the new active faction.\n\t */\n\tvoid set_control(size_t faction_id);\n\n\t/**\n\t * Get the ID of the faction actively controlled by the controller.\n\t *\n\t * @return ID of the active faction.\n\t */\n\tsize_t get_controlled() const;\n\n\t/**\n\t * Get the currently selected entities.\n\t *\n\t * @return Selected entities.\n\t */\n\tconst std::vector<gamestate::entity_id_t> &get_selected() const;\n\n\t/**\n\t * Set the currently selected entities.\n\t *\n\t * @param ids Selected entities.\n\t */\n\tvoid set_selected(const std::vector<gamestate::entity_id_t> ids);\n\n\t/**\n\t * Process an input event from the input manager.\n\t *\n\t * @param ev_args Input event and arguments.\n\t * @param ctx Binding context for looking up the event transformation.\n\t *\n\t * @return true if the event is accepted, else false.\n\t */\n\tbool process(const event_arguments &ev_args, const std::shared_ptr<BindingContext> &ctx);\n\n\t/**\n\t * Set the start position of a drag selection.\n\t *\n\t * @param start Start position of the drag selection.\n\t */\n\tvoid set_drag_select_start(const coord::input &start);\n\n\t/**\n\t * Get the start position of a drag selection.\n\t *\n\t * @return Start position of the drag selection.\n\t */\n\tconst coord::input get_drag_select_start() const;\n\n\t/**\n\t * Reset the drag select start position.\n\t */\n\tvoid reset_drag_select();\n\nprivate:\n\t/**\n\t * Factions controllable by this controller.\n\t */\n\tstd::unordered_set<size_t> controlled_factions;\n\n\t/**\n\t * ID of the currently active faction.\n\t */\n\tsize_t active_faction_id;\n\n\t/**\n\t * Currently selected entities.\n\t */\n\tstd::vector<gamestate::entity_id_t> selected;\n\n\t/**\n\t * Queue for gamestate events generated from inputs.\n\t */\n\tstd::vector<std::shared_ptr<event::Event>> outqueue;\n\n\t/**\n\t * Start position of a drag selection.\n\t *\n\t * TODO: Move this into an input event.\n\t */\n\tstd::optional<coord::input> drag_select_start;\n\n\t/**\n\t * Mutex for threaded access.\n\t */\n\tmutable std::recursive_mutex mutex;\n};\n\n/**\n * Setup default controller action bindings:\n *\n * - CTRL + Left Mouse click: Create game entity.\n * - Right Mouse click: Move game entity.\n *\n * @param ctx Binding context the actions are added to.\n * @param time_loop Time loop for getting simulation time.\n * @param simulation Game simulation.\n * @param camera Active game camera.\n */\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<time::TimeLoop> &time_loop,\n                    const std::shared_ptr<openage::gamestate::GameSimulation> &simulation,\n                    const std::shared_ptr<renderer::camera::Camera> &camera);\n\n} // namespace input::game\n} // namespace openage\n"
  },
  {
    "path": "libopenage/input/controller/hud/CMakeLists.txt",
    "content": "add_sources(libopenage\n    binding_context.cpp\n    binding.cpp\n    controller.cpp\n)\n"
  },
  {
    "path": "libopenage/input/controller/hud/binding.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding.h\"\n\nnamespace openage::input::hud {\n\n} // namespace openage::input::hud\n"
  },
  {
    "path": "libopenage/input/controller/hud/binding.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <unordered_map>\n\n#include \"event/event.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::hud {\nclass Controller;\n\nusing binding_flags_t = std::unordered_map<std::string, std::string>;\nusing binding_func_t = std::function<void(const event_arguments &e,\n                                          const std::shared_ptr<Controller>)>;\n\n\n/**\n * Action taken by the controller when receiving an input.\n *\n * @param action Maps an input event to a camera action.\n * @param flags Additional parameters for the transformation.\n */\nstruct binding_action {\n\tconst binding_func_t action;\n\tconst binding_flags_t flags = {};\n};\n\n} // namespace openage::input::hud\n"
  },
  {
    "path": "libopenage/input/controller/hud/binding_context.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"binding_context.h\"\n\n\nnamespace openage::input::hud {\n\nBindingContext::BindingContext() :\n\tby_event{} {}\n\nvoid BindingContext::bind(const Event &ev, const binding_action bind) {\n\tthis->by_event.emplace(std::make_pair(ev, bind));\n}\n\nvoid BindingContext::bind(const event_class &cl, const binding_action bind) {\n\tthis->by_class.emplace(std::make_pair(cl, bind));\n}\n\nbool BindingContext::is_bound(const Event &ev) const {\n\treturn this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl);\n}\n\nconst binding_action &BindingContext::lookup(const Event &ev) const {\n\tauto event_lookup = this->by_event.find(ev);\n\tif (event_lookup != std::end(this->by_event)) {\n\t\treturn (*event_lookup).second;\n\t}\n\n\tfor (auto eclass : ev.cc.get_classes()) {\n\t\tauto class_lookup = this->by_class.find(eclass);\n\t\tif (class_lookup != std::end(this->by_class)) {\n\t\t\treturn (*class_lookup).second;\n\t\t}\n\t}\n\n\tthrow Error{MSG(err) << \"Event is not bound in binding_action context.\"};\n}\n\n} // namespace openage::input::hud\n"
  },
  {
    "path": "libopenage/input/controller/hud/binding_context.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n\n#include \"input/controller/hud/binding.h\"\n#include \"input/event.h\"\n\nnamespace openage::input::hud {\n\n/**\n * Maps input events to HUD actons.\n */\nclass BindingContext {\npublic:\n\t/**\n\t * Create a new binding context.\n\t */\n\tBindingContext();\n\n\t~BindingContext() = default;\n\n\t/**\n\t * Bind a specific key combination to a binding.\n\t *\n\t * This is the first matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const Event &ev, const binding_action bind);\n\n\t/**\n\t * Bind an event class to an action.\n\t *\n\t * This is the second matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param bind Binding for the event.\n\t */\n\tvoid bind(const event_class &cl, const binding_action bind);\n\n\t/**\n\t * Check whether a specific key event is bound in this context.\n\t *\n\t * @param ev Input event.\n\t *\n\t * @return true if event is bound, else false.\n\t */\n\tbool is_bound(const Event &ev) const;\n\n\t/**\n\t * Get the bindings for a specific event.\n\t *\n\t * @param ev Input event mapped to the binding.\n\t */\n\tconst binding_action &lookup(const Event &ev) const;\n\nprivate:\n\t/**\n\t * Maps specific input events to bindings.\n\t */\n\tstd::unordered_map<Event, binding_action, event_hash> by_event;\n\n\t/**\n\t * Maps event classes to bindings.\n\t */\n\tstd::unordered_map<event_class, binding_action, event_class_hash> by_class;\n};\n\n} // namespace openage::input::hud\n"
  },
  {
    "path": "libopenage/input/controller/hud/controller.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"controller.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"input/controller/hud/binding_context.h\"\n\n#include \"renderer/stages/hud/render_entity.h\"\n#include \"renderer/stages/hud/render_stage.h\"\n\n\nnamespace openage::input::hud {\n\nController::Controller() :\n\tdrag_entity{nullptr} {}\n\nbool Controller::process(const event_arguments &ev_args,\n                         const std::shared_ptr<BindingContext> &ctx) {\n\tif (not ctx->is_bound(ev_args.e)) {\n\t\treturn false;\n\t}\n\n\tauto bind = ctx->lookup(ev_args.e);\n\tbind.action(ev_args, this->shared_from_this());\n\n\treturn true;\n}\n\nvoid Controller::set_drag_entity(const std::shared_ptr<renderer::hud::DragRenderEntity> &entity) {\n\tthis->drag_entity = entity;\n}\n\nconst std::shared_ptr<renderer::hud::DragRenderEntity> &Controller::get_drag_entity() const {\n\treturn this->drag_entity;\n}\n\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<renderer::hud::HudRenderStage> &hud_renderer) {\n\tbinding_func_t drag_selection_init{[&](const event_arguments &args,\n\t                                       const std::shared_ptr<Controller> controller) {\n\t\tauto render_entity = std::make_shared<renderer::hud::DragRenderEntity>(args.mouse);\n\t\thud_renderer->add_drag_entity(render_entity);\n\t\tcontroller->set_drag_entity(render_entity);\n\t}};\n\n\tbinding_action drag_selection_init_action{drag_selection_init};\n\tEvent ev_mouse_lmb_press{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::LeftButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseButtonPress};\n\n\tctx->bind(ev_mouse_lmb_press, drag_selection_init_action);\n\n\tbinding_func_t drag_selection_move{[&](const event_arguments &args,\n\t                                       const std::shared_ptr<Controller> controller) {\n\t\tif (controller->get_drag_entity()) {\n\t\t\tcontroller->get_drag_entity()->update(args.mouse);\n\t\t}\n\t}};\n\n\tbinding_action drag_selection_move_action{drag_selection_move};\n\tEvent ev_mouse_move{\n\t\tevent_class::MOUSE_MOVE,\n\t\tQt::MouseButton::NoButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseMove};\n\n\tctx->bind(ev_mouse_move, drag_selection_move_action);\n\n\tbinding_func_t drag_selection_end{[&](const event_arguments & /* args */,\n\t                                      const std::shared_ptr<Controller> controller) {\n\t\thud_renderer->remove_drag_entity();\n\t\tcontroller->set_drag_entity(nullptr);\n\t}};\n\n\tbinding_action drag_selection_end_action{drag_selection_end};\n\tEvent ev_mouse_lmb_release{\n\t\tevent_class::MOUSE_BUTTON,\n\t\tQt::MouseButton::LeftButton,\n\t\tQt::KeyboardModifier::NoModifier,\n\t\tQEvent::MouseButtonRelease};\n\n\tctx->bind(ev_mouse_lmb_release, drag_selection_end_action);\n}\n\n} // namespace openage::input::hud\n"
  },
  {
    "path": "libopenage/input/controller/hud/controller.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"input/event.h\"\n\n\nnamespace openage {\n\nnamespace renderer::hud {\nclass DragRenderEntity;\nclass HudRenderStage;\n} // namespace renderer::hud\n\nnamespace input::hud {\nclass BindingContext;\n\n/**\n * Control athe HUD with input from the input manager.\n */\nclass Controller : public std::enable_shared_from_this<Controller> {\npublic:\n\tController();\n\n\t~Controller() = default;\n\n\t/**\n\t * Process an input event from the input manager.\n\t *\n\t * @param ev Input event and arguments.\n\t * @param ctx Binding context that maps input events to HUD actions.\n\t *\n\t * @return true if the event is accepted, else false.\n\t */\n\tbool process(const event_arguments &ev_args,\n\t             const std::shared_ptr<BindingContext> &ctx);\n\n\t/**\n\t * Set the render entity for the selection box.\n\t *\n\t * @param entity New render entity.\n\t */\n\tvoid set_drag_entity(const std::shared_ptr<renderer::hud::DragRenderEntity> &entity);\n\n\t/**\n\t * Get the render entity for the selection box.\n\t *\n\t * @return Render entity for the selection box.\n\t */\n\tconst std::shared_ptr<renderer::hud::DragRenderEntity> &get_drag_entity() const;\n\nprivate:\n\t/**\n\t * Render entity for the selection box.\n\t */\n\tstd::shared_ptr<renderer::hud::DragRenderEntity> drag_entity;\n};\n\n/**\n * Setup default HUD action bindings:\n *\n * - Mouse drag: selection box draw\n *\n * TODO: Make this configurable.\n *\n * @param ctx Binding context the actions are added to.\n * @param hud_renderer HUD render stage that is used to render the selection box.\n */\nvoid setup_defaults(const std::shared_ptr<BindingContext> &ctx,\n                    const std::shared_ptr<renderer::hud::HudRenderStage> &hud_renderer);\n\n} // namespace input::hud\n} // namespace openage\n"
  },
  {
    "path": "libopenage/input/event.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"event.h\"\n\n#include <functional>\n#include <utility>\n\nnamespace openage::input {\n\nint event_class_hash::operator()(const event_class &c) const {\n\treturn std::hash<int>()(static_cast<int>(c));\n}\n\n\nClassCode::ClassCode(event_class cl, code_t code) :\n\tcl{cl},\n\tcode{code} {}\n\n\nstd::vector<event_class> ClassCode::get_classes() const {\n\tstd::vector<event_class> result;\n\n\t// use event_base to traverse up the class tree\n\tevent_class c = this->cl;\n\tresult.push_back(c);\n\twhile (event_class_rel.count(c) > 0) {\n\t\tc = event_class_rel.at(c);\n\t\tresult.push_back(c);\n\t}\n\treturn result;\n}\n\n\nbool ClassCode::operator==(const ClassCode &other) const {\n\treturn this->cl == other.cl && this->code == other.code;\n}\n\n\nbool ClassCode::is_subclass(const event_class &other) const {\n\tfor (auto cl : this->get_classes()) {\n\t\tif (cl == other) {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n\nint class_code_hash::operator()(const ClassCode &cc) const {\n\treturn std::hash<int>()(static_cast<int>(cc.cl))\n\t       ^ std::hash<int>()(cc.code) * 3664657;\n}\n\n\nEvent::Event(const QEvent &ev) :\n\tevent{std::shared_ptr<QEvent>(ev.clone())} {\n\tswitch (this->event->type()) {\n\tcase QEvent::KeyPress:\n\tcase QEvent::KeyRelease: {\n\t\tauto event = dynamic_pointer_cast<QKeyEvent>(this->event);\n\t\tthis->cc = ClassCode(event_class::KEYBOARD, event->key());\n\t\tthis->mod_code = event->modifiers();\n\t\tthis->state = event->type();\n\t} break;\n\tcase QEvent::MouseButtonPress:\n\tcase QEvent::MouseButtonRelease: {\n\t\tauto event = dynamic_pointer_cast<QMouseEvent>(this->event);\n\t\tthis->cc = ClassCode(event_class::MOUSE_BUTTON, event->button());\n\t\tthis->mod_code = event->modifiers();\n\t\tthis->state = event->type();\n\t} break;\n\tcase QEvent::MouseButtonDblClick: {\n\t\tauto event = dynamic_pointer_cast<QMouseEvent>(this->event);\n\t\tthis->cc = ClassCode(event_class::MOUSE_BUTTON_DBL, event->button());\n\t\tthis->mod_code = event->modifiers();\n\t\tthis->state = event->type();\n\t} break;\n\tcase QEvent::MouseMove: {\n\t\tauto event = dynamic_pointer_cast<QMouseEvent>(this->event);\n\t\tthis->cc = ClassCode(event_class::MOUSE_MOVE, event->button());\n\t\tthis->mod_code = event->modifiers();\n\t\tthis->state = event->type();\n\t} break;\n\tcase QEvent::Wheel: {\n\t\tauto event = dynamic_pointer_cast<QWheelEvent>(this->event);\n\t\tif (event->angleDelta().y() > 0) {\n\t\t\t// forward\n\t\t\tthis->cc = ClassCode(event_class::WHEEL, 1);\n\t\t}\n\t\telse {\n\t\t\t// backwards\n\t\t\tthis->cc = ClassCode(event_class::WHEEL, -1);\n\t\t}\n\t\tthis->mod_code = event->modifiers();\n\t\tthis->state = event->type();\n\t} break;\n\t// TODO: GUI events\n\tdefault:\n\t\tthrow Error{MSG(err) << \"Unrecognized input event type.\"};\n\t}\n}\n\nEvent::Event(event_class cl, code_t code, modset_t mod, state_t state) :\n\tcc{cl, code},\n\tmod_code{mod},\n\tstate{state},\n\tevent{nullptr} {}\n\n\nconst std::shared_ptr<QEvent> &Event::get_event() const {\n\treturn this->event;\n}\n\n\nbool Event::operator==(const Event &other) const {\n\treturn this->cc == other.cc\n\t       && this->mod_code == other.mod_code\n\t       && this->state == other.state;\n}\n\n\nstd::string Event::info() const {\n\t// TODO: human-readable info\n\n\tstd::string result = \"[Event: \";\n\tresult += \"class=\" + std::to_string(static_cast<int>(this->cc.cl)) + \", \";\n\tresult += \"code=\" + std::to_string(this->cc.code) + \", \";\n\tresult += \"modset=\" + std::to_string(this->mod_code) + \", \";\n\tresult += \"state=\" + std::to_string(this->state) + \"]\";\n\treturn result;\n}\n\n\nint event_hash::operator()(const Event &e) const {\n\treturn class_code_hash()(e.cc)\n\t       ^ std::hash<int>()(e.mod_code)\n\t       ^ std::hash<int>()(e.state);\n}\n\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/event.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <unordered_map>\n#include <unordered_set>\n#include <vector>\n\n#include <QKeyCombination>\n#include <QMouseEvent>\n#include <QWheelEvent>\n\n#include \"coord/pixel.h\"\n\nnamespace openage {\nnamespace input {\n\n/**\n * Input classes.\n */\nenum class event_class {\n\t// catch-all for all classes of events\n\tANY,\n\n\t// main classes for the different input devices\n\tKEYBOARD,\n\tMOUSE,\n\tWHEEL,\n\tGUI,\n\n\t// keyboard subclasses\n\tALPHA,    // abc\n\tDIGIT,    // 123\n\tPRINT,    // remaining printable chars\n\tNONPRINT, // tab, return, backspace, delete\n\tOTHER,    // arrows, home, end\n\n\t// mouse subclasses\n\tMOUSE_BUTTON,\n\tMOUSE_BUTTON_DBL,\n\tMOUSE_MOVE,\n};\n\nstruct event_class_hash {\n\tint operator()(const event_class &s) const;\n};\n\n/**\n * map event class to parent class\n */\nstatic std::unordered_map<event_class, event_class, event_class_hash> event_class_rel{\n\t{event_class::KEYBOARD, event_class::ANY},\n\t{event_class::MOUSE, event_class::ANY},\n\t{event_class::WHEEL, event_class::ANY},\n\t{event_class::GUI, event_class::ANY},\n\n\t{event_class::ALPHA, event_class::KEYBOARD},\n\t{event_class::DIGIT, event_class::KEYBOARD},\n\t{event_class::PRINT, event_class::KEYBOARD},\n\t{event_class::NONPRINT, event_class::KEYBOARD},\n\t{event_class::OTHER, event_class::KEYBOARD},\n\n\t{event_class::MOUSE_BUTTON, event_class::MOUSE},\n\t{event_class::MOUSE_BUTTON_DBL, event_class::MOUSE},\n\t{event_class::MOUSE_MOVE, event_class::MOUSE},\n};\n\n/**\n * Key/button identifiers.\n */\nusing code_t = int;\n\n/**\n * Base event that contains event class and key/button identifier.\n */\nclass ClassCode {\npublic:\n\tClassCode() = default;\n\tClassCode(event_class cl, code_t code);\n\t~ClassCode() = default;\n\n\t/**\n\t * Get all event classes that are covered by this class code,\n\t * ordered from most specific to most generic.\n\t *\n\t * @return Event classes.\n\t */\n\tstd::vector<event_class> get_classes() const;\n\n\t/**\n\t * Check whether this class code is covered by a given event class.\n\t *\n\t * @param b Event class.\n\t *\n\t * @return true if the class code's event class is a descendant of or equal to \\p other, else false.\n\t */\n\tbool is_subclass(const event_class &other) const;\n\n\t/**\n\t * Check whether two events are equal.\n\t *\n\t * Events are equal if their class, code, modset, and state are matching.\n\t */\n\tbool operator==(const ClassCode &other) const;\n\n\t/**\n\t * Event class.\n\t */\n\tevent_class cl;\n\n\t/**\n\t * Identifier of the key/button that was pressed. It should be unique\n\t * for the given event class.\n\t */\n\tcode_t code;\n};\n\n\nstruct class_code_hash {\n\tint operator()(const ClassCode &cc) const;\n};\n\n\n/**\n * Modifiers and states.\n */\nusing modset_t = int;\nusing state_t = int;\n\n/**\n * Input event, as triggered by some input device like\n * mouse, kezb, joystick, tablet, microwave or dildo.\n * Some modifier keys may also be pressed during the event.\n */\nclass Event {\npublic:\n\t/**\n\t * Create a new input event from a window event.\n\t *\n\t * @param ev Qt input event.\n\t */\n\tEvent(const QEvent &ev);\n\n\t/**\n\t * Create a new input event from custom values.\n\t *\n\t * This can be used to create the bindings for the input contexts.\n\t *\n\t * @param cl Event class.\n\t * @param code Key/button code.\n\t * @param mod Keyboard modifiers bitset.\n\t * @param state Key/button state.\n\t */\n\tEvent(event_class cl,\n\t      code_t code,\n\t      modset_t mod,\n\t      state_t state);\n\n\t~Event() = default;\n\n\t/**\n\t * Get the associated Qt input event from the window manager.\n\t *\n\t * This may return \\p nullptr if this event was not created from a\n\t * Qt input event.\n\t *\n\t * @return Qt input event, or \\p nullptr.\n\t */\n\tconst std::shared_ptr<QEvent> &get_event() const;\n\n\t/**\n\t * Check whether two events are equal.\n\t *\n\t * Events are equal if their class, code, modset, and state are matching.\n\t */\n\tbool operator==(const Event &other) const;\n\n\t/**\n\t * Get loggable debug info about the event.\n\t */\n\tstd::string info() const;\n\n\t/**\n\t * Class code.\n\t */\n\tClassCode cc;\n\n\t/**\n\t * Keyboard modifiers (CTRL, ALT, SHIFT, META) that were active when\n\t * the key/button was pressed.\n\t */\n\tmodset_t mod_code;\n\n\t/**\n\t * Key/button state (pressed, released, double click, ...).\n\t */\n\tstate_t state;\n\nprivate:\n\t/**\n\t * Associated Qt event from the window manager. May be \\p nullptr\n\t * if this event is not generated from a window event.\n\t */\n\tconst std::shared_ptr<QEvent> event;\n};\n\n\nstruct event_hash {\n\tint operator()(const Event &e) const;\n};\n\nusing event_flags_t = std::unordered_map<std::string, std::string>;\n\n/**\n * Contains information about a triggered event and other\n * input meta information.\n */\nstruct event_arguments {\n\t// Triggering event\n\tconst Event e;\n\n\t// Mouse position\n\tconst coord::input mouse;\n\tconst coord::input_delta motion;\n\n\t// additional settings\n\tconst event_flags_t flags = {};\n};\n\n\n} // namespace input\n} // namespace openage\n"
  },
  {
    "path": "libopenage/input/input_context.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"input_context.h\"\n\n\nnamespace openage::input {\n\nInputContext::InputContext(const std::string id) :\n\tid{id},\n\tby_event{},\n\tby_class{} {}\n\n\nconst std::string &InputContext::get_id() {\n\treturn this->id;\n}\n\nvoid InputContext::set_game_bindings(const std::shared_ptr<game::BindingContext> &bindings) {\n\tthis->game_bindings = bindings;\n}\n\nvoid InputContext::set_camera_bindings(const std::shared_ptr<camera::BindingContext> &bindings) {\n\tthis->camera_bindings = bindings;\n}\n\nvoid InputContext::set_hud_bindings(const std::shared_ptr<hud::BindingContext> &bindings) {\n\tthis->hud_bindings = bindings;\n}\n\nconst std::shared_ptr<game::BindingContext> &InputContext::get_game_bindings() {\n\treturn this->game_bindings;\n}\n\nconst std::shared_ptr<camera::BindingContext> &InputContext::get_camera_bindings() {\n\treturn this->camera_bindings;\n}\n\nconst std::shared_ptr<hud::BindingContext> &InputContext::get_hud_bindings() {\n\treturn this->hud_bindings;\n}\n\nvoid InputContext::bind(const Event &ev, const input_action act) {\n\tstd::vector<input_action> actions{act};\n\tthis->by_event.emplace(std::make_pair(ev, actions));\n}\n\nvoid InputContext::bind(const event_class &cl, const input_action act) {\n\tstd::vector<input_action> actions{act};\n\tthis->by_class.emplace(std::make_pair(cl, actions));\n}\n\nvoid InputContext::bind(const Event &ev, const std::vector<input_action> &&acts) {\n\tthis->by_event.emplace(std::make_pair(ev, std::move(acts)));\n}\n\nvoid InputContext::bind(const event_class &cl, const std::vector<input_action> &&acts) {\n\tthis->by_class.emplace(std::make_pair(cl, std::move(acts)));\n}\n\nbool InputContext::is_bound(const Event &ev) const {\n\treturn this->by_event.contains(ev) || this->by_class.contains(ev.cc.cl);\n}\n\nconst std::vector<input_action> &InputContext::lookup(const Event &ev) const {\n\tauto event_lookup = this->by_event.find(ev);\n\tif (event_lookup != std::end(this->by_event)) {\n\t\treturn (*event_lookup).second;\n\t}\n\n\tfor (auto eclass : ev.cc.get_classes()) {\n\t\tauto class_lookup = this->by_class.find(eclass);\n\t\tif (class_lookup != std::end(this->by_class)) {\n\t\t\treturn (*class_lookup).second;\n\t\t}\n\t}\n\n\tthrow Error{MSG(err) << \"Event is not bound in context \" << this->id};\n}\n\nstd::vector<Event> InputContext::get_event_binds() const {\n\tstd::vector<Event> result{};\n\n\tfor (auto bind : this->by_event) {\n\t\tresult.push_back(bind.first);\n\t}\n\n\treturn result;\n}\n\nstd::vector<event_class> InputContext::get_class_binds() const {\n\tstd::vector<event_class> result{};\n\n\tfor (auto bind : this->by_class) {\n\t\tresult.push_back(bind.first);\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/input_context.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n#include <vector>\n\n#include \"input/action.h\"\n#include \"input/event.h\"\n\nnamespace openage::input {\n\nnamespace camera {\nclass BindingContext;\n}\n\nnamespace game {\nclass BindingContext;\n}\n\nnamespace hud {\nclass BindingContext;\n}\n\n/**\n * An input context contains all keybindings and actions\n * active in e.g. the HUD only.\n * That way, each context can have the same keys\n * assigned to different actions, the active context\n * decides, which one to trigger.\n */\nclass InputContext {\npublic:\n\t/**\n\t * Create an input context.\n\t *\n\t * @param id Unique identifier.\n\t */\n\tInputContext(const std::string id);\n\n\tvirtual ~InputContext() = default;\n\n\t/**\n\t * Get the unique ID of the context.\n\t *\n\t * @return Context ID.\n\t */\n\tconst std::string &get_id();\n\n\t/**\n\t * Set the associated context for binding input events to game events.\n\t *\n\t * @param bindings Binding context for gamestate events.\n\t */\n\tvoid set_game_bindings(const std::shared_ptr<game::BindingContext> &bindings);\n\n\t/**\n\t * Set the associated context for binding input events to camera actions.\n\t *\n\t * @param bindings Binding context for camera actions.\n\t */\n\tvoid set_camera_bindings(const std::shared_ptr<camera::BindingContext> &bindings);\n\n\t/**\n\t * Set the associated context for binding input events to HUD actions.\n\t *\n\t * @param bindings Binding context for HUD actions.\n\t */\n\tvoid set_hud_bindings(const std::shared_ptr<hud::BindingContext> &bindings);\n\n\t/**\n\t * Get the associated context for binding input events to game events.\n\t *\n\t * @return Binding context of the input context.\n\t */\n\tconst std::shared_ptr<game::BindingContext> &get_game_bindings();\n\n\t/**\n\t * Get the associated context for binding input events to camera actions.\n\t *\n\t * @return Binding context of the input context.\n\t */\n\tconst std::shared_ptr<camera::BindingContext> &get_camera_bindings();\n\n\t/**\n\t * Get the associated context for binding input events to HUD actions.\n\t *\n\t * @return Binding context of the input context.\n\t */\n\tconst std::shared_ptr<hud::BindingContext> &get_hud_bindings();\n\n\t/**\n\t * Bind a specific key combination to a single action.\n\t *\n\t * This is the first matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param act Action executed by the event.\n\t */\n\tvoid bind(const Event &ev, const input_action act);\n\n\t/**\n\t * Bind an event class to a single action.\n\t *\n\t * This is the second matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param act Action executed by the event.\n\t */\n\tvoid bind(const event_class &cl, const input_action act);\n\n\t/**\n\t * Bind a specific key combination to a list of actions.\n\t *\n\t * This is the first matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param act Actions executed by the event.\n\t */\n\tvoid bind(const Event &ev, const std::vector<input_action> &&acts);\n\n\t/**\n\t * Bind an event class to a list of actions.\n\t *\n\t * This is the second matching priority.\n\t *\n\t * @param ev Input event triggering the action.\n\t * @param act Actions executed by the event.\n\t */\n\tvoid bind(const event_class &cl, const std::vector<input_action> &&acts);\n\n\t/**\n\t * Check whether a specific key event is bound in this context.\n\t *\n\t * @param ev Input event.\n\t *\n\t * @return true if event is bound, else false.\n\t */\n\tbool is_bound(const Event &ev) const;\n\n\t/**\n\t * Get the action(s) bound to the given event.\n\t *\n\t * @param ev Input event triggering the action.\n\t */\n\tconst std::vector<input_action> &lookup(const Event &ev) const;\n\n\t/**\n\t * Get all event->action bindings in this context.\n\t *\n\t * @return Events bound in this context.\n\t */\n\tstd::vector<Event> get_event_binds() const;\n\n\t/**\n\t * Get all class->action bindings in this context.\n\t *\n\t * @return Event classes bound in this context.\n\t */\n\tstd::vector<event_class> get_class_binds() const;\n\n\nprivate:\n\t/**\n\t * Unique ID of the context.\n\t */\n\tstd::string id;\n\n\t/**\n\t * Maps specific input events to actions.\n\t */\n\tstd::unordered_map<Event, std::vector<input_action>, event_hash> by_event;\n\n\t/**\n\t * Maps event classes to actions.\n\t */\n\tstd::unordered_map<event_class, std::vector<input_action>, event_class_hash> by_class;\n\n\t/**\n\t * Additional context for game simulation events.\n\t */\n\tstd::shared_ptr<game::BindingContext> game_bindings;\n\n\t/**\n\t * Additional context for camera actions.\n\t */\n\tstd::shared_ptr<camera::BindingContext> camera_bindings;\n\n\t/**\n\t * Additional context for HUD actions.\n\t */\n\tstd::shared_ptr<hud::BindingContext> hud_bindings;\n};\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/input_manager.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"input_manager.h\"\n\n#include \"input/controller/camera/controller.h\"\n#include \"input/controller/game/controller.h\"\n#include \"input/controller/hud/controller.h\"\n#include \"input/event.h\"\n#include \"input/input_context.h\"\n#include \"renderer/gui/guisys/public/gui_input.h\"\n\n\nnamespace openage::input {\n\nInputManager::InputManager() :\n\tglobal_context{std::make_shared<InputContext>(\"main\")},\n\tactive_contexts{},\n\tavailable_contexts{},\n\tgame_controller{nullptr},\n\tcamera_controller{nullptr},\n\thud_controller{nullptr},\n\tgui_input{nullptr} {\n}\n\nvoid InputManager::set_gui(const std::shared_ptr<qtgui::GuiInput> &gui_input) {\n\tthis->gui_input = gui_input;\n}\n\nvoid InputManager::set_camera_controller(const std::shared_ptr<camera::Controller> &controller) {\n\tthis->camera_controller = controller;\n}\n\nvoid InputManager::set_game_controller(const std::shared_ptr<game::Controller> &controller) {\n\tthis->game_controller = controller;\n}\n\nvoid InputManager::set_hud_controller(const std::shared_ptr<hud::Controller> controller) {\n\tthis->hud_controller = controller;\n}\n\nconst std::shared_ptr<InputContext> &InputManager::get_global_context() {\n\treturn this->global_context;\n}\n\nconst std::shared_ptr<InputContext> &InputManager::get_top_context() {\n\t// return the global input context if the stack is empty\n\tif (this->active_contexts.empty()) {\n\t\treturn this->global_context;\n\t}\n\n\treturn this->active_contexts.back();\n}\n\nstd::vector<std::string> InputManager::active_binds() const {\n\tstd::vector<std::string> result;\n\n\t// remember events and classes bound in already visited contexts\n\tstd::unordered_set<Event, event_hash> used_events;\n\tstd::unordered_set<event_class, event_class_hash> used_classes;\n\n\tfor (auto it = this->active_contexts.rbegin(); it != this->active_contexts.rend(); ++it) {\n\t\tauto event_binds = (*it)->get_event_binds();\n\t\tfor (auto ev : event_binds) {\n\t\t\tif (used_events.contains(ev)) {\n\t\t\t\t// event is handled by a context with a higher priority\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (auto cl : used_classes) {\n\t\t\t\tif (ev.cc.is_subclass(cl)) {\n\t\t\t\t\t// class is handled by a context with a higher priority\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult.push_back(ev.info());\n\t\t\tused_events.insert(ev);\n\t\t}\n\t\tauto classes = (*it)->get_class_binds();\n\t\tused_classes.insert(classes.begin(), classes.end());\n\t}\n\n\treturn result;\n}\n\nvoid InputManager::push_context(const std::shared_ptr<InputContext> &context) {\n\tthis->active_contexts.push_back(context);\n}\n\nvoid InputManager::push_context(const std::string &id) {\n\tauto context = this->available_contexts.at(id);\n\tthis->push_context(context);\n}\n\nvoid InputManager::pop_context() {\n\tif (not this->active_contexts.empty()) {\n\t\tthis->active_contexts.pop_back();\n\t}\n}\n\nvoid InputManager::pop_context(const std::string &id) {\n\tif (this->active_contexts.empty()) {\n\t\treturn;\n\t}\n\n\tfor (auto it = this->active_contexts.begin(); it != this->active_contexts.end(); ++it) {\n\t\tif ((*it)->get_id() == id) {\n\t\t\tthis->active_contexts.erase(it);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nvoid InputManager::remove_context(const std::string &id) {\n\tthis->available_contexts.erase(id);\n}\n\nvoid InputManager::add_context(const std::shared_ptr<InputContext> context) {\n\tthis->available_contexts.emplace(context->get_id(), context);\n}\n\nvoid InputManager::set_mouse(int x, int y) {\n\tauto last_position = this->mouse_position;\n\tthis->mouse_position = coord::input{coord::pixel_t{x}, coord::pixel_t{y}};\n\tthis->mouse_motion = this->mouse_position - last_position;\n}\n\nvoid InputManager::set_motion(int x, int y) {\n\tthis->mouse_motion.x = x;\n\tthis->mouse_motion.y = y;\n}\n\nbool InputManager::process(const QEvent &ev) {\n\tinput::Event input_ev{ev};\n\n\t// Check context list on top of the stack (most recent bound first)\n\tfor (size_t i = this->active_contexts.size(); i > 0; --i) {\n\t\tauto &ctx = this->active_contexts.at(i - 1);\n\t\tif (ctx->is_bound(input_ev)) {\n\t\t\tauto &actions = ctx->lookup(input_ev);\n\t\t\tfor (auto const &action : actions) {\n\t\t\t\tthis->process_action(input_ev, action, ctx);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t// If no local keybinds were bound, check the global keybinds\n\tif (this->global_context->is_bound(input_ev)) {\n\t\tauto &actions = this->global_context->lookup(input_ev);\n\t\tfor (auto const &action : actions) {\n\t\t\tthis->process_action(input_ev, action, this->global_context);\n\t\t}\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nvoid InputManager::process_action(const input::Event &ev,\n                                  const input_action &action,\n                                  const std::shared_ptr<InputContext> &ctx) {\n\tauto actions = action.action;\n\tevent_arguments args{ev, this->mouse_position, this->mouse_motion, action.flags};\n\tif (actions) {\n\t\tactions.value()(args);\n\t}\n\telse {\n\t\t// do default action if possible\n\t\tswitch (action.action_type) {\n\t\tcase input_action_t::PUSH_CONTEXT: {\n\t\t\tauto ctx_id = action.flags.at(\"id\");\n\t\t\tif (ctx_id != this->get_top_context()->get_id()) {\n\t\t\t\t// prevent unnecessary stacking of the same context\n\t\t\t\tthis->push_context(ctx_id);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase input_action_t::POP_CONTEXT:\n\t\t\tthis->pop_context();\n\t\t\tbreak;\n\n\t\tcase input_action_t::REMOVE_CONTEXT: {\n\t\t\tauto ctx_id = action.flags.at(\"id\");\n\t\t\tthis->pop_context(ctx_id);\n\t\t\tbreak;\n\t\t}\n\t\tcase input_action_t::GAME:\n\t\t\tthis->game_controller->process(args, ctx->get_game_bindings());\n\t\t\tbreak;\n\n\t\tcase input_action_t::CAMERA:\n\t\t\tthis->camera_controller->process(args, ctx->get_camera_bindings());\n\t\t\tbreak;\n\n\t\tcase input_action_t::HUD:\n\t\t\tthis->hud_controller->process(args, ctx->get_hud_bindings());\n\t\t\tbreak;\n\n\t\tcase input_action_t::GUI:\n\t\t\tthis->gui_input->process(args.e.get_event());\n\t\t\tbreak;\n\n\t\tcase input_action_t::CUSTOM:\n\t\t\tthrow Error{MSG(err) << \"CUSTOM action type has no default action.\"};\n\n\t\tdefault:\n\t\t\tthrow Error{MSG(err) << \"Unrecognized action type.\"};\n\t\t}\n\t}\n}\n\n\nvoid setup_defaults(const std::shared_ptr<InputContext> &ctx) {\n\t// hud\n\tinput_action hud_action{input_action_t::HUD};\n\n\t// camera\n\tinput_action camera_action{input_action_t::CAMERA};\n\n\tEvent ev_left{event_class::KEYBOARD, Qt::Key_Left, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_right{event_class::KEYBOARD, Qt::Key_Right, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_up{event_class::KEYBOARD, Qt::Key_Up, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_down{event_class::KEYBOARD, Qt::Key_Down, Qt::NoModifier, QEvent::KeyPress};\n\tEvent ev_wheel_up{event_class::WHEEL, 1, Qt::NoModifier, QEvent::Wheel};\n\tEvent ev_wheel_down{event_class::WHEEL, -1, Qt::NoModifier, QEvent::Wheel};\n\n\tctx->bind(ev_left, camera_action);\n\tctx->bind(ev_right, camera_action);\n\tctx->bind(ev_up, camera_action);\n\tctx->bind(ev_down, camera_action);\n\tctx->bind(ev_wheel_up, camera_action);\n\tctx->bind(ev_wheel_down, camera_action);\n\tctx->bind(event_class::MOUSE_MOVE, {camera_action, hud_action});\n\n\t// game\n\tinput_action game_action{input_action_t::GAME};\n\n\tEvent ev_mouse_lmb{event_class::MOUSE_BUTTON, Qt::LeftButton, Qt::NoModifier, QEvent::MouseButtonRelease};\n\tEvent ev_mouse_rmb{event_class::MOUSE_BUTTON, Qt::RightButton, Qt::NoModifier, QEvent::MouseButtonRelease};\n\n\tctx->bind(ev_mouse_lmb, {game_action, hud_action});\n\tctx->bind(ev_mouse_rmb, {game_action, hud_action});\n\n\t// also forward all other mouse button events\n\tctx->bind(event_class::MOUSE_BUTTON, {game_action, hud_action});\n}\n\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/input_manager.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include <QKeyEvent>\n#include <QMouseEvent>\n#include <QWheelEvent>\n\n#include \"coord/pixel.h\"\n#include \"input/action.h\"\n\nnamespace qtgui {\nclass GuiInput;\n}\n\nnamespace openage::input {\n\nnamespace camera {\nclass Controller;\n} // namespace camera\n\nnamespace game {\nclass Controller;\n} // namespace game\n\nnamespace hud {\nclass Controller;\n} // namespace hud\n\nclass InputContext;\n\n/**\n * The input manager tracks input signals from peripherals or the\n * GUI and turns them into input events that execute some action.\n */\nclass InputManager {\npublic:\n\tInputManager();\n\n\t~InputManager() = default;\n\n\t/**\n\t * Set the GUI input handler.\n\t *\n\t * @param gui_input GUI input handler.\n\t */\n\tvoid set_gui(const std::shared_ptr<qtgui::GuiInput> &gui_input);\n\n\t/**\n\t * Set the controller for the camera.\n\t *\n\t * @param controller Camera controller.\n\t */\n\tvoid set_camera_controller(const std::shared_ptr<camera::Controller> &controller);\n\n\t/**\n\t * Set the controller for the game simulation.\n\t *\n\t * @param controller Game controller.\n\t */\n\tvoid set_game_controller(const std::shared_ptr<game::Controller> &controller);\n\n\t/**\n\t * Set the controller for the HUD.\n\t *\n\t * @param controller HUD controller.\n\t */\n\tvoid set_hud_controller(const std::shared_ptr<hud::Controller> controller);\n\n\t/**\n\t * returns the global keybind context.\n\t * actions bound here will be retained even when override_context is called.\n\t */\n\tconst std::shared_ptr<InputContext> &get_global_context();\n\n\t/**\n\t * Returns the context on top.\n\t * Note there is always a top context\n\t * since the global context will be\n\t * considered on top when none are registered\n\t */\n\tconst std::shared_ptr<InputContext> &get_top_context();\n\n\t/**\n\t * Get the info for all events which are bound currently.\n\t */\n\tstd::vector<std::string> active_binds() const;\n\n\t/**\n\t * Push a context on top of the stack, making it the\n\t * current top context.\n\t *\n\t * if other contexts are registered afterwards,\n\t * it wanders down the stack, i.e. loses priority.\n\t */\n\tvoid push_context(const std::shared_ptr<InputContext> &context);\n\n\t/**\n\t * Push the context with the specified ID on top of the stack,\n\t * making it the current top context.\n\t *\n\t * if other contexts are registered afterwards,\n\t * it wanders down the stack, i.e. loses priority.\n\t */\n\tvoid push_context(const std::string &id);\n\n\t/**\n\t * Remove the current top context from the stack.\n\t */\n\tvoid pop_context();\n\n\t/**\n\t * Removes any registered context matching the specified ID from the stack.\n\t *\n\t * the removal is done by finding the given pointer\n\t * in the `active_contexts` lists, then deleting it in there.\n\t */\n\tvoid pop_context(const std::string &id);\n\n\t/**\n\t * Remove a context from the available contexts.\n\t */\n\tvoid remove_context(const std::string &id);\n\n\t/**\n\t * Add a context to the available contexts.\n\t */\n\tvoid add_context(const std::shared_ptr<InputContext> context);\n\n\t/**\n\t * updates mouse position state and motion\n\t */\n\tvoid set_mouse(int x, int y);\n\n\t/**\n\t * updates mouse motion only\n\t */\n\tvoid set_motion(int x, int y);\n\n\t/**\n\t * Process an input event from the Qt window management.\n\t *\n\t * @param ev Qt input event.\n\t *\n\t * @return true if the event is accepted, else false.\n\t */\n\tbool process(const QEvent &ev);\n\n\nprivate:\n\t/**\n\t * Process the (default) action for an input event.\n\t *\n\t * @param ev Input event.\n\t * @param action Action bound to the event.\n\t * @param bind_ctx Context the action is bound in.\n\t */\n\tvoid process_action(const input::Event &ev,\n\t                    const input_action &action,\n\t                    const std::shared_ptr<InputContext> &ctx);\n\n\t/**\n\t * The global context. Used as fallback.\n\t */\n\tstd::shared_ptr<InputContext> global_context;\n\n\t/**\n\t * Stack of active input contexts.\n\t * The most recent entry is pushed on top of the stack.\n\t */\n\tstd::vector<std::shared_ptr<InputContext>> active_contexts;\n\n\t/**\n\t * Map of all available contexts, referencable by an ID.\n\t *\n\t * TODO: Move this to cvar manager?\n\t */\n\tstd::unordered_map<std::string, std::shared_ptr<InputContext>> available_contexts;\n\n\t/**\n\t * Interface to the game simulation.\n\t */\n\tstd::shared_ptr<game::Controller> game_controller;\n\n\t/**\n\t * Interface to the camera.\n\t */\n\tstd::shared_ptr<camera::Controller> camera_controller;\n\n\t/**\n\t * Interface to the HUD.\n\t */\n\tstd::shared_ptr<hud::Controller> hud_controller;\n\n\t/**\n\t * Interface to the GUI.\n\t */\n\tstd::shared_ptr<qtgui::GuiInput> gui_input;\n\n\t/**\n\t * mouse position in the window\n\t */\n\tcoord::input mouse_position{0, 0};\n\n\t/**\n\t * mouse position relative to the last frame position.\n\t */\n\tcoord::input_delta mouse_motion{0, 0};\n};\n\n/**\n * Setup default input actions:\n *\n * - Camera movement\n *\n * TODO: Make this configurable.\n *\n * @param ctx Input context the actions are added to.\n */\nvoid setup_defaults(const std::shared_ptr<InputContext> &ctx);\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/tests.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"input/input_context.h\"\n#include \"input/input_manager.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n\nnamespace openage::input::tests {\n\nvoid action_demo() {\n\tauto qtapp = std::make_shared<renderer::gui::GuiApplicationWithLogger>();\n\n\t// create a window where we get our inputs from\n\n\trenderer::opengl::GlWindow window(\"openage input test\");\n\n\t// manager that receives window inputs\n\t// the manager creates its own global context with ID \"main\"\n\tauto mgr = input::InputManager();\n\n\t// create 2 additional input contexts to switch to\n\t// context A accepts WASD inputs\n\t// context B accepts LMB + RMB inputs\n\tauto context1 = std::make_shared<input::InputContext>(\"A\");\n\tauto context2 = std::make_shared<input::InputContext>(\"B\");\n\tmgr.add_context(context1);\n\tmgr.add_context(context2);\n\n\t// get the inputs from the Qt window management and forward them to the input manager\n\twindow.add_key_callback([&](const QKeyEvent &ev) {\n\t\tmgr.process(ev);\n\t});\n\twindow.add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tmgr.set_mouse(ev.position().x(), ev.position().y());\n\t\tmgr.process(ev);\n\t});\n\n\t// create action functions\n\t// these are run when an input event is detected\n\taction_func_t push_context{[&](const event_arguments &args) {\n\t\tlog::log(INFO << args.e.info());\n\t\tif (args.flags.at(\"id\") != mgr.get_top_context()->get_id()) {\n\t\t\tmgr.push_context(args.flags.at(\"id\"));\n\t\t\tlog::log(INFO << \"Context pushed: \" << args.flags.at(\"id\"));\n\t\t\tlog::log(INFO << \"Current top context: \" << mgr.get_top_context()->get_id());\n\t\t}\n\t}};\n\taction_func_t remove_context{[&](const event_arguments &args) {\n\t\tlog::log(INFO << args.e.info());\n\t\tmgr.pop_context(args.flags.at(\"id\"));\n\t\tlog::log(INFO << \"Context popped: \" << args.flags.at(\"id\"));\n\t\tlog::log(INFO << \"Current top context: \" << mgr.get_top_context()->get_id());\n\t}};\n\taction_func_t pop_context{[&](const event_arguments &args) {\n\t\tlog::log(INFO << args.e.info());\n\t\tauto popped_id = mgr.get_top_context()->get_id();\n\t\tmgr.pop_context();\n\t\tlog::log(INFO << \"Context popped: \" << popped_id);\n\t\tlog::log(INFO << \"Current top context: \" << mgr.get_top_context()->get_id());\n\t}};\n\taction_func_t key_press{[&](const event_arguments &args) {\n\t\tlog::log(INFO << args.e.info());\n\t\tlog::log(INFO << \"Mouse position at: \" << args.mouse);\n\t}};\n\taction_func_t nop{[&](const event_arguments & /*args*/) {\n\t\t// Do nothing\n\t}};\n\n\t// create the actual actions which have a type, an optional function, and some flags\n\tinput_action push_a{input_action_t::PUSH_CONTEXT, push_context, {{\"id\", \"A\"}}};\n\tinput_action push_b{input_action_t::PUSH_CONTEXT, push_context, {{\"id\", \"B\"}}};\n\tinput_action remove_a{input_action_t::REMOVE_CONTEXT, remove_context, {{\"id\", \"A\"}}};\n\tinput_action remove_b{input_action_t::REMOVE_CONTEXT, remove_context, {{\"id\", \"B\"}}};\n\tinput_action pop{input_action_t::POP_CONTEXT, pop_context};\n\n\tinput_action press_w{input_action_t::CUSTOM, key_press};\n\tinput_action press_a{input_action_t::CUSTOM, key_press};\n\tinput_action press_s{input_action_t::CUSTOM, key_press};\n\tinput_action press_d{input_action_t::CUSTOM, key_press};\n\n\tinput_action press_lmb{input_action_t::CUSTOM, key_press};\n\tinput_action press_rmb{input_action_t::CUSTOM, key_press};\n\n\tinput_action catch_all{input_action_t::CUSTOM, nop};\n\n\t// events that map to specific keys/buttons\n\tEvent ev_up{event_class::KEYBOARD,\n\t            Qt::Key::Key_Up,\n\t            Qt::KeyboardModifier::NoModifier,\n\t            QEvent::KeyRelease};\n\tEvent ev_down{event_class::KEYBOARD,\n\t              Qt::Key::Key_Down,\n\t              Qt::KeyboardModifier::NoModifier,\n\t              QEvent::KeyRelease};\n\n\tEvent ev_w{event_class::KEYBOARD,\n\t           Qt::Key::Key_W,\n\t           Qt::KeyboardModifier::NoModifier,\n\t           QEvent::KeyRelease};\n\tEvent ev_a{event_class::KEYBOARD,\n\t           Qt::Key::Key_A,\n\t           Qt::KeyboardModifier::NoModifier,\n\t           QEvent::KeyRelease};\n\tEvent ev_s{event_class::KEYBOARD,\n\t           Qt::Key::Key_S,\n\t           Qt::KeyboardModifier::NoModifier,\n\t           QEvent::KeyRelease};\n\tEvent ev_d{event_class::KEYBOARD,\n\t           Qt::Key::Key_D,\n\t           Qt::KeyboardModifier::NoModifier,\n\t           QEvent::KeyRelease};\n\n\tEvent ev_lmb{event_class::MOUSE_BUTTON,\n\t             Qt::MouseButton::LeftButton,\n\t             Qt::KeyboardModifier::NoModifier,\n\t             QEvent::MouseButtonRelease};\n\tEvent ev_rmb{event_class::MOUSE_BUTTON,\n\t             Qt::MouseButton::RightButton,\n\t             Qt::KeyboardModifier::NoModifier,\n\t             QEvent::MouseButtonRelease};\n\n\t// bind events to actions in the contexts\n\tmgr.get_global_context()->bind(ev_up, push_a);\n\n\tcontext1->bind(ev_up, push_b);\n\tcontext1->bind(ev_down, pop);\n\tcontext1->bind(ev_w, press_w);\n\tcontext1->bind(ev_a, press_a);\n\tcontext1->bind(ev_s, press_s);\n\tcontext1->bind(ev_d, press_d);\n\tcontext1->bind(event_class::ANY, catch_all);\n\n\tcontext2->bind(ev_down, pop);\n\tcontext2->bind(ev_lmb, press_lmb);\n\tcontext2->bind(ev_rmb, press_rmb);\n\tcontext2->bind(event_class::ANY, catch_all);\n\n\tlog::log(INFO << \"Instructions:\");\n\tlog::log(INFO << \"  1. Press UP and DOWN to add/remove contexts\");\n\tlog::log(INFO << \"  2. Context A accepts WASD events as inputs\");\n\tlog::log(INFO << \"  3. Context B accepts LMB+RMB as inputs\");\n\n\twhile (not window.should_close()) {\n\t\tqtapp->process_events();\n\t\twindow.update();\n\t}\n}\n\n} // namespace openage::input::tests\n"
  },
  {
    "path": "libopenage/input/text_to_event.cpp",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#include \"text_to_event.h\"\n\n#include <QKeySequence>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n\nnamespace openage::input {\n\nEvent text_to_event(const std::string &event_str) {\n\tauto qkey_sequence = QKeySequence::fromString(QString::fromStdString(event_str));\n\n\tif (qkey_sequence.isEmpty()) {\n\t\tthrow Error{MSG(err) << \"Invalid event string: key sequence is empty\"};\n\t}\n\n\tauto qkey_combination = qkey_sequence[0];\n\tauto event = Event(event_class::KEYBOARD,\n\t                   qkey_combination.key(),\n\t                   qkey_combination.keyboardModifiers(),\n\t                   QEvent::KeyRelease // TODO: configurable?\n\t);\n\n\treturn event;\n}\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/input/text_to_event.h",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n#include \"input/event.h\"\n\nnamespace openage::input {\n\n/**\n * Convert a string to an event.\n *\n * TODO: Mouse/Wheel/GUI events\n *\n * @throws if the string is not a valid event.\n */\nEvent text_to_event(const std::string &event_str);\n\n} // namespace openage::input\n"
  },
  {
    "path": "libopenage/job/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tjob_group.cpp\n\tjob_manager.cpp\n\ttests.cpp\n\tworker.cpp\n)\n"
  },
  {
    "path": "libopenage/job/abortable_job_state.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\n#include \"job_aborted_exception.h\"\n#include \"typed_job_state_base.h\"\n#include \"types.h\"\n\nnamespace openage {\nnamespace job {\n\n\n/**\n * An abortable job state supports job that can be aborted. This is done by\n * providing two function objects to the job's function. One is used to check\n * whether the job should be aborted, while the other one aborts the job.\n */\ntemplate <class T>\nclass AbortableJobState : public TypedJobStateBase<T> {\npublic:\n\t/** The job's function. */\n\tabortable_function_t<T> function;\n\n\t/** Creates a new abortable job with the given function and callback. */\n\tAbortableJobState(abortable_function_t<T> function,\n\t                  callback_function_t<T> callback) :\n\t\tTypedJobStateBase<T>{callback},\n\t\tfunction{function} {\n\t}\n\n\t/** Default destructor. */\n\tvirtual ~AbortableJobState() = default;\n\nprotected:\n\tvirtual T execute_and_get(should_abort_t should_abort) {\n\t\t// abort is a function, that can be used to abort a running job. It is\n\t\t// passed to the executing function.\n\t\tstatic auto abort = []() {\n\t\t\tthrow JobAbortedException{};\n\t\t};\n\t\t// should_abort is a function, that returns, whether a running job\n\t\t// should be aborted.\n\t\treturn std::move(this->function(should_abort, abort));\n\t}\n};\n\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <exception>\n#include <memory>\n\n#include \"typed_job_state_base.h\"\n\nnamespace openage {\nnamespace job {\n\nclass JobGroup;\nclass JobManager;\n\n/**\n * A job is a wrapper around a shared job state object and is returned by the\n * job manager. It can be used to retrieve the current state of the job and its\n * result.\n * A job is a lightweight object which only contains a pointer to its internal\n * shared state. Thus it can be copied around without worrying about\n * performance. Further it is not necessary to create or pass pointers to job\n * objects.\n *\n * @param T the job's result type\n */\ntemplate <class T>\nclass Job {\nprivate:\n\t/** A shared pointer to the job's shared state. */\n\tstd::shared_ptr<TypedJobStateBase<T>> state;\n\npublic:\n\t/**\n\t * Creates an empty job object that is not bound to any state. Should only\n\t * be used as dummy object.\n\t */\n\tJob() = default;\n\n\t/**\n\t * Returns whether this job has finished. If this job wrapper has no\n\t * assigned background job, true will be returned.\n\t */\n\tbool is_finished() const {\n\t\tif (this->state) {\n\t\t\treturn this->state->finished.load();\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Returns this job's result if the background execution was successful. If\n\t * an exception has happened, it will be rethrown. This method must not be\n\t * called, if the job's execution has not yet finished.\n\t */\n\tT get_result() {\n\t\tENSURE(this->state, \"getting result of a destroyed or uninitialised job\");\n\t\tENSURE(this->state->finished.load(), \"trying to report a result of an unfinished job\");\n\t\tif (this->state->exception != nullptr) {\n\t\t\tstd::rethrow_exception(this->state->exception);\n\t\t}\n\t\telse {\n\t\t\treturn std::move(this->state->result);\n\t\t}\n\t}\n\nprivate:\n\t/**\n\t * Creates a job with the given shared state. This method may only be called\n\t * by the job manager.\n\t */\n\tJob(std::shared_ptr<TypedJobStateBase<T>> state) :\n\t\tstate{state} {\n\t}\n\n\t/*\n\t * Job manager and job group have to be friends of job in order to access the\n\t * private constructor.\n\t */\n\tfriend class JobGroup;\n\tfriend class JobManager;\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job_aborted_exception.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <stdexcept>\n\nnamespace openage {\nnamespace job {\n\n/** An exception that is thrown when a job wants to abort itself. */\nclass JobAbortedException : public std::exception {\npublic:\n\tconst char *what() const noexcept override {\n\t\treturn \"job aborted\";\n\t}\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job_group.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"job_group.h\"\n\nnamespace openage::job {\n\nJobGroup::JobGroup()\n\t:\n\tJobGroup{nullptr} {\n}\n\nJobGroup::JobGroup(Worker *parent_worker)\n\t:\n\tparent_worker{parent_worker} {\n}\n\n} // namespace openage::job\n"
  },
  {
    "path": "libopenage/job/job_group.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"../error/error.h\"\n#include \"abortable_job_state.h\"\n#include \"job.h\"\n#include \"job_state.h\"\n#include \"types.h\"\n#include \"worker.h\"\n\nnamespace openage {\nnamespace job {\n\nclass JobManager;\n\n/**\n * A job group is a proxy object that forwards job's to a single worker thread.\n * It can be used the assure that multiple jobs are executed on the same\n * background thread.\n */\nclass JobGroup {\nprivate:\n\t/** The parent worker that executes all jobs from this job group. */\n\tWorker *parent_worker;\n\npublic:\n\t/**\n\t * Creates a new empty job group with no parent worker. Should only be used\n\t * as dummy object.\n\t */\n\tJobGroup();\n\n\t/**\n\t * Enqueues the given function into the job group's worker thread. A\n\t * lightweight job object is returned, that allows to keep track of the\n\t * job's state.\n\t *\n\t * @param function the function that is executed as background job\n\t * @param callback the callback function that is executed, when the background\n\t *        job has finished\n\t */\n\ttemplate <class T>\n\tJob<T> enqueue(job_function_t<T> function,\n\t               callback_function_t<T> callback = {}) {\n\t\tENSURE(this->parent_worker, \"job group has no worker thread associated\");\n\t\tauto state = std::make_shared<JobState<T>>(function, callback);\n\t\tthis->parent_worker->enqueue(state);\n\t\treturn Job<T>{state};\n\t}\n\n\t/**\n\t * Enqueues the given function into the job group's worker thread. A\n\t * lightweight job object is returned, that allows to keep track of the\n\t * job's state.The passed function must accept a function object that\n\t * returns, whether the job should be aborted at any time. Further it must\n\t * accept a function that can be used to abort the execution of the\n\t * function.\n\t *\n\t * @param function the function that is executed as background job\n\t * @param callback the callback function that is executed, when the background\n\t *        job has finished\n\t */\n\ttemplate <class T>\n\tJob<T> enqueue(abortable_function_t<T> function,\n\t               callback_function_t<T> callback = {}) {\n\t\tENSURE(this->parent_worker, \"job group has no worker thread associated\");\n\t\tauto state = std::make_shared<AbortableJobState<T>>(function, callback);\n\t\tthis->parent_worker->enqueue(state);\n\t\treturn Job<T>{state};\n\t}\n\nprivate:\n\t/** Creates a new job group with the given parent worker. */\n\tJobGroup(Worker *parent_worker);\n\n\t/**\n\t * The job manager must be a friend of the worker in order to call the\n\t * private constructor.\n\t */\n\tfriend class JobManager;\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job_manager.cpp",
    "content": "// Copyright 2014-2019 the openage authors. See copying.md for legal info.\n\n#include \"job_manager.h\"\n\n#include \"../log/log.h\"\n#include \"../util/thread_id.h\"\n#include \"worker.h\"\n\n\nnamespace openage::job {\n\n\nJobManager::JobManager(int number_of_workers)\n\t:\n\tnumber_of_workers{number_of_workers},\n\tgroup_index{0},\n\tis_running{false} {\n\n\tfor (int i = 0; i < number_of_workers; i++) {\n\t\tthis->workers.emplace_back(new Worker{this});\n\t}\n}\n\n\nJobManager::~JobManager() {\n\tthis->stop();\n}\n\n\nvoid JobManager::start() {\n\t// if the workers have not been started, start them now\n\tif (not this->is_running.load()) {\n\t\tthis->is_running.store(true);\n\t\tfor (auto &worker : this->workers) {\n\t\t\tworker->start();\n\t\t}\n\t\tlog::log(DBG << \"Started JobManager with \" << this->number_of_workers << \" worker threads\");\n\t}\n}\n\n\nvoid JobManager::stop() {\n\t// set is_running to false, notify and join all workers\n\tif (this->is_running.load()) {\n\t\tthis->is_running.store(false);\n\t\tfor (auto &worker : this->workers) {\n\t\t\tworker->stop();\n\t\t}\n\t\tfor (auto &worker : this->workers) {\n\t\t\tworker->join();\n\t\t}\n\n\t\tlog::log(DBG << \"Stopped JobManager with \" << this->number_of_workers << \" worker threads\");\n\t}\n}\n\n\nvoid JobManager::execute_callbacks() {\n\t// run callbacks for finished jobs on this thread id.\n\tsize_t id = util::get_current_thread_id();\n\n\tstd::unique_lock<std::mutex> lock{this->finished_jobs_mutex};\n\tauto it = this->finished_jobs.find(id);\n\n\tif (it != std::end(this->finished_jobs)) {\n\t\t// move the  the finished job list here so we can disable the lock\n\t\tstd::vector<std::shared_ptr<JobStateBase>> jobs;\n\t\tstd::swap(jobs, it->second);\n\n\t\t// clear the worker's old finished job list\n\t\tit->second.clear();\n\t\tlock.unlock();\n\n\t\tfor (auto &job : jobs) {\n\t\t\t// the job may throw an exception here\n\t\t\tjob->execute_callback();\n\t\t}\n\t}\n}\n\n\nJobGroup JobManager::create_job_group() {\n\tauto index = this->group_index;\n\tthis->group_index = (this->group_index + 1) % this->number_of_workers;\n\treturn JobGroup{this->workers[index].get()};\n}\n\n\nvoid JobManager::enqueue_state(const std::shared_ptr<JobStateBase> &state) {\n\tstd::lock_guard<std::mutex> lock{this->pending_jobs_mutex};\n\tthis->pending_jobs.push(state);\n\tfor (auto &worker : this->workers) {\n\t\tworker->notify();\n\t}\n}\n\n\nstd::shared_ptr<JobStateBase> JobManager::fetch_job() {\n\tstd::lock_guard<std::mutex> lock{this->pending_jobs_mutex};\n\tif (this->pending_jobs.empty()) {\n\t\treturn std::shared_ptr<JobStateBase>{};\n\t}\n\n\tauto job = this->pending_jobs.front();\n\tthis->pending_jobs.pop();\n\treturn job;\n}\n\n\nbool JobManager::has_job() {\n\tstd::lock_guard<std::mutex> lock(this->pending_jobs_mutex);\n\treturn not this->pending_jobs.empty();\n}\n\n\nvoid JobManager::finish_job(const std::shared_ptr<JobStateBase> &job) {\n\tstd::lock_guard<std::mutex> lock{this->finished_jobs_mutex};\n\tauto it = this->finished_jobs.find(job->get_thread_id());\n\t// if there hasn't been a finished job for the thread_id, create a new\n\t// entry\n\tif (it == std::end(this->finished_jobs)) {\n\t\tthis->finished_jobs.insert({job->get_thread_id(), {job}});\n\t// otherwise, we append the job to the existing entry\n\t} else {\n\t\tit->second.push_back(job);\n\t}\n}\n\n\n} // namespace openage::job\n"
  },
  {
    "path": "libopenage/job/job_manager.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <atomic>\n#include <condition_variable>\n#include <memory>\n#include <mutex>\n#include <queue>\n#include <thread>\n#include <unordered_map>\n\n#include \"abortable_job_state.h\"\n#include \"job.h\"\n#include \"job_group.h\"\n#include \"job_state.h\"\n#include \"job_state_base.h\"\n#include \"types.h\"\n\nnamespace openage {\nnamespace job {\n\nclass Worker;\n\n/**\n * A job manager can be used to execute functions within separate worker\n * threads.\n */\nclass JobManager {\nprivate:\n\t/** The number of internal worker threads. */\n\tint number_of_workers;\n\n\t/**\n\t * The index of the worker thread, that is used for the next returned job\n\t * group.\n\t */\n\tint group_index;\n\n\t/** A vector of all worker threads. */\n\tstd::vector<std::unique_ptr<Worker>> workers;\n\n\t/** A mutex to synchronize accesses to the internal job queue. */\n\tstd::mutex pending_jobs_mutex;\n\n\t/** A queue of jobs that are to be executed. */\n\tstd::queue<std::shared_ptr<JobStateBase>> pending_jobs;\n\n\t/** A mutex to synchronize the finished job map. */\n\tstd::mutex finished_jobs_mutex;\n\n\t/**\n\t * Mapping from thread id's to a list of jobs, that have been created by the\n\t * corresponding thread and have finished.\n\t */\n\tstd::unordered_map<size_t, std::vector<std::shared_ptr<JobStateBase>>> finished_jobs;\n\n\t/** Whether the job manager is currently running. */\n\tstd::atomic_bool is_running;\n\npublic:\n\t/** Create a new job manager with a specified number of worker threads. */\n\tJobManager(int number_of_workers);\n\n\t/** Destructor that stops the job manager if it is still running. */\n\t~JobManager();\n\n\tJobManager(const JobManager &) = delete;\n\tJobManager(JobManager &&) = delete;\n\n\tJobManager &operator=(const JobManager &) = delete;\n\tJobManager &operator=(JobManager &&) = delete;\n\n\t/** Start the job manager's worker threads. */\n\tvoid start();\n\n\t/**\n\t * Stop the job manager's worker threads. This method blocks until all\n\t * currently working threads have finished.\n\t */\n\tvoid stop();\n\n\t/**\n\t * Enqueues the given function into the job manager's queue, so that it will\n\t * be dispatched by one of the worker threads. A lightweight Job object is\n\t * returned, that allows to keep track of the job's state.\n\t *\n\t * @param function the function that is executed as background job\n\t * @param callback the callback function that is executed, when the background\n\t *        job has finished\n\t */\n\ttemplate <class T>\n\tJob<T> enqueue(job_function_t<T> function,\n\t               callback_function_t<T> callback = {}) {\n\t\tauto state = std::make_shared<JobState<T>>(function, callback);\n\t\tthis->enqueue_state(state);\n\t\treturn Job<T>{state};\n\t}\n\n\t/**\n\t * Enqueues the given function into the job manager's queue, so that it will\n\t * be dispatched by one of the worker threads. A lightweight job object is\n\t * returned, that allows to keep track of the job's state. The passed\n\t * function must accept a function object that returns, whether the job\n\t * should be aborted at any time. Further it must accept a function that\n\t * can be used to abort the execution of the function.\n\t *\n\t * @param function the function that is executed as background job\n\t * @param callback the callback function that is executed, when the background\n\t *        job has finished\n\t */\n\ttemplate <class T>\n\tJob<T> enqueue(abortable_function_t<T> function,\n\t               callback_function_t<T> callback = {}) {\n\t\tauto state = std::make_shared<AbortableJobState<T>>(function, callback);\n\t\tthis->enqueue_state(state);\n\t\treturn Job<T>{state};\n\t}\n\n\t/**\n\t * Creates a job group, in order to be able to execute multiple jobs on the\n\t * same worker thread.\n\t */\n\tJobGroup create_job_group();\n\n\t/**\n\t * Executes callbacks for all job's, that were created by the current thread\n\t * and have finished.\n\t */\n\tvoid execute_callbacks();\n\nprivate:\n\t/** Enqueues the given job into the internal job queue. */\n\tvoid enqueue_state(const std::shared_ptr<JobStateBase> &state);\n\n\t/**\n\t * Returns a job from the internal job queue. If the queue is empty, a\n\t * nullptr is returned.\n\t */\n\tstd::shared_ptr<JobStateBase> fetch_job();\n\n\t/** Returns whether there are jobs to be executed. */\n\tbool has_job();\n\n\t/** Adds a finished job to the internal finished job map. */\n\tvoid finish_job(const std::shared_ptr<JobStateBase> &job);\n\n\t/**\n\t * A worker has to be a friend of the job manager in order to call the\n\t * private finish_job method.\n\t */\n\tfriend class Worker;\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job_state.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\n#include \"typed_job_state_base.h\"\n#include \"types.h\"\n\nnamespace openage {\nnamespace job {\n\n/**\n * A job state supports simple job's with functions that return a single\n * result. While executing the job, it cannot be aborted safely.\n */\ntemplate <class T>\nclass JobState : public TypedJobStateBase<T> {\npublic:\n\t/** A function object which is executed by the JobManager. */\n\tjob_function_t<T> function;\n\n\t/** Creates a new JobState with the given function, that is to be executed. */\n\tJobState(job_function_t<T> function, callback_function_t<T> callback) :\n\t\tTypedJobStateBase<T>{callback},\n\t\tfunction{function} {\n\t}\n\n\t/** Default destructor. */\n\tvirtual ~JobState() = default;\n\nprotected:\n\tT execute_and_get(should_abort_t /*should_abort*/) override {\n\t\treturn this->function();\n\t}\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/job_state_base.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\n#include \"types.h\"\n\nnamespace openage {\nnamespace job {\n\n/**\n * An abstract base class for a shared state of a job. A job state keeps track\n * of its execution state and store's the job's result. Further it keeps track\n * of exceptions that occurred during the job's execution. The real shared state\n * implementation is done in templated subclasses. This is necessary to support\n * arbitrary result types.\n */\nclass JobStateBase {\npublic:\n\t/** Default constructor. */\n\tvirtual ~JobStateBase() = default;\n\n\t/**\n\t * This function executes the job. It returns whether the job has been\n\t * aborted.\n\t */\n\tvirtual bool execute(should_abort_t should_abort) = 0;\n\n\t/**\n\t * Executes the job's callback, if a callback function has been provided\n\t * while constructing this job. This function may only be called if the job\n\t * has already finished.\n\t */\n\tvirtual void execute_callback() = 0;\n\n\t/** Returns the id of the thread that has created this job. */\n\tvirtual size_t get_thread_id() = 0;\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/tests.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"../log/log.h\"\n#include \"../testing/testing.h\"\n\n#include \"job_manager.h\"\n\n#include <atomic>\n\nnamespace openage {\nnamespace job {\nnamespace tests {\n\n\nvoid test_simple_job() {\n\tJobManager manager{4};\n\tmanager.start();\n\n\tstd::atomic<int> finish_count(0);\n\tint job_count = 1337;\n\tbool result = false;\n\n\tauto job_function = []() -> int {\n\t\treturn 42;\n\t};\n\n\tauto job_callback = [&](const result_function_t<int> &get_result) {\n\t\tint job_result = get_result();\n\t\tif (job_result == 42) {\n\t\t\tresult = true;\n\t\t}\n\t\tfinish_count++;\n\t};\n\n\tfor (int i = 0; i < job_count; i++) {\n\t\tmanager.enqueue<int>(job_function, job_callback);\n\t}\n\n\twhile (finish_count.load() < job_count) {\n\t\tmanager.execute_callbacks();\n\t}\n\n\tmanager.stop();\n\n\tresult or TESTFAIL;\n}\n\n\nvoid test_simple_job_with_exception() {\n\tJobManager manager{4};\n\tmanager.start();\n\n\tint good_jobs = 5;\n\tint bad_jobs = 5;\n\tstd::atomic<int> finished(0);\n\tstd::atomic<int> errors(0);\n\n\tauto exception_job_function = []() -> int {\n\t\tthrow \"error string\";\n\t};\n\n\tauto job_function = []() -> int {\n\t\treturn 42;\n\t};\n\n\tauto job_callback = [&](const result_function_t<int> &get_result) {\n\t\ttry {\n\t\t\tget_result();\n\t\t} catch (const char *e) {\n\t\t\terrors++;\n\t\t}\n\t\tfinished++;\n\t};\n\n\tfor (int i = 0; i < good_jobs; i++) {\n\t\tmanager.enqueue<int>(job_function, job_callback);\n\t}\n\n\tfor (int i = 0; i < bad_jobs; i++) {\n\t\tmanager.enqueue<int>(exception_job_function, job_callback);\n\t}\n\n\twhile (finished.load() < good_jobs + bad_jobs) {\n\t\tmanager.execute_callbacks();\n\t}\n\n\tmanager.stop();\n\n\terrors.load() == bad_jobs or TESTFAIL;\n}\n\n\nvoid test_job_manager() {\n\ttest_simple_job();\n\ttest_simple_job_with_exception();\n}\n\n\n}}} // namespace openage::job::tests\n"
  },
  {
    "path": "libopenage/job/typed_job_state_base.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <atomic>\n#include <exception>\n#include <functional>\n\n#include \"../error/error.h\"\n#include \"../util/thread_id.h\"\n#include \"job_aborted_exception.h\"\n#include \"job_state_base.h\"\n#include \"types.h\"\n\nnamespace openage {\nnamespace job {\n\n/**\n * A templated job state base class, that implements basic functionality of job\n * states.\n *\n * @param T the result type of this job state. This type must have a default\n *          constructor and support move semantics.\n */\ntemplate <class T>\nclass TypedJobStateBase : public JobStateBase {\npublic:\n\t/** Id of the thread, that created this job state. */\n\tsize_t thread_id;\n\n\t/**\n\t * A callback function that is called when the job has finished. Can be\n\t * empty.\n\t */\n\tcallback_function_t<T> callback;\n\n\t/**\n\t * Whether the Job's execution has already been finished. An atomic_bool is\n\t * used, as this field can be used by multiple threads. Thus explicit\n\t * synchronization is avoided.\n\t */\n\tstd::atomic_bool finished;\n\n\t/** The result of the Job's executed function. */\n\tT result;\n\n\t/** If executing the Job throws an exception, it is stored here. */\n\tstd::exception_ptr exception;\n\n\t/** Creates a new typed job with the given callback. */\n\tTypedJobStateBase(callback_function_t<T> callback) :\n\t\tthread_id{openage::util::get_current_thread_id()},\n\t\tcallback{callback},\n\t\tfinished{false} {\n\t}\n\n\t/** Default destructor. */\n\tvirtual ~TypedJobStateBase() = default;\n\n\t/**\n\t * Executes the internal function object and stores its result. Occuring\n\t * exceptions are stored, as well. Returns whether this job has been\n\t * aborted.\n\t */\n\tbool execute(should_abort_t should_abort) override {\n\t\ttry {\n\t\t\tthis->result = this->execute_and_get(should_abort);\n\t\t}\n\t\tcatch (JobAbortedException &e) {\n\t\t\treturn true;\n\t\t}\n\t\tcatch (...) {\n\t\t\tthis->exception = std::current_exception();\n\t\t}\n\t\tthis->finished.store(true);\n\t\treturn false;\n\t}\n\n\t/**\n\t * Called when the job was finished.\n\t * This is the result notification for the place where the job was constructed.\n\t */\n\tvoid execute_callback() override {\n\t\tENSURE(this->finished.load(), \"trying to report a result of an unfinished job\");\n\t\tif (this->callback) {\n\t\t\tauto get_result = [this]() {\n\t\t\t\tif (this->exception != nullptr) {\n\t\t\t\t\tstd::rethrow_exception(this->exception);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\treturn std::move(this->result);\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis->callback(get_result);\n\t\t}\n\t}\n\n\tsize_t get_thread_id() override {\n\t\treturn this->thread_id;\n\t}\n\nprotected:\n\t/**\n\t * Executes the job and returns the result. If an exception is thrown it\n\t * must be passed to the calling function.\n\t */\n\tvirtual T execute_and_get(should_abort_t should_abort) = 0;\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/types.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\nnamespace openage {\nnamespace job {\n\n/**\n * Type of a function that is executed by a job and returns a result.\n *\n * @param T the job's result type\n */\ntemplate <typename T>\nusing job_function_t = std::function<T()>;\n\n/**\n * Type of a function that is executed by a job and returns a result. It can be\n * aborted using the two passed function objects.\n *\n * @param T the job's result type\n */\ntemplate <typename T>\nusing abortable_function_t = std::function<T(std::function<bool()>, std::function<void()>)>;\n\n/**\n * Type of a function to retrieve the result of a job. If the job threw an\n * exception, it is rethrown by this function.\n *\n * @param T the job's result type\n */\ntemplate <typename T>\nusing result_function_t = std::function<T()>;\n\n/**\n * Type of a job's callback function, which is called, when a job has finished.\n * The passed result function object is used to retrieve the job's result.\n *\n * @param T the job's result type\n */\ntemplate <typename T>\nusing callback_function_t = std::function<void(result_function_t<T>)>;\n\n/** Type of a function that returns whether a job should be aborted. */\nusing should_abort_t = std::function<bool()>;\n\n/** Type of a function that aborts a job. */\nusing abort_t = std::function<void()>;\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/job/worker.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"job_aborted_exception.h\"\n#include \"job_manager.h\"\n#include \"worker.h\"\n\n#include <memory>\n\n\nnamespace openage {\nnamespace job {\n\n\nWorker::Worker(JobManager *manager)\n\t:\n\tmanager{manager},\n\tis_running{false} {\n}\n\n\nvoid Worker::start() {\n\tthis->is_running = true;\n\tthis->executor = std::make_unique<std::thread>(&Worker::process, this);\n}\n\n\nvoid Worker::stop() {\n\tstd::unique_lock<std::mutex> lock{this->pending_jobs_mutex};\n\tthis->is_running = false;\n\tlock.unlock();\n\tthis->notify();\n}\n\n\nvoid Worker::enqueue(const std::shared_ptr<JobStateBase> &job) {\n\tstd::unique_lock<std::mutex> lock{this->pending_jobs_mutex};\n\tthis->pending_jobs.push(job);\n\tlock.unlock();\n\tthis->notify();\n}\n\n\nvoid Worker::notify() {\n\tthis->jobs_available.notify_all();\n}\n\n\nvoid Worker::join() {\n\tthis->executor->join();\n}\n\n\nvoid Worker::execute_job(std::shared_ptr<JobStateBase> &job) {\n\tauto should_abort = [this]() {\n\t\treturn not this->is_running;\n\t};\n\n\tbool aborted = job->execute(should_abort);\n\t// if the job was not aborted, tell the job manager, that the job has\n\t// finished\n\tif (not aborted) {\n\t\tthis->manager->finish_job(job);\n\t}\n}\n\n\nvoid Worker::process() {\n\t// as long as this worker thread is running repeat all steps\n\twhile (true) {\n\t\t// lock the local thread queue\n\t\tstd::unique_lock<std::mutex> lock{this->pending_jobs_mutex};\n\n\t\t// if this thread shall not run any longer, exit the loop\n\t\tif (not this->is_running) {\n\t\t\treturn;\n\t\t}\n\n\t\t// as long as there are no jobs from the local queue or the job manager\n\t\twhile (this->pending_jobs.empty() and not this->manager->has_job()) {\n\t\t\t// the thread should wait\n\t\t\tthis->jobs_available.wait(lock);\n\n\t\t\t// when the thread is notified, first check if the thread should be\n\t\t\t// stopped\n\t\t\tif (not this->is_running) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// check if there are jobs in the local queue\n\t\tif (not this->pending_jobs.empty()) {\n\t\t\t// fetch the job\n\t\t\tauto job = this->pending_jobs.front();\n\t\t\tthis->pending_jobs.pop();\n\t\t\t// release the local queue lock\n\t\t\tlock.unlock();\n\t\t\t// and execute the job\n\t\t\tthis->execute_job(job);\n\t\t} else {\n\t\t\t// otherwise just unlock the local queue\n\t\t\tlock.unlock();\n\t\t}\n\n\t\t// after possibly executing a job from the local queue, check again if\n\t\t// the thread should still continue running\n\t\tif (not this->is_running) {\n\t\t\treturn;\n\t\t}\n\n\t\t// now try to fetch a job from the job manager\n\t\tauto manager_job = this->manager->fetch_job();\n\t\tif (manager_job.get() != nullptr) {\n\t\t\t// and execute it\n\t\t\tthis->execute_job(manager_job);\n\t\t}\n\t}\n}\n\n\n}} // namespace openage::job\n"
  },
  {
    "path": "libopenage/job/worker.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <atomic>\n#include <condition_variable>\n#include <memory>\n#include <queue>\n#include <thread>\n\n#include \"job_state_base.h\"\n\nnamespace openage {\nnamespace job {\n\nclass JobManager;\n\n/**\n * A worker encapsulates the execution of multiple jobs in a single background\n * thread.\n */\nclass Worker {\nprivate:\n\t/** The parent job manager, this worker is fetching jobs from. */\n\tJobManager *manager;\n\n\t/** Whether this worker thread is still running. */\n\tbool is_running;\n\n\t/** The executing thread. */\n\tstd::unique_ptr<std::thread> executor;\n\n\t/** A mutex to synchronize the internal pending jobs queue. */\n\tstd::mutex pending_jobs_mutex;\n\n\t/** A queue of jobs that are to be executed. */\n\tstd::queue<std::shared_ptr<JobStateBase>> pending_jobs;\n\n\t/** A condition variable to wait for new jobs. */\n\tstd::condition_variable jobs_available;\n\npublic:\n\t/** Constructs a new worker with the parent job manager. */\n\tWorker(JobManager *manager);\n\n\t/** Default destructor. */\n\t~Worker() = default;\n\n\t/** Starts this worker. */\n\tvoid start();\n\n\t/** Stops this worker. */\n\tvoid stop();\n\n\t/** Joins the internal executing thread. */\n\tvoid join();\n\n\t/** Adds the given job to the internal pending job queue. */\n\tvoid enqueue(const std::shared_ptr<JobStateBase> &job);\n\n\t/**\n\t * Notifies this worker, that new jobs are available. This method is\n\t * although called from the parent job manager.\n\t */\n\tvoid notify();\n\nprivate:\n\t/**\n\t * Executes the given job and tells the parent job manager, when it has\n\t * finished.\n\t */\n\tvoid execute_job(std::shared_ptr<JobStateBase> &job);\n\n\t/**\n\t * Fetches pending jobs from the parent job manager and from the internal\n\t * pending job queue and executes them. If no jobs are available the\n\t * internal execution thread is wait using a condition variable.\n\t */\n\tvoid process();\n};\n\n} // namespace job\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tfile_logsink.cpp\n\tlevel.cpp\n\tlog.cpp\n\tlogsink.cpp\n\tlogsource.cpp\n\tmessage.cpp\n\tnamed_logsource.cpp\n\tstdout_logsink.cpp\n\ttest.cpp\n)\n\npxdgen(\n\tlevel.h\n\tlog.h\n\tlogsource.h\n\tnamed_logsource.h\n\tmessage.h\n)\n"
  },
  {
    "path": "libopenage/log/file_logsink.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"file_logsink.h\"\n\n#include <iomanip>\n#include <string>\n\n#include \"log/level.h\"\n#include \"log/logsource.h\"\n#include \"log/message.h\"\n#include \"util/enum.h\"\n\nnamespace openage::log {\n\n\nFileSink::FileSink(const char *filename, bool append) :\n\toutfile{filename, std::ios_base::out | (append ? std::ios_base::app : std::ios_base::trunc)} {}\n\n\nvoid FileSink::output_log_message(const message &msg, LogSource *source) {\n\tthis->outfile << msg.lvl->name << \"|\";\n\tthis->outfile << source->logsource_name() << \"|\";\n\tthis->outfile << msg.filename << \":\" << msg.lineno << \"|\";\n\tthis->outfile << msg.functionname << \"|\";\n\tthis->outfile << msg.thread_id << \"|\";\n\tthis->outfile << std::setprecision(7) << std::fixed << msg.timestamp / 1e9 << \"|\";\n\tthis->outfile << msg.text << std::endl;\n}\n\n} // namespace openage::log\n"
  },
  {
    "path": "libopenage/log/file_logsink.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <fstream>\n\n#include \"logsink.h\"\n\nnamespace openage {\nnamespace log {\nclass LogSource;\nstruct message;\n\nclass FileSink : public LogSink {\npublic:\n\tFileSink(const char *filename, bool append);\n\nprivate:\n\tvirtual void output_log_message(const message &msg, LogSource *source) override;\n\n\tstd::ofstream outfile;\n};\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/level.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"level.h\"\n\nnamespace openage {\n\nnamespace log {\n\nstatic constexpr level_value undefined {{\"UNDEFINED\", 999}, \"5\"};\n\nlevel::level() : Enum{undefined} {}\n\n}} // openage::log\n"
  },
  {
    "path": "libopenage/log/level.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libopenage.util.enum cimport Enum, EnumValue\n#include \"../util/compiler.h\"\n#include \"../util/enum.h\"\n\n\nnamespace openage {\nnamespace log {\n\n/**\n * Data associated with a log level.\n *\n * pxd:\n *\n * cppclass level_value(EnumValue[level_value, int]):\n *     const char *colorcode\n */\nstruct OAAPI level_value : util::EnumValue<level_value> {\n\tconst char *colorcode;\n};\n\n\n/**\n * Available logging levels.\n *\n * pxd:\n *\n * cppclass level(Enum[level_value]):\n *     pass\n *\n * level MIN  \"::openage::log::level::MIN\"\n * level spam \"::openage::log::level::spam\"\n * level dbg  \"::openage::log::level::dbg\"\n * level info \"::openage::log::level::info\"\n * level warn \"::openage::log::level::warn\"\n * level err  \"::openage::log::level::err\"\n * level crit \"::openage::log::level::crit\"\n * level MAX  \"::openage::log::level::MAX\"\n */\n\nstruct OAAPI level : util::Enum<level_value> {\n\tusing util::Enum<level_value>::Enum;\n\n\t// a default constructor for level.\n\t// this is needed to allow use from Cython.\n\t// initializes the level to an internal UNDEFINED value.\n\tlevel();\n\n#ifdef __MINGW32__\n\t// Do not try to optimize these out even if it seems they are not used.\n\t// Namely MIN that is not used within the library.\n\t#define NOOPTIMIZE __attribute__((__used__))\n#else\n\t#define NOOPTIMIZE\n#endif // _win32\n\n\tstatic constexpr level_value MIN NOOPTIMIZE{{\"min loglevel\", -1000}, \"5\"};\n\n\tstatic constexpr level_value spam NOOPTIMIZE{{\"SPAM\", -100}, \"\"};\n\tstatic constexpr level_value dbg NOOPTIMIZE{{\"DBG\", -20}, \"\"};\n\tstatic constexpr level_value info NOOPTIMIZE{{\"INFO\", 0}, \"\"};\n\tstatic constexpr level_value warn NOOPTIMIZE{{\"WARN\", 100}, \"33\"};\n\tstatic constexpr level_value err NOOPTIMIZE{{\"ERR\", 200}, \"31;1\"};\n\tstatic constexpr level_value crit NOOPTIMIZE{{\"CRIT\", 500}, \"31;1;47\"};\n\n\tstatic constexpr level_value MAX NOOPTIMIZE{{\"max loglevel\", 1000}, \"5\"};\n\n#undef NOOPTIMIZE\n};\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/log.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"log.h\"\n\n#include \"log/named_logsource.h\"\n#include \"log/stdout_logsink.h\"\n\n\nnamespace openage {\nnamespace log {\n\n\nvoid log(const message &msg) {\n\tgeneral_source().log(msg);\n}\n\n\nvoid set_level(level lvl) {\n\tglobal_stdoutsink().set_loglevel(lvl);\n}\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/log.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libopenage.log.level cimport level\n#include \"../util/compiler.h\"\n#include \"./level.h\"\n#include \"./message.h\"\n\n\nnamespace openage {\nnamespace log {\n\nstruct message;\n\n/**\n * Convenience method that makes use of the 'general' LogSource.\n *\n * Invokes general_source()->log(msg).\n */\nvoid log(const message &msg);\n\n\n/**\n * Sets the log level of the global stdout sink.\n *\n * pxd: void set_level(level lvl) except +\n */\nOAAPI void set_level(level lvl);\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/logsink.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"logsink.h\"\n\n#include <algorithm>\n\n#include \"message.h\"\n\nnamespace openage {\nnamespace log {\n\n\nLogSink::LogSink() {\n\tLogSinkList::instance().add(this);\n\n\tthis->set_loglevel(level::dbg);\n}\n\n\nLogSink::~LogSink() {\n\tLogSinkList::instance().remove(this);\n}\n\n\nvoid LogSink::set_loglevel(level loglevel) {\n\tthis->loglevel = loglevel;\n\tLogSinkList::instance().loglevel_changed();\n}\n\n\nLogSinkList::LogSinkList() {\n\tthis->set_lowest_loglevel();\n}\n\n\nLogSinkList &LogSinkList::instance() {\n\tstatic LogSinkList instance;\n\treturn instance;\n}\n\n\nvoid LogSinkList::log(const message &msg, class LogSource *source) const {\n\tstd::lock_guard<std::mutex> lock(this->sinks_mutex);\n\tfor (auto *sink : this->sinks) {\n\t\t// TODO: more sophisticated filtering (iptables-chains-like)\n\t\tif (msg.lvl >= sink->loglevel) {\n\t\t\tsink->output_log_message(msg, source);\n\t\t}\n\t}\n}\n\n\nvoid LogSinkList::add(LogSink *sink) {\n\tstd::lock_guard<std::mutex> lock(this->sinks_mutex);\n\tthis->sinks.push_back(sink);\n\tthis->set_lowest_loglevel();\n}\n\n\nvoid LogSinkList::loglevel_changed() {\n\tstd::lock_guard<std::mutex> lock(this->sinks_mutex);\n\tthis->set_lowest_loglevel();\n}\n\n\nvoid LogSinkList::set_lowest_loglevel() {\n\tthis->lowest_loglevel = level::MAX;\n\tfor (auto *sink : this->sinks) {\n\t\tthis->lowest_loglevel = std::min(this->lowest_loglevel, sink->loglevel);\n\t}\n}\n\n\nvoid LogSinkList::remove(LogSink *sink) {\n\tstd::lock_guard<std::mutex> lock(this->sinks_mutex);\n\tthis->sinks.remove(sink);\n\tthis->set_lowest_loglevel();\n}\n\n\nbool LogSinkList::supports_loglevel(level loglevel) const {\n\tstd::lock_guard<std::mutex> lock(this->sinks_mutex);\n\treturn loglevel >= this->lowest_loglevel;\n}\n\n}} // namespace openage::log\n"
  },
  {
    "path": "libopenage/log/logsink.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <list>\n#include <mutex>\n\n#include \"../util/compiler.h\"\n#include \"./level.h\"\n\n\nnamespace openage::log {\nstruct message;\n\n/**\n * Abstract base for classes that - in one way or an other - print log messages.\n *\n * Instances of this class are automatically added to LogSource::global_sink_list\n * vector by their constructors (and removed by their destructors).\n */\nclass LogSink {\npublic:\n\tLogSink();\n\tvirtual ~LogSink();\n\n\n\t/**\n\t * TODO: Add iptables-like chains that decide whether a message will be\n\t *       logged, depending on msg.info, logger id, thread id, etc.\n\t *       This member variable is only a make-shift solution with\n\t *       obvious limitations.\n\t */\n\tvoid set_loglevel(level loglevel);\n\nprivate:\n\tlevel loglevel;\n\n\t/**\n\t * Called internally by put_log_message if a message is accepted\n\t */\n\tvirtual void output_log_message(const struct message &msg, class LogSource *source) = 0;\n\n\n\tfriend class LogSinkList;\n};\n\n\n/**\n * Holds a list of all registered log sinks;\n * Maintained from the LogSink constructors/destructors.\n */\nclass OAAPI LogSinkList {\npublic:\n\tstatic LogSinkList &instance();\n\n\tLogSinkList(LogSinkList const &) = delete;\n\n\tvoid operator=(LogSinkList const &) = delete;\n\n\tvoid log(const message &msg, class LogSource *source) const;\n\n\tvoid add(LogSink *sink);\n\n\tvoid remove(LogSink *sink);\n\n\tbool supports_loglevel(level loglevel) const;\n\n\tvoid loglevel_changed();\n\nprivate:\n\tLogSinkList();\n\n\tstd::list<LogSink *> sinks;\n\n\tmutable std::mutex sinks_mutex;\n\n\tvoid set_lowest_loglevel();\n\n\tlevel lowest_loglevel;\n};\n\n\n} // namespace openage::log\n"
  },
  {
    "path": "libopenage/log/logsource.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"logsource.h\"\n\n#include <cstddef>\n#include <string>\n\n#include \"log/logsink.h\"\n#include \"log/stdout_logsink.h\"\n#include \"util/compiler.h\"\n\n\nnamespace openage {\nnamespace log {\n\n\nLogSource::LogSource() :\n\tlogger_id{LogSource::get_unique_logger_id()} {}\n\n\nvoid LogSource::log(const message &msg) {\n\t// ensure that the global stdoutsink has been constructed\n\t// (and thus at least one sink exists).\n\tglobal_stdoutsink();\n\n\tLogSinkList::instance().log(msg, this);\n}\n\n\nsize_t LogSource::get_unique_logger_id() {\n\t// Strictly-monotonically increasing counter.\n\tstatic std::atomic<size_t> ctr{0};\n\n\treturn ctr++;\n}\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/logsource.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n\n// pxd: from libcpp.string cimport string\n#include <string>\n\n// pxd: from libopenage.log.message cimport message\n#include \"message.h\"\n\n#include \"../util/compiler.h\"\n\n\nnamespace openage {\nnamespace log {\nstruct message;\n\n/**\n * Any class that wants to provide .log() shall inherit from this.\n * Examples: Engine, Unit, ...\n *\n * pxd:\n *\n * cppclass LogSource:\n *     void log(message msg) except +\n *     const size_t logger_id\n *     string logsource_name() except +\n */\nclass OAAPI LogSource {\npublic:\n\tLogSource();\n\n\t// make class polymorphic to provide TypeInfo for dynamic casting.\n\tvirtual ~LogSource() = default;\n\n\t/**\n\t * Logs a message (get one via MSG(level)).\n\t */\n\tvoid log(const message &msg);\n\n\t/**\n\t * Initialized during the LogSource constructor,\n\t * guaranteed to be unique.\n\t */\n\tconst size_t logger_id;\n\n\tvirtual std::string logsource_name() = 0;\n\nprivate:\n\t/**\n\t * Provides unique logger ids.\n\t */\n\tstatic size_t get_unique_logger_id();\n};\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/message.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"message.h\"\n\n#include <iomanip>\n#include <mutex>\n#include <ostream>\n#include <unordered_set>\n#include <utility>\n\n#include \"log/level.h\"\n#include \"util/enum.h\"\n#include \"util/stringformatter.h\"\n#include \"util/thread_id.h\"\n#include \"util/timing.h\"\n\n\nnamespace openage {\nnamespace log {\n\n\nvoid message::init() {\n\tthis->thread_id = util::get_current_thread_id();\n\tthis->timestamp = timing::get_real_time();\n}\n\n\nvoid message::init_with_metadata_copy(const std::string &filename, const std::string &functionname) {\n\tstatic std::unordered_set<std::string> stringconstants;\n\tstatic std::mutex stringconstants_mutex;\n\n\tstd::lock_guard<std::mutex> lock{stringconstants_mutex};\n\n\tthis->init();\n\tthis->filename = stringconstants.insert(filename).first->c_str();\n\tthis->functionname = stringconstants.insert(functionname).first->c_str();\n}\n\n\nMessageBuilder::MessageBuilder(const char *filename,\n                               unsigned lineno,\n                               const char *functionname,\n                               level lvl) :\n\tStringFormatter<MessageBuilder>{msg.text} {\n\tthis->msg.filename = filename;\n\tthis->msg.lineno = lineno;\n\tthis->msg.functionname = functionname;\n\tthis->msg.lvl = lvl;\n\n\tthis->msg.init();\n}\n\n\nstd::ostream &operator<<(std::ostream &os, const message &msg) {\n\tos << \"\\x1b[\" << msg.lvl->colorcode << \"m\" << std::setw(4) << msg.lvl->name << \"\\x1b[m \";\n\tos << msg.filename << \":\" << msg.lineno << \" \";\n\tos << \"(\" << msg.functionname;\n\tos << \", thread \" << msg.thread_id << \")\";\n\tos << \": \" << msg.text;\n\n\treturn os;\n}\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/message.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <iosfwd>\n\n// pxd: from libc.stdint cimport int64_t\n#include <cstdint>\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"../util/compiler.h\"\n#include \"../util/consteval.h\"\n#include \"../util/stringformatter.h\"\n#include \"config.h\"\n#include \"logsink.h\"\n\n// pxd: from libopenage.log.level cimport level\n#include \"level.h\"\n\n\n#if defined(__GNUC__)\n\t#define OPENAGE_FUNC_NAME __PRETTY_FUNCTION__\n#elif defined(_MSC_VER)\n\t#define OPENAGE_FUNC_NAME __FUNCSIG__\n#else\n\t#define OPENAGE_FUNC_NAME __FUNCTION__\n#endif\n\nnamespace openage {\n\n\n// forward-declaration for use in 'friend' declaration below.\nnamespace error {\nclass Error;\n}\n\n\nnamespace log {\n\n\n/**\n * A complete log/exception message, containing a text message and metadata.\n *\n * The preferred way of creating a `message` object is to use MessageBuilder\n * via the MSG macro.\n * MessageBuilder is auto-converted to message when needed.\n *\n * pxd:\n *\n * cppclass message:\n *    string text\n *    char *filename\n *    unsigned lineno\n *    const char *functionname\n *    level lvl\n *    size_t thread_id\n *    int64_t timestamp\n *\n *    void init() except +\n *    void init_with_metadata_copy(string filename, string functionname) except +\n */\nstruct OAAPI message {\n\tstd::string text;\n\n\t/**\n\t * The filename where the message has been constructed.\n\t */\n\tconst char *filename;\n\n\t/**\n\t * The line number where the message has been constructed.\n\t */\n\tunsigned lineno;\n\n\t/**\n\t * The (pretty) function name where the message has been constructed.\n\t */\n\tconst char *functionname;\n\n\t/**\n\t * The log level.\n\t */\n\tlevel lvl;\n\n\t/**\n\t * A unique id for the thread where the message has been constructed.\n\t */\n\tsize_t thread_id;\n\n\t/**\n\t * A (nanosecond-resolution) timestamp of the message construction.\n\t */\n\tint64_t timestamp;\n\n\t/**\n\t * Sets all members except for filename, lineno, functionname and level.\n\t */\n\tvoid init();\n\n\t/**\n\t * Sets all members except for lineno and level.\n\t *\n\t * filename and functionname are copied to an internal cache.\n\t *\n\t * Designed to be used when filename and functionname are temporary\n\t * objects (e.g. from Python).\n\t */\n\tvoid init_with_metadata_copy(const std::string &filename, const std::string &functionname);\n};\n\n\n/**\n * prints message to a stream (with color codes and everything!)\n */\nstd::ostream &operator<<(std::ostream &os, const message &msg);\n\n/**\n * Wrapper around a log message that allows appending to the message with operator <<.\n *\n * Auto-converts to message; via the MSG macro, this is the preferred way of creating\n * log and exc messages.\n */\nclass OAAPI MessageBuilder : public util::StringFormatter<MessageBuilder> {\npublic:\n\t/**\n\t * Don't use this constructor directly; instead use the MSG macro.\n\t *\n\t *\n\t * @param filename, lineno source file name and line number (__FILE__, __LINE__).\n\t * @param functionname       (fully qualified) function name (OPENAGE_FUNC_NAME).\n\t * @param lvl                loglevel of the message. Also required for exception messages.\n\t */\n\tMessageBuilder(const char *filename, unsigned lineno, const char *functionname, level lvl = level::info);\n\n\t// auto-convert to message\n\tinline operator const message &() const {\n\t\treturn this->msg;\n\t}\n\n\tinline operator message &() {\n\t\treturn this->msg;\n\t}\n\n\tinline bool should_format() const override {\n\t\t// only format if this message will actually be logged\n\t\treturn LogSinkList::instance().supports_loglevel(this->msg.lvl);\n\t}\n\nprivate:\n\tmessage msg;\n\n\tfriend error::Error;\n\tfriend class LogSource;\n};\n\n\n// MSG is resolved to a MessageBuilder constructor invocation;\n// it fills in information such as __FILE__, __LINE__ and OPENAGE_FUNC_NAME.\n//\n// Unfortunately, macros are the only way to achieve that.\n\n\n// for use with existing log::level objects\n#define MSG_LVLOBJ(LVLOBJ) \\\n\t::openage::log::MessageBuilder( \\\n\t\t::openage::util::consteval_::strip_prefix( \\\n\t\t\t__FILE__, \\\n\t\t\t::openage::config::buildsystem_sourcefile_dir), \\\n\t\t__LINE__, \\\n\t\tOPENAGE_FUNC_NAME, \\\n\t\tLVLOBJ)\n\n\n// for use with log::level literals (auto-prefixes full qualification)\n#define MSG(LVL) MSG_LVLOBJ(::openage::log::level::LVL)\n\n\n// some convenience shorteners for MSG(...).\n#define SPAM MSG(spam)\n#define DBG MSG(dbg)\n#define INFO MSG(info)\n#define WARN MSG(warn)\n#define ERR MSG(err)\n#define CRIT MSG(crit)\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/named_logsource.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"named_logsource.h\"\n\n#include <utility>\n\nnamespace openage::log {\n\n\nNamedLogSource::NamedLogSource(std::string name)\n\t:\n\tname{std::move(name)} {}\n\n\nstd::string NamedLogSource::logsource_name() {\n\treturn this->name;\n}\n\n\nNamedLogSource &general_source() {\n\tstatic NamedLogSource value{\"general\"};\n\treturn value;\n}\n\n\n} // namespace openage::log\n"
  },
  {
    "path": "libopenage/log/named_logsource.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp.string cimport string\n#include <string>\n\n// pxd: from libopenage.log.logsource cimport LogSource\n#include \"logsource.h\"\n\n#include \"../util/compiler.h\"\n\n\nnamespace openage {\nnamespace log {\n\n/**\n * Simple pure log source class with a string name.\n *\n * pxd:\n *\n * cppclass NamedLogSource(LogSource):\n *     NamedLogSource(string name) except +\n */\nclass OAAPI NamedLogSource : public LogSource {\npublic:\n\tNamedLogSource(std::string name);\n\n\tstd::string logsource_name() override;\n\nprivate:\n\tstd::string name;\n};\n\n\n/**\n * Returns a reference to a general named log source, for use by log::log().\n */\nNamedLogSource &general_source();\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/stdout_logsink.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"stdout_logsink.h\"\n\n#include <iomanip>\n#include <iostream>\n#include <string>\n\n#ifdef _MSC_VER\n\t#define WIN32_LEAN_AND_MEAN\n\t#include <Windows.h>\n#endif\n\n#include \"log/level.h\"\n#include \"log/logsource.h\"\n#include \"log/message.h\"\n#include \"log/named_logsource.h\"\n#include \"util/enum.h\"\n\n\nnamespace openage::log {\n\nnamespace {\n\n\n// Try to enable ansi color codes on windows machines.\nvoid enable_ansi_color_codes() {\n#ifdef _MSC_VER\n\tHANDLE console_output_handle = GetStdHandle(STD_OUTPUT_HANDLE);\n\tif (console_output_handle == INVALID_HANDLE_VALUE) {\n\t\treturn;\n\t}\n\n\tDWORD original_output_mode;\n\tif (!GetConsoleMode(console_output_handle, &original_output_mode)) {\n\t\treturn;\n\t}\n\n\tDWORD request_output_mode = original_output_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;\n\tSetConsoleMode(console_output_handle, request_output_mode);\n#endif\n}\n\n\n} // namespace\n\n\nStdOutSink::StdOutSink() {\n\tenable_ansi_color_codes();\n}\n\n\nvoid StdOutSink::output_log_message(const message &msg, LogSource *source) {\n\t// print log level (width 4)\n\tstd::cout << \"\\x1b[\" << msg.lvl->colorcode << \"m\" << std::setw(4) << msg.lvl->name << \"\\x1b[m\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t  \" \";\n\n\tif (msg.thread_id != 0) {\n\t\tstd::cout << \"\\x1b[32m\"\n\t\t\t\t\t \"[T\"\n\t\t\t\t  << msg.thread_id << \"]\\x1b[m \";\n\t}\n\n\tif (source != &general_source()) {\n\t\tstd::cout << \"\\x1b[36m\"\n\t\t\t\t\t \"[\"\n\t\t\t\t  << source->logsource_name() << \"]\\x1b[m \";\n\t}\n\n\tstd::cout << msg.text << std::endl;\n}\n\n\nStdOutSink &global_stdoutsink() {\n\tstatic StdOutSink value;\n\treturn value;\n}\n\n\n} // namespace openage::log\n"
  },
  {
    "path": "libopenage/log/stdout_logsink.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"logsink.h\"\n\nnamespace openage {\nnamespace log {\nclass LogSource;\nstruct message;\n\n/**\n * Simple logsink that prints messages to stdout (via std::cout).\n */\nclass StdOutSink : public LogSink {\npublic:\n\tStdOutSink();\n\nprivate:\n\tvoid output_log_message(const message &msg, LogSource *source) override;\n};\n\n\n/**\n * Returns a reference to the global stdout logsink object;\n * Initializes the object if needed.\n */\nStdOutSink &global_stdoutsink();\n\n\n} // namespace log\n} // namespace openage\n"
  },
  {
    "path": "libopenage/log/test.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include <iostream>\n#include <string>\n#include <thread>\n\n#include \"log/logsink.h\"\n#include \"log/logsource.h\"\n#include \"log/message.h\"\n#include \"util/stringformatter.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::log::tests {\n\n\nclass TestLogSource : public LogSource {\npublic:\n\tstd::string logsource_name() override {\n\t\treturn \"TestLogSource\";\n\t}\n};\n\n\nclass TestLogSink : public LogSink {\npublic:\n\texplicit TestLogSink(std::ostream &os) :\n\t\tos{os} {}\n\nprivate:\n\tstd::ostream &os;\n\n\tvoid output_log_message(const message &msg, LogSource * /*source*/) override {\n\t\tthis->os << msg << std::endl;\n\t}\n};\n\n\nvoid demo() {\n\tTestLogSource logger;\n\tTestLogSink sink{std::cout};\n\n\tlogger.log(MSG(err) << 1337 << \"lol...\");\n\tlogger.log(MSG(crit) << util::sformat(\"wtf %s %d\", \"test\", 1337));\n\n\tstd::thread t0([&]() {\n\t\tlogger.log(MSG(info) << \"this msg comes from a thread\");\n\t});\n\n\tstd::thread t1([&]() {\n\t\tlogger.log(MSG(dbg) << \"this one, too!\");\n\t});\n\n\tauto m = MSG(warn);\n\tfor (int i = 0; i < 4; i++) {\n\t\tm << i << \", \";\n\t}\n\n\tlogger.log(m);\n\n\tt0.join();\n\tt1.join();\n}\n\n} // namespace openage::log::tests\n"
  },
  {
    "path": "libopenage/main/CMakeLists.txt",
    "content": "add_sources(libopenage\n\ttests.cpp\n)\n\npxdgen(\n\ttests.h\n)\n\nadd_subdirectory(\"demo\")\n"
  },
  {
    "path": "libopenage/main/demo/CMakeLists.txt",
    "content": "add_subdirectory(\"pong\")\nadd_subdirectory(\"presenter\")\nadd_subdirectory(\"interactive\")\n"
  },
  {
    "path": "libopenage/main/demo/interactive/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tinteractive.cpp\n)\n"
  },
  {
    "path": "libopenage/main/demo/interactive/interactive.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"interactive.h\"\n\n#include <algorithm>\n\n#include \"log/log.h\"\n\n#include \"assets/mod_manager.h\"\n#include \"engine/engine.h\"\n\n\nnamespace openage::main::tests::interactive {\n\nvoid demo(const util::Path &path) {\n\tlog::log(INFO << \"Launching interactive demo...\");\n\n\t// check for available modpacks\n\tauto modpack_folder = path / \"assets\" / \"converted\";\n\tauto mods = assets::ModManager::enumerate_modpacks(modpack_folder);\n\n\t// check if the engine modpack exists\n\tauto mod_engine = std::find_if(mods.begin(), mods.end(), [](const assets::ModpackInfo &mod) {\n\t\treturn mod.id == \"engine\";\n\t});\n\tif (mod_engine == std::end(mods)) {\n\t\tthrow Error{MSG(err) << \"Modpack 'engine' not found.\\n\"\n\t\t                     << \"You need to export it first by running \"\n\t\t                     << \"'python -m openage convert-export-api'\"};\n\t}\n\telse {\n\t\t// remove the engine modpack from the mod selection\n\t\t// because it's not playable\n\t\tmods.erase(mod_engine);\n\t}\n\n\t// check if there are is at least one playable modpack\n\tif (mods.empty()) {\n\t\tthrow Error{MSG(err) << \"No playable modpacks found.\\n\"\n\t\t                     << \"Try exporting some with \"\n\t\t                     << \"'python -m openage convert'\"};\n\t}\n\n\t// select one of the modpacks\n\tstd::cout << \"Choose modpack to use:\" << std::endl;\n\tfor (size_t i = 0; i < mods.size(); ++i) {\n\t\tstd::cout << \"(\" << i << \")\"\n\t\t\t\t  << \" \" << mods[i].id\n\t\t\t\t  << std::endl;\n\t}\n\tstd::cout << \"> \";\n\tsize_t mod_idx;\n\tstd::cin >> mod_idx;\n\n\t// start the engine with the selected modpack\n\tauto engine = engine::Engine(engine::Engine::mode::FULL,\n\t                             path,\n\t                             {mods[mod_idx].id});\n\n\t// engine main thread has to loop, otherwise RAII frees up\n\t// the members of engine and we get allocation errors\n\tengine.loop();\n}\n\n} // namespace openage::main::tests::interactive\n"
  },
  {
    "path": "libopenage/main/demo/interactive/interactive.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\n\nnamespace openage::main::tests::interactive {\n\nvoid demo(const util::Path &path);\n\n} // namespace openage::main::tests::interactive\n"
  },
  {
    "path": "libopenage/main/demo/pong/CMakeLists.txt",
    "content": "add_sources(libopenage\n\taicontroller.cpp\n\tgamestate.cpp\n\tgui.cpp\n\tphysics.cpp\n\tpong.cpp\n)\n"
  },
  {
    "path": "libopenage/main/demo/pong/aicontroller.cpp",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#include \"aicontroller.h\"\n\n\nnamespace openage::main::tests::pong {\n\n\nstd::vector<PongEvent> get_ai_inputs(const std::shared_ptr<PongPlayer> &player,\n                                     const std::shared_ptr<PongBall> &ball,\n                                     const std::shared_ptr<curve::Discrete<util::Vector2s>> &area_size_curve,\n                                     const time::time_t &now,\n                                     bool right_player) {\n\tstd::vector<PongEvent> ret;\n\n\tauto player_pos = player->position->get(now);\n\tauto player_size = player->size->get(now);\n\tauto ball_pos = ball->position->get(now);\n\tauto speed = ball->speed->get(now);\n\n\tauto area_size = area_size_curve->get(now);\n\tdouble area_width = area_size[0];\n\tdouble area_height = area_size[1];\n\n\ttime::time_t hit_time;\n\tutil::Vector2d hit_pos;\n\n\t// calculate ball trajectory\n\t// extrapolate trajectory to player's panel wall\n\t// follow reflections at walls\n\t// move panel to predicted hit position of panel wall\n\n\twhile (true) {\n\t\ttime::time_t ty_hit = 0, tx_hit = 0;\n\n\t\tif (speed[0] == 0) {\n\t\t\ttx_hit = time::TIME_MAX;\n\t\t}\n\t\telse if (speed[0] > 0) {\n\t\t\ttx_hit = time::time_t::from_double((area_width - ball_pos[0]) / speed[0]);\n\t\t}\n\t\telse if (speed[0] < 0) {\n\t\t\ttx_hit = time::time_t::from_double(ball_pos[0] / -speed[0]);\n\t\t}\n\n\t\tif (speed[1] == 0) {\n\t\t\tty_hit = time::TIME_MAX;\n\t\t}\n\t\telse if (speed[1] > 0) {\n\t\t\tty_hit = time::time_t::from_double((area_height - ball_pos[1]) / speed[1]);\n\t\t}\n\t\telse if (speed[1] < 0) {\n\t\t\tty_hit = time::time_t::from_double(ball_pos[1] / -speed[1]);\n\t\t}\n\n\t\t// actual hit has lowest time:\n\t\thit_time = std::min(ty_hit, tx_hit);\n\t\thit_pos = ball_pos + (speed * hit_time.to_double());\n\n\t\t// continue calculating until panel hit\n\t\tif (ty_hit < tx_hit) {\n\t\t\tspeed[1] *= -1;\n\t\t\tball_pos = hit_pos;\n\t\t\tcontinue;\n\t\t}\n\t\telse {\n\t\t\t// stop the iteration if the ball is moving towards us\n\t\t\t// i.e. it is not hitting the panel on the oposite wall.\n\t\t\tif (right_player and speed[0] > 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\telse if (not right_player and speed[0] < 0) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tspeed[0] *= -1;\n\t\t\tball_pos = hit_pos;\n\t\t\tcontinue;\n\t\t}\n\t}\n\n\t// move the panel towards the ball hit pos, but only\n\t// if the ball does not hit the middle piece of the 3-divided panel\n\tif (hit_pos[1] > player_pos + (player_size / 3)) {\n\t\tret.push_back(PongEvent{player->id(), PongEvent::DOWN});\n\t}\n\telse if (hit_pos[1] < player_pos - (player_size / 3)) {\n\t\tret.push_back(PongEvent{player->id(), PongEvent::UP});\n\t}\n\telse {\n\t\tret.push_back(PongEvent{player->id(), PongEvent::IDLE});\n\t}\n\n\treturn ret;\n}\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/aicontroller.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"main/demo/pong/gamestate.h\"\n\n\nnamespace openage::main::tests::pong {\n\nstd::vector<PongEvent> get_ai_inputs(\n\tconst std::shared_ptr<PongPlayer> &player,\n\tconst std::shared_ptr<PongBall> &ball,\n\tconst std::shared_ptr<curve::Discrete<util::Vector2s>> &area_size,\n\tconst time::time_t &now,\n\tbool right_player);\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/gamestate.cpp",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#include \"gamestate.h\"\n\n#include <sstream>\n\n#include \"main/demo/pong/gui.h\"\n#include \"log/log.h\"\n#include \"util/strings.h\"\n\nnamespace openage::main::tests::pong {\n\n\nusing namespace std::placeholders;\nPongPlayer::PongPlayer(const std::shared_ptr<event::EventLoop> &mgr, size_t id)\n\t:\n\tEventEntity{mgr},\n\tspeed(std::make_shared<curve::Discrete<float>>(\n\t\t      mgr,\n\t\t      (id << 4) + 1,\n\t\t      util::sformat(\"PongPlayer(%zu).speed\", id),\n\t\t      std::bind(&PongPlayer::child_changes, this, _1))),\n\tposition(std::make_shared<curve::Continuous<float>>(\n\t\t         mgr,\n\t\t         (id << 4) + 2,\n\t\t         util::sformat(\"PongPlayer(%zu).position\", id),\n\t\t         std::bind(&PongPlayer::child_changes, this, _1))),\n\tlives(std::make_shared<curve::Discrete<int>>(\n\t\t      mgr,\n\t\t      (id << 4) + 3,\n\t\t      util::sformat(\"PongPlayer(%zu).lives\", id),\n\t\t      std::bind(&PongPlayer::child_changes, this, _1))),\n\tstate(std::make_shared<curve::Discrete<PongEvent>>(\n\t\t      mgr,\n\t\t      (id << 4) + 4,\n\t\t      util::sformat(\"PongPlayer(%zu).state\", id),\n\t\t      std::bind(&PongPlayer::child_changes, this, _1))),\n\tsize(std::make_shared<curve::Discrete<float>>(\n\t\t     mgr,\n\t\t     (id << 4) + 5,\n\t\t     util::sformat(\"PongPlayer(%zu).size\", id),\n\t\t     std::bind(&PongPlayer::child_changes, this, _1))),\n\t_id{id} {}\n\n\nsize_t PongPlayer::id() const {\n\treturn _id;\n}\n\nstd::string PongPlayer::idstr() const {\n\tstd::stringstream ss;\n\tss << \"PongPlayer(\" << this->id() << \")\";\n\treturn ss.str();\n}\n\n\nvoid PongPlayer::child_changes(const time::time_t &time) {\n\tthis->changes(time);\n}\n\n\nPongBall::PongBall(const std::shared_ptr<event::EventLoop> &mgr, size_t id)\n\t:\n\tEventEntity{mgr},\n\tspeed(std::make_shared<curve::Discrete<util::Vector2d>>(\n\t\t      mgr,\n\t\t      (id << 2) + 1,\n\t\t      util::sformat(\"PongBall(%zu).speed\", id),\n\t\t      std::bind(&PongBall::child_changes, this, _1))),\n\tposition(std::make_shared<curve::Segmented<util::Vector2d>>(\n\t\t         mgr,\n\t\t         (id << 2) + 2,\n\t\t         util::sformat(\"PongBall(%zu).position\", id),\n\t\t         std::bind(&PongBall::child_changes, this, _1))),\n\t_id{id} {}\n\n\nsize_t PongBall::id() const {\n\treturn _id;\n}\n\n\nstd::string PongBall::idstr() const {\n\tstd::stringstream ss;\n\tss << \"PongBall(\" << this->id() << \")\";\n\treturn ss.str();\n}\n\n\nvoid PongBall::child_changes(const time::time_t &time) {\n\tthis->changes(time);\n}\n\n\nPongState::PongState(const std::shared_ptr<event::EventLoop> &mgr,\n                     const std::shared_ptr<Gui> &gui,\n                     const std::shared_ptr<nyan::View> &dbroot)\n\t:\n\tState{mgr},\n\tp1(std::make_shared<PongPlayer>(mgr, 0)),\n\tp2(std::make_shared<PongPlayer>(mgr, 1)),\n\tball(std::make_shared<PongBall>(mgr, 2)),\n\tarea_size(\n\t\tstd::make_shared<curve::Discrete<util::Vector2s>>(\n\t\t\tmgr,\n\t\t\t1001,\n\t\t\t\"area_size\")),\n\tgui{gui},\n\tdbroot{dbroot} {\n\n\t\tauto size = gui->get_window_size();\n\n\t\tlog::log(INFO << \"initializing pong state with area_size=\"\n\t\t         << size << \"...\");\n\n\t\t// initialize display size with real window size\n\t\tthis->area_size->set_last(0, size);\n\n\t\t// nyan initialization\n\t\tnyan::Object gamecfg = this->dbroot->get_object(\"pong.GameTest\");\n\t\tif (not gamecfg.extends(\"pong.PongGame\")) {\n\t\t\tthrow Error{ERR << \"GameTest is not a PongGame\"};\n\t\t}\n\n\t\tthis->ballcfg = *gamecfg.get<nyan::Object>(\"ball\");\n\n\t\tnyan::Object ballcolor = this->ballcfg.get_object(\"color\");\n\t\tlog::log(INFO << \"initial ball color: (r, g, b) = (\"\n\t\t         << ballcolor.get_int(\"r\")\n\t\t         << \", \" << ballcolor.get_int(\"g\")\n\t\t         << \", \" << ballcolor.get_int(\"b\")\n\t\t         << \")\");\n\t}\n\n} // openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/gamestate.h",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\n#include <nyan/nyan.h>\n\n#include \"config.h\"\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"curve/segmented.h\"\n#include \"event/event_loop.h\"\n#include \"event/evententity.h\"\n#include \"event/state.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::main::tests::pong {\n\nclass Gui;\n\n\nclass PongEvent {\npublic:\n\tenum state_e {\n\t\tUP,\n\t\tDOWN,\n\t\tSTART,\n\t\tIDLE,\n\t\tLOST\n\t};\n\n\tPongEvent(size_t id, state_e s) :\n\t\tplayer(id), state(s) {}\n\tPongEvent() :\n\t\tplayer(0), state(IDLE) {}\n\n\tsize_t player;\n\tstate_e state;\n};\n\n\nclass PongPlayer : public event::EventEntity {\npublic:\n\tPongPlayer(const std::shared_ptr<event::EventLoop> &mgr, size_t id);\n\n\tsize_t id() const override;\n\tstd::string idstr() const override;\n\n\tstd::shared_ptr<curve::Discrete<float>> speed;\n\tstd::shared_ptr<curve::Continuous<float>> position;\n\tstd::shared_ptr<curve::Discrete<int>> lives;\n\tstd::shared_ptr<curve::Discrete<PongEvent>> state;\n\tstd::shared_ptr<curve::Discrete<float>> size;\n\n\tsize_t _id;\n\nprivate:\n\tvoid child_changes(const time::time_t &time);\n};\n\n\nclass PongBall : public event::EventEntity {\npublic:\n\tPongBall(const std::shared_ptr<event::EventLoop> &mgr, size_t id);\n\n\tsize_t id() const override;\n\tstd::string idstr() const override;\n\n\tstd::shared_ptr<curve::Discrete<util::Vector2d>> speed;\n\tstd::shared_ptr<curve::Segmented<util::Vector2d>> position;\n\nprivate:\n\tvoid child_changes(const time::time_t &time);\n\tsize_t _id;\n};\n\n\nclass PongState : public event::State {\npublic:\n\tPongState(const std::shared_ptr<event::EventLoop> &mgr,\n\t          const std::shared_ptr<Gui> &gui,\n\t          const std::shared_ptr<nyan::View> &dbroot);\n\n\tstd::shared_ptr<PongPlayer> p1;\n\tstd::shared_ptr<PongPlayer> p2;\n\tstd::shared_ptr<PongBall> ball;\n\tstd::shared_ptr<curve::Discrete<util::Vector2s>> area_size;\n\n\tstd::shared_ptr<Gui> gui;\n\n\tstd::shared_ptr<nyan::View> dbroot;\n\tnyan::Object ballcfg;\n};\n\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/gui.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"gui.h\"\n\n#include <algorithm>\n#include <eigen3/Eigen/Dense>\n#include <vector>\n\n#include \"log/log.h\"\n#include \"main/demo/pong/gamestate.h\"\n#include \"renderer/geometry.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/shader.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/texture.h\"\n#include \"renderer/util.h\"\n\n\nnamespace openage::main::tests::pong {\n\n\nconst std::vector<PongEvent> &Gui::get_inputs(const std::shared_ptr<PongPlayer> &player) {\n\tthis->input_cache.clear();\n\n\tPongEvent evnt;\n\tevnt.player = player->id();\n\tevnt.state = PongEvent::IDLE;\n\n\tstd::vector<int> inputs;\n\n\t/*\n\tfor (all inputs from window) {\n\t    add key to inputs vector;\n\t}\n\t*/\n\n\tfor (auto input : inputs) {\n\t\tswitch (input) {\n\t\tcase 0:\n\t\t\tevnt.state = PongEvent::DOWN;\n\t\t\tbreak;\n\n\t\tcase 1:\n\t\t\tevnt.state = PongEvent::UP;\n\t\t\tbreak;\n\n\t\tcase 27:\n\t\t\texit(0);\n\t\t\tbreak;\n\n\t\tcase 'r':\n\t\tcase ' ':\n\t\t\tevnt.state = PongEvent::START;\n\t\t\tbreak;\n\n\t\tdefault:\n\t\t\tevnt.state = PongEvent::IDLE;\n\t\t\tbreak;\n\t\t}\n\n\t\tthis->input_cache.push_back(evnt);\n\t}\n\n\tif (this->input_cache.empty()) {\n\t\t// store 'idle' input to cancel movement\n\t\tthis->input_cache.push_back(evnt);\n\t}\n\n\treturn this->input_cache;\n}\n\n\nconstexpr const int max_log_msgs = 10;\n\n\nGui::Gui() :\n\twindow{\"openage engine test\", {800, 600}},\n\trenderer{window.make_renderer()} {\n\tauto vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tR\"s(\n#version 330\n\nlayout(location=0) in vec2 position;\nuniform mat4 pos;\nuniform mat4 proj;\n\nvoid main() {\n\tgl_Position = proj * pos * vec4(position, 0.0, 1.0);\n}\n)s\");\n\n\tauto fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tR\"s(\n#version 330\n\nuniform vec3 col;\nout vec4 frag;\n\nvoid main() {\n\tfrag = vec4(col.r, col.g, col.b, 1.0);\n}\n)s\");\n\n\tauto shader = renderer->add_shader({vshader_src, fshader_src});\n\n\tauto proj_in = shader->new_uniform_input(\n\t\t\"proj\",\n\t\tEigen::Affine3f::Identity().matrix());\n\n\tauto col_in = shader->new_uniform_input(\n\t\t\"col\",\n\t\tEigen::Vector3f());\n\tauto col_red_in = shader->new_uniform_input(\n\t\t\"col\",\n\t\tEigen::Vector3f(1, 0, 0));\n\n\tauto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\n\tthis->ball = renderer::Renderable{\n\t\tshader->new_uniform_input(\n\t\t\t\"pos\",\n\t\t\tEigen::Affine3f::Identity().matrix()),\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tthis->p1paddle = renderer::Renderable{\n\t\tshader->new_uniform_input(\n\t\t\t\"pos\",\n\t\t\tEigen::Affine3f::Identity().matrix()),\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tthis->p2paddle = renderer::Renderable{\n\t\tshader->new_uniform_input(\n\t\t\t\"pos\",\n\t\t\tEigen::Affine3f::Identity().matrix()),\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tauto set_projmatrix = renderer::ShaderUpdate{proj_in};\n\tauto set_col = renderer::ShaderUpdate{col_in};\n\tauto set_col_red = renderer::ShaderUpdate{col_red_in};\n\n\tthis->render_pass = this->renderer->add_render_pass(\n\t\tstd::vector<renderer::Renderable>{\n\t\t\tstd::move(set_projmatrix),\n\t\t\tstd::move(set_col),\n\t\t\tthis->ball,\n\t\t\tstd::move(set_col_red),\n\t\t\tthis->p1paddle,\n\t\t\tthis->p2paddle},\n\t\tthis->renderer->get_display_target());\n\n\tthis->window.add_resize_callback(\n\t\t[=, this](size_t w, size_t h, double scale) {\n\t\t\tEigen::Matrix4f proj_matrix = renderer::util::ortho_matrix_f(\n\t\t\t\t0.0f,\n\t\t\t\tw,\n\t\t\t\t0.0f,\n\t\t\t\th,\n\t\t\t\t0.0f,\n\t\t\t\t1.0f);\n\n\t\t\tproj_in->update(\"proj\", proj_matrix);\n\n\t\t\tfor (auto &cb : this->resize_callbacks) {\n\t\t\t\tcb(w, h, scale);\n\t\t\t}\n\t\t});\n}\n\n\nvoid Gui::draw(const std::shared_ptr<PongState> &state, const time::time_t &now) {\n\tconstexpr float ball_size = 50.0f;\n\tconstexpr float paddle_width = 20.0f;\n\n\tauto screen_size = state->area_size->get(now);\n\n\t// TODO draw score\n\t// TODO: draw middle line\n\t// TODO: draw debug info\n\n\t// draw the ball\n\t// TODO: use \"⚽\"\n\tauto ball_pos = state->ball->position->get(now);\n\tauto ball_pos_matrix = Eigen::Affine3f::Identity();\n\tball_pos_matrix.prescale(Eigen::Vector3f(ball_size, ball_size, 1.0f));\n\t// ball_pos_matrix.prerotate(Eigen::AngleAxisf(45.0f * math::PI / 180.0f, Eigen::Vector3f::UnitZ()));\n\tball_pos_matrix.pretranslate(Eigen::Vector3f(ball_pos[0], ball_pos[1], 0.0f));\n\tthis->ball.uniform->update(\"pos\", ball_pos_matrix.matrix());\n\n\tnyan::Object ballcolor = state->ballcfg.get_object(\"color\");\n\tthis->ball.uniform->update(\n\t\t\"col\",\n\t\tEigen::Vector3f{\n\t\t\tballcolor.get_int(\"r\", now.to_int()) / 255.0f,\n\t\t\tballcolor.get_int(\"g\", now.to_int()) / 255.0f,\n\t\t\tballcolor.get_int(\"b\", now.to_int()) / 255.0f,\n\t\t});\n\n\tauto p1_pos = state->p1->position->get(now);\n\tauto p1_size = state->p1->size->get(now);\n\tauto p1_pos_matrix = Eigen::Affine3f::Identity();\n\tp1_pos_matrix.prescale(Eigen::Vector3f(paddle_width, p1_size, 1.0f));\n\tp1_pos_matrix.pretranslate(Eigen::Vector3f(0, p1_pos, 0.0f));\n\tthis->p1paddle.uniform->update(\"pos\", p1_pos_matrix.matrix());\n\n\tauto p2_pos = state->p2->position->get(now);\n\tauto p2_size = state->p2->size->get(now);\n\tauto p2_pos_matrix = Eigen::Affine3f::Identity();\n\tp2_pos_matrix.prescale(Eigen::Vector3f(paddle_width, p2_size, 1.0f));\n\tp2_pos_matrix.pretranslate(Eigen::Vector3f(screen_size[0], p2_pos, 0.0f));\n\tthis->p2paddle.uniform->update(\"pos\", p2_pos_matrix.matrix());\n\n\tthis->renderer->render(this->render_pass);\n\tthis->window.update();\n\trenderer::opengl::GlContext::check_error();\n\n\tthis->exit_requested = window.should_close();\n\tif (this->exit_requested) {\n\t\tlog::log(INFO << \"game exit requested due to window close\");\n\t}\n}\n\n\nconst util::Vector2s &Gui::get_window_size() const {\n\treturn this->window.get_size();\n}\n\n\nvoid Gui::log(const std::string &msg) {\n\tlog::log(INFO << \"Gui::log: \" << msg);\n\n\tif (this->log_msgs.size() >= max_log_msgs) {\n\t\tthis->log_msgs.pop_back();\n\t}\n\tthis->log_msgs.push_front(msg);\n}\n\n\nvoid Gui::add_resize_callback(const renderer::Window::resize_cb_t &cb) {\n\tthis->resize_callbacks.push_back(cb);\n}\n\n\nvoid Gui::clear_resize_callbacks() {\n\tthis->resize_callbacks.clear();\n}\n\nvoid Gui::update() {\n\tthis->window.update();\n}\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/gui.h",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n#include <deque>\n#include <vector>\n\n#include \"renderer/opengl/window.h\"\n#include \"renderer/renderer.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::main::tests::pong {\n\nclass PongEvent;\nclass PongState;\nclass PongPlayer;\n\n\nclass Gui {\npublic:\n\tGui();\n\n\tconst std::vector<PongEvent> &get_inputs(const std::shared_ptr<PongPlayer> &player);\n\n\tvoid draw(const std::shared_ptr<PongState> &state, const time::time_t &now);\n\n\tconst util::Vector2s &get_window_size() const;\n\n\tvoid log(const std::string &msg);\n\n\tvoid add_resize_callback(const renderer::Window::resize_cb_t &);\n\tvoid clear_resize_callbacks();\n\n\tvoid update();\n\n\tbool exit_requested = false;\n\nprivate:\n\trenderer::opengl::GlWindow window;\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\tstd::vector<PongEvent> input_cache;\n\tstd::deque<std::string> log_msgs;\n\n\t// rendering objects\n\trenderer::Renderable ball;\n\trenderer::Renderable p1paddle;\n\trenderer::Renderable p2paddle;\n\n\t// render pass for the game area\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\tstd::vector<renderer::Window::resize_cb_t> resize_callbacks;\n};\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/physics.cpp",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#include \"physics.h\"\n\n#include <cmath>\n\n#include \"error/error.h\"\n#include \"main/demo/pong/gui.h\"\n#include \"rng/global_rng.h\"\n#include \"util/stringformatter.h\"\n\n\nnamespace openage::main::tests::pong {\n\n\nclass BallReflectWall : public event::DependencyEventHandler {\npublic:\n\tBallReflectWall() :\n\t\tevent::DependencyEventHandler(\"demo.ball.reflect_wall\") {}\n\n\tvoid setup_event(const std::shared_ptr<event::Event> &evnt,\n\t                 const std::shared_ptr<event::State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\t// FIXME dependency to a full ball object so that any curve\n\t\t// triggers a change\n\t\tevnt->depend_on(state->ball);\n\t\tevnt->depend_on(state->area_size);\n\t}\n\n\t// FIXME we REALLY need dependencies to objects i.e. Ball : public EventEntity()\n\tvoid invoke(event::EventLoop &,\n\t            const std::shared_ptr<event::EventEntity> &target,\n\t            const std::shared_ptr<event::State> &gstate,\n\t            const time::time_t &now,\n\t            const event::EventHandler::param_map & /*param*/) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Segmented<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\t\tauto speedcurve = state->ball->speed;\n\n\t\t// get speed and position to insert new movement keyframe\n\t\tauto speed = speedcurve->get(now);\n\t\tauto pos = positioncurve->get(now);\n\t\tauto screen_size = state->area_size->get(now);\n\n\t\tspeed[1] *= -1.0;\n\t\tstate->ball->speed->set_last(now, speed);\n\t\tstate->ball->position->set_last(now, pos);\n\n\t\tif (speed[1] == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[1] > 0) {\n\t\t\tty = time::time_t::from_double(\n\t\t\t\t(screen_size[1] - pos[1]) / speed[1]);\n\t\t}\n\t\telse if (speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(\n\t\t\t\tpos[1] / -speed[1]);\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, pos + (speed * ty.to_double()));\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<event::EventEntity> &target,\n\t                                 const std::shared_ptr<event::State> &gstate,\n\t                                 const time::time_t &now) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Segmented<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto speed = state->ball->speed->get(now);\n\t\tauto pos = positioncurve->get(now);\n\t\tauto screen_size = state->area_size->get(now);\n\n\t\tif (speed[1] == 0) {\n\t\t\treturn time::TIME_MAX;\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[1] > 0) {\n\t\t\tty = time::time_t::from_double((screen_size[1] - pos[1]) / speed[1]);\n\t\t}\n\t\telse if (speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(pos[1] / -speed[1]);\n\t\t}\n\n\t\tutil::FString str;\n\t\tstr.fmt(\"WALL TY %f NOW %f, NOWTY %f \",\n\t\t        ty.to_double(),\n\t\t        now.to_double(),\n\t\t        (now + ty).to_double());\n\t\tstate->gui->log(str);\n\n\t\treturn now + ty;\n\t}\n};\n\n\nclass BallReflectPanel : public event::DependencyEventHandler {\npublic:\n\tBallReflectPanel() :\n\t\tevent::DependencyEventHandler(\"demo.ball.reflect_panel\") {}\n\n\tvoid setup_event(const std::shared_ptr<event::Event> &evnt,\n\t                 const std::shared_ptr<event::State> &gstate) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tevnt->depend_on(state->ball);\n\t\tevnt->depend_on(state->p1->position);\n\t\tevnt->depend_on(state->p2->position);\n\t\tevnt->depend_on(state->area_size);\n\t}\n\n\tvoid invoke(event::EventLoop &mgr,\n\t            const std::shared_ptr<event::EventEntity> & /*target*/,\n\t            const std::shared_ptr<event::State> &gstate,\n\t            const time::time_t &now,\n\t            const event::EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto pos = state->ball->position->get(now);\n\t\tauto speed = state->ball->speed->get(now);\n\t\tauto screen_size = state->area_size->get(now);\n\n\t\tstatic int cnt = 0;\n\t\tutil::FString str;\n\t\tstr.fmt(\"Panel hit [%i]\", ++cnt);\n\t\tstate->gui->log(str);\n\n\t\tif (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)) {\n\t\t\t// ball missed the paddle of player 1\n\t\t\tauto l = state->p1->lives->get(now);\n\t\t\tl--;\n\n\t\t\tstate->p1->lives->set_last(now, l);\n\t\t\tstate->ball->position->set_last(now, pos);\n\n\t\t\tPhysics::reset(state, mgr, now);\n\t\t}\n\t\telse 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)) {\n\t\t\t// ball missed the paddel of player 2\n\t\t\tauto l = state->p2->lives->get(now);\n\t\t\tl--;\n\t\t\tstate->p2->lives->set_last(now, l);\n\t\t\tstate->ball->position->set_last(now, pos);\n\n\t\t\tPhysics::reset(state, mgr, now);\n\t\t}\n\t\telse if (pos[0] >= screen_size[0] - 1 || pos[0] <= 1) {\n\t\t\tspeed[0] *= -1;\n\t\t\tstate->ball->speed->set_last(now, speed);\n\t\t\tstate->ball->position->set_last(now, pos);\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\t\tif (speed[0] > 0) {\n\t\t\tty = time::time_t::from_double((screen_size[0] - pos[0]) / speed[0]);\n\t\t}\n\t\telse if (speed[0] < 0) {\n\t\t\tty = time::time_t::from_double(pos[0] / -speed[0]);\n\t\t}\n\n\t\tauto hit_pos = pos + speed * ty.to_double();\n\t\tif (speed[0] > 0) {\n\t\t\thit_pos[0] = screen_size[0];\n\t\t}\n\t\telse {\n\t\t\thit_pos[0] = 0;\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, hit_pos);\n\n\t\t// update ball color through nyan transaction (just as a demo...)\n\t\tnyan::Transaction tx = state->dbroot->new_transaction(now.to_int());\n\n\t\tif (speed[0] > 0) {\n\t\t\ttx.add(state->dbroot->get_object(\"pong.RightColor\"));\n\t\t}\n\t\telse {\n\t\t\ttx.add(state->dbroot->get_object(\"pong.LeftColor\"));\n\t\t}\n\n\t\tbool txok = tx.commit();\n\t\tif (not txok) {\n\t\t\tthrow Error{ERR << \"failed nyan transaction\"};\n\t\t}\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<event::EventEntity> &target,\n\t                                 const std::shared_ptr<event::State> &gstate,\n\t                                 const time::time_t &now) override {\n\t\tauto positioncurve = std::dynamic_pointer_cast<curve::Segmented<util::Vector2d>>(target);\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto speed = state->ball->speed->get(now);\n\t\tauto pos = positioncurve->get(now);\n\t\tauto screen_size = state->area_size->get(now);\n\n\t\tif (speed[0] == 0)\n\t\t\treturn time::TIME_MAX;\n\n\t\ttime::time_t ty = 0;\n\n\t\tif (speed[0] > 0) {\n\t\t\tty = time::time_t::from_double((screen_size[0] - pos[0]) / speed[0]);\n\t\t}\n\t\telse if (speed[0] < 0) {\n\t\t\tty = time::time_t::from_double(pos[0] / -speed[0]);\n\t\t}\n\n\t\t{\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"PANEL REFLECT AT %f NEXT %f\", now.to_double(), (now + ty).to_double());\n\t\t\tstate->gui->log(str);\n\t\t}\n\n\t\tauto hit_pos = pos + speed * ty.to_double();\n\t\tif (speed[0] > 0) {\n\t\t\thit_pos[0] = screen_size[0];\n\t\t}\n\t\telse {\n\t\t\thit_pos[0] = 0;\n\t\t}\n\n\t\tENSURE(hit_pos[0] >= 0, \"hit position x must be positive\");\n\n\t\treturn now + ty;\n\t}\n};\n\n\nclass ResetGame : public event::OnceEventHandler {\npublic:\n\tResetGame() :\n\t\tevent::OnceEventHandler(\"demo.reset\") {}\n\n\tvoid setup_event(const std::shared_ptr<event::Event> & /*target*/,\n\t                 const std::shared_ptr<event::State> & /*state*/) override {}\n\n\tvoid invoke(event::EventLoop & /*mgr*/,\n\t            const std::shared_ptr<event::EventEntity> & /*target*/,\n\t            const std::shared_ptr<event::State> &gstate,\n\t            const time::time_t &now,\n\t            const event::EventHandler::param_map & /*param*/) override {\n\t\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\n\t\tauto screen_size = state->area_size->get(now);\n\n\t\t// Check if the condition still applies\n\t\t{\n\t\t\tauto pos = state->ball->position->get(now);\n\t\t\tif (pos[0] > 0 && pos[0] < screen_size[0]) {\n\t\t\t\t// the gamestate is still valid - there is no need to reset\n\t\t\t\tthrow Error(ERR << \"reset invoked even though unnecessary\");\n\t\t\t}\n\t\t}\n\n\t\t// move ball to the center\n\t\tstate->ball->position->set_last_jump(now,\n\t\t                                     state->ball->position->get(now),\n\t\t                                     screen_size.casted<double>() / 2);\n\n\t\t// move paddles to center\n\t\tstate->p1->position->set_last(now, screen_size[1] / 2);\n\t\tstate->p2->position->set_last(now, screen_size[1] / 2);\n\n\t\t// initial speed of the ball:\n\t\tfloat dirx = (rng::random() % 2) ? 1 : -1;\n\t\tfloat diry = (rng::random() % 2) ? 1 : -1;\n\t\tauto init_speed = util::Vector2d(\n\t\t\tdirx * (screen_size[0] / 8.0 + (rng::random() % (screen_size[0] / 10))),\n\t\t\tdiry * (screen_size[1] / 10.0 + (rng::random() % (screen_size[1] / 10))));\n\n\t\tstate->ball->speed->set_last(now, init_speed);\n\t\tauto pos = state->ball->position->get(now);\n\n\t\t{\n\t\t\tstatic int cnt = 0;\n\n\t\t\tutil::FString str;\n\t\t\tstr.fmt(\"Reset #%i. Speed %f | %f POS %f | %f\",\n\t\t\t        ++cnt,\n\t\t\t        init_speed[0],\n\t\t\t        init_speed[1],\n\t\t\t        pos[0],\n\t\t\t        pos[1]);\n\t\t\tstate->gui->log(str);\n\t\t}\n\n\t\ttime::time_t ty = 0;\n\n\t\t// calculate the wall-hit-times\n\t\tif (init_speed[1] > 0) {\n\t\t\tty = time::time_t::from_double((screen_size[1] - pos[1]) / init_speed[1]);\n\t\t}\n\t\telse if (init_speed[1] < 0) {\n\t\t\tty = time::time_t::from_double(pos[1] / -init_speed[1]);\n\t\t}\n\t\telse {\n\t\t\t// currently never happens, but this would be a non-vertically-moving ball\n\t\t\t// fallback to calculating panel-hit-times\n\t\t\tif (init_speed[0] > 0) {\n\t\t\t\tty = time::time_t::from_double((screen_size[0] - pos[0]) / init_speed[0]);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tty = time::time_t::from_double(pos[0] / -init_speed[0]);\n\t\t\t}\n\t\t}\n\n\t\tstate->ball->position->set_last(now + ty, pos + init_speed * ty.to_double());\n\t}\n\n\ttime::time_t predict_invoke_time(const std::shared_ptr<event::EventEntity> & /*target*/,\n\t                                 const std::shared_ptr<event::State> & /*state*/,\n\t                                 const time::time_t &old_time) override {\n\t\treturn old_time;\n\t}\n};\n\n\nvoid Physics::init(const std::shared_ptr<PongState> &state,\n                   const std::shared_ptr<event::EventLoop> &loop,\n                   const time::time_t &now) {\n\tloop->add_event_handler(std::make_shared<BallReflectPanel>());\n\tloop->add_event_handler(std::make_shared<BallReflectWall>());\n\tloop->add_event_handler(std::make_shared<ResetGame>());\n\n\tloop->create_event(\"demo.ball.reflect_wall\", state->ball->position, state, now);\n\tloop->create_event(\"demo.ball.reflect_panel\", state->ball->position, state, now);\n\n\t// FIXME once \"reset\": deregister\n\t//reset(state, mgr, now);\n}\n\n\nvoid Physics::reset(const std::shared_ptr<event::State> &gstate,\n                    event::EventLoop &mgr,\n                    const time::time_t &now) {\n\tauto state = std::dynamic_pointer_cast<PongState>(gstate);\n\tmgr.create_event(\"demo.reset\", state->ball->position, state, now);\n}\n\n\nvoid Physics::process_input(const std::shared_ptr<PongState> &state,\n                            const std::shared_ptr<PongPlayer> &player,\n                            const std::vector<PongEvent> &events,\n                            const std::shared_ptr<event::EventLoop> &mgr,\n                            const time::time_t &now) {\n\t// seconds into the future\n\tconstexpr static auto predicted_movement_time = time::time_t::from_double(5.0);\n\n\t// pixels per second for paddle movement\n\tconstexpr static double movement_speed = 350.0;\n\n\tfor (auto &evnt : events) {\n\t\t// Process only if the future has changed\n\t\tif (player->state->get(now).state != evnt.state) {\n\t\t\t// log the new input in the state curve\n\t\t\tplayer->state->set_last(now, evnt);\n\n\t\t\tauto screen_size = state->area_size->get(now);\n\n\t\t\t// if the state is active longer than predicted,\n\t\t\t// we have to extend the prediction!\n\t\t\tbool extend_previous_prediction = false;\n\t\t\tauto prev_state = player->state->get_previous(now);\n\t\t\tif (prev_state and prev_state->second.state == evnt.state) {\n\t\t\t\textend_previous_prediction = true;\n\t\t\t}\n\n\t\t\tswitch (evnt.state) {\n\t\t\tcase PongEvent::UP:\n\t\t\tcase PongEvent::DOWN: {\n\t\t\t\tfloat current_pos = player->position->get(now);\n\n\t\t\t\tif (not extend_previous_prediction) {\n\t\t\t\t\t// create a keyframe for the new movement\n\t\t\t\t\tplayer->position->set_last(now, current_pos);\n\t\t\t\t\t// TODO: drop the intermediate keyframes\n\t\t\t\t\t//       for position and speed.\n\t\t\t\t}\n\n\t\t\t\tswitch (evnt.state) {\n\t\t\t\tcase PongEvent::UP:\n\t\t\t\t\tplayer->speed->set_last(now, -movement_speed);\n\t\t\t\t\tbreak;\n\t\t\t\tcase PongEvent::DOWN:\n\t\t\t\t\tplayer->speed->set_last(now, movement_speed);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\t// never reached.\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\ttime::time_t move_stop_guess = now + predicted_movement_time;\n\n\t\t\t\t// change the position by integrating the speed curve.\n\t\t\t\t// TODO: add native integral to curves.\n\t\t\t\tfloat new_pos = current_pos + (((player->speed->get(move_stop_guess) + player->speed->get(now)) / 2.0) * predicted_movement_time.to_double());\n\n\t\t\t\t// if paddle will get out-of-bounds, calculate the bound hit time\n\n\t\t\t\t// TODO: add native key/value finding in range to curves\n\t\t\t\tif (new_pos < 0) {\n\t\t\t\t\tmove_stop_guess = now + (current_pos / movement_speed);\n\t\t\t\t\tnew_pos = 0;\n\t\t\t\t}\n\n\t\t\t\tif (new_pos > screen_size[1]) {\n\t\t\t\t\tmove_stop_guess = now + ((screen_size[1] - current_pos) / movement_speed);\n\t\t\t\t\tnew_pos = screen_size[1];\n\t\t\t\t}\n\n\t\t\t\tPongEvent set_idle{evnt.player, PongEvent::IDLE};\n\t\t\t\tplayer->state->set_last(move_stop_guess, set_idle);\n\t\t\t\tplayer->speed->set_last(move_stop_guess, 0);\n\t\t\t\tplayer->position->set_last(move_stop_guess, new_pos);\n\n\t\t\t\t// TODO: predict_reflect_panel(state, queue, now);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase PongEvent::IDLE:\n\t\t\t\tplayer->position->set_last(now, player->position->get(now));\n\t\t\t\tplayer->speed->set_last(now, 0);\n\t\t\t\tbreak;\n\n\t\t\tcase PongEvent::START:\n\t\t\t\tPhysics::reset(state, *mgr, now);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/physics.h",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"main/demo/pong/gamestate.h\"\n#include \"time/time.h\"\n\n#include <vector>\n\n\nnamespace openage::event {\n\nclass EventLoop;\n\n} // namespace openage::event\n\nnamespace openage::main::tests::pong {\n\nclass Physics {\npublic:\n\tstatic void init(const std::shared_ptr<PongState> &,\n\t                 const std::shared_ptr<event::EventLoop> &mgr,\n\t                 const time::time_t &);\n\n\tvoid process_input(const std::shared_ptr<PongState> &,\n\t                   const std::shared_ptr<PongPlayer> &,\n\t                   const std::vector<PongEvent> &input,\n\t                   const std::shared_ptr<event::EventLoop> &mgr,\n\t                   const time::time_t &now);\n\n\tstatic void reset(const std::shared_ptr<event::State> &,\n\t                  event::EventLoop &mgr,\n\t                  const time::time_t &);\n};\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/pong.cpp",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#include <chrono>\n#include <cstdlib>\n#include <epoxy/gl.h>\n#include <functional>\n#include <memory>\n#include <thread>\n#include <unordered_map>\n\n#include <nyan/nyan.h>\n\n#include \"error/error.h\"\n#include \"event/event_loop.h\"\n#include \"log/log.h\"\n#include \"main/demo/pong/aicontroller.h\"\n#include \"main/demo/pong/gui.h\"\n#include \"main/demo/pong/physics.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"util/math_constants.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::main::tests::pong {\n\n\nusing Clock = std::chrono::high_resolution_clock;\nusing namespace std::literals::chrono_literals;\n\n\nenum class timescale {\n\tNOSLEEP,\n\tREALTIME,\n\tSLOW,\n\tFAST,\n};\n\nvoid main(const util::Path &path) {\n\trenderer::gui::GuiApplicationWithLogger gui_app{};\n\tbool human_player = false;\n\n\ttimescale speed = timescale::REALTIME;\n\n\tstd::shared_ptr<nyan::Database> db = nyan::Database::create();\n\n\tdb->load(\n\t\t\"pong.nyan\",\n\t\t[&path](const std::string &filename) {\n\t\t\t// TODO: nyan should provide a file api that we can inherit from\n\t\t\t// then we can natively interface to the openage file abstraction\n\t\t\t// and do not need to read the whole file here.\n\t\t\t// ideally, the returned file lazily accesses all data through openage::util::File.\n\t\t\treturn std::make_shared<nyan::File>(\n\t\t\t\tfilename,\n\t\t\t\t(path / (\"assets/test/nyan/\" + filename)).open_r().read());\n\t\t});\n\n\tstd::shared_ptr<nyan::View> dbview = db->new_view();\n\tstd::shared_ptr<Gui> gui = std::make_shared<Gui>();\n\n\tbool running = true;\n\n\twhile (running) {\n\t\tlog::log(INFO << \"initializing new pong game...\");\n\n\t\tauto loop = std::make_shared<event::EventLoop>();\n\t\ttime::time_t now = 0;\n\t\tPhysics phys;\n\n\t\t// the window size is fetched in here already,\n\t\t// so the resize callback doesn't need to be called for\n\t\t// a new state.\n\t\tauto state = std::make_shared<PongState>(loop, gui, dbview);\n\n\t\tgui->clear_resize_callbacks();\n\t\tgui->add_resize_callback(\n\t\t\t[&](size_t w, size_t h, double /*scale*/) {\n\t\t\t\tlog::log(INFO << \"update pong area size=(\"\n\t\t\t                  << w << \",\" << h << \")...\");\n\n\t\t\t\tstate->area_size->set_last(now, {w, h});\n\t\t\t});\n\n\t\t// process one round of window events, mainly to get the initial screen size.\n\t\tgui->update();\n\n\t\tPhysics::init(state, loop, now);\n\n\t\t// initialize player properties\n\t\tstate->p1->lives->set_last(now, 3);\n\t\tstate->p1->size->set_last(now, 200);\n\n\t\tstate->p2->lives->set_last(now, 3);\n\t\tstate->p2->size->set_last(now, 200);\n\n\t\t// initialize the game enqueuing a physics reset should happen.\n\t\tlog::log(INFO << \"initializing the physics state...\");\n\t\tstd::vector<PongEvent> start_event{PongEvent{0, PongEvent::START}};\n\t\tphys.process_input(state, state->p1, start_event, loop, now);\n\t\tloop->reach_time(now, state);\n\n\t\tstd::vector<PongEvent> inputs;\n\n\t\tlog::log(INFO << \"starting game loop...\");\n\n\t\t// this is the game loop, running while both players live!\n\t\twhile (running and state->p1->lives->get(now) > 0 and state->p2->lives->get(now) > 0) {\n\t\t\tlog::log(DBG << \"=================== LOOPING ====================\");\n\n\t\t\tauto loop_start = Clock::now();\n\n\t\t\tgui_app.process_events();\n\n\t\t\t// process the input for both players\n\t\t\t// player 1 can be AI or human.\n\t\t\tif (human_player) {\n\t\t\t\tphys.process_input(state, state->p1, inputs, loop, now);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tphys.process_input(state, state->p1, get_ai_inputs(state->p1, state->ball, state->area_size, now, false), loop, now);\n\t\t\t}\n\n\t\t\tphys.process_input(state, state->p2, get_ai_inputs(state->p2, state->ball, state->area_size, now, true), loop, now);\n\n\t\t\t// evaluate the event queue to reach the desired game time!\n\t\t\tloop->reach_time(now, state);\n\n\t\t\t// draw the new state\n\t\t\tgui->draw(state, now);\n\n\t\t\t// TODO: print event queue from loop->get_queue().get_event_queue().get_sorted_events()\n\t\t\t//       and show event->get_time and event->get_eventhandler()->id()\n\n\t\t\t// get the input after the current frame, because the get_inputs\n\t\t\t// implicitly updates the screen. if this is done too early,\n\t\t\t// the screen will be updated to be empty -> flickering.\n\t\t\t// TODO: take terminal input without ncurses, if on tty?\n\t\t\t//       e.g. 'q' to close it?\n\t\t\tinputs = gui->get_inputs(state->p1);\n\n\t\t\tif (gui->exit_requested) {\n\t\t\t\trunning = false;\n\t\t\t}\n\n\t\t\t// handle timing for screen refresh and simulation advancement\n\t\t\tusing dt_s_t = std::chrono::duration<double, std::ratio<1>>;\n\t\t\tusing dt_ms_t = std::chrono::duration<double, std::milli>;\n\n\t\t\t// microseconds per frame\n\t\t\t// 30fps = 1s/30 = 1000000us/30 per frame\n\t\t\tconstexpr static std::chrono::microseconds per_frame = 33333us;\n\t\t\tconstexpr static time::time_t per_frame_s = std::chrono::duration_cast<dt_s_t>(per_frame).count();\n\n\t\t\tif (speed == timescale::NOSLEEP) {\n\t\t\t\t// increase the simulation loop time a bit\n\t\t\t\tstd::this_thread::sleep_for(std::chrono::milliseconds(5));\n\t\t\t}\n\n\t\t\tdt_ms_t dt_us = Clock::now() - loop_start;\n\n\t\t\tif (speed != timescale::NOSLEEP) {\n\t\t\t\tdt_ms_t wait_time = per_frame - dt_us;\n\n\t\t\t\tif (wait_time > dt_ms_t::zero()) {\n\t\t\t\t\tstd::this_thread::sleep_for(wait_time);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch (speed) {\n\t\t\tcase timescale::NOSLEEP:\n\t\t\t\t// advance the frame calculation time only,\n\t\t\t\t// because we didn't sleep\n\t\t\t\tnow += std::chrono::duration_cast<dt_s_t>(dt_us).count();\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::REALTIME:\n\t\t\t\t// advance the game time in seconds\n\t\t\t\tnow += per_frame_s;\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::SLOW:\n\t\t\t\tnow += per_frame_s / 10;\n\t\t\t\tbreak;\n\n\t\t\tcase timescale::FAST:\n\t\t\t\tnow += per_frame_s * 4;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/pong/pong.h",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\n\nnamespace openage::main::tests::pong {\n\n/**\n * Run pong, the non-terminal variant.\n */\nvoid main(const util::Path &path);\n\n\n} // namespace openage::main::tests::pong\n"
  },
  {
    "path": "libopenage/main/demo/presenter/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tpresenter.cpp\n)\n"
  },
  {
    "path": "libopenage/main/demo/presenter/presenter.cpp",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#include \"presenter.h\"\n\n#include \"log/log.h\"\n#include \"presenter/presenter.h\"\n#include \"time/time_loop.h\"\n\n\nnamespace openage::main::tests::presenter {\n\nvoid demo(const util::Path &path) {\n\tlog::log(INFO << \"launching presenter test...\");\n\n\tauto time_loop = std::make_shared<openage::time::TimeLoop>();\n\topenage::presenter::Presenter presenter{path};\n\tpresenter.set_time_loop(time_loop);\n\tpresenter.run();\n}\n\n} // namespace openage::main::tests::presenter\n"
  },
  {
    "path": "libopenage/main/demo/presenter/presenter.h",
    "content": "// Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\n\nnamespace openage::main::tests::presenter {\n\nvoid demo(const util::Path &path);\n\n} // namespace openage::main::tests::presenter\n"
  },
  {
    "path": "libopenage/main/tests.cpp",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#include \"tests.h\"\n\n#include \"demo/interactive/interactive.h\"\n#include \"demo/pong/pong.h\"\n#include \"demo/presenter/presenter.h\"\n\n\nnamespace openage::main::tests {\n\n\nvoid engine_demo(int demo_id, const util::Path &path) {\n\tswitch (demo_id) {\n\tcase 0: {\n\t\tpong::main(path);\n\t} break;\n\n\tcase 1: {\n\t\tpresenter::demo(path);\n\t} break;\n\n\tcase 2: {\n\t\tinteractive::demo(path);\n\t} break;\n\n\tdefault:\n\t\tthrow Error(ERR << \"unknown engine demo \" << demo_id << \" requested.\");\n\t}\n}\n\n} // namespace openage::main::tests\n"
  },
  {
    "path": "libopenage/main/tests.h",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../util/compiler.h\"\n// pxd: from libopenage.util.path cimport Path\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n} // namespace util\n\nnamespace main::tests {\n\n// pxd: void engine_demo(int demo_id, Path path) except +\nOAAPI void engine_demo(int demo_id, const util::Path &path);\n\n} // namespace main::tests\n} // namespace openage\n"
  },
  {
    "path": "libopenage/main.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"main.h\"\n\n#include <memory>\n\n#include \"cvar/cvar.h\"\n#include \"engine/engine.h\"\n#include \"util/timer.h\"\n\nnamespace openage {\n\n/*\n * This function is called from Cython, see main_cpp.pyx.\n *\n * This is the main entry point to the C++ part.\n */\nint run_game(const main_arguments &args) {\n\t// TODO: store args.gl_debug as default in the cvar system.\n\n\tutil::Timer timer;\n\ttimer.start();\n\n\t// read and apply the configuration files\n\tauto cvar_manager = std::make_shared<cvar::CVarManager>(args.root_path[\"cfg\"]);\n\tcvar_manager->load_all();\n\n\t// set engine run_mode\n\topenage::engine::Engine::mode run_mode = openage::engine::Engine::mode::FULL;\n\tif (args.headless) {\n\t\trun_mode = openage::engine::Engine::mode::HEADLESS;\n\t}\n\n\t// convert window arguments to window settings\n\trenderer::window_settings win_settings = {};\n\twin_settings.width = args.window_args.width;\n\twin_settings.height = args.window_args.height;\n\twin_settings.vsync = args.window_args.vsync;\n\n\trenderer::window_mode wmode;\n\tif (args.window_args.mode == \"fullscreen\") {\n\t\twmode = renderer::window_mode::FULLSCREEN;\n\t}\n\telse if (args.window_args.mode == \"borderless\") {\n\t\twmode = renderer::window_mode::BORDERLESS;\n\t}\n\telse if (args.window_args.mode == \"windowed\") {\n\t\twmode = renderer::window_mode::WINDOWED;\n\t}\n\telse {\n\t\tthrow Error(MSG(err) << \"Invalid window mode: \" << args.window_args.mode);\n\t}\n\twin_settings.mode = wmode;\n\twin_settings.debug = args.gl_debug;\n\n\topenage::engine::Engine engine{run_mode, args.root_path, args.mods, win_settings};\n\n\tengine.loop();\n\n\treturn 0;\n}\n\n} // namespace openage\n"
  },
  {
    "path": "libopenage/main.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool\n// pxd: from libcpp.string cimport string\n// pxd: from libcpp.vector cimport vector\n#include <string>\n\n// pxd: from libopenage.util.path cimport Path\n#include \"util/path.h\"\n\n#include <vector>\n\n#include \"util/compiler.h\"\n\nnamespace openage {\n\n/**\n * Window parameters struct.\n *\n * pxd:\n *\n * cppclass window_arguments:\n *     int width\n *     int height\n *     bool vsync\n *     string mode\n */\nstruct window_arguments {\n\tint width;\n\tint height;\n\tbool vsync;\n\tstd::string mode;\n};\n\n/**\n * Used for passing arguments to run_game.\n *\n * pxd:\n *\n * cppclass main_arguments:\n *     Path root_path\n *     bool gl_debug\n *     bool headless\n *     vector[string] mods\n *     window_arguments window_args\n */\nstruct main_arguments {\n\tutil::Path root_path;\n\tbool gl_debug;\n\tbool headless;\n\tstd::vector<std::string> mods;\n\twindow_arguments window_args;\n};\n\n\n/**\n * runs the game.\n *\n * pxd: int run_game(const main_arguments &args) except +\n */\nOAAPI int run_game(const main_arguments &args);\n\n} // namespace openage\n"
  },
  {
    "path": "libopenage/options.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"options.h\"\n#include <utility>\n\nnamespace openage::options {\n\n\nOptionValue::OptionValue(bool b)\n\t:\n\ttype{option_type::bool_type},\n\towner{true},\n\tvar{new util::Variable<bool>(b)} {}\n\n\nOptionValue::OptionValue(int i)\n\t:\n\ttype{option_type::int_type},\n\towner{true},\n\tvar{new util::Variable<int>(i)} {}\n\n\nOptionValue::OptionValue(double d)\n\t:\n\ttype{option_type::double_type},\n\towner{true},\n\tvar{new util::Variable<double>(d)} {}\n\n\nOptionValue::OptionValue(const char *s)\n\t:\n\ttype{option_type::string_type},\n\towner{true},\n\tvar{new util::Variable<std::string>(std::string(s))} {}\n\n\nOptionValue::OptionValue(const std::string &s)\n\t:\n\ttype{option_type::string_type},\n\towner{true},\n\tvar{new util::Variable<std::string>(s)} {}\n\n\nOptionValue::OptionValue(const option_list &l)\n\t:\n\ttype{option_type::list_type},\n\towner{true},\n\tvar{new util::Variable<option_list>(l)} {}\n\n\nOptionValue::OptionValue(const OptionValue &v)\n\t:\n\ttype{v.type},\n\towner{false},\n\tvar{nullptr} {\n\tthis->set(v);\n}\n\n\nOptionValue::OptionValue(util::Variable<bool> *b)\n\t:\n\ttype{option_type::bool_type},\n\towner{false},\n\tvar{b} {}\n\n\nOptionValue::OptionValue(util::Variable<int> *i)\n\t:\n\ttype{option_type::int_type},\n\towner{false},\n\tvar{i} {}\n\n\nOptionValue::OptionValue(util::Variable<double> *d)\n\t:\n\ttype{option_type::double_type},\n\towner{false},\n\tvar{d} {}\n\n\nOptionValue::OptionValue(util::Variable<std::string> *s)\n\t:\n\ttype{option_type::string_type},\n\towner{false},\n\tvar{s} {}\n\n\nOptionValue::OptionValue(util::Variable<option_list> *l)\n\t:\n\ttype{option_type::list_type},\n\towner{false},\n\tvar{l} {}\n\n\nOptionValue::~OptionValue() {\n\tif (this->owner) {\n\t\tdelete this->var;\n\t}\n}\n\n\nbool OptionValue::is_reference() const {\n\treturn !this->owner;\n}\n\n\nbool OptionValue::operator ==(const OptionValue &other) const {\n\tif (this->type != other.type) {\n\t\treturn false;\n\t}\n\n\t// check each type\n\tswitch (this->type) {\n\n\tcase option_type::bool_type:\n\t\treturn this->value<bool>() == other.value<bool>();\n\n\tcase option_type::int_type:\n\t\treturn this->value<int>() == other.value<int>();\n\n\tcase option_type::double_type:\n\t\treturn this->value<double>() == other.value<double>();\n\n\tcase option_type::string_type:\n\t\treturn this->value<std::string>() == other.value<std::string>();\n\n\tcase option_type::list_type:\n\t\treturn this->value<option_list>() == other.value<option_list>();\n\tdefault:\n\t\treturn false;\n\t}\n}\n\n\nconst OptionValue &OptionValue::operator =(const OptionValue &other) {\n\tif (this->type == other.type) {\n\t\tthis->set(other);\n\t}\n\treturn *this;\n}\n\n\nvoid OptionValue::set(const OptionValue &other) {\n\tswitch (this->type) {\n\n\tcase option_type::bool_type:\n\t\tthis->set_value<bool>(other);\n\t\tbreak;\n\n\tcase option_type::int_type:\n\t\tthis->set_value<int>(other);\n\t\tbreak;\n\n\tcase option_type::double_type:\n\t\tthis->set_value<double>(other);\n\t\tbreak;\n\n\tcase option_type::string_type:\n\t\tthis->set_value<std::string>(other);\n\t\tbreak;\n\n\tcase option_type::list_type:\n\t\tthis->set_value<option_list>(other);\n\t\tbreak;\n\n\t}\n}\n\n\nstd::string OptionValue::str_value() const {\n\tswitch (this->type) {\n\n\tcase option_type::bool_type:\n\t\treturn this->value<bool>() ? \"true\" : \"false\";\n\n\tcase option_type::int_type:\n\t\treturn std::to_string(this->value<int>());\n\n\tcase option_type::double_type:\n\t\treturn std::to_string(this->value<double>());\n\n\tcase option_type::string_type:\n\t\treturn this->value<std::string>();\n\n\tcase option_type::list_type: {\n\t\tstd::string result = \"[ \";\n\t\tfor (auto &i : this->value<option_list>()) {\n\t\t\tresult += i.str_value() + \" \";\n\t\t}\n\t\tresult += \"]\";\n\t\treturn result;\n\t}\n\tdefault:\n\t\treturn \"\";\n\t}\n}\n\n\nOptionValue parse(option_type t, const std::string &s) {\n\tswitch(t) {\n\tcase options::option_type::bool_type:\n\t\treturn options::OptionValue(s == \"true\");\n\n\tcase option_type::int_type:\n\t\treturn options::OptionValue(stoi(s));\n\n\tcase option_type::double_type:\n\t\treturn options::OptionValue(stod(s));\n\n\tcase option_type::string_type:\n\t\treturn options::OptionValue(s);\n\n\tcase option_type::list_type:\n\t\t// TODO: Missing case\n\t\treturn options::OptionValue(false);\n\n\tdefault:\n\t\treturn options::OptionValue(false);\n\t}\n}\n\n\nOptionAction::OptionAction(std::string name, opt_func_t f)\n\t:\n\tname{std::move(name)},\n\tfunction{std::move(f)} {\n}\n\n\nOptionValue OptionAction::do_action() {\n\treturn this->function();\n}\n\n\nOptionNode::OptionNode(std::string panel_name)\n\t:\n\tname{std::move(panel_name)},\n\tparent{nullptr} {\n}\n\n\nOptionNode::~OptionNode() {\n\tif (this->parent) {\n\t\tthis->parent->remove_panel(this);\n\t}\n}\n\n\nstd::vector<std::string> OptionNode::list_options(bool recurse, const std::string &indent) {\n\tstd::vector<std::string> result;\n\tresult.push_back(indent + \"node \" + this->name + \" {\");\n\tstd::string inner_indent = indent + \"\\t\";\n\tfor (auto &v : varmap) {\n\t\tresult.push_back(inner_indent + \"var \" + v.first + \" = \" + v.second.str_value());\n\t}\n\tfor (auto &f : actions) {\n\t\tresult.push_back(inner_indent + \"func \" + f.first + \"()\");\n\t}\n\tfor (auto &p : children) {\n\t\tfor (auto &line : p.second->list_options(recurse, inner_indent)) {\n\t\t\tresult.push_back(line);\n\t\t}\n\t}\n\tresult.push_back(indent + \"}\");\n\treturn result;\n}\n\n\nstd::vector<std::string> OptionNode::list_variables() const {\n\tstd::vector<std::string> result;\n\tfor (auto &v : this->varmap) {\n\t\tresult.push_back(v.first);\n\t}\n\treturn result;\n}\n\n\nstd::vector<std::string> OptionNode::list_functions() const {\n\tstd::vector<std::string> result;\n\tfor (auto &f : this->actions) {\n\t\tresult.push_back(f.first);\n\t}\n\treturn result;\n}\n\n\nOptionValue &OptionNode::get_variable_rw(const std::string &name) {\n\treturn this->varmap.at(name);\n}\n\n\nconst OptionValue &OptionNode::get_variable(const std::string &name) const {\n\treturn this->varmap.at(name);\n}\n\n\nOptionNode *OptionNode::get_child(const std::string &name) const {\n\tauto child = this->children.find(name);\n\tif (child == this->children.end()) {\n\t\treturn nullptr;\n\t}\n\treturn child->second;\n}\n\n\nvoid OptionNode::set_parent(OptionNode *new_parent) {\n\tif (this->parent) {\n\t\tthis->parent->remove_panel(this);\n\t}\n\tthis->parent = new_parent;\n\tnew_parent->add_panel(this);\n}\n\n\nOptionValue OptionNode::do_action(const std::string &aname) {\n\tauto a = this->actions.find(aname);\n\tif (a != this->actions.end()) {\n\t\treturn a->second.do_action();\n\t}\n\treturn OptionValue(false);\n}\n\n\nvoid OptionNode::add(const std::string &vname, const OptionValue &value) {\n\tthis->varmap.emplace(std::make_pair(vname, value));\n}\n\n\nvoid OptionNode::add_action(const OptionAction &action) {\n\tthis->actions.emplace(std::make_pair(action.name, action));\n}\n\n\nvoid OptionNode::add_panel(OptionNode *child) {\n\tthis->children.emplace(std::make_pair(child->name, child));\n}\n\n\nvoid OptionNode::remove_panel(OptionNode *child) {\n\tthis->children.erase(child->name);\n}\n\n\nOptionHud::OptionHud() = default;\n\n\n} // namespace openage\n"
  },
  {
    "path": "libopenage/options.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"util/variable.h\"\n\nnamespace openage {\nnamespace options {\n\n// TODO: load option values from cvar system.\n\n/**\n * list of possible value types\n */\nenum class option_type {\n\tbool_type,\n\tint_type,\n\tdouble_type,\n\tstring_type,\n\tlist_type\n};\n\nclass OptionValue;\n\n/**\n * set the container used for lists\n */\nusing option_list = std::vector<OptionValue>;\n\n\n/**\n * stores a type and value\n *\n * TODO: What is this used for?\n */\nclass OptionValue {\npublic:\n\t/**\n\t * value ownership managed by this\n\t */\n\tOptionValue(bool b);\n\tOptionValue(int i);\n\tOptionValue(double d);\n\tOptionValue(const char *s);\n\tOptionValue(const std::string &s);\n\tOptionValue(const option_list &l);\n\tOptionValue(const OptionValue &v);\n\n\t/**\n\t * value owned by another object\n\t */\n\tOptionValue(util::Variable<bool> *b);\n\tOptionValue(util::Variable<int> *i);\n\tOptionValue(util::Variable<double> *d);\n\tOptionValue(util::Variable<std::string> *s);\n\tOptionValue(util::Variable<option_list> *l);\n\n\tvirtual ~OptionValue();\n\n\t/**\n\t * True if this value references an object\n\t * not owned by itself\n\t */\n\tbool is_reference() const;\n\n\t/**\n\t * Checks equality\n\t */\n\tbool operator==(const OptionValue &other) const;\n\n\t/**\n\t * Assignment, reference values share their values\n\t * non reference values are copied\n\t */\n\tconst OptionValue &operator=(const OptionValue &other);\n\n\t/**\n\t * Value converted to a string\n\t */\n\tstd::string str_value() const;\n\n\t/**\n\t * read inner type - the templated type must match\n\t */\n\ttemplate <class T>\n\tconst T &value() const {\n\t\treturn var->get<T>();\n\t}\n\n\tconst option_type type;\n\nprivate:\n\t/**\n\t * set the value\n\t */\n\tvoid set(const OptionValue &other);\n\n\ttemplate <class T>\n\tvoid set_value(const OptionValue &other) {\n\t\tconst T &other_value = other.value<T>();\n\t\tif (this->var) {\n\t\t\tthis->var->set<T>(other_value);\n\t\t}\n\t\telse if (other.owner) {\n\t\t\tthis->owner = true;\n\t\t\tthis->var = new util::Variable<T>(other_value);\n\t\t}\n\t\telse {\n\t\t\tthis->var = other.var;\n\t\t}\n\t}\n\n\n\t/**\n\t * Use of shared_ptr and unique_ptr doesnt work here\n\t * possibly solved by templating ownership type\n\t *\n\t * non-owned variables act as references\n\t */\n\tbool owner;\n\tutil::VariableBase *var;\n};\n\nOptionValue parse(option_type t, const std::string &s);\n\n\nusing opt_func_t = std::function<OptionValue()>;\n\n// TODO string description\n// This is a base class for event_class, event, action_t mapped to a function\n// This could be constructed with a context and bind itself using the required types\nclass OptionAction {\npublic:\n\tOptionAction(std::string name, opt_func_t f);\n\n\t/**\n\t * pass mouse position\n\t */\n\tOptionValue do_action();\n\n\t/**\n\t * name of this action\n\t */\n\tconst std::string name;\n\nprivate:\n\tconst opt_func_t function;\n};\n\n/**\n * Allows objects to expose customisable features\n *\n * This is used to bind inner game classes\n * with console interaction or gui elements\n */\nclass OptionNode {\n\ttemplate <class T>\n\tfriend class Var;\n\npublic:\n\tOptionNode(std::string panel_name);\n\tvirtual ~OptionNode();\n\n\t/**\n\t * lists all available options in a readable format\n\t */\n\tstd::vector<std::string> list_options(bool recurse = false, const std::string &indent = \"\");\n\n\t/**\n\t * shows all available variable names\n\t */\n\tstd::vector<std::string> list_variables() const;\n\n\t/**\n\t * shows all the available function names\n\t */\n\tstd::vector<std::string> list_functions() const;\n\n\tOptionValue &get_variable_rw(const std::string &name);\n\tconst OptionValue &get_variable(const std::string &name) const;\n\n\t/**\n\t * shortcut for get_variable(name).value<T>()\n\t */\n\ttemplate <class T>\n\tconst T &getv(const std::string &name) {\n\t\treturn this->get_variable(name).value<T>();\n\t}\n\n\t/**\n\t * return a child by name, or null if not available\n\t */\n\tOptionNode *get_child(const std::string &name) const;\n\n\t/**\n\t * Set a parent panel which\n\t * implies adding a child to the parent\n\t */\n\tvoid set_parent(OptionNode *parent);\n\n\t/**\n\t * performs the named action\n\t * returning the actions response\n\t */\n\tOptionValue do_action(const std::string &aname);\n\n\t/**\n\t * name of this node\n\t */\n\tconst std::string name;\n\nprotected:\n\t/**\n\t * add types to the interface\n\t */\n\tvoid add(const std::string &vname, const OptionValue &value);\n\n\tvoid add_action(const OptionAction &action);\n\nprivate:\n\t/**\n\t * add child nodes\n\t */\n\tvoid add_panel(OptionNode *child);\n\n\t/**\n\t * remove child nodes\n\t */\n\tvoid remove_panel(OptionNode *child);\n\n\t/**\n\t * parent of this node\n\t */\n\tOptionNode *parent;\n\n\t/**\n\t * Variables which can be used\n\t * TODO: read only variables\n\t */\n\tstd::unordered_map<std::string, OptionValue> varmap;\n\n\n\t// list available functions for interaction\n\tstd::unordered_map<std::string, OptionAction> actions;\n\n\t/**\n\t * children of this node\n\t */\n\tstd::unordered_map<std::string, OptionNode *> children;\n};\n\n\n/**\n * A interaface variable which gets monitored by an\n * option node allowing reflection, while also\n * being directly accessable as a typed member\n */\ntemplate <class T>\nclass Var : public util::Variable<T> {\npublic:\n\tVar(OptionNode *owner, const std::string &name, const T &init) :\n\t\tutil::Variable<T>{init} {\n\t\towner->add(name, this->value);\n\t}\n};\n\n\n/**\n * A thing for drawing OptionNodes\n *\n * generates required gui elements for each\n * variable on a panel\n */\nclass OptionHud {\npublic:\n\tOptionHud();\n};\n\n} // namespace options\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/CMakeLists.txt",
    "content": "add_sources(libopenage\n    cost_field.cpp\n\tdefinitions.cpp\n\tfield_cache.cpp\n    flow_field.cpp\n\tgrid.cpp\n\tintegration_field.cpp\n    integrator.cpp\n\tpath.cpp\n\tpathfinder.cpp\n\tportal.cpp\n\tsector.cpp\n\ttests.cpp\n    types.cpp\n)\n\nadd_subdirectory(\"demo\")\nadd_subdirectory(\"legacy\")\n"
  },
  {
    "path": "libopenage/pathfinding/cost_field.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"cost_field.h\"\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"coord/tile.h\"\n#include \"pathfinding/definitions.h\"\n\n\nnamespace openage::path {\n\nCostField::CostField(size_t size) :\n\tsize{size},\n\tvalid_until{time::TIME_MIN},\n\tcells(this->size * this->size, COST_MIN) {\n\tlog::log(DBG << \"Created cost field with size \" << this->size << \"x\" << this->size);\n}\n\nsize_t CostField::get_size() const {\n\treturn this->size;\n}\n\ncost_t CostField::get_cost(const coord::tile_delta &pos) const {\n\treturn this->cells.at(pos.ne + pos.se * this->size);\n}\n\ncost_t CostField::get_cost(size_t x, size_t y) const {\n\treturn this->cells.at(x + y * this->size);\n}\n\ncost_t CostField::get_cost(size_t idx) const {\n\treturn this->cells.at(idx);\n}\n\nvoid CostField::set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until) {\n\tthis->set_cost(pos.ne + pos.se * this->size, cost, valid_until);\n}\n\nvoid CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until) {\n\tthis->set_cost(x + y * this->size, cost, valid_until);\n}\n\nconst std::vector<cost_t> &CostField::get_costs() const {\n\treturn this->cells;\n}\n\nvoid CostField::set_costs(std::vector<cost_t> &&cells, const time::time_t &valid_until) {\n\tENSURE(cells.size() == this->cells.size(),\n\t       \"cells vector has wrong size: \" << cells.size()\n\t                                       << \"; expected: \"\n\t                                       << this->cells.size());\n\n\tthis->cells = std::move(cells);\n\tthis->valid_until = valid_until;\n}\n\nbool CostField::is_dirty(const time::time_t &time) const {\n\treturn time >= this->valid_until;\n}\n\nvoid CostField::clear_dirty() {\n\tthis->valid_until = time::TIME_MAX;\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/cost_field.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <vector>\n\n#include \"pathfinding/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace coord {\nstruct tile_delta;\n} // namespace coord\n\nnamespace path {\n\n/**\n * Cost field in the flow-field pathfinding algorithm.\n */\nclass CostField {\npublic:\n\t/**\n\t * Create a square cost field with a specified size.\n\t *\n\t * @param size Side length of the field.\n\t */\n\tCostField(size_t size);\n\n\t/**\n\t * Get the size of the cost field.\n\t *\n\t * @return Size of the cost field.\n\t */\n\tsize_t get_size() const;\n\n\t/**\n\t * Get the cost at a specified position.\n\t *\n\t * @param pos Coordinates of the cell (relative to field origin).\n\t * @return Cost at the specified position.\n\t */\n\tcost_t get_cost(const coord::tile_delta &pos) const;\n\n\t/**\n\t * Get the cost at a specified position.\n\t *\n\t * @param x X-coordinate of the cell.\n\t * @param y Y-coordinate of the cell.\n\t * @return Cost at the specified position.\n\t */\n\tcost_t get_cost(size_t x, size_t y) const;\n\n\t/**\n\t * Get the cost at a specified position.\n\t *\n\t * @param idx Index of the cell.\n\t * @return Cost at the specified position.\n\t */\n\tcost_t get_cost(size_t idx) const;\n\n\t/**\n\t * Set the cost at a specified position.\n\t *\n\t * @param pos Coordinates of the cell (relative to field origin).\n\t * @param cost Cost to set.\n\t * @param valid_until Time at which the cost value expires.\n\t */\n\tvoid set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until);\n\n\t/**\n\t * Set the cost at a specified position.\n\t *\n\t * @param x X-coordinate of the cell.\n\t * @param y Y-coordinate of the cell.\n\t * @param cost Cost to set.\n\t * @param valid_until Time at which the cost value expires.\n\t */\n\tvoid set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until);\n\n\t/**\n\t * Set the cost at a specified position.\n\t *\n\t * @param idx Index of the cell.\n\t * @param cost Cost to set.\n\t * @param valid_until Time at which the cost value expires.\n\t */\n\tinline void set_cost(size_t idx, cost_t cost, const time::time_t &valid_until) {\n\t\tthis->cells[idx] = cost;\n\t\tthis->valid_until = valid_until;\n\t}\n\n\t/**\n\t * Get the cost field values.\n\t *\n\t * @return Cost field values.\n\t */\n\tconst std::vector<cost_t> &get_costs() const;\n\n\t/**\n\t * Set the cost field values.\n\t *\n\t * @param cells Cost field values.\n\t * @param valid_until Time at which the cost value expires.\n\t */\n\tvoid set_costs(std::vector<cost_t> &&cells, const time::time_t &changed);\n\n\t/**\n\t * Check if the cost field is dirty at the specified time.\n\t *\n\t * @param time Time of access to the cost field.\n\t *\n\t * @return Whether the cost field is dirty.\n\t */\n\tbool is_dirty(const time::time_t &time) const;\n\n\t/**\n\t * Clear the dirty flag.\n\t */\n\tvoid clear_dirty();\n\nprivate:\n\t/**\n\t * Side length of the field.\n\t */\n\tsize_t size;\n\n\t/**\n\t * Time the cost field expires.\n\t */\n\ttime::time_t valid_until;\n\n\t/**\n\t * Cost field values.\n\t */\n\tstd::vector<cost_t> cells;\n};\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/definitions.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::path {\n\n// this file is intentionally empty\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/definitions.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <limits>\n\n#include \"pathfinding/types.h\"\n\n\nnamespace openage::path {\n\n/**\n * Init value for a cells in the cost grid.\n *\n * Should not be used for actual costs.\n */\nconstexpr cost_t COST_INIT = 0;\n\n/**\n * Minimum possible cost for a passable cell in the cost grid.\n */\nconstexpr cost_t COST_MIN = 1;\n\n/**\n * Maximum possible cost for a passable cell in the cost grid.\n */\nconstexpr cost_t COST_MAX = 254;\n\n/**\n * Cost value for an impassable cell in the cost grid.\n */\nconstexpr cost_t COST_IMPASSABLE = 255;\n\n\n/**\n * Start value for goal cells.\n */\nconstexpr integrated_cost_t INTEGRATED_COST_START = 0;\n\n/**\n * Unreachable value for a cells in the integration grid.\n */\nconstexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits<integrated_cost_t>::max();\n\n/**\n * Line of sight flag in an integrated_flags_t value.\n */\nconstexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x20;\n\n/**\n * Target flag in an integrated_flags_t value.\n */\nconstexpr integrated_flags_t INTEGRATE_TARGET_MASK = 0x40;\n\n/**\n * Found flag in an integrated_flags_t value.\n */\nconstexpr integrated_flags_t INTEGRATE_FOUND_MASK = 0x02;\n\n/**\n * Wavefront blocked flag in an integrated_flags_t value.\n */\nconstexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x04;\n\n/**\n * Initial value for a cell in the integration grid.\n */\nconstexpr integrated_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0};\n\n\n/**\n * Initial value for a flow field cell.\n */\nconstexpr flow_t FLOW_INIT = 0;\n\n/**\n * Mask for the flow direction bits in a flow_t value.\n */\nconstexpr flow_t FLOW_DIR_MASK = 0x0F;\n\n/**\n * Mask for the flow flag bits in a flow_t value.\n */\nconstexpr flow_t FLOW_FLAGS_MASK = 0xF0;\n\n/**\n * Pathable flag in a flow_t value.\n */\nconstexpr flow_t FLOW_PATHABLE_MASK = 0x10;\n\n/**\n * Line of sight flag in a flow_t value.\n */\nconstexpr flow_t FLOW_LOS_MASK = 0x20;\n\n/**\n * Target flag in a flow_t value.\n */\nconstexpr flow_t FLOW_TARGET_MASK = 0x40;\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/demo/CMakeLists.txt",
    "content": "add_sources(libopenage\n    demo_0.cpp\n    demo_1.cpp\n    tests.cpp\n)\n\npxdgen(\n\ttests.h\n)\n"
  },
  {
    "path": "libopenage/pathfinding/demo/demo_0.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"demo_0.h\"\n\n#include <QKeyEvent>\n#include <QMouseEvent>\n\n#include \"coord/tile.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/flow_field.h\"\n#include \"pathfinding/integration_field.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/camera/definitions.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/renderable.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"util/math_constants.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::path::tests {\n\nvoid path_demo_0(const util::Path &path) {\n\t// Side length of the field\n\t// Creates a 10x10 grid\n\tauto field_length = 10;\n\n\t// Cost field with some obstacles\n\tauto cost_field = std::make_shared<CostField>(field_length);\n\n\tconst time::time_t time = time::TIME_ZERO;\n\tcost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE, time);\n\tcost_field->set_cost(coord::tile_delta{1, 0}, 254, time);\n\tcost_field->set_cost(coord::tile_delta{4, 3}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{5, 3}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{6, 3}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{4, 4}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{5, 4}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{6, 4}, 128, time);\n\tcost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE, time);\n\tlog::log(INFO << \"Created cost field\");\n\n\t// Create an integration field from the cost field\n\tauto integration_field = std::make_shared<IntegrationField>(field_length);\n\tlog::log(INFO << \"Created integration field\");\n\n\t// Set cell (7, 7) to be the initial target cell\n\tauto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile_delta{7, 7});\n\tintegration_field->integrate_cost(cost_field, std::move(wavefront_blocked));\n\tlog::log(INFO << \"Calculated integration field for target cell (7, 7)\");\n\n\t// Create a flow field from the integration field\n\tauto flow_field = std::make_shared<FlowField>(field_length);\n\tlog::log(INFO << \"Created flow field\");\n\n\tflow_field->build(integration_field);\n\tlog::log(INFO << \"Built flow field from integration field\");\n\n\t// Render the grid and the pathfinding results\n\tauto qtapp = std::make_shared<renderer::gui::GuiApplicationWithLogger>();\n\n\t// Create a window for rendering\n\trenderer::window_settings settings;\n\tsettings.width = 1440;\n\tsettings.height = 720;\n\tauto window = std::make_shared<renderer::opengl::GlWindow>(\"openage pathfinding test\", settings);\n\tauto render_manager = std::make_shared<RenderManager0>(qtapp, window, path);\n\tlog::log(INFO << \"Created render manager for pathfinding demo\");\n\n\t// Show the cost field on startup\n\trender_manager->show_cost_field(cost_field);\n\tauto current_field = RenderManager0::field_t::COST;\n\tlog::log(INFO << \"Showing cost field\");\n\n\t// Make steering vector visibility toggleable\n\tauto vectors_visible = false;\n\n\t// Enable mouse button callbacks\n\twindow->add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tif (ev.type() == QEvent::MouseButtonRelease) {\n\t\t\tif (ev.button() == Qt::RightButton) { // Set target cell\n\t\t\t\tauto tile_pos = render_manager->select_tile(ev.position().x(), ev.position().y());\n\t\t\t\tauto grid_x = tile_pos.first;\n\t\t\t\tauto grid_y = tile_pos.second;\n\n\t\t\t\tif (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) {\n\t\t\t\t\tlog::log(INFO << \"Selected new target cell (\" << grid_x << \", \" << grid_y << \")\");\n\n\t\t\t\t\t// Recalculate the integration field and the flow field\n\t\t\t\t\tintegration_field->reset();\n\t\t\t\t\tauto wavefront_blocked = integration_field->integrate_los(cost_field,\n\t\t\t\t\t                                                          coord::tile_delta{grid_x, grid_y});\n\t\t\t\t\tintegration_field->integrate_cost(cost_field, std::move(wavefront_blocked));\n\t\t\t\t\tlog::log(INFO << \"Calculated integration field for target cell (\"\n\t\t\t\t\t              << grid_x << \", \" << grid_y << \")\");\n\n\t\t\t\t\tflow_field->reset();\n\t\t\t\t\tflow_field->build(integration_field);\n\t\t\t\t\tlog::log(INFO << \"Built flow field from integration field\");\n\n\t\t\t\t\t// Show the new field values and vectors\n\t\t\t\t\tswitch (current_field) {\n\t\t\t\t\tcase RenderManager0::field_t::COST:\n\t\t\t\t\t\trender_manager->show_cost_field(cost_field);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase RenderManager0::field_t::INTEGRATION:\n\t\t\t\t\t\trender_manager->show_integration_field(integration_field);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase RenderManager0::field_t::FLOW:\n\t\t\t\t\t\trender_manager->show_flow_field(flow_field, integration_field);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (vectors_visible) {\n\t\t\t\t\t\trender_manager->show_vectors(flow_field);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\t// Enable key callbacks\n\twindow->add_key_callback([&](const QKeyEvent &ev) {\n\t\tif (ev.type() == QEvent::KeyRelease) {\n\t\t\tif (ev.key() == Qt::Key_F1) { // Show cost field\n\t\t\t\trender_manager->show_cost_field(cost_field);\n\t\t\t\tcurrent_field = RenderManager0::field_t::COST;\n\t\t\t\tlog::log(INFO << \"Showing cost field\");\n\t\t\t}\n\t\t\telse if (ev.key() == Qt::Key_F2) { // Show integration field\n\t\t\t\trender_manager->show_integration_field(integration_field);\n\t\t\t\tcurrent_field = RenderManager0::field_t::INTEGRATION;\n\t\t\t\tlog::log(INFO << \"Showing integration field\");\n\t\t\t}\n\t\t\telse if (ev.key() == Qt::Key_F3) { // Show flow field\n\t\t\t\trender_manager->show_flow_field(flow_field, integration_field);\n\t\t\t\tcurrent_field = RenderManager0::field_t::FLOW;\n\t\t\t\tlog::log(INFO << \"Showing flow field\");\n\t\t\t}\n\t\t\telse if (ev.key() == Qt::Key_F4) { // Show steering vectors\n\t\t\t\tif (vectors_visible) {\n\t\t\t\t\trender_manager->hide_vectors();\n\t\t\t\t\tvectors_visible = false;\n\t\t\t\t\tlog::log(INFO << \"Hiding steering vectors\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\trender_manager->show_vectors(flow_field);\n\t\t\t\t\tvectors_visible = true;\n\t\t\t\t\tlog::log(INFO << \"Showing steering vectors\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\tlog::log(INFO << \"Instructions:\");\n\tlog::log(INFO << \"  1. Press F1/F2/F3 to show cost/integration/flow field\");\n\tlog::log(INFO << \"  2. Press F4 to toggle steering vectors\");\n\n\t// Run the render loop\n\trender_manager->run();\n}\n\n\nRenderManager0::RenderManager0(const std::shared_ptr<renderer::gui::GuiApplicationWithLogger> &app,\n                               const std::shared_ptr<renderer::Window> &window,\n                               const util::Path &path) :\n\tpath{path},\n\tapp{app},\n\twindow{window},\n\trenderer{window->make_renderer()},\n\tcamera{std::make_shared<renderer::camera::Camera>(renderer, window->get_size())} {\n\t// Position camera to look at center of the grid\n\tcamera->look_at_coord({5, 5, 0});\n\twindow->add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\tcamera->resize(w, h);\n\t});\n\n\tthis->init_shaders();\n\tthis->init_passes();\n}\n\n\nvoid RenderManager0::run() {\n\twhile (not this->window->should_close()) {\n\t\tthis->app->process_events();\n\n\t\tthis->renderer->render(this->background_pass);\n\t\tthis->renderer->render(this->field_pass);\n\t\tthis->renderer->render(this->vector_pass);\n\t\tthis->renderer->render(this->grid_pass);\n\t\tthis->renderer->render(this->display_pass);\n\n\t\tthis->window->update();\n\n\t\tthis->renderer->check_error();\n\t}\n\tthis->window->close();\n}\n\nvoid RenderManager0::show_cost_field(const std::shared_ptr<path::CostField> &field) {\n\tEigen::Matrix4f model = Eigen::Matrix4f::Identity();\n\tauto unifs = this->cost_shader->new_uniform_input(\n\t\t\"model\",\n\t\tmodel,\n\t\t\"view\",\n\t\tthis->camera->get_view_matrix(),\n\t\t\"proj\",\n\t\tthis->camera->get_projection_matrix());\n\tauto mesh = RenderManager0::get_cost_field_mesh(field);\n\tauto geometry = this->renderer->add_mesh_geometry(mesh);\n\trenderer::Renderable renderable{\n\t\tunifs,\n\t\tgeometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->field_pass->set_renderables({renderable});\n}\n\nvoid RenderManager0::show_integration_field(const std::shared_ptr<path::IntegrationField> &field) {\n\tEigen::Matrix4f model = Eigen::Matrix4f::Identity();\n\tauto unifs = this->integration_shader->new_uniform_input(\n\t\t\"model\",\n\t\tmodel,\n\t\t\"view\",\n\t\tthis->camera->get_view_matrix(),\n\t\t\"proj\",\n\t\tthis->camera->get_projection_matrix());\n\tauto mesh = get_integration_field_mesh(field, 4);\n\tauto geometry = this->renderer->add_mesh_geometry(mesh);\n\trenderer::Renderable renderable{\n\t\tunifs,\n\t\tgeometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->field_pass->set_renderables({renderable});\n}\n\nvoid RenderManager0::show_flow_field(const std::shared_ptr<path::FlowField> &flow_field,\n                                     const std::shared_ptr<path::IntegrationField> &int_field) {\n\tEigen::Matrix4f model = Eigen::Matrix4f::Identity();\n\tauto unifs = this->flow_shader->new_uniform_input(\n\t\t\"model\",\n\t\tmodel,\n\t\t\"view\",\n\t\tthis->camera->get_view_matrix(),\n\t\t\"proj\",\n\t\tthis->camera->get_projection_matrix());\n\tauto mesh = get_flow_field_mesh(flow_field, int_field, 4);\n\tauto geometry = this->renderer->add_mesh_geometry(mesh);\n\trenderer::Renderable renderable{\n\t\tunifs,\n\t\tgeometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->field_pass->set_renderables({renderable});\n}\n\nvoid RenderManager0::show_vectors(const std::shared_ptr<path::FlowField> &field) {\n\tstatic const std::unordered_map<flow_dir_t, Eigen::Vector3f> offset_dir{\n\t\t{flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}},\n\t\t{flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}},\n\t\t{flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}},\n\t\t{flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}},\n\t\t{flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}},\n\t\t{flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}},\n\t\t{flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}},\n\t\t{flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}},\n\t};\n\n\tthis->vector_pass->clear_renderables();\n\tfor (size_t y = 0; y < field->get_size(); ++y) {\n\t\tfor (size_t x = 0; x < field->get_size(); ++x) {\n\t\t\tauto cell = field->get_cell(coord::tile_delta(x, y));\n\t\t\tif (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) {\n\t\t\t\tEigen::Affine3f arrow_model = Eigen::Affine3f::Identity();\n\t\t\t\tarrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5));\n\t\t\t\tauto dir = static_cast<flow_dir_t>(cell & FLOW_DIR_MASK);\n\t\t\t\tarrow_model.translate(offset_dir.at(dir));\n\n\t\t\t\tauto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD;\n\t\t\t\tarrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY()));\n\t\t\t\tauto arrow_unifs = this->vector_shader->new_uniform_input(\n\t\t\t\t\t\"model\",\n\t\t\t\t\tarrow_model.matrix(),\n\t\t\t\t\t\"view\",\n\t\t\t\t\tcamera->get_view_matrix(),\n\t\t\t\t\t\"proj\",\n\t\t\t\t\tcamera->get_projection_matrix(),\n\t\t\t\t\t\"color\",\n\t\t\t\t\tEigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f});\n\t\t\t\tauto arrow_mesh = get_arrow_mesh();\n\t\t\t\tauto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh);\n\t\t\t\trenderer::Renderable arrow_renderable{\n\t\t\t\t\tarrow_unifs,\n\t\t\t\t\tarrow_geometry,\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue,\n\t\t\t\t};\n\t\t\t\tthis->vector_pass->add_renderables(std::move(arrow_renderable));\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid RenderManager0::hide_vectors() {\n\tthis->vector_pass->clear_renderables();\n}\n\nstd::pair<int, int> RenderManager0::select_tile(double x, double y) {\n\tauto grid_plane_normal = Eigen::Vector3f{0, 1, 0};\n\tauto grid_plane_point = Eigen::Vector3f{0, 0, 0};\n\tauto camera_direction = renderer::camera::CAM_DIRECTION;\n\tauto camera_position = camera->get_input_pos(\n\t\tcoord::input(x, y));\n\n\tEigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal);\n\tauto grid_x = static_cast<int>(-1 * intersect[2]);\n\tauto grid_y = static_cast<int>(intersect[0]);\n\n\treturn {grid_x, grid_y};\n}\n\nvoid RenderManager0::init_shaders() {\n\t// Shader sources\n\tauto shaderdir = this->path / \"assets\" / \"test\" / \"shaders\" / \"pathfinding\";\n\n\t// Shader for rendering the cost field\n\tauto cf_vshader_file = shaderdir / \"demo_0_cost_field.vert.glsl\";\n\tauto cf_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tcf_vshader_file);\n\n\tauto cf_fshader_file = shaderdir / \"demo_0_cost_field.frag.glsl\";\n\tauto cf_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tcf_fshader_file);\n\n\t// Shader for rendering the integration field\n\tauto if_vshader_file = shaderdir / \"demo_0_integration_field.vert.glsl\";\n\tauto if_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tif_vshader_file);\n\n\tauto if_fshader_file = shaderdir / \"demo_0_integration_field.frag.glsl\";\n\tauto if_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tif_fshader_file);\n\n\t// Shader for rendering the flow field\n\tauto ff_vshader_file = shaderdir / \"demo_0_flow_field.vert.glsl\";\n\tauto ff_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tff_vshader_file);\n\n\tauto ff_fshader_file = shaderdir / \"demo_0_flow_field.frag.glsl\";\n\tauto ff_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tff_fshader_file);\n\n\t// Shader for rendering steering vectors\n\tauto vec_vshader_file = shaderdir / \"demo_0_vector.vert.glsl\";\n\tauto vec_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tvec_vshader_file);\n\n\tauto vec_fshader_file = shaderdir / \"demo_0_vector.frag.glsl\";\n\tauto vec_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tvec_fshader_file);\n\n\t// Shader for rendering the grid\n\tauto grid_vshader_file = shaderdir / \"demo_0_grid.vert.glsl\";\n\tauto grid_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tgrid_vshader_file);\n\n\tauto grid_fshader_file = shaderdir / \"demo_0_grid.frag.glsl\";\n\tauto grid_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tgrid_fshader_file);\n\n\t// Shader for 2D monocolored objects\n\tauto obj_vshader_file = shaderdir / \"demo_0_obj.vert.glsl\";\n\tauto obj_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tobj_vshader_file);\n\n\tauto obj_fshader_file = shaderdir / \"demo_0_obj.frag.glsl\";\n\tauto obj_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tobj_fshader_file);\n\n\t// Shader for rendering to the display target\n\tauto display_vshader_file = shaderdir / \"demo_0_display.vert.glsl\";\n\tauto display_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file);\n\n\tauto display_fshader_file = shaderdir / \"demo_0_display.frag.glsl\";\n\tauto display_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file);\n\n\t// Create the shaders\n\tthis->cost_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src});\n\tthis->integration_shader = renderer->add_shader({if_vshader_src, if_fshader_src});\n\tthis->flow_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src});\n\tthis->vector_shader = renderer->add_shader({vec_vshader_src, vec_fshader_src});\n\tthis->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src});\n\tthis->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tthis->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n}\n\nvoid RenderManager0::init_passes() {\n\tauto size = this->window->get_size();\n\n\t// Make a framebuffer for the background render pass to draw into\n\tauto background_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto fbo = renderer->create_texture_target({background_texture, depth_texture});\n\n\t// Background object for contrast between field and display\n\tauto background_unifs = obj_shader->new_uniform_input(\n\t\t\"color\",\n\t\tEigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0});\n\tauto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\trenderer::Renderable background_obj{\n\t\tbackground_unifs,\n\t\tbackground,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->background_pass = renderer->add_render_pass({background_obj}, fbo);\n\n\t// Make a framebuffer for the field render pass to draw into\n\tauto field_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_2 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2});\n\tthis->field_pass = renderer->add_render_pass({}, field_fbo);\n\n\t// Make a framebuffer for the vector render passes to draw into\n\tauto vector_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_3 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto vector_fbo = renderer->create_texture_target({vector_texture, depth_texture_3});\n\tthis->vector_pass = renderer->add_render_pass({}, vector_fbo);\n\n\t// Make a framebuffer for the grid render passes to draw into\n\tauto grid_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_4 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_4});\n\n\t// Create object for the grid\n\tEigen::Matrix4f model = Eigen::Matrix4f::Identity();\n\tauto grid_unifs = grid_shader->new_uniform_input(\n\t\t\"model\",\n\t\tmodel,\n\t\t\"view\",\n\t\tcamera->get_view_matrix(),\n\t\t\"proj\",\n\t\tcamera->get_projection_matrix());\n\tauto grid_mesh = this->get_grid_mesh(10);\n\tauto grid_geometry = renderer->add_mesh_geometry(grid_mesh);\n\trenderer::Renderable grid_obj{\n\t\tgrid_unifs,\n\t\tgrid_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo);\n\n\t// Make two objects that draw the results of the previous passes onto the screen\n\t// in the display render pass\n\tauto bg_texture_unif = display_shader->new_uniform_input(\"color_texture\", background_texture);\n\tauto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\trenderer::Renderable bg_pass_obj{\n\t\tbg_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto field_texture_unif = display_shader->new_uniform_input(\"color_texture\", field_texture);\n\trenderer::Renderable field_pass_obj{\n\t\tfield_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto vector_texture_unif = display_shader->new_uniform_input(\"color_texture\", vector_texture);\n\trenderer::Renderable vector_pass_obj{\n\t\tvector_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto grid_texture_unif = display_shader->new_uniform_input(\"color_texture\", grid_texture);\n\trenderer::Renderable grid_pass_obj{\n\t\tgrid_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->display_pass = renderer->add_render_pass(\n\t\t{bg_pass_obj, field_pass_obj, vector_pass_obj, grid_pass_obj},\n\t\trenderer->get_display_target());\n}\n\nrenderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::shared_ptr<CostField> &field,\n                                                                  size_t resolution) {\n\t// increase by 1 in every dimension because to get the vertex length\n\t// of each dimension\n\tutil::Vector2s size{\n\t\tfield->get_size() * resolution + 1,\n\t\tfield->get_size() * resolution + 1,\n\t};\n\tauto vert_distance = 1.0f / resolution;\n\n\t// add vertices for the cells of the grid\n\tstd::vector<float> verts{};\n\tauto vert_count = size[0] * size[1];\n\tverts.reserve(vert_count * 4);\n\tfor (int i = 0; i < static_cast<int>(size[0]); ++i) {\n\t\tfor (int j = 0; j < static_cast<int>(size[1]); ++j) {\n\t\t\t// for each vertex, compare the surrounding tiles\n\t\t\tstd::vector<float> surround{};\n\t\t\tif (j - 1 >= 0 and i - 1 >= 0) {\n\t\t\t\tauto cost = field->get_cost((i - 1) / resolution, (j - 1) / resolution);\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(field->get_size()) and i - 1 >= 0) {\n\t\t\t\tauto cost = field->get_cost((i - 1) / resolution, j / resolution);\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(field->get_size()) and i < static_cast<int>(field->get_size())) {\n\t\t\t\tauto cost = field->get_cost(i / resolution, j / resolution);\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j - 1 >= 0 and i < static_cast<int>(field->get_size())) {\n\t\t\t\tauto cost = field->get_cost(i / resolution, (j - 1) / resolution);\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\t// use the cost of the most expensive surrounding tile\n\t\t\tauto max_cost = *std::max_element(surround.begin(), surround.end());\n\n\t\t\tcoord::scene3 v{\n\t\t\t\tstatic_cast<float>(i * vert_distance),\n\t\t\t\tstatic_cast<float>(j * vert_distance),\n\t\t\t\t0,\n\t\t\t};\n\t\t\tauto world_v = v.to_world_space();\n\t\t\tverts.push_back(world_v[0]);\n\t\t\tverts.push_back(world_v[1]);\n\t\t\tverts.push_back(world_v[2]);\n\t\t\tverts.push_back(max_cost);\n\t\t}\n\t}\n\n\t// split the grid into triangles using an index array\n\tstd::vector<uint16_t> idxs;\n\tidxs.reserve((size[0] - 1) * (size[1] - 1) * 6);\n\t// iterate over all tiles in the grid by columns, i.e. starting\n\t// from the left corner to the bottom corner if you imagine it from\n\t// the camera's point of view\n\tfor (size_t i = 0; i < size[0] - 1; ++i) {\n\t\tfor (size_t j = 0; j < size[1] - 1; ++j) {\n\t\t\t// since we are working on tiles, we split each tile into two triangles\n\t\t\t// with counter-clockwise vertex order\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t}\n\t}\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLES,\n\t\trenderer::resources::index_t::U16};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\treturn {std::move(vert_data), std::move(idx_data), info};\n}\n\nrenderer::resources::MeshData RenderManager0::get_integration_field_mesh(const std::shared_ptr<IntegrationField> &field,\n                                                                         size_t resolution) {\n\t// increase by 1 in every dimension because to get the vertex length\n\t// of each dimension\n\tutil::Vector2s size{\n\t\tfield->get_size() * resolution + 1,\n\t\tfield->get_size() * resolution + 1,\n\t};\n\tauto vert_distance = 1.0f / resolution;\n\n\t// add vertices for the cells of the grid\n\tstd::vector<float> verts{};\n\tauto vert_count = size[0] * size[1];\n\tverts.reserve(vert_count * 4);\n\tfor (int i = 0; i < static_cast<int>(size[0]); ++i) {\n\t\tfor (int j = 0; j < static_cast<int>(size[1]); ++j) {\n\t\t\t// for each vertex, compare the surrounding tiles\n\t\t\tstd::vector<float> surround{};\n\t\t\tif (j - 1 >= 0 and i - 1 >= 0) {\n\t\t\t\tauto cost = field->get_cell((i - 1) / resolution, (j - 1) / resolution).cost;\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(field->get_size()) and i - 1 >= 0) {\n\t\t\t\tauto cost = field->get_cell((i - 1) / resolution, j / resolution).cost;\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(field->get_size()) and i < static_cast<int>(field->get_size())) {\n\t\t\t\tauto cost = field->get_cell(i / resolution, j / resolution).cost;\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\tif (j - 1 >= 0 and i < static_cast<int>(field->get_size())) {\n\t\t\t\tauto cost = field->get_cell(i / resolution, (j - 1) / resolution).cost;\n\t\t\t\tsurround.push_back(cost);\n\t\t\t}\n\t\t\t// use the cost of the most expensive surrounding tile\n\t\t\tauto max_cost = *std::max_element(surround.begin(), surround.end());\n\n\t\t\tcoord::scene3 v{\n\t\t\t\tstatic_cast<float>(i * vert_distance),\n\t\t\t\tstatic_cast<float>(j * vert_distance),\n\t\t\t\t0,\n\t\t\t};\n\t\t\tauto world_v = v.to_world_space();\n\t\t\tverts.push_back(world_v[0]);\n\t\t\tverts.push_back(world_v[1]);\n\t\t\tverts.push_back(world_v[2]);\n\t\t\tverts.push_back(max_cost);\n\t\t}\n\t}\n\n\t// split the grid into triangles using an index array\n\tstd::vector<uint16_t> idxs;\n\tidxs.reserve((size[0] - 1) * (size[1] - 1) * 6);\n\t// iterate over all tiles in the grid by columns, i.e. starting\n\t// from the left corner to the bottom corner if you imagine it from\n\t// the camera's point of view\n\tfor (size_t i = 0; i < size[0] - 1; ++i) {\n\t\tfor (size_t j = 0; j < size[1] - 1; ++j) {\n\t\t\t// since we are working on tiles, we split each tile into two triangles\n\t\t\t// with counter-clockwise vertex order\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t}\n\t}\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLES,\n\t\trenderer::resources::index_t::U16};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\treturn {std::move(vert_data), std::move(idx_data), info};\n}\n\nrenderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr<FlowField> &flow_field,\n                                                                  const std::shared_ptr<path::IntegrationField> &int_field,\n                                                                  size_t resolution) {\n\t// increase by 1 in every dimension because to get the vertex length\n\t// of each dimension\n\tutil::Vector2s size{\n\t\tflow_field->get_size() * resolution + 1,\n\t\tflow_field->get_size() * resolution + 1,\n\t};\n\tauto vert_distance = 1.0f / resolution;\n\n\t// add vertices for the cells of the grid\n\tstd::vector<float> verts{};\n\tauto vert_count = size[0] * size[1];\n\tverts.reserve(vert_count * 5);\n\tfor (int i = 0; i < static_cast<int>(size[0]); ++i) {\n\t\tfor (int j = 0; j < static_cast<int>(size[1]); ++j) {\n\t\t\t// for each vertex, compare the surrounding tiles\n\t\t\tstd::vector<flow_t> ff_surround{};\n\t\t\tstd::vector<integrated_flags_t> int_surround{};\n\t\t\tif (j - 1 >= 0 and i - 1 >= 0) {\n\t\t\t\tauto ff_cost = flow_field->get_cell((i - 1) / resolution, (j - 1) / resolution);\n\t\t\t\tff_surround.push_back(ff_cost);\n\n\t\t\t\tauto int_flags = int_field->get_cell((i - 1) / resolution, (j - 1) / resolution).flags;\n\t\t\t\tint_surround.push_back(int_flags);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(flow_field->get_size()) and i - 1 >= 0) {\n\t\t\t\tauto ff_cost = flow_field->get_cell((i - 1) / resolution, j / resolution);\n\t\t\t\tff_surround.push_back(ff_cost);\n\n\t\t\t\tauto int_flags = int_field->get_cell((i - 1) / resolution, j / resolution).flags;\n\t\t\t\tint_surround.push_back(int_flags);\n\t\t\t}\n\t\t\tif (j < static_cast<int>(flow_field->get_size()) and i < static_cast<int>(flow_field->get_size())) {\n\t\t\t\tauto ff_cost = flow_field->get_cell(i / resolution, j / resolution);\n\t\t\t\tff_surround.push_back(ff_cost);\n\n\t\t\t\tauto int_flags = int_field->get_cell(i / resolution, j / resolution).flags;\n\t\t\t\tint_surround.push_back(int_flags);\n\t\t\t}\n\t\t\tif (j - 1 >= 0 and i < static_cast<int>(flow_field->get_size())) {\n\t\t\t\tauto ff_cost = flow_field->get_cell(i / resolution, (j - 1) / resolution);\n\t\t\t\tff_surround.push_back(ff_cost);\n\n\t\t\t\tauto int_flags = int_field->get_cell(i / resolution, (j - 1) / resolution).flags;\n\t\t\t\tint_surround.push_back(int_flags);\n\t\t\t}\n\t\t\t// combine the flags of the sorrounding tiles\n\t\t\tauto ff_max_flags = 0;\n\t\t\tfor (auto &val : ff_surround) {\n\t\t\t\tff_max_flags |= val & 0xF0;\n\t\t\t}\n\t\t\tauto int_max_flags = 0;\n\t\t\tfor (auto &val : int_surround) {\n\t\t\t\tint_max_flags |= val;\n\t\t\t}\n\n\t\t\tcoord::scene3 v{\n\t\t\t\tstatic_cast<float>(i * vert_distance),\n\t\t\t\tstatic_cast<float>(j * vert_distance),\n\t\t\t\t0,\n\t\t\t};\n\t\t\tauto world_v = v.to_world_space();\n\t\t\tverts.push_back(world_v[0]);\n\t\t\tverts.push_back(world_v[1]);\n\t\t\tverts.push_back(world_v[2]);\n\t\t\tverts.push_back(ff_max_flags);\n\t\t\tverts.push_back(int_max_flags);\n\t\t}\n\t}\n\n\t// split the grid into triangles using an index array\n\tstd::vector<uint16_t> idxs;\n\tidxs.reserve((size[0] - 1) * (size[1] - 1) * 6);\n\t// iterate over all tiles in the grid by columns, i.e. starting\n\t// from the left corner to the bottom corner if you imagine it from\n\t// the camera's point of view\n\tfor (size_t i = 0; i < size[0] - 1; ++i) {\n\t\tfor (size_t j = 0; j < size[1] - 1; ++j) {\n\t\t\t// since we are working on tiles, we split each tile into two triangles\n\t\t\t// with counter-clockwise vertex order\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t}\n\t}\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V3F32,\n\t     renderer::resources::vertex_input_t::F32,\n\t     renderer::resources::vertex_input_t::F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLES,\n\t\trenderer::resources::index_t::U16};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\treturn {std::move(vert_data), std::move(idx_data), info};\n}\n\nrenderer::resources::MeshData RenderManager0::get_arrow_mesh() {\n\t// vertices for the arrow\n\t// x, y, z\n\tstd::vector<float> verts{\n\t\t// clang-format off\n         0.2f, 0.01f, -0.05f,\n         0.2f, 0.01f,  0.05f,\n        -0.2f, 0.01f, -0.05f,\n        -0.2f, 0.01f,  0.05f,\n\t\t// clang-format on\n\t};\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V3F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\treturn {std::move(vert_data), info};\n}\n\nrenderer::resources::MeshData RenderManager0::get_grid_mesh(size_t side_length) {\n\t// increase by 1 in every dimension because to get the vertex length\n\t// of each dimension\n\tutil::Vector2s size{side_length + 1, side_length + 1};\n\n\t// add vertices for the cells of the grid\n\tstd::vector<float> verts{};\n\tauto vert_count = size[0] * size[1];\n\tverts.reserve(vert_count * 3);\n\tfor (int i = 0; i < (int)size[0]; ++i) {\n\t\tfor (int j = 0; j < (int)size[1]; ++j) {\n\t\t\tcoord::scene3 v{\n\t\t\t\tstatic_cast<float>(i),\n\t\t\t\tstatic_cast<float>(j),\n\t\t\t\t0,\n\t\t\t};\n\t\t\tauto world_v = v.to_world_space();\n\t\t\tverts.push_back(world_v[0]);\n\t\t\tverts.push_back(world_v[1]);\n\t\t\tverts.push_back(world_v[2]);\n\t\t}\n\t}\n\n\t// split the grid into lines using an index array\n\tstd::vector<uint16_t> idxs;\n\tidxs.reserve((size[0] - 1) * (size[1] - 1) * 8);\n\t// iterate over all tiles in the grid by columns, i.e. starting\n\t// from the left corner to the bottom corner if you imagine it from\n\t// the camera's point of view\n\tfor (size_t i = 0; i < size[0] - 1; ++i) {\n\t\tfor (size_t j = 0; j < size[1] - 1; ++j) {\n\t\t\t// since we are working on tiles, we just draw a square using the 4 vertices\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t}\n\t}\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V3F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::LINES,\n\t\trenderer::resources::index_t::U16};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\treturn {std::move(vert_data), std::move(idx_data), info};\n}\n\n\n} // namespace openage::path::tests\n"
  },
  {
    "path": "libopenage/pathfinding/demo/demo_0.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"util/path.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass RenderPass;\nclass Renderer;\nclass ShaderProgram;\nclass Window;\n\nnamespace camera {\nclass Camera;\n} // namespace camera\n\nnamespace gui {\nclass GuiApplicationWithLogger;\n} // namespace gui\n\nnamespace resources {\nclass MeshData;\n} // namespace resources\n} // namespace renderer\n\nnamespace path {\nclass CostField;\nclass IntegrationField;\nclass FlowField;\n\nnamespace tests {\n\n/**\n * Show the functionality of the different flowfield types:\n *     - Cost field\n *     - Integration field\n *     - Flow field\n *\n * Visualizes the pathfinding results using our rendering backend.\n *\n * @param path Path to the project rootdir.\n */\nvoid path_demo_0(const util::Path &path);\n\n\n/**\n * Manages the graphical display of the pathfinding demo.\n */\nclass RenderManager0 {\npublic:\n\tenum class field_t {\n\t\tCOST,\n\t\tINTEGRATION,\n\t\tFLOW\n\t};\n\n\t/**\n\t * Create a new render manager.\n\t *\n\t * @param app GUI application.\n\t * @param window Window to render to.\n\t * @param path Path to the project rootdir.\n\t */\n\tRenderManager0(const std::shared_ptr<renderer::gui::GuiApplicationWithLogger> &app,\n\t               const std::shared_ptr<renderer::Window> &window,\n\t               const util::Path &path);\n\t~RenderManager0() = default;\n\n\t/**\n\t * Run the render loop.\n\t */\n\tvoid run();\n\n\t/**\n\t * Draw a cost field to the screen.\n\t *\n\t * @param field Cost field.\n\t */\n\tvoid show_cost_field(const std::shared_ptr<path::CostField> &field);\n\n\t/**\n\t * Draw an integration field to the screen.\n\t *\n\t * @param field Integration field.\n\t */\n\tvoid show_integration_field(const std::shared_ptr<path::IntegrationField> &field);\n\n\t/**\n\t * Draw a flow field to the screen.\n\t *\n\t * @param flow_field Flow field.\n\t * @param int_field Integration field.\n\t */\n\tvoid show_flow_field(const std::shared_ptr<path::FlowField> &flow_field,\n\t                     const std::shared_ptr<path::IntegrationField> &int_field);\n\n\t/**\n\t * Draw the steering vectors of a flow field to the screen.\n\t *\n\t * @param field Flow field.\n\t */\n\tvoid show_vectors(const std::shared_ptr<path::FlowField> &field);\n\n\t/**\n\t * Hide drawn steering vectors.\n\t */\n\tvoid hide_vectors();\n\n\t/**\n\t * Get the cell coordinates at a given screen position.\n\t *\n\t * @param x X coordinate.\n\t * @param y Y coordinate.\n\t */\n\tstd::pair<int, int> select_tile(double x, double y);\n\nprivate:\n\t/**\n\t * Load the shader sources for the demo and create the shader programs.\n\t */\n\tvoid init_shaders();\n\n\t/**\n\t * Create the following render passes for the demo:\n\t *   - Background pass: Mono-colored background object.\n\t *   - Field pass; Renders the cost, integration and flow fields.\n\t *   - Vector pass: Renders the steering vectors of a flow field.\n\t *   - Grid pass: Renders a grid on top of the fields.\n\t *   - Display pass: Draws the results of previous passes to the screen.\n\t */\n\tvoid init_passes();\n\n\n\t/**\n\t * Create a mesh for the cost field.\n\t *\n\t * @param field Cost field to visualize.\n\t * @param resolution Determines the number of subdivisions per grid cell. The number of\n\t *                   quads per cell is resolution^2. (default = 2)\n\t *\n\t * @return Mesh data for the cost field.\n\t */\n\tstatic renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr<path::CostField> &field,\n\t                                                         size_t resolution = 2);\n\n\t/**\n\t * Create a mesh for the integration field.\n\t *\n\t * @param field Integration field to visualize.\n\t * @param resolution Determines the number of subdivisions per grid cell. The number of\n\t *                   quads per cell is resolution^2. (default = 2)\n\t *\n\t * @return Mesh data for the integration field.\n\t */\n\tstatic renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr<path::IntegrationField> &field,\n\t                                                                size_t resolution = 2);\n\n\t/**\n\t * Create a mesh for the flow field.\n\t *\n\t * @param flow_field Flow field to visualize.\n\t * @param int_field Integration field.\n\t */\n\tstatic renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr<path::FlowField> &flow_field,\n\t                                                         const std::shared_ptr<path::IntegrationField> &int_field,\n\t                                                         size_t resolution = 2);\n\n\t/**\n\t * Create a mesh for an arrow.\n\t *\n\t * @return Mesh data for an arrow.\n\t */\n\tstatic renderer::resources::MeshData get_arrow_mesh();\n\n\t/**\n\t * Create a mesh for the grid.\n\t *\n\t * @param side_length Length of the grid's side.\n\t *\n\t * @return Mesh data for the grid.\n\t */\n\tstatic renderer::resources::MeshData get_grid_mesh(size_t side_length);\n\n\t/**\n\t * Path to the project rootdir.\n\t */\n\tconst util::Path &path;\n\n\t/* Renderer objects */\n\n\t/**\n\t * Qt GUI application.\n\t */\n\tstd::shared_ptr<renderer::gui::GuiApplicationWithLogger> app;\n\n\t/**\n\t * openage window to render to.\n\t */\n\tstd::shared_ptr<renderer::Window> window;\n\n\t/**\n\t * openage renderer instance.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Camera to view the scene.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/* Shader programs */\n\n\t/**\n\t * Shader program for rendering a cost field.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> cost_shader;\n\n\t/**\n\t * Shader program for rendering a integration field.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> integration_shader;\n\n\t/**\n\t * Shader program for rendering a flow field.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> flow_shader;\n\n\t/**\n\t * Shader program for rendering steering vectors.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> vector_shader;\n\n\t/**\n\t * Shader program for rendering a grid.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> grid_shader;\n\n\t/**\n\t * Shader program for rendering 2D mono-colored objects.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> obj_shader;\n\n\t/**\n\t * Shader program for rendering the final display.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n\n\t/* Render passes */\n\n\t/**\n\t * Background pass: Mono-colored background object.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> background_pass;\n\n\t/**\n\t * Field pass: Renders the cost, integration and flow fields.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> field_pass;\n\n\t/**\n\t * Vector pass: Renders the steering vectors of a flow field.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> vector_pass;\n\n\t/**\n\t * Grid pass: Renders a grid on top of the fields.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> grid_pass;\n\n\t/**\n\t * Display pass: Draws the results of previous passes to the screen.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> display_pass;\n};\n\n} // namespace tests\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/demo/demo_1.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"demo_1.h\"\n\n#include <QMouseEvent>\n\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/grid.h\"\n#include \"pathfinding/path.h\"\n#include \"pathfinding/pathfinder.h\"\n#include \"pathfinding/portal.h\"\n#include \"pathfinding/sector.h\"\n#include \"util/timer.h\"\n\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/renderable.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/window.h\"\n\n\nnamespace openage::path::tests {\n\nvoid path_demo_1(const util::Path &path) {\n\tauto grid = std::make_shared<Grid>(0, util::Vector2s{4, 3}, 10);\n\n\t// Initialize the cost field for each sector.\n\tfor (auto &sector : grid->get_sectors()) {\n\t\tauto cost_field = sector->get_cost_field();\n\n\t\t// Read the data from the preconfigured table\n\t\tstd::vector<cost_t> sector_cost = SECTORS_COST.at(sector->get_id());\n\n\t\t// Set the cost field for the sector\n\t\tcost_field->set_costs(std::move(sector_cost), time::TIME_MAX);\n\t}\n\n\t// Initialize portals between sectors\n\tutil::Vector2s grid_size = grid->get_size();\n\tportal_id_t portal_id = 0;\n\tfor (size_t y = 0; y < grid_size[1]; y++) {\n\t\tfor (size_t x = 0; x < grid_size[0]; x++) {\n\t\t\t// For each sector on the grid, we will seatch for portals to the east and south\n\t\t\t// sectors and connect them\n\n\t\t\tauto sector = grid->get_sector(x, y);\n\n\t\t\tif (x < grid_size[0] - 1) {\n\t\t\t\t// Check the sector to the east\n\t\t\t\tauto neighbor = grid->get_sector(x + 1, y);\n\t\t\t\tauto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id);\n\n\t\t\t\t// Add the portals to the sector and the neighbor\n\t\t\t\tfor (auto &portal : portals) {\n\t\t\t\t\tsector->add_portal(portal);\n\t\t\t\t\tneighbor->add_portal(portal);\n\t\t\t\t}\n\t\t\t\tportal_id += portals.size();\n\t\t\t}\n\t\t\tif (y < grid_size[1] - 1) {\n\t\t\t\t// Check the sector to the south\n\t\t\t\tauto neighbor = grid->get_sector(x, y + 1);\n\t\t\t\tauto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id);\n\n\t\t\t\t// Add the portals to the sector and the neighbor\n\t\t\t\tfor (auto &portal : portals) {\n\t\t\t\t\tsector->add_portal(portal);\n\t\t\t\t\tneighbor->add_portal(portal);\n\t\t\t\t}\n\t\t\t\tportal_id += portals.size();\n\t\t\t}\n\t\t}\n\t}\n\n\t// Connect portals that can mutually reach each other\n\t// within the same sector\n\tfor (auto sector : grid->get_sectors()) {\n\t\tsector->connect_exits();\n\n\t\tlog::log(MSG(info) << \"Sector \" << sector->get_id() << \" has \" << sector->get_portals().size() << \" portals.\");\n\t\tfor (auto portal : sector->get_portals()) {\n\t\t\tlog::log(MSG(info) << \"  Portal \" << portal->get_id() << \":\");\n\t\t\tlog::log(MSG(info) << \"    Connects sectors:  \" << sector->get_id() << \" to \"\n\t\t\t                   << portal->get_exit_sector(sector->get_id()));\n\t\t\tlog::log(MSG(info) << \"    Entry start:       \" << portal->get_entry_start(sector->get_id()));\n\t\t\tlog::log(MSG(info) << \"    Entry end:         \" << portal->get_entry_end(sector->get_id()));\n\t\t\tlog::log(MSG(info) << \"    Connected portals: \" << portal->get_connected(sector->get_id()).size());\n\t\t}\n\t}\n\n\t// Create a pathfinder for searching paths on the grid\n\tauto pathfinder = std::make_shared<path::Pathfinder>();\n\tpathfinder->add_grid(grid);\n\n\t// Add a timer to measure the pathfinding speed\n\tutil::Timer timer;\n\n\t// Create a path request from one end of the grid to the other\n\tcoord::tile start{2, 26};\n\tcoord::tile target{36, 2};\n\tPathRequest path_request{\n\t\tgrid->get_id(),\n\t\tstart,\n\t\ttarget,\n\t\ttime::TIME_ZERO,\n\t};\n\n\t// Initialize the portal nodes of the grid\n\t// This is used for the A* pathfinding search at the beginning\n\tgrid->init_portal_nodes();\n\n\ttimer.start();\n\t// Let the pathfinder search for a path\n\tPath path_result = pathfinder->get_path(path_request);\n\ttimer.stop();\n\n\tlog::log(INFO << \"Pathfinding took \" << timer.getval() / 1000 << \" us\");\n\n\t// Create a renderer to display the grid and path\n\tauto qtapp = std::make_shared<renderer::gui::GuiApplicationWithLogger>();\n\n\trenderer::window_settings settings;\n\tsettings.width = 1024;\n\tsettings.height = 768;\n\tauto window = std::make_shared<renderer::opengl::GlWindow>(\"openage pathfinding test\", settings);\n\tauto render_manager = std::make_shared<RenderManager1>(qtapp, window, path, grid);\n\tlog::log(INFO << \"Created render manager for pathfinding demo\");\n\n\t// Callbacks for mouse button events\n\t// Used to set the start and target cells for pathfinding\n\twindow->add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tif (ev.type() == QEvent::MouseButtonRelease) {\n\t\t\t// From the mouse position, calculate the position/cell on the grid\n\t\t\tauto cell_count_x = grid->get_size()[0] * grid->get_sector_size();\n\t\t\tauto cell_count_y = grid->get_size()[1] * grid->get_sector_size();\n\t\t\tauto window_size = window->get_size();\n\n\t\t\tdouble cell_size_x = static_cast<double>(window_size[0]) / cell_count_x;\n\t\t\tdouble cell_size_y = static_cast<double>(window_size[1]) / cell_count_y;\n\n\t\t\tcoord::tile_t grid_x = ev.position().x() / cell_size_x;\n\t\t\tcoord::tile_t grid_y = ev.position().y() / cell_size_y;\n\n\t\t\tif (ev.button() == Qt::RightButton) { // Set target cell\n\t\t\t\ttarget = coord::tile{grid_x, grid_y};\n\t\t\t\tPathRequest new_path_request{\n\t\t\t\t\tgrid->get_id(),\n\t\t\t\t\tstart,\n\t\t\t\t\ttarget,\n\t\t\t\t\ttime::TIME_ZERO,\n\t\t\t\t};\n\n\t\t\t\ttimer.reset();\n\t\t\t\ttimer.start();\n\t\t\t\tpath_result = pathfinder->get_path(new_path_request);\n\t\t\t\ttimer.stop();\n\n\t\t\t\tlog::log(INFO << \"Pathfinding took \" << timer.getval() / 1000 << \" us\");\n\n\t\t\t\tif (path_result.status == PathResult::FOUND) {\n\t\t\t\t\t// Create renderables for the waypoints of the path\n\t\t\t\t\trender_manager->create_waypoint_tiles(path_result);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (ev.button() == Qt::LeftButton) { // Set start cell\n\t\t\t\tstart = coord::tile{grid_x, grid_y};\n\t\t\t\tPathRequest new_path_request{\n\t\t\t\t\tgrid->get_id(),\n\t\t\t\t\tstart,\n\t\t\t\t\ttarget,\n\t\t\t\t\ttime::TIME_ZERO,\n\t\t\t\t};\n\n\t\t\t\ttimer.reset();\n\t\t\t\ttimer.start();\n\t\t\t\tpath_result = pathfinder->get_path(new_path_request);\n\t\t\t\ttimer.stop();\n\n\t\t\t\tlog::log(INFO << \"Pathfinding took \" << timer.getval() / 1000 << \" us\");\n\n\t\t\t\tif (path_result.status == PathResult::FOUND) {\n\t\t\t\t\t// Create renderables for the waypoints of the path\n\t\t\t\t\trender_manager->create_waypoint_tiles(path_result);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\t// Create renderables for the waypoints of the path\n\trender_manager->create_waypoint_tiles(path_result);\n\n\t// Run the renderer pss to draw the grid and path into a window\n\trender_manager->run();\n}\n\n\nRenderManager1::RenderManager1(const std::shared_ptr<renderer::gui::GuiApplicationWithLogger> &app,\n                               const std::shared_ptr<renderer::Window> &window,\n                               const util::Path &path,\n                               const std::shared_ptr<path::Grid> &grid) :\n\tpath{path},\n\tgrid{grid},\n\tapp{app},\n\twindow{window},\n\trenderer{window->make_renderer()} {\n\tthis->init_shaders();\n\tthis->init_passes();\n}\n\nvoid RenderManager1::run() {\n\twhile (not this->window->should_close()) {\n\t\tthis->app->process_events();\n\n\t\tthis->renderer->render(this->background_pass);\n\t\tthis->renderer->render(this->field_pass);\n\t\tthis->renderer->render(this->waypoint_pass);\n\t\tthis->renderer->render(this->grid_pass);\n\t\tthis->renderer->render(this->display_pass);\n\n\t\tthis->window->update();\n\n\t\tthis->renderer->check_error();\n\t}\n\tthis->window->close();\n}\n\nvoid RenderManager1::init_passes() {\n\tauto size = this->window->get_size();\n\n\t// Make a framebuffer for the background render pass to draw into\n\tauto background_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto fbo = renderer->create_texture_target({background_texture, depth_texture});\n\n\t// Background object for contrast between field and display\n\tEigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();\n\tauto background_unifs = obj_shader->new_uniform_input(\n\t\t\"color\",\n\t\tEigen::Vector4f{1.0, 1.0, 1.0, 1.0},\n\t\t\"model\",\n\t\tid_matrix,\n\t\t\"view\",\n\t\tid_matrix,\n\t\t\"proj\",\n\t\tid_matrix);\n\tauto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\trenderer::Renderable background_obj{\n\t\tbackground_unifs,\n\t\tbackground,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->background_pass = renderer->add_render_pass({background_obj}, fbo);\n\n\t// Make a framebuffer for the field render pass to draw into\n\tauto field_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_2 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2});\n\tthis->field_pass = renderer->add_render_pass({}, field_fbo);\n\tthis->create_impassible_tiles(this->grid);\n\tthis->create_portal_tiles(this->grid);\n\n\t// Make a framebuffer for the grid render passes to draw into\n\tauto grid_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_3 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3});\n\n\t// Make a framebuffer for the waypoint render pass to draw into\n\tauto waypoint_texture = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::rgba8));\n\tauto depth_texture_4 = renderer->add_texture(\n\t\trenderer::resources::Texture2dInfo(size[0],\n\t                                       size[1],\n\t                                       renderer::resources::pixel_format::depth24));\n\tauto waypoint_fbo = renderer->create_texture_target({waypoint_texture, depth_texture_4});\n\tthis->waypoint_pass = renderer->add_render_pass({}, waypoint_fbo);\n\n\t// Create object for the grid\n\tauto model = Eigen::Affine3f::Identity();\n\tmodel.prescale(Eigen::Vector3f{\n\t\t2.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()),\n\t\t2.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()),\n\t\t1.0f});\n\tmodel.pretranslate(Eigen::Vector3f{-1.0f, -1.0f, 0.0f});\n\tauto grid_unifs = grid_shader->new_uniform_input(\n\t\t\"model\",\n\t\tmodel.matrix(),\n\t\t\"view\",\n\t\tid_matrix,\n\t\t\"proj\",\n\t\tid_matrix);\n\tauto grid_mesh = this->get_grid_mesh(this->grid);\n\tauto grid_geometry = renderer->add_mesh_geometry(grid_mesh);\n\trenderer::Renderable grid_obj{\n\t\tgrid_unifs,\n\t\tgrid_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo);\n\n\t// Make two objects that draw the results of the previous passes onto the screen\n\t// in the display render pass\n\tauto bg_texture_unif = display_shader->new_uniform_input(\"color_texture\", background_texture);\n\tauto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\trenderer::Renderable bg_pass_obj{\n\t\tbg_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto field_texture_unif = display_shader->new_uniform_input(\"color_texture\", field_texture);\n\trenderer::Renderable field_pass_obj{\n\t\tfield_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto waypoint_texture_unif = display_shader->new_uniform_input(\"color_texture\", waypoint_texture);\n\trenderer::Renderable waypoint_pass_obj{\n\t\twaypoint_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto grid_texture_unif = display_shader->new_uniform_input(\"color_texture\", grid_texture);\n\trenderer::Renderable grid_pass_obj{\n\t\tgrid_texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tthis->display_pass = renderer->add_render_pass(\n\t\t{bg_pass_obj, field_pass_obj, waypoint_pass_obj, grid_pass_obj},\n\t\trenderer->get_display_target());\n}\n\nvoid RenderManager1::init_shaders() {\n\t// Shader sources\n\tauto shaderdir = this->path / \"assets\" / \"test\" / \"shaders\" / \"pathfinding\";\n\n\t// Shader for rendering the grid\n\tauto grid_vshader_file = shaderdir / \"demo_1_grid.vert.glsl\";\n\tauto grid_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tgrid_vshader_file);\n\n\tauto grid_fshader_file = shaderdir / \"demo_1_grid.frag.glsl\";\n\tauto grid_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tgrid_fshader_file);\n\n\t// Shader for 2D monocolored objects\n\tauto obj_vshader_file = shaderdir / \"demo_1_obj.vert.glsl\";\n\tauto obj_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tobj_vshader_file);\n\n\tauto obj_fshader_file = shaderdir / \"demo_1_obj.frag.glsl\";\n\tauto obj_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tobj_fshader_file);\n\n\t// Shader for rendering to the display target\n\tauto display_vshader_file = shaderdir / \"demo_1_display.vert.glsl\";\n\tauto display_vshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file);\n\n\tauto display_fshader_file = shaderdir / \"demo_1_display.frag.glsl\";\n\tauto display_fshader_src = renderer::resources::ShaderSource(\n\t\trenderer::resources::shader_lang_t::glsl,\n\t\trenderer::resources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file);\n\n\t// Create the shaders\n\tthis->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src});\n\tthis->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tthis->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n}\n\n\nrenderer::resources::MeshData RenderManager1::get_grid_mesh(const std::shared_ptr<path::Grid> &grid) {\n\t// increase by 1 in every dimension because to get the vertex length\n\t// of each dimension\n\tutil::Vector2s size{\n\t\tgrid->get_size()[0] * grid->get_sector_size() + 1,\n\t\tgrid->get_size()[1] * grid->get_sector_size() + 1};\n\n\t// add vertices for the cells of the grid\n\tstd::vector<float> verts{};\n\tauto vert_count = size[0] * size[1];\n\tverts.reserve(vert_count * 2);\n\tfor (int i = 0; i < (int)size[0]; ++i) {\n\t\tfor (int j = 0; j < (int)size[1]; ++j) {\n\t\t\tverts.push_back(i);\n\t\t\tverts.push_back(j);\n\t\t}\n\t}\n\n\t// split the grid into lines using an index array\n\tstd::vector<uint16_t> idxs;\n\tidxs.reserve((size[0] - 1) * (size[1] - 1) * 8);\n\t// iterate over all tiles in the grid by columns, i.e. starting\n\t// from the left corner to the bottom corner if you imagine it from\n\t// the camera's point of view\n\tfor (size_t i = 0; i < size[0] - 1; ++i) {\n\t\tfor (size_t j = 0; j < size[1] - 1; ++j) {\n\t\t\t// since we are working on tiles, we just draw a square using the 4 vertices\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + 1 + i * size[1]);           // bottom right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + 1 + i * size[1]); // top right\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + size[1] + i * size[1]);     // top left\n\t\t\tidxs.push_back(j + i * size[1]);               // bottom left\n\t\t}\n\t}\n\n\trenderer::resources::VertexInputInfo info{\n\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::LINES,\n\t\trenderer::resources::index_t::U16};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\treturn {std::move(vert_data), std::move(idx_data), info};\n}\n\nvoid RenderManager1::create_impassible_tiles(const std::shared_ptr<path::Grid> &grid) {\n\tauto width = grid->get_size()[0];\n\tauto height = grid->get_size()[1];\n\tauto sector_size = grid->get_sector_size();\n\n\tfloat tile_offset_width = 2.0f / (width * sector_size);\n\tfloat tile_offset_height = 2.0f / (height * sector_size);\n\n\tfor (size_t sector_x = 0; sector_x < width; sector_x++) {\n\t\tfor (size_t sector_y = 0; sector_y < height; sector_y++) {\n\t\t\tauto sector = grid->get_sector(sector_x, sector_y);\n\t\t\tauto cost_field = sector->get_cost_field();\n\t\t\tfor (size_t y = 0; y < sector_size; y++) {\n\t\t\t\tfor (size_t x = 0; x < sector_size; x++) {\n\t\t\t\t\tauto cost = cost_field->get_cost(x, y);\n\t\t\t\t\tif (cost == COST_IMPASSABLE) {\n\t\t\t\t\t\tstd::array<float, 8> tile_data{\n\t\t\t\t\t\t\t-1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t\t1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t\t-1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t\t1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t\t-1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t\t1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t\t-1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t\t1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t};\n\t\t\t\t\t\trenderer::resources::VertexInputInfo info{\n\t\t\t\t\t\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\t\t\t\t\t\trenderer::resources::vertex_layout_t::AOS,\n\t\t\t\t\t\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\t\t\t\t\t\tauto const data_size = tile_data.size() * sizeof(float);\n\t\t\t\t\t\tstd::vector<uint8_t> verts(data_size);\n\t\t\t\t\t\tstd::memcpy(verts.data(), tile_data.data(), data_size);\n\n\t\t\t\t\t\tauto tile_mesh = renderer::resources::MeshData(std::move(verts), info);\n\t\t\t\t\t\tauto tile_geometry = renderer->add_mesh_geometry(tile_mesh);\n\n\t\t\t\t\t\tEigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();\n\t\t\t\t\t\tauto tile_unifs = obj_shader->new_uniform_input(\n\t\t\t\t\t\t\t\"color\",\n\t\t\t\t\t\t\tEigen::Vector4f{0.0, 0.0, 0.0, 1.0},\n\t\t\t\t\t\t\t\"model\",\n\t\t\t\t\t\t\tid_matrix,\n\t\t\t\t\t\t\t\"view\",\n\t\t\t\t\t\t\tid_matrix,\n\t\t\t\t\t\t\t\"proj\",\n\t\t\t\t\t\t\tid_matrix);\n\t\t\t\t\t\tauto tile_obj = renderer::Renderable{\n\t\t\t\t\t\t\ttile_unifs,\n\t\t\t\t\t\t\ttile_geometry,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tthis->field_pass->add_renderables({tile_obj});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid RenderManager1::create_portal_tiles(const std::shared_ptr<path::Grid> &grid) {\n\tauto width = grid->get_size()[0];\n\tauto height = grid->get_size()[1];\n\tauto sector_size = grid->get_sector_size();\n\n\tfloat tile_offset_width = 2.0f / (width * sector_size);\n\tfloat tile_offset_height = 2.0f / (height * sector_size);\n\n\tfor (size_t sector_x = 0; sector_x < width; sector_x++) {\n\t\tfor (size_t sector_y = 0; sector_y < height; sector_y++) {\n\t\t\tauto sector = grid->get_sector(sector_x, sector_y);\n\n\t\t\tfor (auto &portal : sector->get_portals()) {\n\t\t\t\tauto start = portal->get_entry_start(sector->get_id());\n\t\t\t\tauto end = portal->get_entry_end(sector->get_id());\n\t\t\t\tauto direction = portal->get_direction();\n\n\t\t\t\tstd::vector<coord::tile> tiles;\n\t\t\t\tif (direction == PortalDirection::NORTH_SOUTH) {\n\t\t\t\t\tauto y = start.se;\n\t\t\t\t\tfor (auto x = start.ne; x <= end.ne; ++x) {\n\t\t\t\t\t\ttiles.push_back(coord::tile(x, y));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tauto x = start.ne;\n\t\t\t\t\tfor (auto y = start.se; y <= end.se; ++y) {\n\t\t\t\t\t\ttiles.push_back(coord::tile(x, y));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (auto tile : tiles) {\n\t\t\t\t\tstd::array<float, 8> tile_data{\n\t\t\t\t\t\t-1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t-1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t-1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t\t-1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width,\n\t\t\t\t\t\t1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height,\n\t\t\t\t\t};\n\t\t\t\t\trenderer::resources::VertexInputInfo info{\n\t\t\t\t\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\t\t\t\t\trenderer::resources::vertex_layout_t::AOS,\n\t\t\t\t\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\t\t\t\t\tauto const data_size = tile_data.size() * sizeof(float);\n\t\t\t\t\tstd::vector<uint8_t> verts(data_size);\n\t\t\t\t\tstd::memcpy(verts.data(), tile_data.data(), data_size);\n\n\t\t\t\t\tauto tile_mesh = renderer::resources::MeshData(std::move(verts), info);\n\t\t\t\t\tauto tile_geometry = renderer->add_mesh_geometry(tile_mesh);\n\n\t\t\t\t\tEigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();\n\t\t\t\t\tauto tile_unifs = obj_shader->new_uniform_input(\n\t\t\t\t\t\t\"color\",\n\t\t\t\t\t\tEigen::Vector4f{0.0, 0.0, 0.0, 0.3},\n\t\t\t\t\t\t\"model\",\n\t\t\t\t\t\tid_matrix,\n\t\t\t\t\t\t\"view\",\n\t\t\t\t\t\tid_matrix,\n\t\t\t\t\t\t\"proj\",\n\t\t\t\t\t\tid_matrix);\n\t\t\t\t\tauto tile_obj = renderer::Renderable{\n\t\t\t\t\t\ttile_unifs,\n\t\t\t\t\t\ttile_geometry,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t};\n\t\t\t\t\tthis->field_pass->add_renderables({tile_obj});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nvoid RenderManager1::create_waypoint_tiles(const Path &path) {\n\tauto width = grid->get_size()[0];\n\tauto height = grid->get_size()[1];\n\tauto sector_size = grid->get_sector_size();\n\n\tfloat tile_offset_width = 2.0f / (width * sector_size);\n\tfloat tile_offset_height = 2.0f / (height * sector_size);\n\n\tEigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity();\n\n\tthis->waypoint_pass->clear_renderables();\n\n\t// Draw in-between waypoints\n\tfor (size_t i = 1; i < path.waypoints.size() - 1; i++) {\n\t\tauto tile = path.waypoints[i];\n\t\tstd::array<float, 8> tile_data{\n\t\t\t-1.0f + tile.ne * tile_offset_width,\n\t\t\t1.0f - tile.se * tile_offset_height,\n\t\t\t-1.0f + tile.ne * tile_offset_width,\n\t\t\t1.0f - (tile.se + 1) * tile_offset_height,\n\t\t\t-1.0f + (tile.ne + 1) * tile_offset_width,\n\t\t\t1.0f - tile.se * tile_offset_height,\n\t\t\t-1.0f + (tile.ne + 1) * tile_offset_width,\n\t\t\t1.0f - (tile.se + 1) * tile_offset_height,\n\t\t};\n\n\t\trenderer::resources::VertexInputInfo info{\n\t\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\t\trenderer::resources::vertex_layout_t::AOS,\n\t\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\t\tauto const data_size = tile_data.size() * sizeof(float);\n\t\tstd::vector<uint8_t> verts(data_size);\n\t\tstd::memcpy(verts.data(), tile_data.data(), data_size);\n\n\t\tauto tile_mesh = renderer::resources::MeshData(std::move(verts), info);\n\t\tauto tile_geometry = renderer->add_mesh_geometry(tile_mesh);\n\n\t\tauto tile_unifs = obj_shader->new_uniform_input(\n\t\t\t\"color\",\n\t\t\tEigen::Vector4f{0.0, 0.25, 1.0, 1.0},\n\t\t\t\"model\",\n\t\t\tid_matrix,\n\t\t\t\"view\",\n\t\t\tid_matrix,\n\t\t\t\"proj\",\n\t\t\tid_matrix);\n\t\tauto tile_obj = renderer::Renderable{\n\t\t\ttile_unifs,\n\t\t\ttile_geometry,\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t};\n\t\tthis->waypoint_pass->add_renderables({tile_obj});\n\t}\n\n\t// Draw start and end waypoints with different colors\n\tauto start_tile = path.waypoints.front();\n\tstd::array<float, 8> start_tile_data{\n\t\t-1.0f + start_tile.ne * tile_offset_width,\n\t\t1.0f - start_tile.se * tile_offset_height,\n\t\t-1.0f + start_tile.ne * tile_offset_width,\n\t\t1.0f - (start_tile.se + 1) * tile_offset_height,\n\t\t-1.0f + (start_tile.ne + 1) * tile_offset_width,\n\t\t1.0f - start_tile.se * tile_offset_height,\n\t\t-1.0f + (start_tile.ne + 1) * tile_offset_width,\n\t\t1.0f - (start_tile.se + 1) * tile_offset_height,\n\t};\n\n\trenderer::resources::VertexInputInfo start_info{\n\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\tauto const start_data_size = start_tile_data.size() * sizeof(float);\n\tstd::vector<uint8_t> start_verts(start_data_size);\n\tstd::memcpy(start_verts.data(), start_tile_data.data(), start_data_size);\n\n\tauto start_tile_mesh = renderer::resources::MeshData(std::move(start_verts), start_info);\n\tauto start_tile_geometry = renderer->add_mesh_geometry(start_tile_mesh);\n\tauto start_tile_unifs = obj_shader->new_uniform_input(\n\t\t\"color\",\n\t\tEigen::Vector4f{0.0, 0.5, 0.0, 1.0},\n\t\t\"model\",\n\t\tid_matrix,\n\t\t\"view\",\n\t\tid_matrix,\n\t\t\"proj\",\n\t\tid_matrix);\n\tauto start_tile_obj = renderer::Renderable{\n\t\tstart_tile_unifs,\n\t\tstart_tile_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tauto end_tile = path.waypoints.back();\n\tstd::array<float, 8> end_tile_data{\n\t\t-1.0f + end_tile.ne * tile_offset_width,\n\t\t1.0f - end_tile.se * tile_offset_height,\n\t\t-1.0f + end_tile.ne * tile_offset_width,\n\t\t1.0f - (end_tile.se + 1) * tile_offset_height,\n\t\t-1.0f + (end_tile.ne + 1) * tile_offset_width,\n\t\t1.0f - end_tile.se * tile_offset_height,\n\t\t-1.0f + (end_tile.ne + 1) * tile_offset_width,\n\t\t1.0f - (end_tile.se + 1) * tile_offset_height,\n\t};\n\n\trenderer::resources::VertexInputInfo end_info{\n\t\t{renderer::resources::vertex_input_t::V2F32},\n\t\trenderer::resources::vertex_layout_t::AOS,\n\t\trenderer::resources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\tauto const end_data_size = end_tile_data.size() * sizeof(float);\n\tstd::vector<uint8_t> end_verts(end_data_size);\n\tstd::memcpy(end_verts.data(), end_tile_data.data(), end_data_size);\n\n\tauto end_tile_mesh = renderer::resources::MeshData(std::move(end_verts), end_info);\n\tauto end_tile_geometry = renderer->add_mesh_geometry(end_tile_mesh);\n\tauto end_tile_unifs = obj_shader->new_uniform_input(\n\t\t\"color\",\n\t\tEigen::Vector4f{1.0, 0.5, 0.0, 1.0},\n\t\t\"model\",\n\t\tid_matrix,\n\t\t\"view\",\n\t\tid_matrix,\n\t\t\"proj\",\n\t\tid_matrix);\n\n\tauto end_tile_obj = renderer::Renderable{\n\t\tend_tile_unifs,\n\t\tend_tile_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tthis->waypoint_pass->add_renderables({start_tile_obj, end_tile_obj});\n}\n\n} // namespace openage::path::tests\n"
  },
  {
    "path": "libopenage/pathfinding/demo/demo_1.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"pathfinding/definitions.h\"\n#include \"pathfinding/path.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"util/path.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass Renderer;\nclass RenderPass;\nclass ShaderProgram;\nclass Window;\n\nnamespace gui {\nclass GuiApplicationWithLogger;\n} // namespace gui\n\n} // namespace renderer\n\nnamespace path {\nclass Grid;\n\nnamespace tests {\n\n\n/**\n * Show the functionality of the high-level pathfinder:\n *     - Grids\n *     - Sectors\n *     - Portals\n *\n * Visualizes the pathfinding results using our rendering backend.\n *\n * @param path Path to the project rootdir.\n */\nvoid path_demo_1(const util::Path &path);\n\n\n/**\n * Manages the graphical display of the pathfinding demo.\n */\nclass RenderManager1 {\npublic:\n\t/**\n\t * Create a new render manager.\n\t *\n\t * @param app GUI application.\n\t * @param window Window to render to.\n\t * @param path Path to the project rootdir.\n\t */\n\tRenderManager1(const std::shared_ptr<renderer::gui::GuiApplicationWithLogger> &app,\n\t               const std::shared_ptr<renderer::Window> &window,\n\t               const util::Path &path,\n\t               const std::shared_ptr<path::Grid> &grid);\n\t~RenderManager1() = default;\n\n\t/**\n\t * Run the render loop.\n\t */\n\tvoid run();\n\n\t/**\n\t * Create renderables for the waypoint tiles of a path.\n\t *\n\t * @param path Path object.\n\t */\n\tvoid create_waypoint_tiles(const Path &path);\n\nprivate:\n\t/**\n\t * Load the shader sources for the demo and create the shader programs.\n\t */\n\tvoid init_shaders();\n\n\t/**\n\t * Create the following render passes for the demo:\n\t *   - Background pass: Mono-colored background object.\n\t *   - Field pass; Renders the cost, integration and flow fields.\n\t *   - Grid pass: Renders a grid on top of the cost field.\n\t *   - Display pass: Draws the results of previous passes to the screen.\n\t */\n\tvoid init_passes();\n\n\t/**\n\t * Create a mesh for the grid.\n\t *\n\t * @param grid Pathing grid.\n\t *\n\t * @return Mesh data for the grid.\n\t */\n\tstatic renderer::resources::MeshData get_grid_mesh(const std::shared_ptr<path::Grid> &grid);\n\n\t/**\n\t * Create renderables for the impassible tiles in the grid.\n\t *\n\t * @param grid Pathing grid.\n\t */\n\tvoid create_impassible_tiles(const std::shared_ptr<path::Grid> &grid);\n\n\t/**\n\t * Create renderables for the portal tiles in the grid.\n\t *\n\t * @param grid Pathing grid.\n\t */\n\tvoid create_portal_tiles(const std::shared_ptr<path::Grid> &grid);\n\n\t/**\n\t * Path to the project rootdir.\n\t */\n\tconst util::Path &path;\n\n\t/**\n\t * Pathing grid of the demo.\n\t */\n\tstd::shared_ptr<path::Grid> grid;\n\n\t/* Renderer objects */\n\n\t/**\n\t * Qt GUI application.\n\t */\n\tstd::shared_ptr<renderer::gui::GuiApplicationWithLogger> app;\n\n\t/**\n\t * openage window to render to.\n\t */\n\tstd::shared_ptr<renderer::Window> window;\n\n\t/**\n\t * openage renderer instance.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/* Shader programs */\n\n\t/**\n\t * Shader program for rendering a grid.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> grid_shader;\n\n\t/**\n\t * Shader program for rendering 2D mono-colored objects.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> obj_shader;\n\n\t/**\n\t * Shader program for rendering the final display.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n\n\t/* Render passes */\n\n\t/**\n\t * Background pass: Mono-colored background object.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> background_pass;\n\n\t/**\n\t * Field pass: Renders the cost field.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> field_pass;\n\n\t/**\n\t * Grid pass: Renders a grid on top of the cost field.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> grid_pass;\n\n\t/**\n\t * Waypoint pass: Renders the path and its waypoints.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> waypoint_pass;\n\n\t/**\n\t * Display pass: Draws the results of previous passes to the screen.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> display_pass;\n};\n\n// Cost for the sectors in the grid\n// taken from Figure 23.1 in \"Crowd Pathfinding and Steering Using Flow Field Tiles\"\nconst std::vector<std::vector<cost_t>> SECTORS_COST = {\n\t{\n\t\t// clang-format off\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1, 255, 255, 255, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255, 255, 255, 255,   1,   1,\n          1,   1,   1, 255, 255, 255, 255, 255,   1,   1,\n          1,   1, 255, 255, 255, 255, 255,   1,   1,   1,\n          1,   1, 255,   1,   1,   1,   1,   1,   1,   1,\n          1,   1, 255,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1, 255, 255, 255, 255, 255, 255, 255, 255,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1, 255,   1,   1, 255,   1,   1,   1,\n          1,   1,   1, 255,   1, 255, 255,   1,   1,   1,\n          1,   1,   1, 255, 255, 255, 255, 255, 255, 255,\n          1,   1,   1,   1, 255, 255, 255,   1,   1,   1,\n          1,   1,   1,   1, 255, 255,   1,   1,   1,   1,\n          1,   1,   1, 255,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n        255, 255,   1,   1,   1,   1,   1, 255, 255, 255,\n          1,   1,   1,   1,   1,   1,   1,   1, 255, 255,\n          1,   1,   1,   1,   1,   1,   1,   1, 255, 255,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255, 255, 255,   1,   1,   1,\n          1,   1,   1,   1,   1, 255, 255, 255, 255, 255,\n          1,   1,   1,   1, 255, 255, 255, 255, 255,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1, 255,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n        255, 255,   1,   1,   1,   1,   1,   1,   1,   1,\n        255, 255,   1,   1,   1,   1,   1,   1,   1,   1,\n        255, 255,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1, 255,   1,   1,   1,   1,   1,   1,\n          1,   1, 255, 255,   1,   1,   1,   1,   1,   1,\n        255, 255, 255, 255,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1, 255,   1,   1,   1,   1,   1,   1,   1,\n          1,   1, 255,   1,   1,   1,   1,   1, 255,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1, 255, 255,   1,   1,   1,   1,   1,\n          1,   1, 255,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255, 255, 255, 255, 255, 255,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1, 255, 255, 255,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1,   1, 255,   1,   1,   1,   1,   1, 255,\n          1,   1,   1, 255,   1, 255, 255, 255, 255, 255,\n          1,   1,   1, 255,   1,   1,   1,   1, 255, 255,\n          1,   1,   1, 255,   1,   1,   1,   1, 255, 255,\n          1, 255, 255,   1,   1,   1,   1,   1, 255, 255,\n          1,   1, 255,   1,   1, 255,   1,   1,   1,   1,\n          1,   1, 255,   1,   1, 255,   1,   1,   1,   1,\n        255, 255, 255, 255, 255, 255,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n        255,   1,   1,   1,   1,   1,   1,   1, 255, 255,\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n        255,   1,   1,   1,   1,   1, 255,   1,   1,   1,\n        255,   1,   1,   1,   1,   1, 255,   1,   1,   1,\n        255,   1,   1,   1,   1,   1, 255,   1,   1,   1,\n        255,   1,   1,   1,   1,   1, 255, 255, 255, 255,\n        255,   1,   1,   1, 255, 255, 255, 255, 255,   1,\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n        255, 255,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1, 255, 255, 255, 255, 255,   1,   1,\n          1,   1,   1,   1,   1,   1,   1, 255,   1,   1,\n          1,   1,   1,   1,   1,   1,   1, 255,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1, 255,   1,   1,   1,   1, 255, 255, 255,\n          1,   1, 255,   1,   1,   1, 255, 255,   1,   1,\n          1,   1,   1,   1,   1, 255, 255, 255,   1,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1, 255,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1, 255,\n          1,   1,   1,   1,   1,   1,   1,   1, 255,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1, 255,   1,   1,   1,   1,   1,   1,\n          1,   1, 255, 255, 255, 255, 255, 255,   1,   1,\n          1, 255, 255, 255,   1,   1,   1,   1,   1,   1,\n        255, 255, 255, 255,   1,   1,   1,   1,   1,   1,\n        255,   1,   1,   1,   1,   1,   1,   1,   1, 255,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n        255,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1, 255, 255, 255, 255, 255,   1,   1,\n          1,   1, 255, 255,   1,   1,   1, 255,   1,   1,\n          1, 255, 255,   1,   1,   1,   1,   1,   1,   1,\n        255, 255,   1,   1,   1,   1,   1,   1,   1,   1,\n        255, 255,   1,   1,   1, 255, 255, 255,   1,   1,\n          1,   1,   1,   1,   1,   1, 255,   1,   1,   1,\n          1,   1,   1, 255, 255, 255, 255,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t},\n\t{\n\t\t// clang-format off\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n          1,   1,   1,   1,   1,   1,   1,   1,   1,   1,\n\t\t// clang-format on\n\t}};\n\n} // namespace tests\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/demo/tests.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"tests.h\"\n\n#include \"log/log.h\"\n\n#include \"pathfinding/demo/demo_0.h\"\n#include \"pathfinding/demo/demo_1.h\"\n\n\nnamespace openage::path::tests {\n\nvoid path_demo(int demo_id, const util::Path &path) {\n\tswitch (demo_id) {\n\tcase 0:\n\t\tpath_demo_0(path);\n\t\tbreak;\n\n\tcase 1:\n\t\tpath_demo_1(path);\n\t\tbreak;\n\n\tdefault:\n\t\tlog::log(MSG(err) << \"Unknown pathfinding demo requested: \" << demo_id << \".\");\n\t\tbreak;\n\t}\n}\n\n} // namespace openage::path::tests\n"
  },
  {
    "path": "libopenage/pathfinding/demo/tests.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../../util/compiler.h\"\n// pxd: from libopenage.util.path cimport Path\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n} // namespace util\n\nnamespace path::tests {\n\n// pxd: void path_demo(int demo_id, Path path) except +\nOAAPI void path_demo(int demo_id, const util::Path &path);\n\n} // namespace path::tests\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/field_cache.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"field_cache.h\"\n\nnamespace openage::path {\n\nvoid FieldCache::add(const cache_key_t cache_key,\n                     const field_cache_t cache_entry) {\n\tthis->cache[cache_key] = cache_entry;\n}\n\nbool FieldCache::evict(const cache_key_t cache_key) {\n\treturn this->cache.erase(cache_key) != 0;\n}\n\nbool FieldCache::is_cached(const cache_key_t cache_key) const {\n\treturn this->cache.contains(cache_key);\n}\n\nstd::shared_ptr<IntegrationField> FieldCache::get_integration_field(const cache_key_t cache_key) const {\n\treturn this->cache.at(cache_key).first;\n}\n\nstd::shared_ptr<FlowField> FieldCache::get_flow_field(const cache_key_t cache_key) const {\n\treturn this->cache.at(cache_key).second;\n}\n\nfield_cache_t FieldCache::get(const cache_key_t cache_key) const {\n\treturn this->cache.at(cache_key);\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/field_cache.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <unordered_map>\n\n#include \"pathfinding/types.h\"\n#include \"util/hash.h\"\n\n\nnamespace openage {\nnamespace coord {\nstruct tile_delta;\n} // namespace coord\n\nnamespace path {\nclass IntegrationField;\nclass FlowField;\n\n/**\n * Cache to store already calculated flow and integration fields for the pathfinding algorithm.\n */\nclass FieldCache {\npublic:\n\tFieldCache() = default;\n\t~FieldCache() = default;\n\n\t/**\n\t * Adds a new field entry to the cache.\n\t *\n\t * @param cache_key Cache key for the field entry.\n\t * @param cache_entry Field entry (integration field, flow field).\n\t */\n\tvoid add(const cache_key_t cache_key,\n\t         const field_cache_t cache_entry);\n\n\t/**\n\t * Evict field entry from the cache.\n\t *\n\t * @param cache_key Cache key for the field entry to evict.\n\t *\n\t * @return true if the cache key was found and evicted, false otherwise.\n\t */\n\tbool evict(const cache_key_t cache_key);\n\n\t/**\n\t * Check if there is a cached entry for a specific cache key.\n\t *\n\t * @param cache_key Cache key to check.\n\t *\n\t * @return true if a field entry is found for the cache key, false otherwise.\n\t */\n\tbool is_cached(const cache_key_t cache_key) const;\n\n\t/**\n\t * Get a cached integration field.\n\t *\n\t * @param cache_key Cache key for the field entry.\n\t *\n\t * @return Integration field.\n\t */\n\tstd::shared_ptr<IntegrationField> get_integration_field(const cache_key_t cache_key) const;\n\n\t/**\n\t * Get a cached flow field.\n\t *\n\t * @param cache_key Cache key for the field entry.\n\t *\n\t * @return Flow field.\n\t */\n\tstd::shared_ptr<FlowField> get_flow_field(const cache_key_t cache_key) const;\n\n\t/**\n\t * Get a cached field entry.\n\t *\n\t * Contains both integration field and flow field.\n\t *\n\t * @param cache_key Cache key for the field entry.\n\t *\n\t * @return Field entry (integration field, flow field).\n\t */\n\tfield_cache_t get(const cache_key_t cache_key) const;\n\nprivate:\n\t/**\n\t * Hash function for the field cache.\n\t */\n\tstruct pair_hash {\n\t\ttemplate <class T1, class T2>\n\t\tstd::size_t operator()(const std::pair<T1, T2> &pair) const {\n\t\t\treturn util::hash_combine(std::hash<T1>{}(pair.first), std::hash<T2>{}(pair.second));\n\t\t}\n\t};\n\n\t/**\n\t * Cache for already computed fields.\n\t *\n\t * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are\n\t * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated\n\t * when the field is reused.\n\t */\n\tstd::unordered_map<cache_key_t,\n\t                   field_cache_t,\n\t                   pair_hash>\n\t\tcache;\n};\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/flow_field.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"flow_field.h\"\n\n#include <bitset>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"coord/tile.h\"\n#include \"pathfinding/integration_field.h\"\n#include \"pathfinding/portal.h\"\n\n\nnamespace openage::path {\n\nFlowField::FlowField(size_t size) :\n\tsize{size},\n\tcells(this->size * this->size, FLOW_INIT) {\n\tlog::log(DBG << \"Created flow field with size \" << this->size << \"x\" << this->size);\n}\n\nFlowField::FlowField(const std::shared_ptr<IntegrationField> &integration_field) :\n\tsize{integration_field->get_size()},\n\tcells(this->size * this->size, FLOW_INIT) {\n\tthis->build(integration_field);\n}\n\nsize_t FlowField::get_size() const {\n\treturn this->size;\n}\n\nflow_t FlowField::get_cell(const coord::tile_delta &pos) const {\n\treturn this->cells.at(pos.ne + pos.se * this->size);\n}\n\nflow_t FlowField::get_cell(size_t x, size_t y) const {\n\treturn this->cells.at(x + y * this->size);\n}\n\nflow_t FlowField::get_cell(size_t idx) const {\n\treturn this->cells.at(idx);\n}\n\nflow_dir_t FlowField::get_dir(const coord::tile_delta &pos) const {\n\treturn static_cast<flow_dir_t>(this->get_cell(pos) & FLOW_DIR_MASK);\n}\n\nflow_dir_t FlowField::get_dir(size_t x, size_t y) const {\n\treturn static_cast<flow_dir_t>(this->get_cell(x, y) & FLOW_DIR_MASK);\n}\n\nflow_dir_t FlowField::get_dir(size_t idx) const {\n\treturn static_cast<flow_dir_t>(this->get_cell(idx) & FLOW_DIR_MASK);\n}\n\nvoid FlowField::build(const std::shared_ptr<IntegrationField> &integration_field) {\n\tENSURE(integration_field->get_size() == this->get_size(),\n\t       \"integration field size \"\n\t           << integration_field->get_size() << \"x\" << integration_field->get_size()\n\t           << \" does not match flow field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\tauto &integrate_cells = integration_field->get_cells();\n\tauto &flow_cells = this->cells;\n\n\tfor (size_t y = 0; y < this->size; ++y) {\n\t\tfor (size_t x = 0; x < this->size; ++x) {\n\t\t\tsize_t idx = y * this->size + x;\n\n\t\t\tconst auto &integrate_cell = integrate_cells[idx];\n\t\t\tauto &flow_cell = flow_cells[idx];\n\n\t\t\tif (integrate_cell.cost == INTEGRATED_COST_UNREACHABLE) {\n\t\t\t\t// Cell cannot be used as path\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tflow_t transfer_flags = integrate_cell.flags & FLOW_FLAGS_MASK;\n\t\t\tflow_cell |= transfer_flags;\n\n\t\t\tif (flow_cell & FLOW_TARGET_MASK) {\n\t\t\t\t// target cells are pathable\n\t\t\t\tflow_cell |= FLOW_PATHABLE_MASK;\n\n\t\t\t\t// they also have a preset flow direction so we can skip here\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Store which of the non-diagonal directions are unreachable.\n\t\t\t// north == 0x01, east == 0x02, south == 0x04, west == 0x08\n\t\t\tuint8_t directions_unreachable = 0x00;\n\n\t\t\t// Find the neighbor with the smallest cost.\n\t\t\tflow_dir_t direction = static_cast<flow_dir_t>(flow_cell & FLOW_DIR_MASK);\n\t\t\tauto smallest_cost = INTEGRATED_COST_UNREACHABLE;\n\n\t\t\t// Cardinal directions\n\t\t\tif (y > 0) {\n\t\t\t\tauto cost = integrate_cells[idx - this->size].cost;\n\t\t\t\tif (cost == INTEGRATED_COST_UNREACHABLE) {\n\t\t\t\t\tdirections_unreachable |= 0x01;\n\t\t\t\t}\n\t\t\t\telse if (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::NORTH;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (x < this->size - 1) {\n\t\t\t\tauto cost = integrate_cells[idx + 1].cost;\n\t\t\t\tif (cost == INTEGRATED_COST_UNREACHABLE) {\n\t\t\t\t\tdirections_unreachable |= 0x02;\n\t\t\t\t}\n\t\t\t\telse if (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::EAST;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (y < this->size - 1) {\n\t\t\t\tauto cost = integrate_cells[idx + this->size].cost;\n\t\t\t\tif (cost == INTEGRATED_COST_UNREACHABLE) {\n\t\t\t\t\tdirections_unreachable |= 0x04;\n\t\t\t\t}\n\t\t\t\telse if (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::SOUTH;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (x > 0) {\n\t\t\t\tauto cost = integrate_cells[idx - 1].cost;\n\t\t\t\tif (cost == INTEGRATED_COST_UNREACHABLE) {\n\t\t\t\t\tdirections_unreachable |= 0x08;\n\t\t\t\t}\n\t\t\t\telse if (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::WEST;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Diagonal directions\n\t\t\tif (x < this->size - 1 and y > 0\n\t\t\t    and not(directions_unreachable & 0x01 and directions_unreachable & 0x02)) {\n\t\t\t\tauto cost = integrate_cells[idx - this->size + 1].cost;\n\t\t\t\tif (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::NORTH_EAST;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (x < this->size - 1 and y < this->size - 1\n\t\t\t    and not(directions_unreachable & 0x02 and directions_unreachable & 0x04)) {\n\t\t\t\tauto cost = integrate_cells[idx + this->size + 1].cost;\n\t\t\t\tif (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::SOUTH_EAST;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (x > 0 and y < this->size - 1\n\t\t\t    and not(directions_unreachable & 0x04 and directions_unreachable & 0x08)) {\n\t\t\t\tauto cost = integrate_cells[idx + this->size - 1].cost;\n\t\t\t\tif (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::SOUTH_WEST;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (x > 0 and y > 0\n\t\t\t    and not(directions_unreachable & 0x01 and directions_unreachable & 0x08)) {\n\t\t\t\tauto cost = integrate_cells[idx - this->size - 1].cost;\n\t\t\t\tif (cost < smallest_cost) {\n\t\t\t\t\tsmallest_cost = cost;\n\t\t\t\t\tdirection = flow_dir_t::NORTH_WEST;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the flow field cell to pathable.\n\t\t\tflow_cell |= FLOW_PATHABLE_MASK;\n\n\t\t\t// Set the flow field cell to the direction of the smallest cost.\n\t\t\tflow_cell |= static_cast<uint8_t>(direction);\n\t\t}\n\t}\n}\n\nvoid FlowField::build(const std::shared_ptr<IntegrationField> &integration_field,\n                      const std::shared_ptr<IntegrationField> & /* other */,\n                      sector_id_t other_sector_id,\n                      const std::shared_ptr<Portal> &portal) {\n\tENSURE(integration_field->get_size() == this->get_size(),\n\t       \"integration field size \"\n\t           << integration_field->get_size() << \"x\" << integration_field->get_size()\n\t           << \" does not match flow field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\tauto &flow_cells = this->cells;\n\tauto direction = portal->get_direction();\n\n\t// portal entry and exit cell coordinates\n\tauto entry_start = portal->get_entry_start(other_sector_id);\n\tauto exit_start = portal->get_exit_start(other_sector_id);\n\tauto exit_end = portal->get_exit_end(other_sector_id);\n\n\t// TODO: Compare integration values from other side of portal\n\t// auto &integrate_cells = integration_field->get_cells();\n\n\t// set the direction for the flow field cells that are part of the portal\n\tif (direction == PortalDirection::NORTH_SOUTH) {\n\t\tbool other_is_north = entry_start.se > exit_start.se;\n\t\tif (other_is_north) {\n\t\t\tauto y = exit_start.se;\n\t\t\tfor (auto x = exit_start.ne; x <= exit_end.ne; ++x) {\n\t\t\t\tauto idx = y * this->size + x;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | static_cast<uint8_t>(flow_dir_t::NORTH);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tauto y = exit_start.se;\n\t\t\tfor (auto x = exit_start.ne; x <= exit_end.ne; ++x) {\n\t\t\t\tauto idx = y * this->size + x;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | static_cast<uint8_t>(flow_dir_t::SOUTH);\n\t\t\t}\n\t\t}\n\t}\n\telse if (direction == PortalDirection::EAST_WEST) {\n\t\tbool other_is_east = entry_start.ne < exit_start.ne;\n\t\tif (other_is_east) {\n\t\t\tauto x = exit_start.ne;\n\t\t\tfor (auto y = exit_start.se; y <= exit_end.se; ++y) {\n\t\t\t\tauto idx = y * this->size + x;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | static_cast<uint8_t>(flow_dir_t::EAST);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tauto x = exit_start.ne;\n\t\t\tfor (auto y = exit_start.se; y <= exit_end.se; ++y) {\n\t\t\t\tauto idx = y * this->size + x;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK;\n\t\t\t\tflow_cells[idx] = flow_cells[idx] | static_cast<uint8_t>(flow_dir_t::WEST);\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\tthrow Error(ERR << \"Invalid portal direction: \" << static_cast<int>(direction));\n\t}\n\n\tthis->build(integration_field);\n}\n\nconst std::vector<flow_t> &FlowField::get_cells() const {\n\treturn this->cells;\n}\n\nvoid FlowField::reset() {\n\tstd::fill(this->cells.begin(), this->cells.end(), FLOW_INIT);\n\n\tlog::log(DBG << \"Flow field has been reset\");\n}\n\nvoid FlowField::reset_dynamic_flags() {\n\tflow_t mask = 0xFF & ~(FLOW_LOS_MASK);\n\tfor (flow_t &cell : this->cells) {\n\t\tcell = cell & mask;\n\t}\n\n\tlog::log(DBG << \"Flow field dynamic flags have been reset\");\n}\n\nvoid FlowField::transfer_dynamic_flags(const std::shared_ptr<IntegrationField> &integration_field) {\n\tauto &integrate_cells = integration_field->get_cells();\n\tauto &flow_cells = this->cells;\n\n\tfor (size_t idx = 0; idx < integrate_cells.size(); ++idx) {\n\t\tif (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) {\n\t\t\t// Cell is in line of sight\n\t\t\tflow_cells[idx] |= FLOW_LOS_MASK;\n\t\t}\n\t}\n\n\tlog::log(DBG << \"Flow field dynamic flags have been transferred\");\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/flow_field.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <unordered_set>\n#include <vector>\n\n#include \"pathfinding/definitions.h\"\n#include \"pathfinding/types.h\"\n\n\nnamespace openage {\nnamespace coord {\nstruct tile_delta;\n} // namespace coord\n\nnamespace path {\nclass IntegrationField;\nclass Portal;\n\nclass FlowField {\npublic:\n\t/**\n\t * Create a square flow field with a specified size.\n\t *\n\t * @param size Side length of the field.\n\t */\n\tFlowField(size_t size);\n\n\t/**\n\t * Create a flow field from an existing integration field.\n\t *\n\t * @param integration_field Integration field.\n\t */\n\tFlowField(const std::shared_ptr<IntegrationField> &integration_field);\n\n\t/**\n\t * Get the size of the flow field.\n\t *\n\t * @return Size of the flow field.\n\t */\n\tsize_t get_size() const;\n\n\t/**\n\t * Get the flow field value at a specified position.\n\t *\n\t * @param pos Coordinates of the cell (relative to field origin).\n\t *\n\t * @return Flowfield value at the specified position.\n\t */\n\tflow_t get_cell(const coord::tile_delta &pos) const;\n\n\t/**\n\t * Get the flow field value at a specified position.\n\t *\n\t * @param x X-coordinate of the cell.\n\t * @param y Y-coordinate of the cell.\n\t *\n\t * @return Flowfield value at the specified position.\n\t */\n\tflow_t get_cell(size_t x, size_t y) const;\n\n\t/**\n\t * Get the flow field direction at a specified position.\n\t *\n\t * @param idx Index of the cell.\n\t *\n\t * @return Flowfield value at the specified position.\n\t */\n\tflow_t get_cell(size_t idx) const;\n\n\t/**\n\t * Get the flow field direction at a specified position.\n\t *\n\t * @param pos Coordinates of the cell (relative to field origin).\n\t *\n\t * @return Flowfield direction at the specified position.\n\t */\n\tflow_dir_t get_dir(const coord::tile_delta &pos) const;\n\n\t/**\n\t * Get the flow field direction at a specified position.\n\t *\n\t * @param x X-coordinate of the cell.\n\t * @param y Y-coordinate of the cell.\n\t *\n\t * @return Flowfield direction at the specified position.\n\t */\n\tflow_dir_t get_dir(size_t x, size_t y) const;\n\n\t/**\n\t * Get the flow field direction at a specified position.\n\t *\n\t * @param idx Index of the cell.\n\t *\n\t * @return Flowfield direction at the specified position.\n\t */\n\tflow_dir_t get_dir(size_t idx) const;\n\n\t/**\n\t * Build the flow field.\n\t *\n\t * @param integration_field Integration field.\n\t */\n\tvoid build(const std::shared_ptr<IntegrationField> &integration_field);\n\n\t/**\n\t * Build the flow field for a portal.\n\t *\n\t * @param integration_field Integration field.\n\t * @param other Integration field of the other sector.\n\t * @param other_sector_id Sector ID of the other field.\n\t * @param portal Portal connecting the two fields.\n\t */\n\tvoid build(const std::shared_ptr<IntegrationField> &integration_field,\n\t           const std::shared_ptr<IntegrationField> &other,\n\t           sector_id_t other_sector_id,\n\t           const std::shared_ptr<Portal> &portal);\n\n\t/**\n\t * Get the flow field values.\n\t *\n\t * @return Flow field values.\n\t */\n\tconst std::vector<flow_t> &get_cells() const;\n\n\t/**\n\t * Reset the flow field values for rebuilding the field.\n\t */\n\tvoid reset();\n\n\t/**\n\t * Reset all flags that are dependent on the path target location.\n\t *\n\t * These flags should be removed when the field is cached and reused for\n\t * other targets.\n\t *\n\t * Relevant flags are:\n\t * - FLOW_LOS_MASK\n\t * - FLOW_WAVEFRONT_BLOCKED_MASK\n\t */\n\tvoid reset_dynamic_flags();\n\n\t/**\n\t * Transfer dynamic flags from an integration field.\n\t *\n\t * These flags should be transferred when the field is copied from cache.\n\t * Flow field directions are not altered.\n\t *\n\t * Relevant flags are:\n\t * - FLOW_LOS_MASK\n\t * - FLOW_WAVEFRONT_BLOCKED_MASK\n\t *\n\t * @param integration_field Integration field.\n\t */\n\tvoid transfer_dynamic_flags(const std::shared_ptr<IntegrationField> &integration_field);\n\nprivate:\n\t/**\n\t * Side length of the field.\n\t */\n\tsize_t size;\n\n\t/**\n\t * Flow field cells.\n\t */\n\tstd::vector<flow_t> cells;\n};\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/grid.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"grid.h\"\n\n#include \"error/error.h\"\n\n#include \"coord/chunk.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/sector.h\"\n\n\nnamespace openage::path {\n\nGrid::Grid(grid_id_t id,\n           const util::Vector2s &size,\n           size_t sector_size) :\n\tid{id},\n\tsize{size},\n\tsector_size{sector_size} {\n\tfor (size_t y = 0; y < size[1]; y++) {\n\t\tfor (size_t x = 0; x < size[0]; x++) {\n\t\t\tthis->sectors.push_back(\n\t\t\t\tstd::make_shared<Sector>(x + y * this->size[0],\n\t\t\t                             coord::chunk(x, y),\n\t\t\t                             sector_size));\n\t\t}\n\t}\n}\n\nGrid::Grid(grid_id_t id,\n           const util::Vector2s &size,\n           std::vector<std::shared_ptr<Sector>> &&sectors) :\n\tid{id},\n\tsize{size},\n\tsectors{std::move(sectors)} {\n\tENSURE(this->sectors.size() == size[0] * size[1],\n\t       \"Grid has size \" << size[0] << \"x\" << size[1] << \" (\" << size[0] * size[1] << \" sectors), \"\n\t                        << \"but only \" << this->sectors.size() << \" sectors were provided\");\n\n\tthis->sector_size = sectors.at(0)->get_cost_field()->get_size();\n}\n\ngrid_id_t Grid::get_id() const {\n\treturn this->id;\n}\n\nconst util::Vector2s &Grid::get_size() const {\n\treturn this->size;\n}\n\nsize_t Grid::get_sector_size() const {\n\treturn this->sector_size;\n}\n\nconst std::shared_ptr<Sector> &Grid::get_sector(size_t x, size_t y) {\n\treturn this->sectors.at(x + y * this->size[0]);\n}\n\nconst std::shared_ptr<Sector> &Grid::get_sector(sector_id_t id) const {\n\treturn this->sectors.at(id);\n}\n\nconst std::vector<std::shared_ptr<Sector>> &Grid::get_sectors() const {\n\treturn this->sectors;\n}\n\nvoid Grid::init_portals() {\n\t// Create portals between neighboring sectors.\n\tportal_id_t portal_id = 0;\n\tfor (size_t y = 0; y < this->size[1]; y++) {\n\t\tfor (size_t x = 0; x < this->size[0]; x++) {\n\t\t\tauto sector = this->get_sector(x, y);\n\n\t\t\tif (x < this->size[0] - 1) {\n\t\t\t\tauto neighbor = this->get_sector(x + 1, y);\n\t\t\t\tauto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id);\n\t\t\t\tfor (auto &portal : portals) {\n\t\t\t\t\tsector->add_portal(portal);\n\t\t\t\t\tneighbor->add_portal(portal);\n\t\t\t\t}\n\t\t\t\tportal_id += portals.size();\n\t\t\t}\n\t\t\tif (y < this->size[1] - 1) {\n\t\t\t\tauto neighbor = this->get_sector(x, y + 1);\n\t\t\t\tauto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id);\n\t\t\t\tfor (auto &portal : portals) {\n\t\t\t\t\tsector->add_portal(portal);\n\t\t\t\t\tneighbor->add_portal(portal);\n\t\t\t\t}\n\t\t\t\tportal_id += portals.size();\n\t\t\t}\n\t\t}\n\t}\n\n\t// Connect mutually reachable exits of sectors.\n\tfor (auto &sector : this->sectors) {\n\t\tsector->connect_exits();\n\t}\n}\n\nconst nodemap_t &Grid::get_portal_map() {\n\treturn portal_nodes;\n}\n\nvoid Grid::init_portal_nodes() {\n\t// create portal_nodes\n\tfor (auto &sector : this->sectors) {\n\t\tfor (auto &portal : sector->get_portals()) {\n\t\t\tif (!this->portal_nodes.contains(portal->get_id())) {\n\t\t\t\tauto portal_node = std::make_shared<PortalNode>(portal);\n\t\t\t\tportal_node->node_sector_0 = sector->get_id();\n\t\t\t\tportal_node->node_sector_1 = portal_node->portal->get_exit_sector(sector->get_id());\n\t\t\t\tthis->portal_nodes[portal->get_id()] = portal_node;\n\t\t\t}\n\t\t}\n\t}\n\n\t// init portal_node exits\n\tfor (auto &[id, node] : this->portal_nodes) {\n\t\tnode->init_exits(this->portal_nodes);\n\t}\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/grid.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <utility>\n#include <vector>\n\n#include \"pathfinding/pathfinder.h\"\n#include \"pathfinding/types.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::path {\nclass Sector;\n\n/**\n * Grid for flow field pathfinding.\n */\nclass Grid {\npublic:\n\t/**\n\t * Create a new empty grid of width x height sectors with a specified size.\n\t *\n\t * @param id ID of the grid.\n\t * @param size Size of the grid in sectors (width x height).\n\t * @param sector_size Side length of each sector.\n\t */\n\tGrid(grid_id_t id,\n\t     const util::Vector2s &size,\n\t     size_t sector_size);\n\n\t/**\n\t * Create a grid of width x height sectors from a list of existing sectors.\n\t *\n\t * @param id ID of the grid.\n\t * @param size Size of the grid in sectors (width x height).\n\t * @param sectors Existing sectors.\n\t */\n\tGrid(grid_id_t id,\n\t     const util::Vector2s &size,\n\t     std::vector<std::shared_ptr<Sector>> &&sectors);\n\n\t/**\n\t * Get the ID of the grid.\n\t *\n\t * @return ID of the grid.\n\t */\n\tgrid_id_t get_id() const;\n\n\t/**\n\t * Get the size of the grid (in number of sectors).\n\t *\n\t * @return Size of the grid (in number of sectors) (width x height).\n\t */\n\tconst util::Vector2s &get_size() const;\n\n\t/**\n\t * Get the side length of the sectors on the grid (in number of cells).\n\t *\n\t * @return Sector side length (in number of cells).\n\t */\n\tsize_t get_sector_size() const;\n\n\t/**\n\t * Get the sector at a specified position.\n\t *\n\t * @param x X coordinate.\n\t * @param y Y coordinate.\n\t *\n\t * @return Sector at the specified position.\n\t */\n\tconst std::shared_ptr<Sector> &get_sector(size_t x, size_t y);\n\n\t/**\n\t * Get the sector with a specified ID\n\t *\n\t * @param id ID of the sector.\n\t *\n\t * @return Sector with the specified ID.\n\t */\n\tconst std::shared_ptr<Sector> &get_sector(sector_id_t id) const;\n\n\t/**\n\t * Get the sectors of the grid.\n\t *\n\t * @return Sectors of the grid.\n\t */\n\tconst std::vector<std::shared_ptr<Sector>> &get_sectors() const;\n\n\t/**\n\t * Initialize the portals of the sectors on the grid.\n\t *\n\t * This should be called after all sectors' cost fields have been initialized.\n\t */\n\tvoid init_portals();\n\n\t/**\n\t * returns map of portal ids to portal nodes\n\t */\n\tconst nodemap_t &get_portal_map();\n\n\t/**\n\t * Initialize the portal nodes of the grid with neigbouring nodes and distance costs.\n\t */\n\tvoid init_portal_nodes();\n\nprivate:\n\t/**\n\t * ID of the grid.\n\t */\n\tgrid_id_t id;\n\n\t/**\n\t * Size of the grid (width x height).\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Side length of the grid sectors.\n\t */\n\tsize_t sector_size;\n\n\t/**\n\t * Sectors of the grid.\n\t */\n\tstd::vector<std::shared_ptr<Sector>> sectors;\n\n\t/**\n\t *\tmaps portal_ids to portal nodes, which store their neigbouring nodes and associated distance costs\n\t *  for pathfinding\n\t */\n\n\tnodemap_t portal_nodes;\n};\n\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/integration_field.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"integration_field.h\"\n\n#include <cmath>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"coord/tile.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/definitions.h\"\n#include \"pathfinding/portal.h\"\n\n\nnamespace openage::path {\n\nIntegrationField::IntegrationField(size_t size) :\n\tsize{size},\n\tcells(this->size * this->size, INTEGRATE_INIT) {\n\tlog::log(DBG << \"Created integration field with size \" << this->size << \"x\" << this->size);\n}\n\nsize_t IntegrationField::get_size() const {\n\treturn this->size;\n}\n\nconst integrated_t &IntegrationField::get_cell(const coord::tile_delta &pos) const {\n\treturn this->cells.at(pos.ne + pos.se * this->size);\n}\n\nconst integrated_t &IntegrationField::get_cell(size_t x, size_t y) const {\n\treturn this->cells.at(x + y * this->size);\n}\n\nconst integrated_t &IntegrationField::get_cell(size_t idx) const {\n\treturn this->cells.at(idx);\n}\n\nstd::vector<size_t> IntegrationField::integrate_los(const std::shared_ptr<CostField> &cost_field,\n                                                    const coord::tile_delta &target) {\n\tENSURE(cost_field->get_size() == this->get_size(),\n\t       \"cost field size \"\n\t           << cost_field->get_size() << \"x\" << cost_field->get_size()\n\t           << \" does not match integration field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\tENSURE(target.ne >= 0\n\t           and target.se >= 0\n\t           and target.ne < static_cast<coord::tile_t>(this->size)\n\t           and target.se < static_cast<coord::tile_t>(this->size),\n\t       \"target cell (\" << target.ne << \", \" << target.se << \") \"\n\t                       << \"is out of bounds for integration field of size \"\n\t                       << this->size << \"x\" << this->size);\n\n\tstd::vector<size_t> start_cells;\n\tintegrated_cost_t start_cost = INTEGRATED_COST_START;\n\n\t// Target cell index\n\tauto target_idx = target.ne + target.se * this->size;\n\n\tthis->cells[target_idx].cost = start_cost;\n\tthis->cells[target_idx].flags |= INTEGRATE_TARGET_MASK;\n\n\tif (cost_field->get_cost(target_idx) > COST_MIN) {\n\t\t// Do a preliminary LOS integration wave for targets that have cost > min cost\n\t\t// This avoids the bresenham's line algorithm calculations\n\t\t// (which wouldn't return accurate results for blocker == target)\n\t\t// and makes sure that sorrounding cells that are min cost are considered\n\t\t// in line-of-sight.\n\n\t\tthis->cells[target_idx].flags |= INTEGRATE_FOUND_MASK;\n\t\tthis->cells[target_idx].flags |= INTEGRATE_LOS_MASK;\n\n\t\t// Add neighbors to current wave\n\t\tif (target.se > 0) {\n\t\t\tstart_cells.push_back(target_idx - this->size);\n\t\t}\n\t\tif (target.ne > 0) {\n\t\t\tstart_cells.push_back(target_idx - 1);\n\t\t}\n\t\tif (target.se < static_cast<coord::tile_t>(this->size - 1)) {\n\t\t\tstart_cells.push_back(target_idx + this->size);\n\t\t}\n\t\tif (target.ne < static_cast<coord::tile_t>(this->size - 1)) {\n\t\t\tstart_cells.push_back(target_idx + 1);\n\t\t}\n\n\t\t// Increment wave cost as we technically handled the first wave in this block\n\t\tstart_cost += 1;\n\t}\n\telse {\n\t\t// Move outwards from the target cell, updating the integration field\n\t\tstart_cells.push_back(target_idx);\n\t}\n\n\treturn this->integrate_los(cost_field, target, start_cost, std::move(start_cells));\n}\n\nstd::vector<size_t> IntegrationField::integrate_los(const std::shared_ptr<CostField> &cost_field,\n                                                    const std::shared_ptr<IntegrationField> &other,\n                                                    sector_id_t other_sector_id,\n                                                    const std::shared_ptr<Portal> &portal,\n                                                    const coord::tile_delta &target) {\n\tENSURE(cost_field->get_size() == this->get_size(),\n\t       \"cost field size \"\n\t           << cost_field->get_size() << \"x\" << cost_field->get_size()\n\t           << \" does not match integration field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\tstd::vector<size_t> wavefront_blocked_portal;\n\n\tstd::vector<size_t> start_cells;\n\n\tauto exit_start = portal->get_exit_start(other_sector_id);\n\tauto exit_end = portal->get_exit_end(other_sector_id);\n\tauto entry_start = portal->get_entry_start(other_sector_id);\n\n\tauto x_diff = exit_start.ne - entry_start.ne;\n\tauto y_diff = exit_start.se - entry_start.se;\n\n\tauto &cost_cells = cost_field->get_costs();\n\tauto &other_cells = other->get_cells();\n\n\t// transfer masks for flags from the other side of the portal\n\t// only LOS and wavefront blocked flags are relevant\n\tintegrated_flags_t transfer_mask = INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK;\n\n\t// every portal cell is a target cell\n\tfor (auto y = exit_start.se; y <= exit_end.se; ++y) {\n\t\tfor (auto x = exit_start.ne; x <= exit_end.ne; ++x) {\n\t\t\t// cell index on the exit side of the portal\n\t\t\tauto target_idx = x + y * this->size;\n\n\t\t\t// cell index on the entry side of the portal\n\t\t\tauto entry_idx = x - x_diff + (y - y_diff) * this->size;\n\n\t\t\t// Set the cost of all target cells to the start value\n\t\t\tthis->cells[target_idx].cost = INTEGRATED_COST_START;\n\t\t\tthis->cells[target_idx].flags = other_cells[entry_idx].flags & transfer_mask;\n\n\t\t\tthis->cells[target_idx].flags |= INTEGRATE_TARGET_MASK;\n\n\t\t\tif (not(this->cells[target_idx].flags & transfer_mask)) {\n\t\t\t\t// If neither LOS nor wavefront blocked flags are set for the portal entry,\n\t\t\t\t// the portal exit cell doesn't affect the LOS and we can skip further checks\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Get the cost of the current cell\n\t\t\tauto cell_cost = cost_cells[target_idx];\n\n\t\t\tif (cell_cost > COST_MIN or this->cells[target_idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) {\n\t\t\t\t// cell blocks line of sight\n\n\t\t\t\t// set the blocked flag for the cell if it wasn't set already\n\t\t\t\tthis->cells[target_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK;\n\t\t\t\twavefront_blocked_portal.push_back(target_idx);\n\n\t\t\t\t// set the found flag for the cell, so that the start costs\n\t\t\t\t// are not changed in the main LOS integration\n\t\t\t\tthis->cells[target_idx].flags |= INTEGRATE_FOUND_MASK;\n\n\t\t\t\t// check each neighbor for a corner\n\t\t\t\tauto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y));\n\t\t\t\tfor (auto &corner : corners) {\n\t\t\t\t\t// draw a line from the corner to the edge of the field\n\t\t\t\t\t// to get the cells blocked by the corner\n\t\t\t\t\tauto blocked_cells = this->bresenhams_line(target, corner.first, corner.second);\n\t\t\t\t\tfor (auto blocked_idx : blocked_cells) {\n\t\t\t\t\t\tif (cost_cells[blocked_idx] > COST_MIN) {\n\t\t\t\t\t\t\t// stop if blocked_idx is not min cost\n\t\t\t\t\t\t\t// because this idx may create a new corner\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// set the blocked flag for the cell\n\t\t\t\t\t\tthis->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK;\n\n\t\t\t\t\t\t// clear los flag if it was set\n\t\t\t\t\t\tthis->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK;\n\n\t\t\t\t\t\twavefront_blocked_portal.push_back(blocked_idx);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// the cell has the LOS flag and is added to the start cells\n\t\t\tstart_cells.push_back(target_idx);\n\t\t}\n\t}\n\n\tif (start_cells.empty()) {\n\t\t// Main LOS integration will not enter its loop\n\t\t// so we can take a shortcut and just return the\n\t\t// wavefront blocked cells we already found\n\t\treturn wavefront_blocked_portal;\n\t}\n\n\t// Call main LOS integration function\n\tauto wavefront_blocked_main = this->integrate_los(cost_field,\n\t                                                  target,\n\t                                                  INTEGRATED_COST_START,\n\t                                                  std::move(start_cells));\n\twavefront_blocked_main.insert(wavefront_blocked_main.end(),\n\t                              wavefront_blocked_portal.begin(),\n\t                              wavefront_blocked_portal.end());\n\treturn wavefront_blocked_main;\n}\n\nstd::vector<size_t> IntegrationField::integrate_los(const std::shared_ptr<CostField> &cost_field,\n                                                    const coord::tile_delta &target,\n                                                    integrated_cost_t start_cost,\n                                                    std::vector<size_t> &&start_wave) {\n\tENSURE(cost_field->get_size() == this->get_size(),\n\t       \"cost field size \"\n\t           << cost_field->get_size() << \"x\" << cost_field->get_size()\n\t           << \" does not match integration field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\t// Store the wavefront_blocked cells\n\tstd::vector<size_t> wavefront_blocked;\n\n\t// Cells that still have to be visited by the current wave\n\tstd::vector<size_t> current_wave = std::move(start_wave);\n\n\t// Cells that have to be visited in the next wave\n\tstd::vector<size_t> next_wave;\n\n\t// Preallocate ~30% of the field size for the wavefront\n\t// This reduces the number of reallocations on push_back operations\n\t// TODO: Find \"optimal\" value for reserve\n\tcurrent_wave.reserve(this->size * 3);\n\tnext_wave.reserve(this->size * 3);\n\n\t// Cost of the current wave\n\tintegrated_cost_t wave_cost = start_cost;\n\n\t// Get the cost field values\n\tauto &cost_cells = cost_field->get_costs();\n\tauto &integrate_cells = this->cells;\n\n\tdo {\n\t\tfor (size_t i = 0; i < current_wave.size(); ++i) {\n\t\t\t// inner loop: handle a wave\n\t\t\tauto idx = current_wave[i];\n\t\t\tauto &cell = integrate_cells[idx];\n\n\t\t\tif (cell.flags & INTEGRATE_FOUND_MASK) {\n\t\t\t\t// Skip cells that are already in the line of sight\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\telse if (cell.flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) {\n\t\t\t\t// Stop at cells that are blocked by a LOS corner\n\t\t\t\tcell.cost = wave_cost - 1 + cost_cells[idx];\n\t\t\t\tcell.flags |= INTEGRATE_FOUND_MASK;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Add the current cell to the found cells\n\t\t\tcell.flags |= INTEGRATE_FOUND_MASK;\n\n\t\t\t// Get the x and y coordinates of the current cell\n\t\t\tauto x = idx % this->size;\n\t\t\tauto y = idx / this->size;\n\n\t\t\t// Get the cost of the current cell\n\t\t\tauto cell_cost = cost_cells[idx];\n\n\t\t\tif (cell_cost > COST_MIN) {\n\t\t\t\t// cell blocks line of sight\n\t\t\t\t// and we have to check for corners\n\t\t\t\tif (cell_cost != COST_IMPASSABLE) {\n\t\t\t\t\t// Add the current cell to the blocked wavefront if it's not a wall\n\t\t\t\t\twavefront_blocked.push_back(idx);\n\t\t\t\t\tcell.cost = wave_cost - 1 + cell_cost;\n\t\t\t\t\tcell.flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK;\n\t\t\t\t}\n\n\t\t\t\t// check each neighbor for a corner\n\t\t\t\tauto corners = this->get_los_corners(cost_field, target, coord::tile_delta(x, y));\n\t\t\t\tfor (auto &corner : corners) {\n\t\t\t\t\t// draw a line from the corner to the edge of the field\n\t\t\t\t\t// to get the cells blocked by the corner\n\t\t\t\t\tauto blocked_cells = this->bresenhams_line(target, corner.first, corner.second);\n\t\t\t\t\tfor (auto blocked_idx : blocked_cells) {\n\t\t\t\t\t\tif (cost_cells[blocked_idx] > COST_MIN) {\n\t\t\t\t\t\t\t// stop if blocked_idx is impassable\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// set the blocked flag for the cell\n\t\t\t\t\t\tintegrate_cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK;\n\n\t\t\t\t\t\t// clear los flag if it was set\n\t\t\t\t\t\tintegrate_cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK;\n\n\t\t\t\t\t\twavefront_blocked.push_back(blocked_idx);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// The cell is in the line of sight at min cost\n\t\t\t// Set the LOS flag and cost\n\t\t\tcell.cost = wave_cost;\n\t\t\tcell.flags |= INTEGRATE_LOS_MASK;\n\n\t\t\t// Search the neighbors of the current cell\n\t\t\tif (y > 0) {\n\t\t\t\tauto neighbor_idx = idx - this->size;\n\t\t\t\tnext_wave.push_back(neighbor_idx);\n\t\t\t}\n\t\t\tif (x > 0) {\n\t\t\t\tauto neighbor_idx = idx - 1;\n\t\t\t\tnext_wave.push_back(neighbor_idx);\n\t\t\t}\n\t\t\tif (y < this->size - 1) {\n\t\t\t\tauto neighbor_idx = idx + this->size;\n\t\t\t\tnext_wave.push_back(neighbor_idx);\n\t\t\t}\n\t\t\tif (x < this->size - 1) {\n\t\t\t\tauto neighbor_idx = idx + 1;\n\t\t\t\tnext_wave.push_back(neighbor_idx);\n\t\t\t}\n\t\t}\n\n\t\t// increment the cost and advance the wavefront outwards\n\t\twave_cost += 1;\n\t\tcurrent_wave.swap(next_wave);\n\t\tnext_wave.clear();\n\t}\n\twhile (not current_wave.empty());\n\n\treturn wavefront_blocked;\n}\n\nvoid IntegrationField::integrate_cost(const std::shared_ptr<CostField> &cost_field,\n                                      const coord::tile_delta &target) {\n\tENSURE(cost_field->get_size() == this->get_size(),\n\t       \"cost field size \"\n\t           << cost_field->get_size() << \"x\" << cost_field->get_size()\n\t           << \" does not match integration field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\t// Target cell index\n\tauto target_idx = target.ne + target.se * this->size;\n\n\t// Move outwards from the target cell, updating the integration field\n\tthis->cells[target_idx].cost = INTEGRATED_COST_START;\n\tthis->cells[target_idx].flags |= INTEGRATE_TARGET_MASK;\n\tthis->integrate_cost(cost_field, {target_idx});\n}\n\nvoid IntegrationField::integrate_cost(const std::shared_ptr<CostField> &cost_field,\n                                      sector_id_t other_sector_id,\n                                      const std::shared_ptr<Portal> &portal) {\n\tENSURE(cost_field->get_size() == this->get_size(),\n\t       \"cost field size \"\n\t           << cost_field->get_size() << \"x\" << cost_field->get_size()\n\t           << \" does not match integration field size \"\n\t           << this->get_size() << \"x\" << this->get_size());\n\n\t// Integrate the cost of the cells on the exit side (this) of the portal\n\tstd::vector<size_t> start_cells;\n\tauto exit_start = portal->get_exit_start(other_sector_id);\n\tauto exit_end = portal->get_exit_end(other_sector_id);\n\tfor (auto y = exit_start.se; y <= exit_end.se; ++y) {\n\t\tfor (auto x = exit_start.ne; x <= exit_end.ne; ++x) {\n\t\t\t// every portal cell is a target cell\n\t\t\tauto target_idx = x + y * this->size;\n\n\t\t\t// Set the cost of all target cells to the start value\n\t\t\tthis->cells[target_idx].cost = INTEGRATED_COST_START;\n\t\t\tthis->cells[target_idx].flags |= INTEGRATE_TARGET_MASK;\n\t\t\tstart_cells.push_back(target_idx);\n\n\t\t\t// TODO: Transfer flags and cost from the other integration field\n\t\t}\n\t}\n\n\t// Integrate the rest of the cost field\n\tthis->integrate_cost(cost_field, std::move(start_cells));\n}\n\nvoid IntegrationField::integrate_cost(const std::shared_ptr<CostField> &cost_field,\n                                      std::vector<size_t> &&start_cells) {\n\t// Cells that still have to be visited by the current wave\n\tstd::vector<size_t> current_wave = std::move(start_cells);\n\n\t// Cells that have to be visited in the next wave\n\tstd::vector<size_t> next_wave;\n\n\t// Preallocate ~30% of the field size for the wavefront\n\t// This reduces the number of reallocations on push_back operations\n\t// TODO: Find \"optimal\" value for reserve\n\tcurrent_wave.reserve(this->size * 3);\n\tnext_wave.reserve(this->size * 3);\n\n\t// Get the cost field values\n\tauto &cost_cells = cost_field->get_costs();\n\n\t// Move outwards from the wavefront, updating the integration field\n\twhile (not current_wave.empty()) {\n\t\tfor (size_t i = 0; i < current_wave.size(); ++i) {\n\t\t\tauto idx = current_wave[i];\n\n\t\t\t// Get the x and y coordinates of the current cell\n\t\t\tauto x = idx % this->size;\n\t\t\tauto y = idx / this->size;\n\n\t\t\tauto integrated_current = this->cells[idx].cost;\n\n\t\t\t// Get the neighbors of the current cell\n\t\t\tif (y > 0) {\n\t\t\t\tauto neighbor_idx = idx - this->size;\n\t\t\t\tthis->update_neighbor(neighbor_idx,\n\t\t\t\t                      cost_cells[neighbor_idx],\n\t\t\t\t                      integrated_current,\n\t\t\t\t                      next_wave);\n\t\t\t}\n\t\t\tif (x > 0) {\n\t\t\t\tauto neighbor_idx = idx - 1;\n\t\t\t\tthis->update_neighbor(neighbor_idx,\n\t\t\t\t                      cost_cells[neighbor_idx],\n\t\t\t\t                      integrated_current,\n\t\t\t\t                      next_wave);\n\t\t\t}\n\t\t\tif (y < this->size - 1) {\n\t\t\t\tauto neighbor_idx = idx + this->size;\n\t\t\t\tthis->update_neighbor(neighbor_idx,\n\t\t\t\t                      cost_cells[neighbor_idx],\n\t\t\t\t                      integrated_current,\n\t\t\t\t                      next_wave);\n\t\t\t}\n\t\t\tif (x < this->size - 1) {\n\t\t\t\tauto neighbor_idx = idx + 1;\n\t\t\t\tthis->update_neighbor(neighbor_idx,\n\t\t\t\t                      cost_cells[neighbor_idx],\n\t\t\t\t                      integrated_current,\n\t\t\t\t                      next_wave);\n\t\t\t}\n\t\t}\n\n\t\tcurrent_wave.swap(next_wave);\n\t\tnext_wave.clear();\n\t}\n}\n\nconst std::vector<integrated_t> &IntegrationField::get_cells() const {\n\treturn this->cells;\n}\n\nvoid IntegrationField::reset() {\n\tstd::fill(this->cells.begin(), this->cells.end(), INTEGRATE_INIT);\n\n\tlog::log(DBG << \"Integration field has been reset\");\n}\n\nvoid IntegrationField::reset_dynamic_flags() {\n\tintegrated_flags_t mask = 0xFF & ~(INTEGRATE_LOS_MASK | INTEGRATE_WAVEFRONT_BLOCKED_MASK | INTEGRATE_FOUND_MASK);\n\tfor (integrated_t &cell : this->cells) {\n\t\tcell.flags = cell.flags & mask;\n\t}\n\n\tlog::log(DBG << \"Integration field dynamic flags have been reset\");\n}\n\nvoid IntegrationField::update_neighbor(size_t idx,\n                                       cost_t cell_cost,\n                                       integrated_cost_t integrated_cost,\n                                       std::vector<size_t> &wave) {\n\tENSURE(cell_cost > COST_INIT, \"cost field cell value must be non-zero\");\n\n\t// Check if the cell is impassable\n\t// then we don't need to update the integration field\n\tif (cell_cost == COST_IMPASSABLE) {\n\t\treturn;\n\t}\n\n\tauto cost = integrated_cost + cell_cost;\n\tif (cost < this->cells[idx].cost) {\n\t\t// If the new integration value is smaller than the current one,\n\t\t// update the cell and add it to the open list\n\t\tthis->cells[idx].cost = cost;\n\n\t\twave.push_back(idx);\n\t}\n}\n\nstd::vector<std::pair<int, int>> IntegrationField::get_los_corners(const std::shared_ptr<CostField> &cost_field,\n                                                                   const coord::tile_delta &target,\n                                                                   const coord::tile_delta &blocker) {\n\tstd::vector<std::pair<int, int>> corners;\n\n\t// Get the cost of the blocking cell's neighbors\n\n\t// Set all costs to IMPASSABLE at the beginning\n\tauto top_cost = COST_IMPASSABLE;\n\tauto left_cost = COST_IMPASSABLE;\n\tauto bottom_cost = COST_IMPASSABLE;\n\tauto right_cost = COST_IMPASSABLE;\n\n\tstd::pair<int, int> top_left{blocker.ne, blocker.se};\n\tstd::pair<int, int> top_right{blocker.ne + 1, blocker.se};\n\tstd::pair<int, int> bottom_left{blocker.ne, blocker.se + 1};\n\tstd::pair<int, int> bottom_right{blocker.ne + 1, blocker.se + 1};\n\n\t// Get neighbor costs (if they exist)\n\tif (blocker.se > 0) {\n\t\ttop_cost = cost_field->get_cost(blocker.ne, blocker.se - 1);\n\t}\n\tif (blocker.ne > 0) {\n\t\tleft_cost = cost_field->get_cost(blocker.ne - 1, blocker.se);\n\t}\n\tif (static_cast<size_t>(blocker.se) < this->size - 1) {\n\t\tbottom_cost = cost_field->get_cost(blocker.ne, blocker.se + 1);\n\t}\n\tif (static_cast<size_t>(blocker.ne) < this->size - 1) {\n\t\tright_cost = cost_field->get_cost(blocker.ne + 1, blocker.se);\n\t}\n\n\t// Check which corners are blocking LOS\n\t// TODO: Currently super complicated and could likely be optimized\n\tif (blocker.ne == target.ne) {\n\t\t// blocking cell is parallel to target on y-axis\n\t\tif (blocker.se < target.se) {\n\t\t\tif (left_cost == COST_MIN) {\n\t\t\t\t// top\n\t\t\t\tcorners.push_back(bottom_left);\n\t\t\t}\n\t\t\tif (right_cost == COST_MIN) {\n\t\t\t\t// top\n\t\t\t\tcorners.push_back(bottom_right);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (left_cost == COST_MIN) {\n\t\t\t\t// bottom\n\t\t\t\tcorners.push_back(top_left);\n\t\t\t}\n\t\t\tif (right_cost == COST_MIN) {\n\t\t\t\t// bottom\n\t\t\t\tcorners.push_back(top_right);\n\t\t\t}\n\t\t}\n\t}\n\telse if (blocker.se == target.se) {\n\t\t// blocking cell is parallel to target on x-axis\n\t\tif (blocker.ne < target.ne) {\n\t\t\tif (top_cost == COST_MIN) {\n\t\t\t\t// right\n\t\t\t\tcorners.push_back(top_right);\n\t\t\t}\n\t\t\tif (bottom_cost == COST_MIN) {\n\t\t\t\t// right\n\t\t\t\tcorners.push_back(bottom_right);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (top_cost == COST_MIN) {\n\t\t\t\t// left\n\t\t\t\tcorners.push_back(top_left);\n\t\t\t}\n\t\t\tif (bottom_cost == COST_MIN) {\n\t\t\t\t// left\n\t\t\t\tcorners.push_back(bottom_left);\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\t// blocking cell is diagonal to target on\n\t\tif (blocker.ne < target.ne) {\n\t\t\tif (blocker.se < target.se) {\n\t\t\t\t// top and right\n\t\t\t\tif (top_cost == COST_MIN and right_cost == COST_MIN) {\n\t\t\t\t\t// right\n\t\t\t\t\tcorners.push_back(top_right);\n\t\t\t\t}\n\t\t\t\tif (left_cost == COST_MIN and bottom_cost == COST_MIN) {\n\t\t\t\t\t// bottom\n\t\t\t\t\tcorners.push_back(bottom_left);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// bottom and right\n\t\t\t\tif (bottom_cost == COST_MIN and right_cost == COST_MIN) {\n\t\t\t\t\t// right\n\t\t\t\t\tcorners.push_back(bottom_right);\n\t\t\t\t}\n\t\t\t\tif (left_cost == COST_MIN and top_cost == COST_MIN) {\n\t\t\t\t\t// top\n\t\t\t\t\tcorners.push_back(top_left);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tif (blocker.se < target.se) {\n\t\t\t\t// top and left\n\t\t\t\tif (top_cost == COST_MIN and left_cost == COST_MIN) {\n\t\t\t\t\t// left\n\t\t\t\t\tcorners.push_back(top_left);\n\t\t\t\t}\n\t\t\t\tif (right_cost == COST_MIN and bottom_cost == COST_MIN) {\n\t\t\t\t\t// bottom\n\t\t\t\t\tcorners.push_back(bottom_right);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\t// bottom and left\n\t\t\t\tif (bottom_cost == COST_MIN and left_cost == COST_MIN) {\n\t\t\t\t\t// left\n\t\t\t\t\tcorners.push_back(bottom_left);\n\t\t\t\t}\n\t\t\t\tif (right_cost == COST_MIN and top_cost == COST_MIN) {\n\t\t\t\t\t// top\n\t\t\t\t\tcorners.push_back(top_right);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn corners;\n}\n\nstd::vector<size_t> IntegrationField::bresenhams_line(const coord::tile_delta &target,\n                                                      int corner_x,\n                                                      int corner_y) {\n\tstd::vector<size_t> cells;\n\n\t// cell coordinates\n\t// these have to be offset depending on the line direction\n\tauto cell_x = corner_x;\n\tauto cell_y = corner_y;\n\n\t// field edge boundary\n\tint boundary = this->size;\n\n\t// target coordinates\n\t// offset by 0.5 to get the center of the cell\n\tdouble tx = target.ne + 0.5;\n\tdouble ty = target.se + 0.5;\n\n\t// slope of the line\n\tdouble dx = std::abs(tx - corner_x);\n\tdouble dy = std::abs(ty - corner_y);\n\tauto m = dy / dx;\n\n\t// error margin for the line\n\t// if the error is greater than 1.0, we have to move in the y direction\n\tauto error = m;\n\n\t// Check which direction the line is going\n\tif (corner_x < tx) {\n\t\tif (corner_y < ty) { // left and up\n\t\t\tcell_y -= 1;\n\t\t\tcell_x -= 1;\n\t\t\twhile (cell_x >= 0 and cell_y >= 0) {\n\t\t\t\tcells.push_back(cell_x + cell_y * this->size);\n\t\t\t\tif (error > 1.0) {\n\t\t\t\t\tcell_y -= 1;\n\t\t\t\t\terror -= 1.0;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tcell_x -= 1;\n\t\t\t\t\terror += m;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\telse { // left and down\n\t\t\tcell_x -= 1;\n\t\t\twhile (cell_x >= 0 and cell_y < boundary) {\n\t\t\t\tcells.push_back(cell_x + cell_y * this->size);\n\t\t\t\tif (error > 1.0) {\n\t\t\t\t\tcell_y += 1;\n\t\t\t\t\terror -= 1.0;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tcell_x -= 1;\n\t\t\t\t\terror += m;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\telse {\n\t\tif (corner_y < ty) { // right and up\n\t\t\tcell_y -= 1;\n\t\t\twhile (cell_x < boundary and cell_y >= 0) {\n\t\t\t\tcells.push_back(cell_x + cell_y * this->size);\n\t\t\t\tif (error > 1.0) {\n\t\t\t\t\tcell_y -= 1;\n\t\t\t\t\terror -= 1.0;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tcell_x += 1;\n\t\t\t\t\terror += m;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse { // right and down\n\t\t\twhile (cell_x < boundary and cell_y < boundary) {\n\t\t\t\tcells.push_back(cell_x + cell_y * this->size);\n\t\t\t\tif (error > 1.0) {\n\t\t\t\t\tcell_y += 1;\n\t\t\t\t\terror -= 1.0;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tcell_x += 1;\n\t\t\t\t\terror += m;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn cells;\n}\n\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/integration_field.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <deque>\n#include <memory>\n#include <unordered_set>\n#include <vector>\n\n#include \"pathfinding/types.h\"\n\n\nnamespace openage {\nnamespace coord {\nstruct tile_delta;\n} // namespace coord\n\nnamespace path {\nclass CostField;\nclass Portal;\n\n/**\n * Integration field in the flow-field pathfinding algorithm.\n */\nclass IntegrationField {\npublic:\n\t/**\n\t * Create a square integration field with a specified size.\n\t *\n\t * @param size Side length of the field.\n\t */\n\tIntegrationField(size_t size);\n\n\t/**\n\t * Get the size of the integration field.\n\t *\n\t * @return Size of the integration field.\n\t */\n\tsize_t get_size() const;\n\n\t/**\n\t * Get the integration value at a specified position.\n\t *\n\t * @param pos Coordinates of the cell (relative to field origin).\n\t * @return Integration value at the specified position.\n\t */\n\tconst integrated_t &get_cell(const coord::tile_delta &pos) const;\n\n\t/**\n\t * Get the integration value at a specified position.\n\t *\n\t * @param x X-coordinate of the cell.\n\t * @param y Y-coordinate of the cell.\n\t * @return Integration value at the specified position.\n\t */\n\tconst integrated_t &get_cell(size_t x, size_t y) const;\n\n\t/**\n\t * Get the integration value at a specified position.\n\t *\n\t * @param idx Index of the cell.\n\t * @return Integration value at the specified position.\n\t */\n\tconst integrated_t &get_cell(size_t idx) const;\n\n\t/**\n\t * Calculate the line-of-sight integration flags for a target cell.\n\t *\n\t * The target cell coordinates must lie within the field.\n\t *\n\t * Returns a list of cells that are flagged as \"wavefront blocked\". These cells\n\t * can be used as a starting point for the cost integration.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param target Coordinates of the target cell (relative to field origin).\n\t *\n\t * @return Cells flagged as \"wavefront blocked\".\n\t */\n\tstd::vector<size_t> integrate_los(const std::shared_ptr<CostField> &cost_field,\n\t                                  const coord::tile_delta &target);\n\n\t/**\n\t * Calculate the line-of-sight integration flags starting from a portal to another\n\t * integration field.\n\t *\n\t * Returns a list of cells that are flagged as \"wavefront blocked\". These cells\n\t * can be used as a starting point for the cost integration.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param other Integration field of the other sector.\n\t * @param other_sector_id Sector ID of the other integration field.\n\t * @param portal Portal connecting the two fields.\n\t * @param target Coordinates of the target cell (relative to field origin).\n\t *\n\t * @return Cells flagged as \"wavefront blocked\".\n\t */\n\tstd::vector<size_t> integrate_los(const std::shared_ptr<CostField> &cost_field,\n\t                                  const std::shared_ptr<IntegrationField> &other,\n\t                                  sector_id_t other_sector_id,\n\t                                  const std::shared_ptr<Portal> &portal,\n\t                                  const coord::tile_delta &target);\n\n\t/**\n\t * Calculate the line-of-sight integration flags for a target cell.\n\t *\n\t * Returns a list of cells that are flagged as \"wavefront blocked\". These cells\n\t * can be used as a starting point for the cost integration.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param target Coordinates of the target cell (relative to field origin).\n\t * @param start_cost Integration cost for the start wave.\n\t * @param start_wave Cells used for the first LOS integration wave. The wavefront\n\t * \t\t\t\t\t expands outwards from these cells.\n\t *\n\t * @return Cells flagged as \"wavefront blocked\".\n\t */\n\tstd::vector<size_t> integrate_los(const std::shared_ptr<CostField> &cost_field,\n\t                                  const coord::tile_delta &target,\n\t                                  integrated_cost_t start_cost,\n\t                                  std::vector<size_t> &&start_wave);\n\n\t/**\n\t * Calculate the cost integration field starting from a target cell.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param target Coordinates of the target cell.\n\t */\n\tvoid integrate_cost(const std::shared_ptr<CostField> &cost_field,\n\t                    const coord::tile_delta &target);\n\n\t/**\n\t * Calculate the cost integration field starting from a portal to another\n\t * integration field.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param other_sector_id Sector ID of the other integration field.\n\t * @param portal Portal connecting the two fields.\n\t */\n\tvoid integrate_cost(const std::shared_ptr<CostField> &cost_field,\n\t                    sector_id_t other_sector_id,\n\t                    const std::shared_ptr<Portal> &portal);\n\n\t/**\n\t * Calculate the cost integration field starting from a wavefront.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param start_cells Cells flagged as \"wavefront blocked\" from a LOS pass.\n\t */\n\tvoid integrate_cost(const std::shared_ptr<CostField> &cost_field,\n\t                    std::vector<size_t> &&start_cells);\n\n\t/**\n\t * Get the integration field values.\n\t *\n\t * @return Integration field values.\n\t */\n\tconst std::vector<integrated_t> &get_cells() const;\n\n\t/**\n\t * Reset the integration field for a new integration.\n\t */\n\tvoid reset();\n\n\t/**\n\t * Reset all flags that are dependent on the path target location. These\n\t * flags should be removed when the field is cached and reused for\n\t * other targets.\n\t *\n\t * Relevant flags are:\n\t * - INTEGRATE_LOS_MASK\n\t * - INTEGRATE_WAVEFRONT_BLOCKED_MASK\n\t * - INTEGRATE_FOUND_MASK\n\t */\n\tvoid reset_dynamic_flags();\n\nprivate:\n\t/**\n\t * Update a neigbor cell during the cost integration process.\n\t *\n\t * @param idx Index of the neighbor cell that is updated.\n\t * @param cell_cost Cost of the neighbor cell from the cost field.\n\t * @param integrated_cost Current integrated cost of the updating cell in the integration field.\n\t * @param wave List of cells that are part of the next wavefront.\n\t *\n\t * @return New integration value of the cell.\n\t */\n\tvoid update_neighbor(size_t idx,\n\t                     cost_t cell_cost,\n\t                     integrated_cost_t integrated_cost,\n\t                     std::vector<size_t> &wave);\n\n\t/**\n\t * Get the LOS corners around a cell.\n\t *\n\t * @param cost_field Cost field to integrate.\n\t * @param target Cell coordinates of the target (relative to field origin).\n\t * @param blocker Cell coordinates of the cell blocking LOS (relative to field origin).\n\t *\n\t * @return Field coordinates of the LOS corners.\n\t */\n\tstd::vector<std::pair<int, int>> get_los_corners(const std::shared_ptr<CostField> &cost_field,\n\t                                                 const coord::tile_delta &target,\n\t                                                 const coord::tile_delta &blocker);\n\n\t/**\n\t * Get the cells in a bresenham's line between the corner cell and the field edge.\n\t *\n\t * This function is a modified version of the bresenham's line algorithm that\n\t * retrieves the cells between the corner point and the field's edge, rather than\n\t * the cells between two arbitrary points. We do this because the intersection\n\t * point with the field edge is unknown.\n\t *\n\t * @param target Cell coordinates of the target (relative to field origin).\n\t * @param corner_x X field coordinate edge of the LOS corner.\n\t * @param corner_y Y field coordinate edge of the LOS corner.\n\t *\n\t * @return Cell indices of the LOS line.\n\t */\n\tstd::vector<size_t> bresenhams_line(const coord::tile_delta &target,\n\t                                    int corner_x,\n\t                                    int corner_y);\n\n\t/**\n\t * Side length of the field.\n\t */\n\tsize_t size;\n\n\t/**\n\t * Integration field values.\n\t */\n\tstd::vector<integrated_t> cells;\n};\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/integrator.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"integrator.h\"\n\n#include \"log/log.h\"\n\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/field_cache.h\"\n#include \"pathfinding/flow_field.h\"\n#include \"pathfinding/integration_field.h\"\n#include \"pathfinding/portal.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::path {\n\nIntegrator::Integrator() :\n\tfield_cache{std::make_unique<FieldCache>()} {\n}\n\nstd::shared_ptr<IntegrationField> Integrator::integrate(const std::shared_ptr<CostField> &cost_field,\n                                                        const coord::tile_delta &target,\n                                                        bool with_los) {\n\tauto integration_field = std::make_shared<IntegrationField>(cost_field->get_size());\n\n\tlog::log(DBG << \"Integrating cost field for target coord \" << target);\n\tif (with_los) {\n\t\tlog::log(SPAM << \"Performing LOS pass\");\n\t\tauto wavefront_blocked = integration_field->integrate_los(cost_field, target);\n\t\tintegration_field->integrate_cost(cost_field, std::move(wavefront_blocked));\n\t}\n\telse {\n\t\tlog::log(SPAM << \"Skipping LOS pass\");\n\t\tintegration_field->integrate_cost(cost_field, target);\n\t}\n\n\treturn integration_field;\n}\n\nstd::shared_ptr<IntegrationField> Integrator::integrate(const std::shared_ptr<CostField> &cost_field,\n                                                        const std::shared_ptr<IntegrationField> &other,\n                                                        sector_id_t other_sector_id,\n                                                        const std::shared_ptr<Portal> &portal,\n                                                        const coord::tile_delta &target,\n                                                        const time::time_t &time,\n                                                        bool with_los) {\n\tauto cache_key = std::make_pair(portal->get_id(), other_sector_id);\n\tif (cost_field->is_dirty(time)) {\n\t\tlog::log(DBG << \"Evicting cached integration and flow fields for portal \" << portal->get_id()\n\t\t             << \" from sector \" << other_sector_id);\n\t\tthis->field_cache->evict(cache_key);\n\t}\n\telse if (this->field_cache->is_cached(cache_key)) {\n\t\tlog::log(DBG << \"Using cached integration field for portal \" << portal->get_id()\n\t\t             << \" from sector \" << other_sector_id);\n\n\t\t// retrieve cached integration field\n\t\tauto cached_integration_field = this->field_cache->get_integration_field(cache_key);\n\n\t\tif (with_los) {\n\t\t\tlog::log(SPAM << \"Performing LOS pass on cached field\");\n\n\t\t\t// Make a copy of the cached field to avoid modifying the cached field\n\t\t\tauto integration_field = std::make_shared<IntegrationField>(*cached_integration_field);\n\n\t\t\t// Only integrate LOS; leave the rest of the field as is\n\t\t\tintegration_field->integrate_los(cost_field, other, other_sector_id, portal, target);\n\n\t\t\treturn integration_field;\n\t\t}\n\t\treturn cached_integration_field;\n\t}\n\n\tlog::log(DBG << \"Integrating cost field for portal \" << portal->get_id()\n\t             << \" from sector \" << other_sector_id);\n\n\t// Create a new integration field\n\tauto integration_field = std::make_shared<IntegrationField>(cost_field->get_size());\n\n\t// LOS pass\n\tstd::vector<size_t> wavefront_blocked;\n\tif (with_los) {\n\t\tlog::log(SPAM << \"Performing LOS pass\");\n\t\twavefront_blocked = integration_field->integrate_los(cost_field, other, other_sector_id, portal, target);\n\t}\n\n\t// Cost integration\n\tif (wavefront_blocked.empty()) {\n\t\t// No LOS pass or no blocked cells\n\t\t// use the portal as the target\n\t\tintegration_field->integrate_cost(cost_field, other_sector_id, portal);\n\t}\n\telse {\n\t\t// LOS pass was performed and some cells were blocked\n\t\t// use the blocked cells as the start wave\n\t\tintegration_field->integrate_cost(cost_field, std::move(wavefront_blocked));\n\t}\n\n\treturn integration_field;\n}\n\nstd::shared_ptr<FlowField> Integrator::build(const std::shared_ptr<IntegrationField> &integration_field) {\n\tauto flow_field = std::make_shared<FlowField>(integration_field->get_size());\n\n\tlog::log(DBG << \"Building flow field from integration field\");\n\tflow_field->build(integration_field);\n\n\treturn flow_field;\n}\n\nstd::shared_ptr<FlowField> Integrator::build(const std::shared_ptr<IntegrationField> &integration_field,\n                                             const std::shared_ptr<IntegrationField> &other,\n                                             sector_id_t other_sector_id,\n                                             const std::shared_ptr<Portal> &portal,\n                                             bool with_los) {\n\tauto cache_key = std::make_pair(portal->get_id(), other_sector_id);\n\tif (this->field_cache->is_cached(cache_key)) {\n\t\tlog::log(DBG << \"Using cached flow field for portal \" << portal->get_id()\n\t\t             << \" from sector \" << other_sector_id);\n\n\t\t// retrieve cached flow field\n\t\tauto cached_flow_field = this->field_cache->get_flow_field(cache_key);\n\n\t\tif (with_los) {\n\t\t\tlog::log(SPAM << \"Transferring LOS flags to cached flow field\");\n\n\t\t\t// Make a copy of the cached flow field\n\t\t\tauto flow_field = std::make_shared<FlowField>(*cached_flow_field);\n\n\t\t\t// Transfer the LOS flags to the flow field\n\t\t\tflow_field->transfer_dynamic_flags(integration_field);\n\n\t\t\treturn flow_field;\n\t\t}\n\n\t\treturn cached_flow_field;\n\t}\n\n\tlog::log(DBG << \"Building flow field for portal \" << portal->get_id()\n\t             << \" from sector \" << other_sector_id);\n\n\tauto flow_field = std::make_shared<FlowField>(integration_field->get_size());\n\tflow_field->build(integration_field, other, other_sector_id, portal);\n\n\treturn flow_field;\n}\n\nIntegrator::get_return_t Integrator::get(const std::shared_ptr<CostField> &cost_field,\n                                         const coord::tile_delta &target) {\n\tauto integration_field = this->integrate(cost_field, target);\n\tauto flow_field = this->build(integration_field);\n\n\treturn std::make_pair(integration_field, flow_field);\n}\n\nIntegrator::get_return_t Integrator::get(const std::shared_ptr<CostField> &cost_field,\n                                         const std::shared_ptr<IntegrationField> &other,\n                                         sector_id_t other_sector_id,\n                                         const std::shared_ptr<Portal> &portal,\n                                         const coord::tile_delta &target,\n                                         const time::time_t &time,\n                                         bool with_los) {\n\tauto cache_key = std::make_pair(portal->get_id(), other_sector_id);\n\tif (cost_field->is_dirty(time)) {\n\t\tlog::log(DBG << \"Evicting cached integration and flow fields for portal \" << portal->get_id()\n\t\t             << \" from sector \" << other_sector_id);\n\t\tthis->field_cache->evict(cache_key);\n\t}\n\telse if (this->field_cache->is_cached(cache_key)) {\n\t\tlog::log(DBG << \"Using cached integration and flow fields for portal \" << portal->get_id()\n\t\t             << \" from sector \" << other_sector_id);\n\n\t\t// retrieve cached fields\n\t\tauto cached_fields = this->field_cache->get(cache_key);\n\t\tauto cached_integration_field = cached_fields.first;\n\t\tauto cached_flow_field = cached_fields.second;\n\n\t\tif (with_los) {\n\t\t\tlog::log(SPAM << \"Performing LOS pass on cached field\");\n\n\t\t\t// Make a copy of the cached integration field\n\t\t\tauto integration_field = std::make_shared<IntegrationField>(*cached_integration_field);\n\n\t\t\t// Only integrate LOS; leave the rest of the field as is\n\t\t\tintegration_field->integrate_los(cost_field, other, other_sector_id, portal, target);\n\n\t\t\tlog::log(SPAM << \"Transferring LOS flags to cached flow field\");\n\n\t\t\t// Make a copy of the cached flow field\n\t\t\tauto flow_field = std::make_shared<FlowField>(*cached_flow_field);\n\n\t\t\t// Transfer the LOS flags to the flow field\n\t\t\tflow_field->transfer_dynamic_flags(integration_field);\n\n\t\t\treturn std::make_pair(integration_field, flow_field);\n\t\t}\n\n\t\treturn std::make_pair(cached_integration_field, cached_flow_field);\n\t}\n\n\tauto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, time, with_los);\n\tauto flow_field = this->build(integration_field, other, other_sector_id, portal);\n\n\tlog::log(DBG << \"Caching integration and flow fields for portal ID: \" << portal->get_id()\n\t             << \", sector ID: \" << other_sector_id);\n\n\t// Copy the fields to the cache.\n\tstd::shared_ptr<IntegrationField> cached_integration_field = std::make_shared<IntegrationField>(*integration_field);\n\tcached_integration_field->reset_dynamic_flags();\n\n\tstd::shared_ptr<FlowField> cached_flow_field = std::make_shared<FlowField>(*flow_field);\n\tcached_flow_field->reset_dynamic_flags();\n\n\tfield_cache_t field_cache = field_cache_t(cached_integration_field, cached_flow_field);\n\n\tthis->field_cache->add(cache_key, field_cache);\n\n\treturn std::make_pair(integration_field, flow_field);\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/integrator.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"pathfinding/types.h\"\n#include \"pathfinding/field_cache.h\"\n#include \"util/hash.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace coord {\nstruct tile_delta;\n} // namespace coord\n\nnamespace path {\nclass CostField;\nclass FlowField;\nclass IntegrationField;\nclass Portal;\n\n/**\n * Integrator for the flow field pathfinding algorithm.\n */\nclass Integrator {\npublic:\n\t/**\n\t * Create a new integrator.\n\t */\n\tIntegrator();\n\n\t~Integrator() = default;\n\n\t/**\n\t * Integrate the cost field for a target.\n\t *\n\t * This should be used for the field containing the target cell.\n\t * The target coordinates must lie within the boundaries of the cost field.\n\t *\n\t * @param cost_field Cost field.\n\t * @param target Coordinates of the target cell.\n\t * @param with_los If true an LOS pass is performed before cost integration.\n\t *\n\t * @return Integration field.\n\t */\n\tstd::shared_ptr<IntegrationField> integrate(const std::shared_ptr<CostField> &cost_field,\n\t                                            const coord::tile_delta &target,\n\t                                            bool with_los = true);\n\n\t/**\n\t * Integrate the cost field from a portal.\n\t *\n\t * This should be used for the fields on the portal path that are not the target field.\n\t * The target coordinates must be relative to the origin of the sector the cost field belongs to.\n\t *\n\t * @param cost_field Cost field.\n\t * @param other Integration field of the other side of the portal.\n\t * @param other_sector_id Sector ID of the other side of the portal.\n\t * @param portal Portal.\n\t * @param target Coordinates of the target cell, relative to the integration field origin.\n\t * @param time Time of the path request.\n\t * @param with_los If true an LOS pass is performed before cost integration.\n\t *\n\t * @return Integration field.\n\t */\n\tstd::shared_ptr<IntegrationField> integrate(const std::shared_ptr<CostField> &cost_field,\n\t                                            const std::shared_ptr<IntegrationField> &other,\n\t                                            sector_id_t other_sector_id,\n\t                                            const std::shared_ptr<Portal> &portal,\n\t                                            const coord::tile_delta &target,\n\t\t\t\t\t\t\t\t\t\t\t\tconst time::time_t &time,\n\t                                            bool with_los = true);\n\n\t/**\n\t * Build the flow field from an integration field.\n\t *\n\t * @param integration_field Integration field.\n\t *\n\t * @return Flow field.\n\t */\n\tstd::shared_ptr<FlowField> build(const std::shared_ptr<IntegrationField> &integration_field);\n\n\t/**\n\t * Build the flow field from a portal.\n\t *\n\t * @param integration_field Integration field.\n\t * @param other Integration field of the other side of the portal.\n\t * @param other_sector_id Sector ID of the other side of the portal.\n\t * @param portal Portal.\n\t * @param with_los If true LOS flags are calculated if the flow field is in cache.\n\t *\n\t * @return Flow field.\n\t */\n\tstd::shared_ptr<FlowField> build(const std::shared_ptr<IntegrationField> &integration_field,\n\t                                 const std::shared_ptr<IntegrationField> &other,\n\t                                 sector_id_t other_sector_id,\n\t                                 const std::shared_ptr<Portal> &portal,\n\t                                 bool with_los = true);\n\n\tusing get_return_t = std::pair<std::shared_ptr<IntegrationField>, std::shared_ptr<FlowField>>;\n\n\t/**\n\t * Get the integration field and flow field for a target.\n\t *\n\t * @param cost_field Cost field.\n\t * @param target Coordinates of the target cell.\n\t *\n\t * @return Integration field and flow field.\n\t */\n\tget_return_t get(const std::shared_ptr<CostField> &cost_field,\n\t                 const coord::tile_delta &target);\n\n\t/**\n\t * Get the integration field and flow field from a portal.\n\t *\n\t * @param cost_field Cost field.\n\t * @param other Integration field of the other side of the portal.\n\t * @param other_sector_id Sector ID of the other side of the portal.\n\t * @param portal Portal.\n\t * @param target Coordinates of the target cell, relative to the integration field origin.\n\t * @param time Time of the path request.\n\t * @param with_los If true an LOS pass is performed before cost integration.\n\t *\n\t * @return Integration field and flow field.\n\t */\n\tget_return_t get(const std::shared_ptr<CostField> &cost_field,\n\t                 const std::shared_ptr<IntegrationField> &other,\n\t                 sector_id_t other_sector_id,\n\t                 const std::shared_ptr<Portal> &portal,\n\t                 const coord::tile_delta &target,\n\t\t\t\t\t const time::time_t &time,\n\t                 bool with_los = true);\n\nprivate:\n\t/**\n\t * Cache for already computed fields.\n\t */\n\tstd::unique_ptr<FieldCache> field_cache;\n};\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/CMakeLists.txt",
    "content": "add_sources(libopenage\n\ta_star.cpp\n\theuristics.cpp\n\tpath.cpp\n\ttests.cpp\n)\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/a_star.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n/** @file\n *\n * This file implements the A* search algorithm for openage.\n *\n * Literature:\n * Hart, Peter E., Nils J. Nilsson, and Bertram Raphael. \"A formal basis for\n * the heuristic determination of minimum cost paths.\"  Systems Science and\n * Cybernetics, IEEE Transactions on 4, no. 2 (1968): 100-107.\n */\n\n#include \"a_star.h\"\n\n#include <cmath>\n\n#include \"datastructure/pairing_heap.h\"\n#include \"log/log.h\"\n#include \"pathfinding/legacy/heuristics.h\"\n#include \"pathfinding/legacy/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage {\nnamespace path::legacy {\n\n\nPath to_point(coord::phys3 start,\n              coord::phys3 end,\n              std::function<bool(const coord::phys3 &)> passable) {\n\tauto valid_end = [&](const coord::phys3 &point) -> bool {\n\t\treturn euclidean_squared_cost(point, end) < path_grid_size.to_float();\n\t};\n\tauto heuristic = [&](const coord::phys3 &point) -> cost_old_t {\n\t\treturn euclidean_cost(point, end);\n\t};\n\treturn a_star(start, valid_end, heuristic, passable);\n}\n\nPath find_nearest(coord::phys3 start,\n                  std::function<bool(const coord::phys3 &)> valid_end,\n                  std::function<bool(const coord::phys3 &)> passable) {\n\t// Use Dijkstra (heuristic = 0)\n\tauto zero = [](const coord::phys3 &) -> cost_old_t { return .0f; };\n\treturn a_star(start, valid_end, zero, passable);\n}\n\nPath a_star(coord::phys3 start,\n            std::function<bool(const coord::phys3 &)> valid_end,\n            std::function<cost_old_t(const coord::phys3 &)> heuristic,\n            std::function<bool(const coord::phys3 &)> passable) {\n\t// path node storage, always provides cheapest next node.\n\theap_t node_candidates;\n\n\t// list of known tiles and corresponding node.\n\tnodemap_t visited_tiles;\n\n\t// add starting node\n\tnode_pt start_node = std::make_shared<Node>(start, nullptr, .0f, heuristic(start));\n\tvisited_tiles[start_node->position] = start_node;\n\tnode_candidates.push(start_node);\n\n\tstart_node->heap_node = node_candidates.push(start_node);\n\n\t// track the closest we can get to the end position\n\t// used when no path is found\n\tnode_pt closest_node = start_node;\n\n\t// while there are candidates to visit\n\twhile (not node_candidates.empty()) {\n\t\tnode_pt best_candidate = node_candidates.pop();\n\n\t\tbest_candidate->was_best = true;\n\n\t\t// node to terminate the search was found\n\t\tif (valid_end(best_candidate->position)) {\n\t\t\tlog::log(MSG(dbg) << \"path cost is \" << util::FloatFixed<3, 8>{closest_node->future_cost});\n\n\t\t\treturn best_candidate->generate_backtrace();\n\t\t}\n\n\t\t// closest node for cases when target cannot be reached\n\t\tif (best_candidate->heuristic_cost < closest_node->heuristic_cost) {\n\t\t\tclosest_node = best_candidate;\n\t\t}\n\n\t\t// evaluate all neighbors of the current candidate for further progress\n\t\tfor (node_pt neighbor : best_candidate->get_neighbors(visited_tiles)) {\n\t\t\tif (neighbor->was_best) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (not passable_line(best_candidate, neighbor, passable)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tbool not_visited = (visited_tiles.count(neighbor->position) == 0);\n\t\t\tcost_old_t new_past_cost = best_candidate->past_cost + best_candidate->cost_to(*neighbor);\n\n\t\t\t// if new cost is better than the previous path\n\t\t\tif (not_visited or new_past_cost < neighbor->past_cost) {\n\t\t\t\tif (not_visited) {\n\t\t\t\t\t// calculate heuristic only once per node\n\t\t\t\t\tneighbor->heuristic_cost = heuristic(neighbor->position);\n\t\t\t\t}\n\t\t\t\tif (neighbor->heuristic_cost > closest_node->heuristic_cost * 3) {\n\t\t\t\t\tcontinue; // dont search forever...\n\t\t\t\t}\n\n\t\t\t\t// update new cost knowledge\n\t\t\t\tneighbor->past_cost = new_past_cost;\n\t\t\t\tneighbor->future_cost = neighbor->past_cost + neighbor->heuristic_cost;\n\t\t\t\tneighbor->path_predecessor = best_candidate;\n\n\t\t\t\tif (not_visited) {\n\t\t\t\t\tneighbor->heap_node = node_candidates.push(neighbor);\n\t\t\t\t\tvisited_tiles[neighbor->position] = neighbor;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tnode_candidates.decrease(neighbor->heap_node);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlog::log(MSG(dbg) << \"incomplete path cost is \" << util::FloatFixed<3, 8>{closest_node->future_cost});\n\n\treturn closest_node->generate_backtrace();\n}\n\n\n} // namespace path::legacy\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/a_star.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"heuristics.h\"\n#include \"path.h\"\n\nnamespace openage {\n\nnamespace path::legacy {\n\n/**\n * path between two static points\n */\nPath to_point(coord::phys3 start,\n              coord::phys3 end,\n              std::function<bool(const coord::phys3 &)> passable);\n\n/**\n * path to nearest object with lambda\n */\nPath find_nearest(coord::phys3 start,\n                  std::function<bool(const coord::phys3 &)> valid_end,\n                  std::function<bool(const coord::phys3 &)> passable);\n\n/**\n * finds a path between two endpoints\n * @param start the starting tile coords\n * @param end the ending tile coords\n * @param heuristic the heuristic for evaluating cost\n * @param passable lambda to decide which terrain is passable\n * @return path between the given tiles\n */\nPath a_star(coord::phys3 start,\n            std::function<bool(const coord::phys3 &)> valid_end,\n            std::function<cost_old_t(const coord::phys3 &)> heuristic,\n            std::function<bool(const coord::phys3 &)> passable);\n\n} // namespace path::legacy\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/heuristics.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include \"heuristics.h\"\n\n#include <algorithm>\n#include <cmath>\n\n\nnamespace openage {\nnamespace path::legacy {\n\ncost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) {\n\tcost_old_t dx = std::abs(start.ne - end.ne).to_float();\n\tcost_old_t dy = std::abs(start.se - end.se).to_float();\n\treturn dx + dy;\n}\n\ncost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) {\n\tcost_old_t dx = std::abs(start.ne - end.ne).to_float();\n\tcost_old_t dy = std::abs(start.se - end.se).to_float();\n\treturn std::max(dx, dy);\n}\n\ncost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) {\n\treturn (end - start).length();\n}\n\ncost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) {\n\tcost_old_t dx = (start.ne - end.ne).to_float();\n\tcost_old_t dy = (start.se - end.se).to_float();\n\treturn dx * dx + dy * dy;\n}\n\n} // namespace path::legacy\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/heuristics.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"pathfinding/legacy/path.h\"\n\nnamespace openage {\nnamespace path::legacy {\n\n/**\n * function pointer type for distance estimation functions.\n */\nusing heuristic_t = cost_old_t (*)(const coord::phys3 &start, const coord::phys3 &end);\n\n/**\n * Manhattan distance cost estimation.\n * @returns the sum of the x and y difference.\n */\ncost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end);\n\n/**\n * Chebyshev distance cost estimation.\n * @returns y or x difference, whichever is higher.\n */\ncost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end);\n\n/**\n * Euclidean distance cost estimation.\n * @returns the hypotenuse length of the rectangular triangle formed.\n */\ncost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end);\n\n/**\n * Squared euclidean distance cost estimation.\n * @returns the square of the hypotenuse length of the rectangular triangle formed.\n */\ncost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end);\n\n/**\n * Calculate euclidean distance from a already calculated squared euclidean distance\n */\ncost_old_t euclidean_squared_to_euclidean_cost(const cost_old_t euclidean_squared_value);\n\n} // namespace path::legacy\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/path.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include <cmath>\n\n#include \"pathfinding/legacy/path.h\"\n\nnamespace openage::path::legacy {\n\n\nbool compare_node_cost::operator()(const node_pt &lhs, const node_pt &rhs) const {\n\t// TODO: use node operator <\n\treturn lhs->future_cost < rhs->future_cost;\n}\n\n\nNode::Node(const coord::phys3 &pos, node_pt prev) :\n\tposition(pos),\n\ttile_position(pos.to_tile3().to_tile()),\n\tdirection{},\n\tvisited{false},\n\twas_best{false},\n\tfactor{1.0f},\n\tpath_predecessor{prev},\n\theap_node(nullptr) {\n\tif (prev) {\n\t\tthis->direction = (this->position - prev->position).normalize();\n\n\t\t// TODO: add dot product to coord\n\t\tcost_old_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float()));\n\t\tthis->factor += (1 - similarity);\n\t}\n}\n\n\nNode::Node(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic) :\n\tNode{pos, prev} {\n\tthis->past_cost = past;\n\tthis->heuristic_cost = heuristic;\n\tthis->future_cost = past + heuristic;\n}\n\n\nbool Node::operator<(const Node &other) const {\n\treturn this->future_cost < other.future_cost;\n}\n\n\nbool Node::operator==(const Node &other) const {\n\treturn this->position == other.position;\n}\n\n\ncost_old_t Node::cost_to(const Node &other) const {\n\t// ignore the up-position, thus convert to phys2\n\treturn ((this->position - other.position).to_phys2().length()\n\t        * other.factor * this->factor);\n}\n\n\nPath Node::generate_backtrace() {\n\tstd::vector<Node> waypoints;\n\n\tnode_pt current = this->shared_from_this();\n\tdo {\n\t\tNode other = *current;\n\t\twaypoints.push_back(*current);\n\t\tcurrent = current->path_predecessor;\n\t}\n\twhile (current != nullptr);\n\twaypoints.pop_back(); // remove start\n\n\treturn {waypoints};\n}\n\n\nstd::vector<node_pt> Node::get_neighbors(const nodemap_t &nodes, float scale) {\n\tstd::vector<node_pt> neighbors;\n\tneighbors.reserve(8);\n\tfor (int n = 0; n < 8; ++n) {\n\t\tcoord::phys3 n_pos = this->position + (neigh_phys[n] * scale);\n\n\t\tif (nodes.count(n_pos) > 0) {\n\t\t\tneighbors.push_back(nodes.at(n_pos));\n\t\t}\n\t\telse {\n\t\t\tneighbors.push_back(std::make_shared<Node>(n_pos, this->shared_from_this()));\n\t\t}\n\t}\n\treturn neighbors;\n}\n\n\nbool passable_line(node_pt start, node_pt end, std::function<bool(const coord::phys3 &)> passable, float samples) {\n\t// interpolate between points and make passablity checks\n\t// (dont check starting position)\n\tfor (int i = 1; i <= samples; ++i) {\n\t\t// TODO: needs more fixed-point\n\t\tdouble percent = static_cast<double>(i) / samples;\n\t\tcoord::phys_t ne = (1.0 - percent) * start->position.ne.to_double() + percent * end->position.ne.to_double();\n\t\tcoord::phys_t se = (1.0 - percent) * start->position.se.to_double() + percent * end->position.se.to_double();\n\t\tcoord::phys_t up = (1.0 - percent) * start->position.up.to_double() + percent * end->position.up.to_double();\n\n\t\tif (!passable(coord::phys3{ne, se, up})) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n\nPath::Path(const std::vector<Node> &nodes) :\n\twaypoints{nodes} {}\n\n\n} // namespace openage::path::legacy\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/path.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <unordered_map>\n#include <vector>\n\n#include \"coord/phys.h\"\n#include \"coord/tile.h\"\n#include \"datastructure/pairing_heap.h\"\n#include \"util/hash.h\"\n#include \"util/misc.h\"\n\n\nnamespace openage {\n\nnamespace path::legacy {\n\nclass Node;\nclass Path;\n\n/**\n * The data type for movement cost\n */\nusing cost_old_t = float;\n\n/**\n * Type for storing navigation nodes.\n */\nusing node_pt = std::shared_ptr<Node>;\n\n/**\n * Type for mapping tiles to nodes.\n */\nusing nodemap_t = std::unordered_map<coord::phys3, node_pt>;\n\n\n/**\n * Cost comparison for node_pt.\n * Extracts the ptr from the shared_ptr.\n * Calls operator < on Node.\n */\nstruct compare_node_cost {\n\tbool operator()(const node_pt &lhs, const node_pt &rhs) const;\n};\n\n/**\n * Priority queue node item type.\n */\nusing heap_t = datastructure::PairingHeap<node_pt, compare_node_cost>;\n\n/**\n * Size of phys-coord grid for path nodes.\n */\nconstexpr coord::phys_t path_grid_size{1.f / 8};\n\n/**\n * Phys3 delta coordinates to select for path neighbors.\n */\nconstexpr coord::phys3_delta const neigh_phys[] = {\n\t{path_grid_size * 1, path_grid_size * -1, 0},\n\t{path_grid_size * 1, path_grid_size * 0, 0},\n\t{path_grid_size * 1, path_grid_size * 1, 0},\n\t{path_grid_size * 0, path_grid_size * 1, 0},\n\t{path_grid_size * -1, path_grid_size * 1, 0},\n\t{path_grid_size * -1, path_grid_size * 0, 0},\n\t{path_grid_size * -1, path_grid_size * -1, 0},\n\t{path_grid_size * 0, path_grid_size * -1, 0}};\n\n/**\n *\n */\nbool passable_line(node_pt start, node_pt end, std::function<bool(const coord::phys3 &)> passable, float samples = 5.0f);\n\n/**\n * One navigation waypoint in a path.\n */\nclass Node : public std::enable_shared_from_this<Node> {\npublic:\n\tNode(const coord::phys3 &pos, node_pt prev);\n\tNode(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic);\n\n\t/**\n\t * Orders nodes according to their future cost value.\n\t */\n\tbool operator<(const Node &other) const;\n\n\t/**\n\t * Compare the node to another one.\n\t * They are the same if their position is.\n\t */\n\tbool operator==(const Node &other) const;\n\n\t/**\n\t * Calculates the actual movement cose to another node.\n\t */\n\tcost_old_t cost_to(const Node &other) const;\n\n\t/**\n\t * Create a backtrace path beginning at this node.\n\t */\n\tPath generate_backtrace();\n\n\t/**\n\t * Get all neighbors of this graph node.\n\t */\n\tstd::vector<node_pt> get_neighbors(const nodemap_t &, float scale = 1.0f);\n\n\t/**\n\t * The tile position this node is associated to.\n\t * todo make const\n\t */\n\tcoord::phys3 position;\n\tcoord::tile tile_position;\n\tcoord::phys3_delta direction; // for path smoothing\n\n\t/**\n\t * Future cost estimation value for this node.\n\t */\n\tcost_old_t future_cost;\n\n\t/**\n\t * Evaluated past cost value for the node.\n\t * This stores the actual cost from start to this node.\n\t */\n\tcost_old_t past_cost;\n\n\t/**\n\t * Heuristic cost cache.\n\t * Calculated once, is the heuristic distance from this node\n\t * to the goal.\n\t */\n\tcost_old_t heuristic_cost;\n\n\t/**\n\t * Can this node be passed?\n\t */\n\tbool accessible = false;\n\n\t/**\n\t * Has this Node been visited?\n\t */\n\tbool visited = false;\n\n\t/**\n\t * Does this node already have an alternative path?\n\t * If the node was once selected as the best next hop,\n\t * this is set to true.\n\t */\n\tbool was_best = false;\n\n\t/**\n\t * Factor to adjust movement cost.\n\t * default: 1\n\t */\n\tcost_old_t factor;\n\n\t/**\n\t * Node where this one was reached by least cost.\n\t */\n\tnode_pt path_predecessor;\n\n\t/**\n\t * Priority queue node that contains this path node.\n\t */\n\theap_t::element_t heap_node;\n};\n\n\n/**\n * Represents a planned trajectory.\n * Generated by pathfinding algorithms.\n */\nclass Path {\npublic:\n\tPath() = default;\n\tPath(const std::vector<Node> &nodes);\n\n\t/**\n\t * These are the waypoints to navigate in order.\n\t * Includes the start and end node.\n\t */\n\tstd::vector<Node> waypoints;\n};\n\n} // namespace path::legacy\n} // namespace openage\n\n\nnamespace std {\n\n/**\n * Hash function for path nodes.\n * Just uses their position.\n */\ntemplate <>\nstruct hash<openage::path::legacy::Node &> {\n\tsize_t operator()(const openage::path::legacy::Node &x) const {\n\t\topenage::coord::phys3 node_pos = x.position;\n\t\tsize_t hash = openage::util::type_hash<openage::path::legacy::Node>();\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::phys_t>{}(node_pos.ne));\n\t\thash = openage::util::hash_combine(hash, std::hash<openage::coord::phys_t>{}(node_pos.se));\n\t\treturn hash;\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/path_utils.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage {\nnamespace path::legacy {\n\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/legacy/tests.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"log/log.h\"\n#include \"testing/testing.h\"\n\n#include \"pathfinding/legacy/heuristics.h\"\n#include \"pathfinding/legacy/path.h\"\n\nnamespace openage {\nnamespace path {\nnamespace tests {\n\n/**\n * This function tests setting up basic nodes that point to a previous node.\n * Tests that direction is set correctly and that factor is set correctly.\n */\nvoid node_0() {\n\tcoord::phys3 p0{0, 0, 0};\n\tcoord::phys3 p1{1, 0, 0};\n\tcoord::phys3 p2{1, 1, 0};\n\tcoord::phys3 p3{1, -1, 0};\n\tcoord::phys3 p4{2, 0, 0};\n\tcoord::phys3 p5{2, 2, 0};\n\tcoord::phys3 p6{2, -2, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::node_pt n1 = std::make_unique<legacy::Node>(p1, n0);\n\tlegacy::node_pt n2 = std::make_unique<legacy::Node>(p2, n1);\n\tlegacy::node_pt n3 = std::make_unique<legacy::Node>(p3, n1);\n\tlegacy::node_pt n4 = std::make_unique<legacy::Node>(p0, n1);\n\n\t// Testing how the factor is effected from the change in\n\t// direction from one node to another\n\tTESTEQUALS(n1->direction.ne, 1);\n\tTESTEQUALS(n1->direction.se, 0);\n\n\t// Expect this to be 2 since the similarity between nodes is zero\n\tTESTEQUALS(n1->factor, 2);\n\n\tTESTEQUALS(n2->direction.ne, 0);\n\tTESTEQUALS(n2->direction.se, 1);\n\n\t// Expect this to be 2 since it takes a 90 degree turn from n1\n\tTESTEQUALS(n2->factor, 2);\n\n\tTESTEQUALS(n3->direction.ne, 0);\n\tTESTEQUALS(n3->direction.se, -1);\n\n\t// Expect this to be 2 since it takes a 90 degree turn from n1\n\tTESTEQUALS(n3->factor, 2);\n\n\tTESTEQUALS(n4->direction.ne, -1);\n\tTESTEQUALS(n4->direction.se, 0);\n\n\t// Expect this to be 3 since it takes a 180 degree turn from n1\n\tTESTEQUALS(n4->factor, 3);\n\n\t// Testing that the distance from the previous node noes not\n\t// effect the factor, only change in direction\n\n\tn1 = std::make_unique<legacy::Node>(p4, n0);\n\tn2 = std::make_unique<legacy::Node>(p5, n1);\n\tn3 = std::make_unique<legacy::Node>(p6, n1);\n\tn4 = std::make_unique<legacy::Node>(p0, n1);\n\n\tTESTEQUALS(n1->direction.ne, 1);\n\tTESTEQUALS(n1->direction.se, 0);\n\n\t// Expect this to be 2 since the similarity between nodes is zero\n\tTESTEQUALS(n1->factor, 2);\n\tTESTEQUALS(n2->direction.ne, 0);\n\tTESTEQUALS(n2->direction.se, 1);\n\n\t// Expect this to be 2 since it takes a 90 degree turn from n1\n\tTESTEQUALS(n2->factor, 2);\n\tTESTEQUALS(n3->direction.ne, 0);\n\tTESTEQUALS(n3->direction.se, -1);\n\n\t// Expect this to be 2 since it takes a 90 degree turn from n1\n\tTESTEQUALS(n3->factor, 2);\n\tTESTEQUALS(n4->direction.ne, -1);\n\tTESTEQUALS(n4->direction.se, 0);\n\n\t// Expect this to be 3 since it takes a 180 degree turn from n1\n\tTESTEQUALS(n4->factor, 3);\n}\n\n/**\n * This function tests Node->cost_to. The testing is done on 2 unrelated\n * nodes (They have no previous node) to test the basic cost without adding\n * the cost from node->factor.\n */\nvoid node_cost_to_0() {\n\t// Testing basic cost_to with ne only\n\tcoord::phys3 p0{0, 0, 0};\n\tcoord::phys3 p1{10, 0, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::node_pt n1 = std::make_unique<legacy::Node>(p1, nullptr);\n\n\tTESTEQUALS(n0->cost_to(*n1), 10);\n\tTESTEQUALS(n1->cost_to(*n0), 10);\n\n\t// Testing basic cost_to with se only\n\tcoord::phys3 p2{0, 5, 0};\n\n\tlegacy::node_pt n2 = std::make_unique<legacy::Node>(p2, nullptr);\n\n\tTESTEQUALS(n0->cost_to(*n2), 5);\n\tTESTEQUALS(n2->cost_to(*n0), 5);\n\n\t// Testing cost_to with both se and ne:\n\tcoord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5\n\n\tlegacy::node_pt n3 = std::make_unique<legacy::Node>(p3, nullptr);\n\tTESTEQUALS(n0->cost_to(*n3), 5);\n\tTESTEQUALS(n3->cost_to(*n0), 5);\n\n\t// Test cost_to and check that `up` has no effect\n\tcoord::phys3 p4{3, 4, 8};\n\n\tlegacy::node_pt n4 = std::make_unique<legacy::Node>(p4, nullptr);\n\n\tTESTEQUALS(n0->cost_to(*n4), 5);\n\tTESTEQUALS(n4->cost_to(*n0), 5);\n}\n\n/**\n * This function tests Node->cost_to. The testing is done on the neighbor\n * nodes to test how the directional factor effects the cost.\n */\nvoid node_cost_to_1() {\n\t// Set up coords so that n1 will have a direction of ne = 1\n\t// but n0 with not be in n1s neighbors\n\tcoord::phys3 p0{-0.125, 0, 0};\n\tcoord::phys3 p1{0.125, 0, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::node_pt n1 = std::make_unique<legacy::Node>(p1, n0);\n\n\t// We expect twice the normal cost since n0 had not direction\n\t// thus we get a factor of 2 on n1\n\tTESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001);\n\tTESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001);\n\n\tlegacy::nodemap_t visited_tiles;\n\tvisited_tiles[n0->position] = n0;\n\n\t// Collect the costs to go to all the neighbors of n1\n\tstd::vector<float> costs;\n\tfor (legacy::node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) {\n\t\tcosts.push_back(n1->cost_to(*neighbor));\n\t}\n\n\tTESTEQUALS_FLOAT(costs[0], 0.45711, 0.001);\n\tTESTEQUALS_FLOAT(costs[1], 0.25, 0.001);\n\tTESTEQUALS_FLOAT(costs[2], 0.45711, 0.001);\n\tTESTEQUALS_FLOAT(costs[3], 0.5, 0.001);\n\tTESTEQUALS_FLOAT(costs[4], 0.95709, 0.001);\n\tTESTEQUALS_FLOAT(costs[5], 0.75, 0.001);\n\tTESTEQUALS_FLOAT(costs[6], 0.95709, 0.001);\n\tTESTEQUALS_FLOAT(costs[7], 0.5, 0.001);\n}\n\n/**\n * This function does a basic test of generating a backtrace from the\n * last node in a path.\n */\nvoid node_generate_backtrace_0() {\n\tcoord::phys3 p0{0, 0, 0};\n\tcoord::phys3 p1{10, 0, 0};\n\tcoord::phys3 p2{20, 0, 0};\n\tcoord::phys3 p3{30, 0, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::node_pt n1 = std::make_unique<legacy::Node>(p1, n0);\n\tlegacy::node_pt n2 = std::make_unique<legacy::Node>(p2, n1);\n\tlegacy::node_pt n3 = std::make_unique<legacy::Node>(p3, n2);\n\n\tlegacy::Path path = n3->generate_backtrace();\n\n\t(path.waypoints[0] == *n3) or TESTFAIL;\n\t(path.waypoints[1] == *n2) or TESTFAIL;\n\t(path.waypoints[2] == *n1) or TESTFAIL;\n}\n\n/**\n * This function tests Node->get_neighbors and how the scale effects\n * the neighbors given.\n */\nvoid node_get_neighbors_0() {\n\tcoord::phys3 p0{0, 0, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::nodemap_t map;\n\n\t// Testing get_neighbors returning all surounding tiles with\n\t// a factor of 1\n\n\tstd::vector<legacy::node_pt> neighbors = n0->get_neighbors(map, 1);\n\tTESTEQUALS(neighbors.size(), 8);\n\n\tTESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.125, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.125, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.125, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.125, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.125, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.125, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.125, 0.001);\n\n\t// Testing how a larger scale changes the neighbors generated\n\tneighbors = n0->get_neighbors(map, 2);\n\tTESTEQUALS(neighbors.size(), 8);\n\n\tTESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.25, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.25, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.25, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.25, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.25, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.25, 0.001);\n\n\tTESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001);\n\tTESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.25, 0.001);\n}\n\n/**\n * This is a helper passable function that alwalys returns true.\n */\nbool always_passable(const coord::phys3 &) {\n\treturn true;\n}\n\n/**\n * This is a helper passable function that always returns false.\n */\nbool not_passable(const coord::phys3 &) {\n\treturn false;\n}\n\n/**\n * This is a helper passable function that only returns true when\n * pos.ne == 20.\n */\nbool sometimes_passable(const coord::phys3 &pos) {\n\tif (pos.ne == 20) {\n\t\treturn false;\n\t}\n\telse {\n\t\treturn true;\n\t}\n}\n\n/**\n * This function tests passable_line. Tests with always false, always true,\n * and position dependant functions being passed in as args.\n */\nvoid node_passable_line_0() {\n\tcoord::phys3 p0{0, 0, 0};\n\tcoord::phys3 p1{1000, 0, 0};\n\n\tlegacy::node_pt n0 = std::make_unique<legacy::Node>(p0, nullptr);\n\tlegacy::node_pt n1 = std::make_unique<legacy::Node>(p1, n0);\n\n\tTESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::always_passable), true);\n\tTESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::not_passable), false);\n\n\t// The next 2 cases show that a different sample can change the results\n\t// for the same path\n\tTESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 10), true);\n\tTESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 50), false);\n}\n\n/**\n * Top level node test.\n */\nvoid path_node() {\n\tnode_0();\n\tnode_cost_to_0();\n\tnode_cost_to_1();\n\tnode_generate_backtrace_0();\n\tnode_get_neighbors_0();\n\tnode_passable_line_0();\n}\n\n} // namespace tests\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/path.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"path.h\"\n\n\nnamespace openage::path {\n\n// this file is intentionally empty\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/path.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"coord/tile.h\"\n#include \"pathfinding/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::path {\n\n/**\n * Path request for the pathfinder.\n */\nstruct PathRequest {\n\t/// ID of the grid to use for pathfinding.\n\tsize_t grid_id;\n\t/// Start position of the path.\n\tcoord::tile start;\n\t/// Target position of the path.\n\tcoord::tile target;\n\t/// Time the request was made.\n\tconst time::time_t time;\n};\n\n/**\n * Path found by the pathfinder.\n */\nstruct Path {\n\t/// ID of the grid to used for pathfinding.\n\tsize_t grid_id;\n\t/// Status\n\tPathResult status;\n\t/// Waypoints of the path.\n\t/// First waypoint is the start position of the path request.\n\t/// Last waypoint is the target position of the path request.\n\tstd::vector<coord::tile> waypoints;\n};\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/pathfinder.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"pathfinder.h\"\n\n#include \"coord/chunk.h\"\n#include \"coord/phys.h\"\n#include \"error/error.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/flow_field.h\"\n#include \"pathfinding/grid.h\"\n#include \"pathfinding/integration_field.h\"\n#include \"pathfinding/integrator.h\"\n#include \"pathfinding/portal.h\"\n#include \"pathfinding/sector.h\"\n\n\nnamespace openage::path {\n\nPathfinder::Pathfinder() :\n\tgrids{},\n\tintegrator{std::make_shared<Integrator>()} {\n}\n\nconst Path Pathfinder::get_path(const PathRequest &request) {\n\tauto grid = this->grids.at(request.grid_id);\n\tauto sector_size = grid->get_sector_size();\n\n\t// Check if the target is within the grid\n\tauto grid_size = grid->get_size();\n\tauto grid_width = grid_size[0] * sector_size;\n\tauto grid_height = grid_size[1] * sector_size;\n\tif (request.target.ne < 0\n\t    or request.target.se < 0\n\t    or request.target.ne >= static_cast<coord::tile_t>(grid_width)\n\t    or request.target.se >= static_cast<coord::tile_t>(grid_height)) {\n\t\tlog::log(DBG << \"Path not found (start = \"\n\t\t             << request.start << \"; target = \"\n\t\t             << request.target << \"): \"\n\t\t             << \"Target is out of bounds.\");\n\t\treturn Path{request.grid_id, PathResult::OUT_OF_BOUNDS, {}};\n\t}\n\n\tauto start_sector_x = request.start.ne / sector_size;\n\tauto start_sector_y = request.start.se / sector_size;\n\tauto start_sector = grid->get_sector(start_sector_x, start_sector_y);\n\n\tauto target_sector_x = request.target.ne / sector_size;\n\tauto target_sector_y = request.target.se / sector_size;\n\tauto target_sector = grid->get_sector(target_sector_x, target_sector_y);\n\n\tauto target = request.target - target_sector->get_position().to_tile(sector_size);\n\tif (target_sector->get_cost_field()->get_cost(target) == COST_IMPASSABLE) {\n\t\t// TODO: This may be okay if the target is a building or unit\n\t\tlog::log(DBG << \"Path not found (start = \"\n\t\t             << request.start << \"; target = \"\n\t\t             << request.target << \"): \"\n\t\t             << \"Target is impassable.\");\n\t\treturn Path{request.grid_id, PathResult::NOT_FOUND, {}};\n\t}\n\n\t// Integrate the target field\n\tcoord::tile_delta target_delta = request.target - target_sector->get_position().to_tile(sector_size);\n\tauto target_integration_field = this->integrator->integrate(target_sector->get_cost_field(),\n\t                                                            target_delta);\n\n\tif (target_sector == start_sector) {\n\t\tauto start = request.start - start_sector->get_position().to_tile(sector_size);\n\n\t\tif (target_integration_field->get_cell(start.ne, start.se).cost != INTEGRATED_COST_UNREACHABLE) {\n\t\t\t// Exit early if the start and target are in the same sector\n\t\t\t// and are reachable from within the same sector\n\t\t\tauto flow_field = this->integrator->build(target_integration_field);\n\t\t\tauto flow_field_waypoints = this->get_waypoints({std::make_pair(target_sector->get_id(), flow_field)}, request);\n\n\t\t\tstd::vector<coord::tile> waypoints{};\n\t\t\tif (flow_field_waypoints.at(0) != request.start) {\n\t\t\t\twaypoints.push_back(request.start);\n\t\t\t}\n\t\t\twaypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end());\n\n\t\t\tlog::log(DBG << \"Path found (start = \"\n\t\t\t             << request.start << \"; target = \"\n\t\t\t             << request.target << \"): \"\n\t\t\t             << \"Path is within the same sector.\");\n\t\t\treturn Path{request.grid_id, PathResult::FOUND, waypoints};\n\t\t}\n\t}\n\n\t// Check which portals are reachable from the target field\n\tstd::unordered_set<portal_id_t> target_portal_ids;\n\tfor (auto &portal : target_sector->get_portals()) {\n\t\tauto center_cell = portal->get_entry_center(target_sector->get_id());\n\n\t\tif (target_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) {\n\t\t\ttarget_portal_ids.insert(portal->get_id());\n\t\t}\n\t}\n\n\t// Check which portals are reachable from the start field\n\tcoord::tile_delta start = request.start - start_sector->get_position().to_tile(sector_size);\n\tauto start_integration_field = this->integrator->integrate(start_sector->get_cost_field(),\n\t                                                           start,\n\t                                                           false);\n\n\tstd::unordered_set<portal_id_t> start_portal_ids;\n\tfor (auto &portal : start_sector->get_portals()) {\n\t\tauto center_cell = portal->get_entry_center(start_sector->get_id());\n\n\t\tif (start_integration_field->get_cell(center_cell).cost != INTEGRATED_COST_UNREACHABLE) {\n\t\t\tstart_portal_ids.insert(portal->get_id());\n\t\t}\n\t}\n\n\tif (target_portal_ids.empty() or start_portal_ids.empty()) {\n\t\t// Exit early if no portals are reachable from the start or target\n\t\tlog::log(DBG << \"Path not found (start = \"\n\t\t             << request.start << \"; target = \"\n\t\t             << request.target << \"): \"\n\t\t             << \"No portals are reachable from the start or target.\");\n\t\treturn Path{request.grid_id, PathResult::NOT_FOUND, {}};\n\t}\n\n\t// High-level pathfinding\n\t// Find the portals to use to get from the start to the target\n\tauto portal_result = this->portal_a_star(request, target_portal_ids, start_portal_ids);\n\tauto portal_status = portal_result.first;\n\tauto portal_path = portal_result.second;\n\n\t// Low-level pathfinding\n\t// Find the path within the sectors\n\n\t// Build flow field for the target sector\n\tauto prev_integration_field = target_integration_field;\n\tauto prev_flow_field = this->integrator->build(prev_integration_field);\n\tauto prev_sector_id = target_sector->get_id();\n\n\tIntegrator::get_return_t sector_fields{prev_integration_field, prev_flow_field};\n\n\tstd::vector<std::pair<sector_id_t, std::shared_ptr<FlowField>>> flow_fields;\n\tflow_fields.reserve(portal_path.size() + 1);\n\tflow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second));\n\n\tint los_depth = 1;\n\n\tfor (auto &portal : portal_path) {\n\t\tauto prev_sector = grid->get_sector(prev_sector_id);\n\t\tauto next_sector_id = portal->get_exit_sector(prev_sector_id);\n\t\tauto next_sector = grid->get_sector(next_sector_id);\n\n\t\ttarget_delta = request.target - next_sector->get_position().to_tile(sector_size);\n\t\tbool with_los = los_depth > 0;\n\n\t\tsector_fields = this->integrator->get(next_sector->get_cost_field(),\n\t\t                                      prev_integration_field,\n\t\t                                      prev_sector_id,\n\t\t                                      portal,\n\t\t                                      target_delta,\n\t\t\t\t\t\t\t\t\t\t\t  request.time,\n\t\t                                      with_los);\n\t\tflow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second));\n\n\t\tprev_integration_field = sector_fields.first;\n\t\tprev_sector_id = next_sector_id;\n\t\tlos_depth -= 1;\n\t}\n\n\t// reverse the flow fields so they are ordered from start to target\n\tstd::reverse(flow_fields.begin(), flow_fields.end());\n\n\t// traverse the flow fields to get the waypoints\n\tauto flow_field_waypoints = this->get_waypoints(flow_fields, request);\n\tstd::vector<coord::tile> waypoints{};\n\tif (flow_field_waypoints.at(0) != request.start) {\n\t\twaypoints.push_back(request.start);\n\t}\n\twaypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end());\n\n\tif (portal_status == PathResult::NOT_FOUND) {\n\t\tlog::log(DBG << \"Path not found (start = \"\n\t\t             << request.start << \"; target = \"\n\t\t             << request.target << \")\");\n\t}\n\telse {\n\t\tlog::log(DBG << \"Path found (start = \"\n\t\t             << request.start << \"; target = \"\n\t\t             << request.target << \")\");\n\t}\n\n\treturn Path{request.grid_id, portal_status, waypoints};\n}\n\nconst std::shared_ptr<Grid> &Pathfinder::get_grid(grid_id_t id) const {\n\treturn this->grids.at(id);\n}\n\nvoid Pathfinder::add_grid(const std::shared_ptr<Grid> &grid) {\n\tthis->grids[grid->get_id()] = grid;\n}\n\nconst Pathfinder::portal_star_t Pathfinder::portal_a_star(const PathRequest &request,\n                                                          const std::unordered_set<portal_id_t> &target_portal_ids,\n                                                          const std::unordered_set<portal_id_t> &start_portal_ids) const {\n\tstd::vector<std::shared_ptr<Portal>> result;\n\n\tauto grid = this->grids.at(request.grid_id);\n\tauto &portal_map = grid->get_portal_map();\n\tauto sector_size = grid->get_sector_size();\n\n\tauto start_sector_x = request.start.ne / sector_size;\n\tauto start_sector_y = request.start.se / sector_size;\n\tauto start_sector = grid->get_sector(start_sector_x, start_sector_y);\n\n\t// path node storage, always provides cheapest next node.\n\theap_t node_candidates;\n\n\tstd::unordered_set<portal_id_t> visited_portals;\n\n\t// TODO: Compute cost to travel from one portal to another when creating portals\n\t// const int distance_cost = 1;\n\n\t// create start nodes\n\tfor (auto &portal : start_sector->get_portals()) {\n\t\tif (not start_portal_ids.contains(portal->get_id())) {\n\t\t\t// only consider portals that are reachable from the start cell\n\t\t\tcontinue;\n\t\t}\n\n\t\tauto &portal_node = portal_map.at(portal->get_id());\n\t\tportal_node->entry_sector = start_sector->get_id();\n\n\t\tauto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position().to_tile(sector_size);\n\t\tauto portal_pos = portal->get_exit_center(start_sector->get_id());\n\t\tauto portal_abs_pos = sector_pos + portal_pos;\n\t\tauto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target);\n\t\tportal_node->current_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.start);\n\t\tportal_node->heuristic_cost = heuristic_cost;\n\t\tportal_node->future_cost = portal_node->current_cost + heuristic_cost;\n\n\t\tportal_node->heap_node = node_candidates.push(portal_node);\n\t\tportal_node->prev_portal = nullptr;\n\t\tportal_node->was_best = false;\n\t\tvisited_portals.insert(portal->get_id());\n\t}\n\n\t// track the closest we can get to the end position\n\t// used when no path is found\n\tauto closest_node = node_candidates.top();\n\n\t// while there are candidates to visit\n\twhile (not node_candidates.empty()) {\n\t\tauto current_node = node_candidates.pop();\n\n\t\tcurrent_node->was_best = true;\n\n\t\t// check if the current node is a portal in the target sector that can\n\t\t// be reached from the target cell\n\t\tauto exit_portal_id = current_node->portal->get_id();\n\t\tif (target_portal_ids.contains(exit_portal_id)) {\n\t\t\tauto backtrace = current_node->generate_backtrace();\n\t\t\tfor (auto &node : backtrace) {\n\t\t\t\tresult.push_back(node->portal);\n\t\t\t}\n\t\t\tlog::log(DBG << \"Portal path found with \" << result.size() << \" portal traversals.\");\n\t\t\treturn std::make_pair(PathResult::FOUND, result);\n\t\t}\n\n\t\t// check if the current node is the closest to the target\n\t\tif (current_node->heuristic_cost < closest_node->heuristic_cost) {\n\t\t\tclosest_node = current_node;\n\t\t}\n\n\t\t// get the exits of the current node\n\t\tENSURE(current_node->entry_sector != std::nullopt, \"Entry sector not set for portal node.\");\n\t\tconst auto &exits = current_node->get_exits(current_node->entry_sector.value());\n\n\t\t// evaluate all neighbors of the current candidate for further progress\n\t\tfor (auto &[exit, distance_cost] : exits) {\n\t\t\texit->entry_sector = current_node->portal->get_exit_sector(current_node->entry_sector.value());\n\t\t\tbool not_visited = !visited_portals.contains(exit->portal->get_id());\n\n\t\t\tif (not_visited) {\n\t\t\t\texit->was_best = false;\n\t\t\t}\n\t\t\telse if (exit->was_best) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\n\t\t\tauto tentative_cost = current_node->current_cost + distance_cost;\n\n\t\t\tif (not_visited or tentative_cost < exit->current_cost) {\n\t\t\t\tif (not_visited) {\n\t\t\t\t\t// Get heuristic cost (from exit node to target cell)\n\t\t\t\t\tauto exit_sector = grid->get_sector(exit->portal->get_exit_sector(exit->entry_sector.value()));\n\t\t\t\t\tauto exit_sector_pos = exit_sector->get_position().to_tile(sector_size);\n\t\t\t\t\tauto exit_portal_pos = exit->portal->get_exit_center(exit->entry_sector.value());\n\t\t\t\t\texit->heuristic_cost = Pathfinder::heuristic_cost(\n\t\t\t\t\t\texit_sector_pos + exit_portal_pos,\n\t\t\t\t\t\trequest.target);\n\t\t\t\t}\n\n\t\t\t\t// update the cost knowledge\n\t\t\t\texit->current_cost = tentative_cost;\n\t\t\t\texit->future_cost = exit->current_cost + exit->heuristic_cost;\n\t\t\t\texit->prev_portal = current_node;\n\n\t\t\t\tif (not_visited) {\n\t\t\t\t\texit->heap_node = node_candidates.push(exit);\n\t\t\t\t\tvisited_portals.insert(exit->portal->get_id());\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tnode_candidates.decrease(exit->heap_node);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// no path found, return the closest node\n\tauto backtrace = closest_node->generate_backtrace();\n\tfor (auto &node : backtrace) {\n\t\tresult.push_back(node->portal);\n\t}\n\n\tlog::log(DBG << \"Portal path not found.\");\n\tlog::log(DBG << \"Closest portal: \" << closest_node->portal->get_id());\n\treturn std::make_pair(PathResult::NOT_FOUND, result);\n}\n\nconst std::vector<coord::tile> Pathfinder::get_waypoints(const std::vector<std::pair<sector_id_t, std::shared_ptr<FlowField>>> &flow_fields,\n                                                         const PathRequest &request) const {\n\tENSURE(flow_fields.size() > 0, \"At least 1 flow field is required for finding waypoints.\");\n\n\tstd::vector<coord::tile> waypoints;\n\n\tauto grid = this->get_grid(request.grid_id);\n\tauto sector_size = grid->get_sector_size();\n\tcoord::tile_t start_x = request.start.ne % sector_size;\n\tcoord::tile_t start_y = request.start.se % sector_size;\n\n\tbool los_reached = false;\n\n\tcoord::tile_t current_x = start_x;\n\tcoord::tile_t current_y = start_y;\n\tflow_dir_t current_direction = flow_fields.at(0).second->get_dir(current_x, current_y);\n\tfor (size_t i = 0; i < flow_fields.size(); ++i) {\n\t\tauto &sector = grid->get_sector(flow_fields[i].first);\n\t\tauto sector_pos = sector->get_position().to_tile(sector_size);\n\t\tauto &flow_field = flow_fields[i].second;\n\n\t\t// navigate the flow field vectors until we reach its edge (or the target)\n\t\tflow_t cell;\n\t\tdo {\n\t\t\tcell = flow_field->get_cell(current_x, current_y);\n\n\t\t\tif (cell & FLOW_LOS_MASK) {\n\t\t\t\t// check if we reached an LOS cell\n\t\t\t\tauto cell_pos = sector_pos + coord::tile_delta(current_x, current_y);\n\t\t\t\twaypoints.push_back(cell_pos);\n\t\t\t\tlos_reached = true;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// check if we need to change direction\n\t\t\tauto cell_direction = static_cast<flow_dir_t>(cell & FLOW_DIR_MASK);\n\t\t\tif (cell_direction != current_direction) {\n\t\t\t\t// add the current cell as a waypoint\n\t\t\t\tauto cell_pos = sector_pos + coord::tile_delta(current_x, current_y);\n\t\t\t\twaypoints.push_back(cell_pos);\n\t\t\t\tcurrent_direction = cell_direction;\n\t\t\t}\n\n\t\t\t// move to the next cell\n\t\t\tswitch (current_direction) {\n\t\t\tcase flow_dir_t::NORTH:\n\t\t\t\tcurrent_y -= 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::NORTH_EAST:\n\t\t\t\tcurrent_x += 1;\n\t\t\t\tcurrent_y -= 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::EAST:\n\t\t\t\tcurrent_x += 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::SOUTH_EAST:\n\t\t\t\tcurrent_x += 1;\n\t\t\t\tcurrent_y += 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::SOUTH:\n\t\t\t\tcurrent_y += 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::SOUTH_WEST:\n\t\t\t\tcurrent_x -= 1;\n\t\t\t\tcurrent_y += 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::WEST:\n\t\t\t\tcurrent_x -= 1;\n\t\t\t\tbreak;\n\t\t\tcase flow_dir_t::NORTH_WEST:\n\t\t\t\tcurrent_x -= 1;\n\t\t\t\tcurrent_y -= 1;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow Error{ERR << \"Invalid flow direction: \" << static_cast<int>(current_direction)};\n\t\t\t}\n\t\t}\n\t\twhile (not(cell & FLOW_TARGET_MASK));\n\n\t\tif (los_reached or i == flow_fields.size() - 1) {\n\t\t\t// exit the loop if we found an LOS cell or reached\n\t\t\t// the target cell in the last flow field\n\t\t\tbreak;\n\t\t}\n\n\t\t// reset the current position for the next flow field\n\t\tswitch (current_direction) {\n\t\tcase flow_dir_t::NORTH:\n\t\t\tcurrent_y = sector_size - 1;\n\t\t\tbreak;\n\t\tcase flow_dir_t::NORTH_EAST:\n\t\t\tcurrent_x = current_x + 1;\n\t\t\tcurrent_y = sector_size - 1;\n\t\t\tbreak;\n\t\tcase flow_dir_t::EAST:\n\t\t\tcurrent_x = 0;\n\t\t\tbreak;\n\t\tcase flow_dir_t::SOUTH_EAST:\n\t\t\tcurrent_x = 0;\n\t\t\tcurrent_y = current_y + 1;\n\t\t\tbreak;\n\t\tcase flow_dir_t::SOUTH:\n\t\t\tcurrent_y = 0;\n\t\t\tbreak;\n\t\tcase flow_dir_t::SOUTH_WEST:\n\t\t\tcurrent_x = current_x - 1;\n\t\t\tcurrent_y = 0;\n\t\t\tbreak;\n\t\tcase flow_dir_t::WEST:\n\t\t\tcurrent_x = sector_size - 1;\n\t\t\tbreak;\n\t\tcase flow_dir_t::NORTH_WEST:\n\t\t\tcurrent_x = sector_size - 1;\n\t\t\tcurrent_y = current_y - 1;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow Error{ERR << \"Invalid flow direction: \" << static_cast<int>(current_direction)};\n\t\t}\n\t}\n\n\t// add the target position as the last waypoint\n\twaypoints.push_back(request.target);\n\n\treturn waypoints;\n}\n\nint Pathfinder::heuristic_cost(const coord::tile &portal_pos,\n                               const coord::tile &target_pos) {\n\tauto portal_phys_pos = portal_pos.to_phys2();\n\tauto target_phys_pos = target_pos.to_phys2();\n\tauto delta = target_phys_pos - portal_phys_pos;\n\n\treturn delta.length();\n}\n\nint Pathfinder::distance_cost(const coord::tile_delta &portal1_pos,\n                              const coord::tile_delta &portal2_pos) {\n\tauto delta = portal2_pos.to_phys2() - portal1_pos.to_phys2();\n\n\treturn delta.length();\n}\n\n\nPortalNode::PortalNode(const std::shared_ptr<Portal> &portal) :\n\tportal{portal},\n\tentry_sector{std::nullopt},\n\tfuture_cost{std::numeric_limits<int>::max()},\n\tcurrent_cost{std::numeric_limits<int>::max()},\n\theuristic_cost{std::numeric_limits<int>::max()},\n\twas_best{false},\n\tprev_portal{nullptr},\n\theap_node{nullptr} {}\n\nPortalNode::PortalNode(const std::shared_ptr<Portal> &portal,\n                       sector_id_t entry_sector,\n                       const node_t &prev_portal) :\n\tportal{portal},\n\tentry_sector{entry_sector},\n\tfuture_cost{std::numeric_limits<int>::max()},\n\tcurrent_cost{std::numeric_limits<int>::max()},\n\theuristic_cost{std::numeric_limits<int>::max()},\n\twas_best{false},\n\tprev_portal{prev_portal},\n\theap_node{nullptr} {}\n\nPortalNode::PortalNode(const std::shared_ptr<Portal> &portal,\n                       sector_id_t entry_sector,\n                       const node_t &prev_portal,\n                       int past_cost,\n                       int heuristic_cost) :\n\tportal{portal},\n\tentry_sector{entry_sector},\n\tfuture_cost{past_cost + heuristic_cost},\n\tcurrent_cost{past_cost},\n\theuristic_cost{heuristic_cost},\n\twas_best{false},\n\tprev_portal{prev_portal},\n\theap_node{nullptr} {\n}\n\nbool PortalNode::operator<(const PortalNode &other) const {\n\treturn this->future_cost < other.future_cost;\n}\n\nbool PortalNode::operator==(const PortalNode &other) const {\n\treturn this->portal->get_id() == other.portal->get_id();\n}\n\nstd::vector<node_t> PortalNode::generate_backtrace() {\n\tstd::vector<node_t> waypoints;\n\n\tnode_t current = this->shared_from_this();\n\tdo {\n\t\twaypoints.push_back(current);\n\t\tcurrent = current->prev_portal;\n\t}\n\twhile (current != nullptr);\n\n\treturn waypoints;\n}\n\nvoid PortalNode::init_exits(const nodemap_t &node_map) {\n\tauto exits = this->portal->get_exits(this->node_sector_0);\n\tfor (auto &exit : exits) {\n\t\tint distance_cost = Pathfinder::distance_cost(\n\t\t\tthis->portal->get_exit_center(this->node_sector_0),\n\t\t\texit->get_entry_center(this->node_sector_1));\n\n\t\tauto exit_node = node_map.at(exit->get_id());\n\t\tthis->exits_1[exit_node] = distance_cost;\n\t}\n\n\texits = this->portal->get_exits(this->node_sector_1);\n\tfor (auto &exit : exits) {\n\t\tint distance_cost = Pathfinder::distance_cost(\n\t\t\tthis->portal->get_exit_center(this->node_sector_1),\n\t\t\texit->get_entry_center(this->node_sector_0));\n\n\t\tauto exit_node = node_map.at(exit->get_id());\n\t\tthis->exits_0[exit_node] = distance_cost;\n\t}\n}\n\nconst PortalNode::exits_t &PortalNode::get_exits(sector_id_t entry_sector) {\n\tENSURE(entry_sector == this->node_sector_0 || entry_sector == this->node_sector_1, \"Invalid entry sector\");\n\n\tif (this->node_sector_0 == entry_sector) {\n\t\treturn exits_1;\n\t}\n\telse {\n\t\treturn exits_0;\n\t}\n}\n\n\nbool compare_node_cost::operator()(const node_t &lhs, const node_t &rhs) const {\n\treturn *lhs < *rhs;\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/pathfinder.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <map>\n#include <memory>\n#include <optional>\n#include <unordered_map>\n\n#include \"coord/tile.h\"\n#include \"datastructure/pairing_heap.h\"\n#include \"pathfinding/path.h\"\n#include \"pathfinding/types.h\"\n\n\nnamespace openage::path {\nclass Grid;\nclass Integrator;\nclass Portal;\nclass FlowField;\n\n/**\n * Pathfinder for flow field pathfinding.\n *\n * The pathfinder manages the grids defining the pathable ingame areas and\n * provides an interface for making pathfinding requests.\n *\n * Pathfinding consists of a multi-step process: First, there is a high-level\n * search using A* to identify the sectors of the grid that should be traversed.\n * Afterwards, flow fields are calculated from the target sector to the start\n * sector, which are then used to guide the actual unit movement.\n */\nclass Pathfinder {\npublic:\n\t/**\n\t * Create a new pathfinder.\n\t */\n\tPathfinder();\n\t~Pathfinder() = default;\n\n\t/**\n\t * Get the grid at a specified index.\n\t *\n\t * @param id ID of the grid.\n\t *\n\t * @return Pathfinding grid.\n\t */\n\tconst std::shared_ptr<Grid> &get_grid(grid_id_t id) const;\n\n\t/**\n\t * Add a grid to the pathfinder.\n\t *\n\t * @param grid Grid to add.\n\t */\n\tvoid add_grid(const std::shared_ptr<Grid> &grid);\n\n\t/**\n\t * Get the path for a pathfinding request.\n\t *\n\t * @param request Pathfinding request.\n\t *\n\t * @return Path found by the pathfinder.\n\t */\n\tconst Path get_path(const PathRequest &request);\n\n\n\t/**\n\t * Calculate the distance cost between two portals.\n\t *\n\t * @param portal1_pos Center of the first portal (relative to sector origin).\n\t * @param portal2_pos Center of the second portal (relative to sector origin).\n\t *\n\t * @return Distance cost between the portal centers.\n\t */\n\tstatic int distance_cost(const coord::tile_delta &portal1_pos, const coord::tile_delta &portal2_pos);\n\nprivate:\n\tusing portal_star_t = std::pair<PathResult, std::vector<std::shared_ptr<Portal>>>;\n\n\t/**\n\t * High-level pathfinder. Uses A* to find the path through the portals of sectors.\n\t *\n\t * @param request Pathfinding request.\n\t * @param target_portal_ids IDs of portals that can be reached from the target cell.\n\t * @param start_portal_ids IDs of portals that can be reached from the start cell.\n\t *\n\t * @return Portals to traverse in order to reach the target.\n\t */\n\tconst portal_star_t portal_a_star(const PathRequest &request,\n\t                                  const std::unordered_set<portal_id_t> &target_portal_ids,\n\t                                  const std::unordered_set<portal_id_t> &start_portal_ids) const;\n\n\t/**\n\t * Low-level pathfinder. Uses flow fields to find the path through the sectors.\n\t *\n\t * @param flow_fields Flow fields for the sectors.\n\t * @param request Pathfinding request.\n\t *\n\t * @return Waypoint coordinates to traverse in order to reach the target.\n\t */\n\tconst std::vector<coord::tile> get_waypoints(const std::vector<std::pair<sector_id_t, std::shared_ptr<FlowField>>> &flow_fields,\n\t                                             const PathRequest &request) const;\n\n\t/**\n\t * Calculate the heuristic cost between a portal and a target cell.\n\t *\n\t * @param portal_pos Position of the portal (absolute on the grid).\n\t *                   This should be the center of the portal exit.\n\t * @param target_pos Position of the target cell (absolute on the grid).\n\t *\n\t * @return Heuristic cost between the cells.\n\t */\n\tstatic int heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos);\n\n\n\t/**\n\t * Grids managed by this pathfinder.\n\t *\n\t * Each grid can have separate pathing.\n\t */\n\tstd::unordered_map<grid_id_t, std::shared_ptr<Grid>> grids;\n\n\t/**\n\t * Integrator for flow field calculations.\n\t */\n\tstd::shared_ptr<Integrator> integrator;\n};\n\n\nclass PortalNode;\n\nusing node_t = std::shared_ptr<PortalNode>;\n\n/**\n * Cost comparison for node_t on the pairing heap.\n *\n * Extracts the nodes from the shared_ptr and compares them. We have\n * to use a custom comparison function because otherwise the shared_ptr\n * would be compared instead of the actual node.\n */\nstruct compare_node_cost {\n\tbool operator()(const node_t &lhs, const node_t &rhs) const;\n};\n\nusing heap_t = datastructure::PairingHeap<node_t, compare_node_cost>;\nusing nodemap_t = std::unordered_map<portal_id_t, node_t>;\n\n/**\n * One navigation waypoint in a path.\n */\nclass PortalNode : public std::enable_shared_from_this<PortalNode> {\npublic:\n\tPortalNode(const std::shared_ptr<Portal> &portal);\n\tPortalNode(const std::shared_ptr<Portal> &portal,\n\t           sector_id_t entry_sector,\n\t           const node_t &prev_portal);\n\tPortalNode(const std::shared_ptr<Portal> &portal,\n\t           sector_id_t entry_sector,\n\t           const node_t &prev_portal,\n\t           int past_cost,\n\t           int heuristic_cost);\n\n\t/**\n\t * Orders nodes according to their future cost value.\n\t */\n\tbool operator<(const PortalNode &other) const;\n\n\t/**\n\t * Compare the node to another one.\n\t * They are the same if their portal is.\n\t */\n\tbool operator==(const PortalNode &other) const;\n\n\t/**\n\t * Calculates the actual movement cose to another node.\n\t */\n\tint cost_to(const PortalNode &other) const;\n\n\t/**\n\t * Create a backtrace path beginning at this node.\n\t */\n\tstd::vector<node_t> generate_backtrace();\n\n\t/**\n\t * init PortalNode::exits.\n\t */\n\tvoid init_exits(const nodemap_t &node_map);\n\n\n\t/**\n\t * maps node_t of a neigbhour portal to the distance cost to travel between the portals\n\t */\n\tusing exits_t = std::map<const node_t, int>;\n\n\n\t/**\n\t * Get the exit portals reachable via the portal when entering from a specified sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Exit portals nodes reachable from the portal.\n\t */\n\tconst exits_t &get_exits(sector_id_t entry_sector);\n\n\t/**\n\t * The portal this node is associated to.\n\t */\n\tstd::shared_ptr<Portal> portal;\n\n\t/**\n\t * Sector where the portal is entered.\n\t */\n\tstd::optional<sector_id_t> entry_sector;\n\n\t/**\n\t * Future cost estimation value for this node.\n\t */\n\tint future_cost;\n\n\t/**\n\t * Evaluated past cost value for the node.\n\t * This stores the actual cost from start to this node.\n\t */\n\tint current_cost;\n\n\t/**\n\t * Heuristic cost cache.\n\t * Calculated once, is the heuristic distance from this node\n\t * to the goal.\n\t */\n\tint heuristic_cost;\n\n\t/**\n\t * Does this node already have an alternative path?\n\t * If the node was once selected as the best next hop,\n\t * this is set to true.\n\t */\n\tbool was_best = false;\n\n\t/**\n\t * Node where this one was reached by least cost.\n\t */\n\tnode_t prev_portal;\n\n\t/**\n\t * Priority queue node that contains this path node.\n\t */\n\theap_t::element_t heap_node;\n\n\t/**\n\t * First sector connected by the portal.\n\t */\n\tsector_id_t node_sector_0;\n\n\t/**\n\t * Second sector connected by the portal.\n\t */\n\tsector_id_t node_sector_1;\n\n\t/**\n\t * Exits in sector 0 reachable from the portal.\n\t */\n\texits_t exits_0;\n\n\t/**\n\t * Exits in sector 1 reachable from the portal.\n\t */\n\texits_t exits_1;\n};\n\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/portal.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"portal.h\"\n\n#include \"error/error.h\"\n\n\nnamespace openage::path {\n\nPortal::Portal(portal_id_t id,\n               sector_id_t sector0,\n               sector_id_t sector1,\n               PortalDirection direction,\n               const coord::tile_delta &cell_start,\n               const coord::tile_delta &cell_end) :\n\tid{id},\n\tsector0{sector0},\n\tsector1{sector1},\n\tsector0_exits{},\n\tsector1_exits{},\n\tdirection{direction},\n\tcell_start{cell_start},\n\tcell_end{cell_end} {\n}\n\nportal_id_t Portal::get_id() const {\n\treturn this->id;\n}\n\nconst std::vector<std::shared_ptr<Portal>> &Portal::get_connected(sector_id_t sector) const {\n\tENSURE(sector == this->sector0 || sector == this->sector1, \"Portal does not connect to sector\");\n\n\tif (sector == this->sector0) {\n\t\treturn this->sector0_exits;\n\t}\n\treturn this->sector1_exits;\n}\n\nconst std::vector<std::shared_ptr<Portal>> &Portal::get_exits(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->sector1_exits;\n\t}\n\treturn this->sector0_exits;\n}\n\nvoid Portal::set_exits(sector_id_t sector, const std::vector<std::shared_ptr<Portal>> &exits) {\n\tENSURE(sector == this->sector0 || sector == this->sector1, \"Portal does not connect to sector\");\n\n\tif (sector == this->sector0) {\n\t\tthis->sector0_exits = exits;\n\t}\n\telse {\n\t\tthis->sector1_exits = exits;\n\t}\n}\n\nsector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->sector1;\n\t}\n\treturn this->sector0;\n}\n\nconst coord::tile_delta Portal::get_entry_start(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->get_sector0_start();\n\t}\n\n\treturn this->get_sector1_start();\n}\n\nconst coord::tile_delta Portal::get_entry_center(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\tauto start = this->get_sector0_start();\n\t\tauto end = this->get_sector0_end();\n\t\treturn {(start.ne + end.ne) / 2, (start.se + end.se) / 2};\n\t}\n\n\tauto start = this->get_sector1_start();\n\tauto end = this->get_sector1_end();\n\treturn {(start.ne + end.ne) / 2, (start.se + end.se) / 2};\n}\n\nconst coord::tile_delta Portal::get_entry_end(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->get_sector0_end();\n\t}\n\n\treturn this->get_sector1_end();\n}\n\nconst coord::tile_delta Portal::get_exit_start(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->get_sector1_start();\n\t}\n\n\treturn this->get_sector0_start();\n}\n\nconst coord::tile_delta Portal::get_exit_center(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\tauto start = this->get_sector1_start();\n\t\tauto end = this->get_sector1_end();\n\t\treturn {(start.ne + end.ne) / 2, (start.se + end.se) / 2};\n\t}\n\n\tauto start = this->get_sector0_start();\n\tauto end = this->get_sector0_end();\n\treturn {(start.ne + end.ne) / 2, (start.se + end.se) / 2};\n}\n\nconst coord::tile_delta Portal::get_exit_end(sector_id_t entry_sector) const {\n\tENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, \"Invalid entry sector\");\n\n\tif (entry_sector == this->sector0) {\n\t\treturn this->get_sector1_end();\n\t}\n\n\treturn this->get_sector0_end();\n}\n\nPortalDirection Portal::get_direction() const {\n\treturn this->direction;\n}\n\nconst coord::tile_delta &Portal::get_sector0_start() const {\n\treturn this->cell_start;\n}\n\nconst coord::tile_delta &Portal::get_sector0_end() const {\n\treturn this->cell_end;\n}\n\nconst coord::tile_delta Portal::get_sector1_start() const {\n\tif (this->direction == PortalDirection::NORTH_SOUTH) {\n\t\treturn {this->cell_start.ne, 0};\n\t}\n\treturn {0, this->cell_start.se};\n}\n\nconst coord::tile_delta Portal::get_sector1_end() const {\n\tif (this->direction == PortalDirection::NORTH_SOUTH) {\n\t\treturn {this->cell_end.ne, 0};\n\t}\n\treturn {0, this->cell_end.se};\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/portal.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <utility>\n#include <vector>\n\n#include \"coord/tile.h\"\n#include \"pathfinding/types.h\"\n\n\nnamespace openage::path {\nclass CostField;\n\n/**\n * Possible directions of a portal node.\n */\nenum class PortalDirection {\n\tNORTH_SOUTH,\n\tEAST_WEST\n};\n\n/**\n * Biderectional gateway for connecting two sectors in the flow field pathfinder.\n *\n * Portals are located at the border of two sectors (0 and 1), and allow units to move between them.\n * For each of these sectors, the portal stores the start and end coordinates where the\n * sectors overlap as well as the other portals that can be reached in the same\n * sector. For simplicity, the portal is assumed to be a straight line of cells from the start\n * to the end.\n *\n * The portal is bidirectional, meaning that it can be entered from either sector and\n * exited into the other sector. The direction of the portal from one sector to the other\n * is stored in the portal node. As a convention and to simplify computations, sector 0 must be\n * the either the north or east sector on the grid in relation to sector 1.\n */\nclass Portal {\npublic:\n\t/**\n\t * Create a new portal between two sectors.\n\t *\n\t * As a convention, sector 0 must be the either the north or east sector\n\t * on the grid in relation to sector 1.\n\t *\n\t * @param id ID of the portal. Should be unique per grid.\n\t * @param sector0 First sector connected by the portal.\n\t *                Must be north or east on the grid in relation to sector 1.\n\t * @param sector1 Second sector connected by the portal.\n\t *                Must be south or west on the grid in relation to sector 0.\n\t * @param direction Direction of the portal from sector 0 to sector 1.\n\t * @param cell_start Start cell coordinate in sector 0 (relative to sector origin).\n\t * @param cell_end End cell coordinate in sector 0 (relative to sector origin).\n\t */\n\tPortal(portal_id_t id,\n\t       sector_id_t sector0,\n\t       sector_id_t sector1,\n\t       PortalDirection direction,\n\t       const coord::tile_delta &cell_start,\n\t       const coord::tile_delta &cell_end);\n\n\t~Portal() = default;\n\n\t/**\n\t * Get the ID of the portal.\n\t *\n\t * IDs are unique per grid.\n\t *\n\t * @return ID of the portal.\n\t */\n\tportal_id_t get_id() const;\n\n\t/**\n\t * Get the connected portals in the specified sector.\n\t *\n\t * @param sector Sector ID.\n\t *\n\t * @return Connected portals in the sector.\n\t */\n\tconst std::vector<std::shared_ptr<Portal>> &get_connected(sector_id_t sector) const;\n\n\t/**\n\t * Get the exit portals reachable via the portal when entering from a specified sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Exit portals reachable from the portal.\n\t */\n\tconst std::vector<std::shared_ptr<Portal>> &get_exits(sector_id_t entry_sector) const;\n\n\t/**\n\t * Set the exit portals reachable for a specified sector.\n\t *\n\t * @param sector Sector for which the exit portals are set.\n\t * @param exits Exit portals reachable from the portal.\n\t */\n\tvoid set_exits(sector_id_t sector, const std::vector<std::shared_ptr<Portal>> &exits);\n\n\t/**\n\t * Get the cost field of the sector where the portal is exited.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cost field of the sector where the portal is exited.\n\t */\n\tsector_id_t get_exit_sector(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the start of the portal in the entry sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_entry_start(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the center of the portal in the entry sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the center of the portal in the entry sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_entry_center(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the start of the portal in the entry sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the start of the portal in the entry sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_entry_end(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the start of the portal in the exit sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the start of the portal in the exit sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_exit_start(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the center of the portal in the exit sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the center of the portal in the exit sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_exit_center(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the cell coordinates of the end of the portal in the exit sector.\n\t *\n\t * @param entry_sector Sector from which the portal is entered.\n\t *\n\t * @return Cell coordinates of the end of the portal in the exit sector (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_exit_end(sector_id_t entry_sector) const;\n\n\t/**\n\t * Get the direction of the portal from sector 0 to sector 1.\n\t *\n\t * @return Direction of the portal.\n\t */\n\tPortalDirection get_direction() const;\n\nprivate:\n\t/**\n\t * Get the start cell coordinates of the portal.\n\t *\n\t * @return Start cell coordinates of the portal (relative to sector origin).\n\t */\n\tconst coord::tile_delta &get_sector0_start() const;\n\n\t/**\n\t * Get the end cell coordinates of the portal.\n\t *\n\t * @return End cell coordinates of the portal (relative to sector origin).\n\t */\n\tconst coord::tile_delta &get_sector0_end() const;\n\n\t/**\n\t * Get the start cell coordinates of the portal.\n\t *\n\t * @return Start cell coordinates of the portal (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_sector1_start() const;\n\n\t/**\n\t * Get the end cell coordinates of the portal.\n\t *\n\t * @return End cell coordinates of the portal (relative to sector origin).\n\t */\n\tconst coord::tile_delta get_sector1_end() const;\n\n\t/**\n\t * ID of the portal.\n\t */\n\tportal_id_t id;\n\n\t/**\n\t * First sector connected by the portal.\n\t */\n\tsector_id_t sector0;\n\n\t/**\n\t * Second sector connected by the portal.\n\t */\n\tsector_id_t sector1;\n\n\t/**\n\t * Exits in sector 0 reachable from the portal.\n\t *\n\t * TODO: Also store avarage cost to reach each exit.\n\t */\n\tstd::vector<std::shared_ptr<Portal>> sector0_exits;\n\n\t/**\n\t * Exits in sector 1 reachable from the portal.\n\t *\n\t * TODO: Also store avarage cost to reach each exit.\n\t */\n\tstd::vector<std::shared_ptr<Portal>> sector1_exits;\n\n\t/**\n\t * Direction of the portal from sector 0 to sector 1.\n\t */\n\tPortalDirection direction;\n\n\t/**\n\t * Start cell coordinate in sector 0 (relative to sector origin).\n\t *\n\t * Coordinates for sector 1 are calculated on-the-fly using the direction.\n\t */\n\tcoord::tile_delta cell_start;\n\n\t/**\n\t * End cell coordinate in sector 0 (relative to sector origin).\n\t *\n\t * Coordinates for sector 1 are calculated on-the-fly using the direction.\n\t */\n\tcoord::tile_delta cell_end;\n};\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/sector.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"sector.h\"\n\n#include <deque>\n#include <unordered_set>\n\n#include \"error/error.h\"\n\n#include \"coord/tile.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/definitions.h\"\n\n\nnamespace openage::path {\n\nSector::Sector(sector_id_t id, const coord::chunk &position, size_t field_size) :\n\tid{id},\n\tposition{position},\n\tcost_field{std::make_shared<CostField>(field_size)} {\n}\n\nSector::Sector(sector_id_t id, const coord::chunk &position, const std::shared_ptr<CostField> &cost_field) :\n\tid{id},\n\tposition{position},\n\tcost_field{cost_field} {\n}\n\nconst sector_id_t &Sector::get_id() const {\n\treturn this->id;\n}\n\nconst coord::chunk &Sector::get_position() const {\n\treturn this->position;\n}\n\nconst std::shared_ptr<CostField> &Sector::get_cost_field() const {\n\treturn this->cost_field;\n}\n\nconst std::vector<std::shared_ptr<Portal>> &Sector::get_portals() const {\n\treturn this->portals;\n}\n\nvoid Sector::add_portal(const std::shared_ptr<Portal> &portal) {\n\tthis->portals.push_back(portal);\n}\n\nstd::vector<std::shared_ptr<Portal>> Sector::find_portals(const std::shared_ptr<Sector> &other,\n                                                          PortalDirection direction,\n                                                          portal_id_t next_id) const {\n\tENSURE(this->cost_field->get_size() == other->get_cost_field()->get_size(), \"Sector size mismatch\");\n\n\tstd::vector<std::shared_ptr<Portal>> result;\n\n\t// cost field of the other sector\n\tauto other_cost = other->get_cost_field();\n\n\t// compare the edges of the sectors\n\tsize_t start = 0;\n\tsize_t end = 0;\n\tbool passable_edge = false;\n\tfor (size_t i = 0; i < this->cost_field->get_size(); ++i) {\n\t\tauto coord_this = coord::tile_delta{0, 0};\n\t\tauto coord_other = coord::tile_delta{0, 0};\n\t\tif (direction == PortalDirection::NORTH_SOUTH) {\n\t\t\t// right edge; top to bottom\n\t\t\tcoord_this = coord::tile_delta(i, this->cost_field->get_size() - 1);\n\t\t\tcoord_other = coord::tile_delta(i, 0);\n\t\t}\n\t\telse if (direction == PortalDirection::EAST_WEST) {\n\t\t\t// bottom edge; east to west\n\t\t\tcoord_this = coord::tile_delta(this->cost_field->get_size() - 1, i);\n\t\t\tcoord_other = coord::tile_delta(0, i);\n\t\t}\n\n\t\tif (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE\n\t\t    and other_cost->get_cost(coord_other) != COST_IMPASSABLE) {\n\t\t\t// both sides of the edge are passable\n\t\t\tif (not passable_edge) {\n\t\t\t\t// start a new portal\n\t\t\t\tstart = i;\n\t\t\t\tpassable_edge = true;\n\t\t\t}\n\t\t\t// else: we already started a portal\n\n\t\t\tend = i;\n\t\t\tif (i != this->cost_field->get_size() - 1) {\n\t\t\t\t// continue to next tile unless we are at the last tile\n\t\t\t\t// then we have to end the current portal\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (passable_edge) {\n\t\t\t// create a new portal\n\t\t\tauto coord_start = coord::tile_delta{0, 0};\n\t\t\tauto coord_end = coord::tile_delta{0, 0};\n\t\t\tif (direction == PortalDirection::NORTH_SOUTH) {\n\t\t\t\t// right edge; top to bottom\n\t\t\t\tcoord_start = coord::tile_delta(start, this->cost_field->get_size() - 1);\n\t\t\t\tcoord_end = coord::tile_delta(end, this->cost_field->get_size() - 1);\n\t\t\t}\n\t\t\telse if (direction == PortalDirection::EAST_WEST) {\n\t\t\t\t// bottom edge; east to west\n\t\t\t\tcoord_start = coord::tile_delta(this->cost_field->get_size() - 1, start);\n\t\t\t\tcoord_end = coord::tile_delta(this->cost_field->get_size() - 1, end);\n\t\t\t}\n\n\t\t\tresult.push_back(\n\t\t\t\tstd::make_shared<Portal>(\n\t\t\t\t\tnext_id,\n\t\t\t\t\tthis->id,\n\t\t\t\t\tother->get_id(),\n\t\t\t\t\tdirection,\n\t\t\t\t\tcoord_start,\n\t\t\t\t\tcoord_end));\n\t\t\tpassable_edge = false;\n\t\t\tnext_id += 1;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nvoid Sector::connect_exits() {\n\tif (this->portals.empty()) {\n\t\treturn;\n\t}\n\n\tstd::unordered_set<portal_id_t> portal_ids;\n\tfor (const auto &portal : this->portals) {\n\t\tportal_ids.insert(portal->get_id());\n\t}\n\n\t// check all portals in the sector\n\tstd::vector<std::shared_ptr<Portal>> search_portals = this->portals;\n\twhile (not portal_ids.empty()) {\n\t\tauto portal = search_portals.back();\n\t\tsearch_portals.pop_back();\n\t\tportal_ids.erase(portal->get_id());\n\n\t\tauto start = portal->get_entry_start(this->id);\n\t\tauto end = portal->get_entry_end(this->id);\n\n\t\tstd::unordered_set<size_t> visited;\n\t\tstd::deque<size_t> open_list;\n\t\tstd::vector<size_t> neighbors;\n\t\tneighbors.reserve(4);\n\n\t\tif (portal->get_direction() == PortalDirection::NORTH_SOUTH) {\n\t\t\t// right edge; top to bottom\n\t\t\tfor (auto i = start.se; i <= end.se; ++i) {\n\t\t\t\topen_list.push_back(start.ne + i * this->cost_field->get_size());\n\t\t\t}\n\t\t}\n\t\telse if (portal->get_direction() == PortalDirection::EAST_WEST) {\n\t\t\t// bottom edge; east to west\n\t\t\tfor (auto i = start.ne; i <= end.ne; ++i) {\n\t\t\t\topen_list.push_back(i + start.se * this->cost_field->get_size());\n\t\t\t}\n\t\t}\n\n\t\t// flood fill the grid to find connected portals\n\t\twhile (not open_list.empty()) {\n\t\t\tauto current = open_list.front();\n\t\t\topen_list.pop_front();\n\n\t\t\tif (visited.contains(current)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Get the x and y coordinates of the current cell\n\t\t\tauto x = current % this->cost_field->get_size();\n\t\t\tauto y = current / this->cost_field->get_size();\n\n\t\t\t// check the neighbors\n\t\t\tif (y > 0) {\n\t\t\t\tneighbors.push_back(current - this->cost_field->get_size());\n\t\t\t}\n\t\t\tif (x > 0) {\n\t\t\t\tneighbors.push_back(current - 1);\n\t\t\t}\n\t\t\tif (y < this->cost_field->get_size() - 1) {\n\t\t\t\tneighbors.push_back(current + this->cost_field->get_size());\n\t\t\t}\n\t\t\tif (x < this->cost_field->get_size() - 1) {\n\t\t\t\tneighbors.push_back(current + 1);\n\t\t\t}\n\n\t\t\t// add the neighbors to the open list\n\t\t\tfor (const auto &neighbor : neighbors) {\n\t\t\t\tif (this->cost_field->get_cost(neighbor) != COST_IMPASSABLE) {\n\t\t\t\t\topen_list.push_back(neighbor);\n\t\t\t\t}\n\t\t\t}\n\t\t\tneighbors.clear();\n\n\t\t\t// mark the current cell as visited\n\t\t\t// TODO: Record the cost of reaching this cell\n\t\t\tvisited.insert(current);\n\t\t}\n\n\t\t// check if the visited cells are connected to another portal\n\t\tstd::vector<std::shared_ptr<Portal>> connected_portals;\n\t\tfor (auto &exit : this->portals) {\n\t\t\tif (exit->get_id() == portal->get_id()) {\n\t\t\t\t// skip the current portal\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// get the start cell of the exit portal\n\t\t\t// we only have to check one cell since the flood fill\n\t\t\t// should reach any exit cell\n\t\t\tauto exit_start = exit->get_entry_start(this->id);\n\t\t\tauto exit_cell = exit_start.ne + exit_start.se * this->cost_field->get_size();\n\n\t\t\t// check if the exit cell is connected to the visited cells\n\t\t\tif (visited.contains(exit_cell)) {\n\t\t\t\tconnected_portals.push_back(exit);\n\t\t\t}\n\t\t}\n\n\t\t// set the exits for the current portal\n\t\tportal->set_exits(this->id, connected_portals);\n\n\t\t// All connected portals share the same exits\n\t\t// so we can connect them here\n\t\tfor (auto &connected : connected_portals) {\n\t\t\t// make a new vector with all connected portals except the current one\n\t\t\tstd::vector<std::shared_ptr<Portal>> other_connected;\n\t\t\tfor (auto &other : connected_portals) {\n\t\t\t\tif (other->get_id() != connected->get_id()) {\n\t\t\t\t\tother_connected.push_back(other);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// add the original portal as it is not in the connected portals vector\n\t\t\tother_connected.push_back(portal);\n\n\t\t\t// set the exits for the connected portal\n\t\t\tconnected->set_exits(this->id, other_connected);\n\n\t\t\t// we don't need to food fill for this portal since we have\n\t\t\t// found all exits, so we can remove it from the portals that\n\t\t\t// should be searched\n\t\t\tportal_ids.erase(connected->get_id());\n\t\t}\n\t}\n}\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/sector.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"coord/chunk.h\"\n#include \"pathfinding/portal.h\"\n#include \"pathfinding/types.h\"\n\n\nnamespace openage::path {\nclass CostField;\nclass Portal;\n\n/**\n * Sector in a grid for flow field pathfinding.\n *\n * Sectors consist of a cost field and a list of portals connecting them to adjacent\n * sectors.\n */\nclass Sector {\npublic:\n\t/**\n\t * Create a new sector with a specified ID and an uninitialized cost field.\n\t *\n\t * @param id ID of the sector. Should be unique per grid.\n\t * @param position Position of the sector in the grid.\n\t * @param field_size Size of the cost field.\n\t */\n\tSector(sector_id_t id,\n\t       const coord::chunk &position,\n\t       size_t field_size);\n\n\t/**\n\t * Create a new sector with a specified ID and an existing cost field.\n\t *\n\t * @param id ID of the sector. Should be unique per grid.\n\t * @param position Position of the sector in the grid.\n\t * @param cost_field Cost field of the sector.\n\t */\n\tSector(sector_id_t id,\n\t       const coord::chunk &position,\n\t       const std::shared_ptr<CostField> &cost_field);\n\n\t/**\n\t * Get the ID of this sector.\n\t *\n\t * IDs are unique per grid.\n\t *\n\t * @return ID of the sector.\n\t */\n\tconst sector_id_t &get_id() const;\n\n\t/**\n\t * Get the position of this sector in the grid.\n\t *\n\t * @return Position of the sector (absolute on the grid).\n\t */\n\tconst coord::chunk &get_position() const;\n\n\t/**\n\t * Get the cost field of this sector.\n\t *\n\t * @return Cost field of this sector.\n\t */\n\tconst std::shared_ptr<CostField> &get_cost_field() const;\n\n\t/**\n\t * Get the portals connecting this sector to other sectors.\n\t *\n\t * @return Outgoing portals of this sector.\n\t */\n\tconst std::vector<std::shared_ptr<Portal>> &get_portals() const;\n\n\t/**\n\t * Add a portal to another sector.\n\t *\n\t * @param portal Portal to another sector.\n\t */\n\tvoid add_portal(const std::shared_ptr<Portal> &portal);\n\n\t/**\n\t * Find portals connecting this sector to another sector.\n\t *\n\t * @param other Sector to which the portals should connect.\n\t * @param direction Direction from this sector to \\p other sector.\n\t * @param next_id ID of the next portal to be created. Should be unique per grid.\n\t *\n\t * @return Portals connecting this sector to \\p other sector.\n\t */\n\tstd::vector<std::shared_ptr<Portal>> find_portals(const std::shared_ptr<Sector> &other,\n\t                                                  PortalDirection direction,\n\t                                                  portal_id_t next_id) const;\n\n\t/**\n\t * Connect all portals that are mutually reachable.\n\t *\n\t * This method should be called after all sectors and portals have\n\t * been created and initialized.\n\t */\n\tvoid connect_exits();\n\nprivate:\n\t/**\n\t * ID of the sector.\n\t */\n\tsector_id_t id;\n\n\t/**\n\t * Position of the sector (absolute on the grid).\n\t */\n\tcoord::chunk position;\n\n\t/**\n\t * Cost field of the sector.\n\t */\n\tstd::shared_ptr<CostField> cost_field;\n\n\t/**\n\t * Portals of the sector.\n\t */\n\tstd::vector<std::shared_ptr<Portal>> portals;\n};\n\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/tests.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"log/log.h\"\n#include \"testing/testing.h\"\n\n#include \"coord/tile.h\"\n#include \"pathfinding/cost_field.h\"\n#include \"pathfinding/definitions.h\"\n#include \"pathfinding/flow_field.h\"\n#include \"pathfinding/integration_field.h\"\n#include \"pathfinding/integrator.h\"\n#include \"pathfinding/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage {\nnamespace path {\nnamespace tests {\n\nvoid flow_field() {\n\t// Create initial cost grid\n\tauto cost_field = std::make_shared<CostField>(3);\n\n\t// | 1 | 1 | 1 |\n\t// | 1 | X | 1 |\n\t// | 1 | 1 | 1 |\n\tconst time::time_t time = time::TIME_ZERO;\n\tcost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}, time);\n\n\t// Test the different field types\n\t{\n\t\tauto integration_field = std::make_shared<IntegrationField>(3);\n\t\tintegration_field->integrate_cost(cost_field, coord::tile_delta{2, 2});\n\t\tauto &int_cells = integration_field->get_cells();\n\n\t\t// The integration field should look like:\n\t\t// | 4 | 3 | 2 |\n\t\t// | 3 | X | 1 |\n\t\t// | 2 | 1 | 0 |\n\t\tauto int_expected = std::vector<integrated_cost_t>{\n\t\t\t4,\n\t\t\t3,\n\t\t\t2,\n\t\t\t3,\n\t\t\t65535,\n\t\t\t1,\n\t\t\t2,\n\t\t\t1,\n\t\t\t0,\n\t\t};\n\n\t\t// The flow field for targeting (2, 2) hould look like this:\n\t\t// | E  | SE | S |\n\t\t// | SE | X  | S |\n\t\t// | E  | E  | N |\n\t\tauto ff_expected = std::vector<flow_t>{\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH_EAST),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH_EAST),\n\t\t\t0,\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_TARGET_MASK | FLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::NORTH),\n\t\t};\n\n\t\t// Compare the integration field cells with the expected values\n\t\tfor (size_t i = 0; i < int_cells.size(); i++) {\n\t\t\tTESTEQUALS(int_cells[i].cost, int_expected[i]);\n\t\t}\n\n\t\t// Build the flow field\n\t\tauto flow_field = std::make_shared<FlowField>(3);\n\t\tflow_field->build(integration_field);\n\t\tauto ff_cells = flow_field->get_cells();\n\n\t\t// Compare the flow field cells with the expected values\n\t\tfor (size_t i = 0; i < ff_cells.size(); i++) {\n\t\t\tTESTEQUALS(ff_cells[i], ff_expected[i]);\n\t\t}\n\t}\n\n\t// Integrator test\n\t{\n\t\t// Integrator for managing the flow field\n\t\tauto integrator = std::make_shared<Integrator>();\n\n\t\t// Build the flow field\n\t\tauto flow_field = integrator->get(cost_field, coord::tile_delta{2, 2}).second;\n\t\tauto &ff_cells = flow_field->get_cells();\n\n\t\t// The flow field for targeting (2, 2) hould look like this:\n\t\t// | E  | SE | S |\n\t\t// | SE | X  | S |\n\t\t// | E  | E  | N |\n\t\tauto ff_expected = std::vector<flow_t>{\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH_EAST),\n\t\t\tFLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH),\n\t\t\tFLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH_EAST),\n\t\t\t0,\n\t\t\tFLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::SOUTH),\n\t\t\tFLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_LOS_MASK | FLOW_PATHABLE_MASK | static_cast<uint8_t>(flow_dir_t::EAST),\n\t\t\tFLOW_LOS_MASK | FLOW_PATHABLE_MASK | FLOW_TARGET_MASK,\n\t\t};\n\n\t\t// Compare the flow field cells with the expected values\n\t\tfor (size_t i = 0; i < ff_cells.size(); i++) {\n\t\t\tTESTEQUALS(ff_cells[i], ff_expected[i]);\n\t\t}\n\t}\n}\n\n\n} // namespace tests\n} // namespace path\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pathfinding/types.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::path {\n\n// this file is intentionally empty\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/pathfinding/types.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <memory>\n#include <utility>\n\n\nnamespace openage::path {\n\n/**\n * Path result type.\n */\nenum class PathResult {\n\t/// Path was found.\n\tFOUND,\n\t/// Path was not found.\n\tNOT_FOUND,\n\t/// Target is not on grid.\n\tOUT_OF_BOUNDS,\n};\n\n/**\n * Movement cost in the cost field.\n *\n * TODO: Cost stamps\n *\n * 0: uninitialized\n * 1-254: normal cost\n * 255: impassable\n */\nusing cost_t = uint8_t;\n\n/**\n * Integrated cost in the integration field.\n */\nusing integrated_cost_t = uint16_t;\n\n/**\n * Integrated field cell flags.\n */\nusing integrated_flags_t = uint8_t;\n\n/**\n * Integration field cell value.\n */\nstruct integrated_t {\n\t/**\n\t * Total integrated cost.\n\t */\n\tintegrated_cost_t cost;\n\n\t/**\n\t * Flags.\n\t *\n\t * Bit 0-3: Shared flags with the flow field.\n\t *  - 0: Unused.\n\t *  - 1: Target flag.\n\t *  - 2: Line of sight flag.\n\t *  - 3: Unused.\n\t * Bit 4-7: Integration field specific flags.\n\t *  - 4: Unused.\n\t *  - 5: Wave front blocked flag.\n\t *  - 6: LOS found flag.\n\t *  - 7: Unused.\n\t */\n\tintegrated_flags_t flags;\n};\n\n/**\n * Flow field direction types.\n *\n * Encoded into the flow_t values.\n */\nenum class flow_dir_t : uint8_t {\n\tNORTH = 0x00,\n\tNORTH_EAST = 0x01,\n\tEAST = 0x02,\n\tSOUTH_EAST = 0x03,\n\tSOUTH = 0x04,\n\tSOUTH_WEST = 0x05,\n\tWEST = 0x06,\n\tNORTH_WEST = 0x07,\n};\n\n/**\n * Flow field cell value.\n *\n * Bit 0: Unused.\n * Bit 1: Target flag.\n * Bit 2: Line of sight flag.\n * Bit 3: Pathable flag.\n * Bits 4-7: flow direction.\n */\nusing flow_t = uint8_t;\n\n/**\n * Grid identifier.\n */\nusing grid_id_t = size_t;\n\n/**\n * Sector identifier (unique per grid).\n */\nusing sector_id_t = size_t;\n\n/**\n * Portal identifier (unique per grid).\n */\nusing portal_id_t = size_t;\n\nclass FlowField;\nclass IntegrationField;\n\n/**\n * Cache key for accessing the field cache using a portal id and a sector id.\n */\nusing cache_key_t = std::pair<portal_id_t, sector_id_t>;\n\n/**\n * Returnable field cache entry pair containing an integration field and a flow field.\n */\nusing field_cache_t = std::pair<std::shared_ptr<IntegrationField>, std::shared_ptr<FlowField>>;\n\n} // namespace openage::path\n"
  },
  {
    "path": "libopenage/presenter/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tpresenter.cpp\n)\n"
  },
  {
    "path": "libopenage/presenter/presenter.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"presenter.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <iostream>\n#include <string>\n#include <vector>\n\n#include \"gamestate/simulation.h\"\n#include \"input/controller/camera/binding_context.h\"\n#include \"input/controller/camera/controller.h\"\n#include \"input/controller/game/binding_context.h\"\n#include \"input/controller/game/controller.h\"\n#include \"input/controller/hud/binding_context.h\"\n#include \"input/controller/hud/controller.h\"\n#include \"input/input_context.h\"\n#include \"input/input_manager.h\"\n#include \"log/log.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/gui/gui.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/render_factory.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/stages/camera/manager.h\"\n#include \"renderer/stages/hud/render_stage.h\"\n#include \"renderer/stages/screen/render_stage.h\"\n#include \"renderer/stages/skybox/render_stage.h\"\n#include \"renderer/stages/terrain/render_stage.h\"\n#include \"renderer/stages/world/render_stage.h\"\n#include \"time/time_loop.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::presenter {\n\nPresenter::Presenter(const util::Path &root_dir,\n                     const std::shared_ptr<gamestate::GameSimulation> &simulation,\n                     const std::shared_ptr<time::TimeLoop> &time_loop) :\n\troot_dir{root_dir},\n\trender_passes{},\n\tsimulation{simulation},\n\ttime_loop{time_loop} {}\n\n\nvoid Presenter::run(const renderer::window_settings window_settings) {\n\tlog::log(INFO << \"Presenter: Launching subsystems...\");\n\n\tthis->init_graphics(window_settings);\n\n\tthis->init_input();\n\n\twhile (not this->window->should_close()) {\n\t\tthis->gui_app->process_events();\n\t\t// TODO: pass button presses and events from GUI to controller\n\n\t\tthis->render();\n\n\t\tthis->renderer->check_error();\n\n\t\tthis->window->update();\n\t}\n\n\tlog::log(MSG(info) << \"Presenter: Draw loop exited\");\n\n\tif (this->simulation) {\n\t\tthis->simulation->stop();\n\t}\n\n\tif (this->time_loop) {\n\t\tthis->time_loop->stop();\n\t}\n\n\tthis->window->close();\n}\n\nvoid Presenter::set_simulation(const std::shared_ptr<gamestate::GameSimulation> &simulation) {\n\tthis->simulation = simulation;\n\tauto render_factory = std::make_shared<renderer::RenderFactory>(this->terrain_renderer, this->world_renderer);\n\tthis->simulation->attach_renderer(render_factory);\n}\n\nvoid Presenter::set_time_loop(const std::shared_ptr<time::TimeLoop> &time_loop) {\n\tthis->time_loop = time_loop;\n}\n\nstd::shared_ptr<qtgui::GuiApplication> Presenter::init_window_system() {\n\treturn std::make_shared<renderer::gui::GuiApplicationWithLogger>();\n}\n\nvoid Presenter::init_graphics(const renderer::window_settings &window_settings) {\n\tlog::log(INFO << \"Presenter: Initializing graphics subsystems...\");\n\n\t// Start up rendering framework\n\tthis->gui_app = this->init_window_system();\n\n\t// Window and renderer\n\tthis->window = renderer::Window::create(\"openage presenter test\", window_settings);\n\tthis->renderer = this->window->make_renderer();\n\n\t// Asset mangement\n\tthis->asset_manager = std::make_shared<renderer::resources::AssetManager>(\n\t\tthis->renderer,\n\t\tthis->root_dir / \"assets\" / \"converted\");\n\tauto missing_tex = this->root_dir / \"assets\" / \"test\" / \"textures\" / \"test_missing.sprite\";\n\tthis->asset_manager->set_placeholder_animation(missing_tex);\n\n\t// Camera\n\tthis->camera = std::make_shared<renderer::camera::Camera>(this->renderer, this->window->get_size());\n\tthis->camera->look_at_coord(coord::scene3{10.0, 10.0, 0}); // Center camera on the map\n\tthis->window->add_resize_callback([this](size_t w, size_t h, double /*scale*/) {\n\t\tthis->camera->resize(w, h);\n\t});\n\n\t// Camera manager\n\tthis->camera_manager = std::make_shared<renderer::camera::CameraManager>(this->camera);\n\t// TODO: Make boundaries dynamic based on map size.\n\tthis->camera_manager->set_camera_boundaries(\n\t\trenderer::camera::CameraBoundaries{\n\t\t\trenderer::camera::X_BOUND_MIN,\n\t\t\trenderer::camera::X_BOUND_MAX,\n\t\t\trenderer::camera::Y_BOUND_MIN,\n\t\t\trenderer::camera::Y_BOUND_MAX,\n\t\t\trenderer::camera::Z_BOUND_MIN,\n\t\t\trenderer::camera::Z_BOUND_MAX});\n\n\t// Skybox\n\tthis->skybox_renderer = std::make_shared<renderer::skybox::SkyboxRenderStage>(\n\t\tthis->window,\n\t\tthis->renderer,\n\t\tthis->root_dir[\"assets\"][\"shaders\"]);\n\tthis->skybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f);\n\tthis->render_passes.push_back(this->skybox_renderer->get_render_pass());\n\n\t// Terrain\n\tthis->terrain_renderer = std::make_shared<renderer::terrain::TerrainRenderStage>(\n\t\tthis->window,\n\t\tthis->renderer,\n\t\tthis->camera,\n\t\tthis->root_dir[\"assets\"][\"shaders\"],\n\t\tthis->asset_manager,\n\t\tthis->time_loop->get_clock());\n\tthis->render_passes.push_back(this->terrain_renderer->get_render_pass());\n\n\t// Units/buildings\n\tthis->world_renderer = std::make_shared<renderer::world::WorldRenderStage>(\n\t\tthis->window,\n\t\tthis->renderer,\n\t\tthis->camera,\n\t\tthis->root_dir[\"assets\"][\"shaders\"],\n\t\tthis->asset_manager,\n\t\tthis->time_loop->get_clock());\n\tthis->render_passes.push_back(this->world_renderer->get_render_pass());\n\n\t// HUD\n\tthis->hud_renderer = std::make_shared<renderer::hud::HudRenderStage>(\n\t\tthis->window,\n\t\tthis->renderer,\n\t\tthis->camera,\n\t\tthis->root_dir[\"assets\"][\"shaders\"],\n\t\tthis->asset_manager,\n\t\tthis->time_loop->get_clock());\n\tthis->render_passes.push_back(this->hud_renderer->get_render_pass());\n\n\tthis->init_gui();\n\tthis->init_final_render_pass();\n\n\tif (this->simulation) {\n\t\tauto render_factory = std::make_shared<renderer::RenderFactory>(this->terrain_renderer, this->world_renderer);\n\t\tthis->simulation->attach_renderer(render_factory);\n\t}\n\n\tlog::log(INFO << \"Presenter: Graphics subsystems initialized\");\n}\n\nvoid Presenter::init_gui() {\n\tlog::log(INFO << \"Presenter: Initializing GUI with Qt backend\");\n\n\t//// -- gui initialization\n\t// TODO: Do not use test GUI\n\tutil::Path qml_root = this->root_dir / \"assets\" / \"test\" / \"qml\";\n\tlog::log(INFO << \"Presenter: Setting QML root to \" << qml_root.resolve_native_path());\n\tif (not qml_root.is_dir()) {\n\t\tthrow Error{ERR << \"could not find qml root folder \" << qml_root};\n\t}\n\n\tutil::Path qml_assets = this->root_dir / \"assets\";\n\tlog::log(INFO << \"Presenter: Setting QML asset path to \" << qml_assets.resolve_native_path());\n\tif (not qml_assets.is_dir()) {\n\t\tthrow Error{ERR << \"could not find asset root folder \" << qml_assets};\n\t}\n\n\tutil::Path qml_root_file = qml_root / \"main.qml\";\n\tlog::log(INFO << \"Presenter: Setting QML root file to \" << qml_root_file.resolve_native_path());\n\tif (not qml_root_file.is_file()) {\n\t\tthrow Error{ERR << \"could not find main.qml file \" << qml_root_file};\n\t}\n\n\t// TODO: in order to support qml-mods, the fslike and filelike\n\t//       library has to be integrated into qt. For now,\n\t//       figure out the absolute paths here and pass them in.\n\n\tthis->gui = std::make_shared<renderer::gui::GUI>(\n\t\tthis->gui_app, // Qt application wrapper\n\t\tthis->window,  // window for the gui\n\t\tqml_root_file, // entry qml file, absolute path.\n\t\tqml_root,      // directory to watch for qml file changes\n\t\tqml_assets,    // qml data: Engine *, the data directory, ...\n\t\tthis->renderer // openage renderer\n\t);\n\n\tauto gui_pass = this->gui->get_render_pass();\n\tthis->render_passes.push_back(gui_pass);\n}\n\nvoid Presenter::init_input() {\n\tlog::log(INFO << \"Presenter: Initializing input subsystem...\");\n\n\tthis->input_manager = std::make_shared<input::InputManager>();\n\n\tthis->window->add_key_callback([&](const QKeyEvent &ev) {\n\t\tthis->input_manager->process(ev);\n\t});\n\tthis->window->add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tthis->input_manager->process(ev);\n\t});\n\tthis->window->add_mouse_move_callback([&](const QMouseEvent &ev) {\n\t\tthis->input_manager->set_mouse(ev.position().x(), ev.position().y());\n\t\tthis->input_manager->process(ev);\n\t});\n\tthis->window->add_mouse_wheel_callback([&](const QWheelEvent &ev) {\n\t\tthis->input_manager->process(ev);\n\t});\n\n\tauto input_ctx = this->input_manager->get_global_context();\n\tinput::setup_defaults(input_ctx);\n\n\t// setup simulation controls\n\tif (this->simulation) {\n\t\tlog::log(INFO << \"Loading game simulation controls\");\n\n\t\t// TODO: Remove hardcoding\n\t\tauto game_controller = std::make_shared<input::game::Controller>(\n\t\t\tstd::unordered_set<size_t>{0, 1, 2, 3}, 0);\n\t\tauto engine_context = std::make_shared<input::game::BindingContext>();\n\t\tinput::game::setup_defaults(engine_context, this->time_loop, this->simulation, this->camera);\n\t\tthis->input_manager->set_game_controller(game_controller);\n\t\tinput_ctx->set_game_bindings(engine_context);\n\t}\n\n\t// attach GUI if it's initialized\n\tif (this->gui) {\n\t\tlog::log(INFO << \"Loading GUI controls\");\n\t\tthis->input_manager->set_gui(this->gui->get_input_handler());\n\t}\n\n\t// setup camera controls\n\tif (this->camera) {\n\t\tlog::log(INFO << \"Loading camera controls\");\n\t\tauto camera_controller = std::make_shared<input::camera::Controller>();\n\t\tauto camera_context = std::make_shared<input::camera::BindingContext>();\n\t\tinput::camera::setup_defaults(camera_context, this->camera, this->camera_manager);\n\t\tthis->input_manager->set_camera_controller(camera_controller);\n\t\tinput_ctx->set_camera_bindings(camera_context);\n\t}\n\n\t// setup HUD controls\n\tif (this->hud_renderer) {\n\t\tlog::log(INFO << \"Loading HUD controls\");\n\t\tauto hud_controller = std::make_shared<input::hud::Controller>();\n\t\tauto hud_context = std::make_shared<input::hud::BindingContext>();\n\t\tinput::hud::setup_defaults(hud_context, this->hud_renderer);\n\t\tthis->input_manager->set_hud_controller(hud_controller);\n\t\tinput_ctx->set_hud_bindings(hud_context);\n\t}\n\n\tlog::log(INFO << \"Presenter: Input subsystem initialized\");\n}\n\nvoid Presenter::init_final_render_pass() {\n\t// Final output to window\n\tthis->screen_renderer = std::make_shared<renderer::screen::ScreenRenderStage>(\n\t\tthis->window,\n\t\tthis->renderer,\n\t\tthis->root_dir[\"assets\"][\"shaders\"]);\n\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\tfor (auto pass : this->render_passes) {\n\t\ttargets.push_back(pass->get_target());\n\t}\n\tthis->screen_renderer->set_render_targets(targets);\n\tthis->render_passes.push_back(this->screen_renderer->get_render_pass());\n\n\t// Update final render pass if the textures are reassigned on resize\n\t// TODO: This REQUIRES that all other render passes have already been\n\t//       resized\n\tthis->window->add_resize_callback([this](size_t, size_t, double /*scale*/) {\n\t\t// Acquire the render targets for all previous passes\n\t\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\t\tfor (size_t i = 0; i < this->render_passes.size() - 1; ++i) {\n\t\t\ttargets.push_back(this->render_passes[i]->get_target());\n\t\t}\n\t\tthis->screen_renderer->set_render_targets(targets);\n\t});\n}\n\nvoid Presenter::render() {\n\t// TODO: Pass current time to update() instead of fetching it in renderer\n\tthis->camera_manager->update();\n\tthis->terrain_renderer->update();\n\tthis->world_renderer->update();\n\tthis->hud_renderer->update();\n\tthis->gui->render();\n\n\tfor (auto &pass : this->render_passes) {\n\t\tthis->renderer->render(pass);\n\t}\n}\n\n} // namespace openage::presenter\n"
  },
  {
    "path": "libopenage/presenter/presenter.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include \"renderer/window.h\"\n#include \"util/path.h\"\n\n\nnamespace qtgui {\nclass GuiApplication;\n}\n\nnamespace openage {\n\nnamespace gamestate {\nclass GameSimulation;\n}\n\nnamespace input {\nclass InputManager;\n}\n\nnamespace time {\nclass TimeLoop;\n}\n\nnamespace renderer {\nclass RenderPass;\nclass Renderer;\nclass Texture2d;\nclass ShaderProgram;\nclass Window;\n\nnamespace camera {\nclass Camera;\nclass CameraManager;\n} // namespace camera\n\nnamespace gui {\nclass GUI;\n}\n\nnamespace hud {\nclass HudRenderStage;\n}\n\nnamespace screen {\nclass ScreenRenderStage;\n}\n\nnamespace skybox {\nclass SkyboxRenderStage;\n}\n\nnamespace terrain {\nclass TerrainRenderStage;\n}\n\nnamespace world {\nclass WorldRenderStage;\n}\n\nnamespace resources {\nclass AssetManager;\n}\n\n} // namespace renderer\n\nnamespace presenter {\n\nclass Presenter {\npublic:\n\t/**\n\t * Create a new presenter.\n\t *\n\t * @param path Root directory path.\n\t * @param simulation Game simulation. Can be set later with \\p set_engine()\n\t * @param time_loop Time loop which controls simulation time. Can be set later with \\p set_time_loop()\n\t */\n\tPresenter(const util::Path &path,\n\t          const std::shared_ptr<gamestate::GameSimulation> &simulation = nullptr,\n\t          const std::shared_ptr<time::TimeLoop> &time_loop = nullptr);\n\n\t~Presenter() = default;\n\n\t/**\n\t * Start the presenter and initialize subsystems.\n\t *\n\t * @param window_settings The settings to customize the display window (e.g. size, display mode, vsync).\n\t */\n\tvoid run(const renderer::window_settings window_settings = {});\n\n\t/**\n\t * Set the game simulation controlled by this presenter.\n\t *\n\t * @param simulation Game simulation.\n\t */\n\tvoid set_simulation(const std::shared_ptr<gamestate::GameSimulation> &simulation);\n\n\t/**\n\t * Set the time loop controlled by this presenter.\n\t *\n\t * @param time_loop Time loop.\n\t */\n\tvoid set_time_loop(const std::shared_ptr<time::TimeLoop> &time_loop);\n\n\t/**\n\t * Initialize the Qt application managing the graphical views. Required\n\t * for creating windows.\n\t *\n\t * @returns Pointer to openage's application wrapper,\n\t */\n\tstatic std::shared_ptr<qtgui::GuiApplication> init_window_system();\n\nprotected:\n\t/**\n\t * Initialize all graphics subsystems of the presenter, i.e.\n\t *     - window creation\n\t *     - main renderer\n\t *     - component renderers (Terrain, Game Entities, GUI)\n\t */\n\tvoid init_graphics(const renderer::window_settings &window_settings = {});\n\n\t/**\n\t * Initialize the GUI.\n\t */\n\tvoid init_gui();\n\n\t/**\n\t * Initialize the input management.\n\t */\n\tvoid init_input();\n\n\t/**\n\t * Initialize the final render pass that renders the results of all previous\n\t * render passes to the window screen.\n\t */\n\tvoid init_final_render_pass();\n\n\t// void init_audio();\n\n\t/**\n\t * Render all configured render passes in sequence.\n\t */\n\tvoid render();\n\n\t// TODO: remove and move into our config/settings system\n\tutil::Path root_dir;\n\n\t// graphis components\n\t/**\n\t * Windowing GUI Application wrapper.\n\t */\n\tstd::shared_ptr<qtgui::GuiApplication> gui_app;\n\n\t/**\n\t * Display window.\n\t */\n\tstd::shared_ptr<renderer::Window> window;\n\n\t/**\n\t * openage's graphics renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Camera for viewing things.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Qt-based GUI for interface.\n\t */\n\tstd::shared_ptr<renderer::gui::GUI> gui;\n\n\t/**\n\t * Camera manager for camera controls.\n\t */\n\tstd::shared_ptr<renderer::camera::CameraManager> camera_manager;\n\n\t/**\n\t * Graphics output for the map background.\n\t */\n\tstd::shared_ptr<renderer::skybox::SkyboxRenderStage> skybox_renderer;\n\n\t/**\n\t * Graphics output for terrain.\n\t */\n\tstd::shared_ptr<renderer::terrain::TerrainRenderStage> terrain_renderer;\n\n\t/**\n\t * Graphics output for units/buildings.\n\t */\n\tstd::shared_ptr<renderer::world::WorldRenderStage> world_renderer;\n\n\t/**\n\t * Graphics output for the HUD.\n\t */\n\tstd::shared_ptr<renderer::hud::HudRenderStage> hud_renderer;\n\n\t/**\n\t * Final graphics output to the window screen.\n\t */\n\tstd::shared_ptr<renderer::screen::ScreenRenderStage> screen_renderer;\n\n\t/**\n\t * Manager for loading/storing asset resources.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Render passes in the openage renderer.\n\t */\n\tstd::vector<std::shared_ptr<renderer::RenderPass>> render_passes;\n\n\t/**\n\t * Game simulation.\n\t */\n\tstd::shared_ptr<gamestate::GameSimulation> simulation;\n\n\t/**\n\t * Time loop.\n\t */\n\tstd::shared_ptr<time::TimeLoop> time_loop;\n\n\t/**\n\t * Input manager.\n\t */\n\tstd::shared_ptr<input::InputManager> input_manager;\n};\n\n} // namespace presenter\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/CMakeLists.txt",
    "content": "add_sources(libopenage\n\texctranslate.cpp\n\texctranslate_tests.cpp\n\tdefs.cpp\n\tfunctional.cpp\n\thacks.cpp\n\tpyexception.cpp\n\tpyobject.cpp\n\tpyobject_tests.cpp\n\tsetup.cpp\n)\n\npxdgen(\n\texctranslate.h\n\texctranslate_tests.h\n\tdefs.h\n\tfunctional.h\n\thacks.h\n\tpyexception.h\n\tpyobject.h\n\tsetup.h\n)\n"
  },
  {
    "path": "libopenage/pyinterface/defs.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"defs.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\n\n}} // openage::pyinterface\n"
  },
  {
    "path": "libopenage/pyinterface/defs.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// we want to avoid the Python.h import,\n// we only need the prototype anyway.\n#ifndef Py_OBJECT_H\n// pxd: from cpython.ref cimport PyObject\nextern \"C\" {\ntypedef struct _object PyObject;\n}\n#endif\n"
  },
  {
    "path": "libopenage/pyinterface/exctranslate.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"exctranslate.h\"\n\n#include <new>\n#include <typeinfo>\n#include <stdexcept>\n#include <string>\n#include <ios>\n\n#include \"../util/timing.h\"\n#include \"../error/error.h\"\n#include \"../util/compiler.h\"\n#include \"../util/thread_id.h\"\n#include \"../log/level.h\"\n\n#include \"pyexception.h\"\n\nnamespace openage {\nnamespace pyinterface {\n\n\n// python exception translation function pointers\nvoid (*raise_cpp_error)(Error *) = nullptr;\nvoid (*raise_cpp_pyexception)(PyException *) = nullptr;\n\nvoid (*describe_py_exception)(PyException *) = nullptr;\nbool (*check_for_py_exception)() = nullptr;\n\n\nvoid set_exc_translation_funcs(\n\tvoid (*raise_cpp_error_impl)(Error *),\n\tvoid (*raise_cpp_pyexception_impl)(PyException *),\n\tbool (*check_for_py_exception_impl)(),\n\tvoid (*describe_py_exception_impl)(PyException *)) {\n\n\traise_cpp_error = raise_cpp_error_impl;\n\traise_cpp_pyexception = raise_cpp_pyexception_impl;\n\tcheck_for_py_exception = check_for_py_exception_impl;\n\tdescribe_py_exception = describe_py_exception_impl;\n}\n\n\n/*\n * This function is the cython exception handler!\n */\nvoid translate_exc_cpp_to_py() {\n\ttry {\n\t\t// when we reach this, cython caught an error.\n\t\t// and we're now in the handler.\n\n\t\t// to continue, first rethrow the exception so we can analyze it\n\t\t// and to restore its context in case it's an exception that doesn't\n\t\t// store useful information.\n\t\tthrow;\n\n\t} catch (PyException &exc) {\n\n\t\tif (raise_cpp_pyexception == nullptr) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) <<\n\t\t\t\t\"raise_pyexception_in_py is uninitialized; \"\n\t\t\t\t\"can't translate C++ exception to Python exception.\",\n\t\t\t\tfalse, false);\n\t\t}\n\n\t\t// handle the python object directly by PyErr_SetObject\n\t\traise_cpp_pyexception(&exc);\n\n\t} catch (Error &exc) {\n\n\t\tif (raise_cpp_error == nullptr) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) <<\n\t\t\t\t\"raise_error_in_py is uninitialized; \"\n\t\t\t\t\"can't translate C++ exception to Python exception.\",\n\t\t\t\tfalse, false);\n\t\t}\n\n\t\t// translate the exception to python\n\t\t// = insert it in pythons backtrace\n\t\traise_cpp_error(&exc);\n\t}\n\n\t/*\n\t * all other exceptions are more than unexpected; even std::exception.\n\t * they don't contain any useful stack trace information,\n\t * so the safest course of action is not to catch them.\n\t * That way, terminate() is called, where we can analyze the stack\n\t * trace and debug the issue in gdb.\n\t */\n}\n\n\nvoid translate_exc_py_to_cpp() {\n\tif (describe_py_exception == nullptr) [[unlikely]] {\n\t\tthrow Error(MSG(err) <<\n\t\t\t\"describe_py_exception is uninitialized; \"\n\t\t\t\"can't check for and translate Python exception to C++ exception.\");\n\t}\n\n\tif (not check_for_py_exception()) [[likely]] {\n\t\t// no exception has occurred.\n\t\treturn;\n\t}\n\n\tPyException pyex;\n\tdescribe_py_exception(&pyex);\n\n\t// recurse to throw a possible cause.\n\ttry {\n\t\ttranslate_exc_py_to_cpp();\n\t} catch (...) {\n\t\tpyex.store_cause();\n\t}\n\n\t// throw exception without cause.\n\tthrow pyex;\n}\n\n\nvoid init_exc_message(log::message *msg, const std::string &filename, unsigned int lineno, const std::string &functionname) noexcept {\n\ttry {\n\t\tmsg->init_with_metadata_copy(filename, functionname);\n\t} catch (...) {\n\t\t// we cannot afford to raise an exception from this function;\n\t\t// this is part of the exception translation code.\n\t\tstd::cout << \"[WTF] failed so init exception message!\" << std::endl;\n\t}\n\n\tmsg->lvl = log::level::err;\n\tmsg->lineno = lineno;\n}\n\n\n}} // openage::pyinterface\n"
  },
  {
    "path": "libopenage/pyinterface/exctranslate.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/*\n * This hack replaces Cython's default 'except +' exception translator\n * with the one defined here.\n *\n * This is _the_ entry point for the translation magic!\n */\n#define __Pyx_CppExn2PyErr ::openage::pyinterface::translate_exc_cpp_to_py\n\n\n// pxd: from libcpp cimport bool as cppbool\n// pxd: from libcpp.string cimport string\n// pxd: from libopenage.error.error cimport Error\n// pxd: from libopenage.log.message cimport message\n\n// pxd: from libopenage.pyinterface.pyexception cimport PyException\n\n#include \"../error/error.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\n\nclass PyException;\n\n\n/**\n * Analyzes the current C++ exception,\n * and creates and raises an analogous Python CPPException object.\n *\n * If there's no current C++ exception, this terminates the\n * program non-gracefully (std::terminate).\n *\n * Designed to be called by auto-generated Cython code;\n * Do not use in any other way.\n */\nOAAPI void translate_exc_cpp_to_py();\n\n\n/**\n * Analyzes the current Python exception,\n * and creates and throws an analogous C++ PyException object.\n *\n * Works, and is designed to be used in, situations where\n * there is no current Python exception (it's a no-op then).\n */\nOAAPI void translate_exc_py_to_cpp();\n\n\n/**\n * For use by Python, to initialize a message's meta data.\n *\n * Initializes all values.\n *\n * pxd: void init_exc_message(message *msg, string filename, unsigned int lineno, string functionname) noexcept\n */\nOAAPI void init_exc_message(log::message *msg, const std::string &filename, unsigned int lineno, const std::string &functionname) noexcept;\n\n\n/*\n * The PyIfFunc callback wrapper can _not_ be used to wrap these\n * functions, because those methods are used from within the Func code\n * itself, leading to\n *\n *  - infinite loops\n *  - immediate back-conversion of raised Python CPPException objects\n *\n * Thus, we're using old-school function pointers here.\n *\n * Note that this should be the only place in the entire C++ code base where\n * this is necessary.\n *\n * Everybody who calls one of those function pointers must manually ensure\n * that they are not nullptr (which would occur if libopenage has not been\n * properly initialized by the Python part of the code).\n */\n\n\n/**\n * Installs the basic interface functions that are used for exception\n * translation.\n *\n * No PyIfFunc wrappers can be used here, because those internally\n * rely on exception translation themselves, which would lead to\n *\n *  - infinite loops\n *  - immediate back-conversion of raised Python CPPException objects\n *\n * Thus, we instead offer this setter function, to be called during\n * openage.cppinterface.exctranslate.setup().\n *\n * @param raise_cpp_error:\n *\n * Raises the given C++ Exception object as a Python exception.\n * called by translate_exc_cpp_to_py.\n *\n * @param raise_cpp_pyexception:\n *\n * Raises the given C++ PyException object as a Python exception.\n * called by translate_exc_cpp_to_py.\n *\n * The PyException still contains a reference to the original Python object\n * that was translated by describe_py_exception - we just use that.\n *\n * Invoked by openage::pyinterface::cpp_to_py.\n *\n * @param check_for_py_exception\n *\n * Used by translate_exc_cpp_to_py to check whether there is a currently-active\n * Python exception.\n *\n * @param describe_py_exception\n *\n * Used by translate_exc_cpp_to_py to gather information about\n * the current Python exception.\n *\n * Calls PyErr_Fetch and stores all sorts of information in the\n * PyException.\n * The current Python exception is cleared in the process.\n *\n * If the current Python exception had a cause, that cause is set\n * as the new active exception.\n * Call check_exception() afterwards to test whether that has happend.\n *\n * If there's no currently-active Python Exception, the behavior is\n * undefined; make sure to call check_exception() before.\n *\n * Afterwards, the PyException is ready for throwing.\n *\n * pxd:\n *\n * void set_exc_translation_funcs(\n *     void (*)(Error *)       except * with gil,\n *     void (*)(PyException *) except * with gil,\n *     cppbool (*)()                    with gil,\n *     void (*)(PyException *) except * with gil\n * ) noexcept\n */\nOAAPI void set_exc_translation_funcs(\n\tvoid (*raise_cpp_error)(Error *),\n\tvoid (*raise_cpp_pyexception)(PyException *),\n\tbool (*check_for_py_exception)(),\n\tvoid (*describe_py_exception)(PyException *));\n\n\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/exctranslate_tests.cpp",
    "content": "// Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n#include \"exctranslate_tests.h\"\n\n#include <vector>\n\n#include \"../log/log.h\"\n#include \"../testing/testing.h\"\n#include \"pyexception.h\"\n\n\nnamespace openage::pyinterface::tests {\n\n\ntemplate<int i=3>\nvoid throw_foo() {\n\tthrow_foo<i - 1>();\n}\n\n\ntemplate<>\nvoid throw_foo<0>() {\n\tthrow Error(MSG(err) << \"foo\", true, true);\n}\n\n\nvoid err_cpp_to_py_helper() {\n\ttry {\n\t\ttry {\n\t\t\tthrow Error(MSG(err) << \"rofl\");\n\t\t} catch (...) {\n\t\t\tTESTFAIL;\n\t\t}\n\t} catch (...) {\n\t\tthrow_foo();\n\t}\n}\n\n\nvoid err_py_to_cpp() {\n\t// due to the nature of this test, we need to be careful about\n\t// throwing TestError to indicate test failure...\n\n\t// we'll instead call this helper lambda, which will return\n\t// a std::string error message.\n\n\tstd::string testresult = []() -> std::string {\n\n\t\ttry {\n\t\t\terr_py_to_cpp_helper.call();\n\t\t\treturn \"err_py_to_cpp_helper didn't throw an exception\";\n\n\t\t} catch (PyException &exc) {\n\t\t\t// this is what we expected.\n\t\t\t// now let's see whether the object contains the expected data.\n\t\t\tif (exc.type_name() != \"openage.cppinterface.exctranslate_tests.Bar\") {\n\t\t\t\treturn \"unexpected exc typename: \" + exc.type_name();\n\t\t\t}\n\n\t\t\tstd::string msg = exc.what();\n\t\t\tif (msg != \"bar\") {\n\t\t\t\treturn \"unexpected exc message: \" + msg;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\texc.rethrow_cause();\n\t\t\t\treturn \"exc had no cause\";\n\n\t\t\t} catch (PyException &cause) {\n\t\t\t\tif (cause.type_name() != \"openage.testing.testing.TestError\") {\n\t\t\t\t\treturn \"unexpected cause typename: \" + cause.type_name();\n\t\t\t\t}\n\n\t\t\t\tstd::string causemsg = cause.what();\n\t\t\t\tif (causemsg != \"foo\") {\n\t\t\t\t\treturn \"unexpected cause message: \" + causemsg;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tcause.rethrow_cause();\n\t\t\t\t\treturn \"OK\";\n\t\t\t\t} catch (...) {\n\t\t\t\t\treturn \"cause unexpectedly had a cause itself.\";\n\t\t\t\t}\n\n\t\t\t} catch (std::exception &cause) {\n\t\t\t\treturn \"exc had cause of unexpected type \" + util::typestring(cause);\n\t\t\t} catch (...) {\n\t\t\t\treturn \"exc had cause of nonstandard type\";\n\t\t\t}\n\n\t\t} catch (std::exception &cause) {\n\t\t\treturn \"exc had unexpected type \" + util::typestring(cause);\n\t\t} catch (...) {\n\t\t\treturn \"exc had nonstandard type\";\n\t\t}\n\n\t}();\n\n\tif (testresult != \"OK\") {\n\t\tTESTFAILMSG(testresult);\n\t}\n}\n\n\nvoid err_py_to_cpp_demo() {\n\ttry {\n\t\terr_py_to_cpp_helper.call();\n\t\tlog::log(MSG(err) <<\n\t\t\t\"Unexpectedly, the helper did not raise an Exception\");\n\n\t} catch (Error &err) {\n\t\tlog::log(MSG(info) <<\n\t\t\t\"The helper raised the following Exception:\" << std::endl <<\n\t\t\terr);\n\t}\n}\n\n\nvoid bounce_call(const Func<void> &func, int times) {\n\tbounce_call_py.call(func, times);\n}\n\n\nPyIfFunc<void> err_py_to_cpp_helper;\nPyIfFunc<void, Func<void>, int> bounce_call_py;\n\n\n} // openage::pyinterface::tests\n"
  },
  {
    "path": "libopenage/pyinterface/exctranslate_tests.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc0, PyIfFunc2, Func0\n#include \"functional.h\"\n\nnamespace openage {\nnamespace pyinterface {\nnamespace tests {\n\n\n/**\n * Called by cppinterface.demo_cpp_to_py.\n * Throws some exceptions, for translation to Python.\n *\n * pxd: void err_cpp_to_py_helper() except +\n */\nOAAPI void err_cpp_to_py_helper();\n\n\n/**\n * Part of pyinterface::demo_py_to_cpp.\n * Shall throw some exceptions, which we then translate to C++.\n *\n * pxd: PyIfFunc0[void] err_py_to_cpp_helper\n */\nextern OAAPI PyIfFunc<void> err_py_to_cpp_helper;\n\n\n/**\n * See the doc in exctranslate_tests.pyx.\n *\n * pxd: void bounce_call(Func0[void], int) except +\n */\nOAAPI void bounce_call(const Func<void> &func, int times);\n\n\n/**\n * Called by bounce_call() to bounce back to Python.\n *\n * pxd: PyIfFunc2[void, Func0[void], int] bounce_call_py\n */\nextern OAAPI PyIfFunc<void, Func<void>, int> bounce_call_py;\n\n\n} // namespace tests\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/functional.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"functional.h\"\n\nnamespace openage {\nnamespace pyinterface {\n\n\n}} // openage::pyinterface\n"
  },
  {
    "path": "libopenage/pyinterface/functional.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <mutex>\n\n#include \"../util/compiler.h\"\n#include \"../util/language.h\"\n\n#include \"exctranslate.h\"\n#include \"setup.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\n\n\n/**\n * Holds a function pointer, with bound arguments.\n *\n * Can be filled via its implicit copy constructor, operator = or bind().\n *\n * operator = is designed for usage from C++, and accepts\n * set() sets it to an existing std::function object one of its bind()\n * members.\n *\n * It's auto-converted to std::function, and can be called via call().\n *\n * Designed to allow Cython to call C++ functions that accept std::function\n * callbacks, and to provide a callback function type for Cython to offer to\n * C++.\n *\n * As of right now, Cython doesn't support variadic templates\n * [https://mail.python.org/pipermail/cython-devel/2015-June/004429.html].\n *\n * Thus, Func has aliases to allow usage like regular templated types,\n * one for each number of arguments:\n *\n *   from libopenage.pyinterface.functional import Func2\n *   void myfunc(float a0, int a1, int a2): pass\n *   Func2[void, int, int] fobj\n *   Func2.bind1[float](myfunc, 5.0)\n *\n * There are definitions up to Func5 and bind3;\n * if you encounter a situation where you'd need more, maybe you should\n * re-think some of your life choices.\n *\n * Note that you shouldn't use this type purely internally in Cython, since\n * if you bind a Cython function and call it from Cython, every Exception\n * would be needlessly converted from Py to C++ to Py...\n *\n * For global function pointers that should be initialized at pyinterface\n * initialization time, use PyIfFunc instead of Func; that class has some\n * additional code to verify successful initialization.\n */\ntemplate <typename ReturnType, typename... ArgTypes>\nclass Func {\npublic:\n\tFunc() :\n\t\tfptr{nullptr} {}\n\n\t// for construction from lambdas and other callables (from C++).\n\ttemplate <typename F>\n\tFunc(F &&f) {\n\t\tthis->fptr = f;\n\t}\n\n\ttemplate <typename F>\n\tFunc(std::reference_wrapper<F> f) {\n\t\tthis->fptr = f;\n\t}\n\n\t// for construction from std::function objects (from C++).\n\tFunc(const std::function<ReturnType(ArgTypes...)> &f) {\n\t\tthis->fptr = f;\n\t}\n\n\tFunc(std::function<ReturnType(ArgTypes...)> &&f) {\n\t\tthis->fptr = f;\n\t}\n\n\t// for assignment of lambdas and other callables (from C++).\n\ttemplate <typename F>\n\tFunc<ReturnType, ArgTypes...> &operator=(F &&f) {\n\t\tthis->fptr = f;\n\t\treturn *this;\n\t}\n\n\ttemplate <typename F>\n\tFunc<ReturnType, ArgTypes...> &operator=(std::reference_wrapper<F> f) {\n\t\tthis->fptr = f;\n\t\treturn *this;\n\t}\n\n\t// for assignment of std::function objects (from C++).\n\tFunc<ReturnType, ArgTypes...> &operator=(const std::function<ReturnType(ArgTypes...)> &f) {\n\t\tthis->fptr = f;\n\t\treturn *this;\n\t}\n\n\tFunc<ReturnType, ArgTypes...> &operator=(std::function<ReturnType(ArgTypes...)> &&f) {\n\t\tthis->fptr = f;\n\t\treturn *this;\n\t}\n\n\t/**\n\t * raises an Error if this->fptr is still uninitialized.\n\t */\n\tinline void check_fptr() const {\n\t\tif (not this->fptr) [[unlikely]] {\n\t\t\tthrow Error(\n\t\t\t\tMSG(err) << \"Uninitialized Func object at \"\n\t\t\t\t\t\t << util::symbol_name(static_cast<const void *>(this))\n\t\t\t\t\t\t << \": \"\n\t\t\t\t\t\t\t\"Can not call or convert to std::function.\",\n\n\t\t\t\ttrue // collect backtrace info\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * for direct usage (mostly from Cython)\n\t */\n\tReturnType call(ArgTypes... args) const {\n\t\tthis->check_fptr();\n\t\treturn this->fptr(args...);\n\t}\n\n\t/**\n\t * for implicit conversion to std::function,\n\t * for usage in a context where std::function would be expected.\n\t */\n\toperator const std::function<ReturnType(ArgTypes...)> &() const {\n\t\tthis->check_fptr();\n\t\treturn this->fptr;\n\t}\n\n\t/**\n\t * for explicit conversion to std::function.\n\t */\n\tconst std::function<ReturnType(ArgTypes...)> &get() const {\n\t\tthis->check_fptr();\n\t\treturn this->fptr;\n\t}\n\n\t/**\n\t * For manual binding of function pointers and arguments (from Cython; in C++, assign lambdas).\n\t *\n\t * Adds additional code that catches and converts Python exceptions to C++ exceptions.\n\t *\n\t * The 'util::FunctionPtr' argument is due to http://stackoverflow.com/questions/31040075.\n\t * Note that with clang, it's possible to directly pass function pointers, while with\n\t * gcc they need to be explicitly converted. Meh.\n\t */\n\ttemplate <typename... BoundArgTypes>\n\tinline void bind(util::FunctionPtr<ReturnType, BoundArgTypes..., ArgTypes...> f, BoundArgTypes... bound_args) {\n\t\tthis->bind_catchexcept_impl<std::is_void<ReturnType>::value, BoundArgTypes...>(f, bound_args...);\n\t}\n\n\nprivate:\n\t/**\n\t * Specialization for bind() with void return types.\n\t */\n\ttemplate <bool return_type_is_void, typename... BoundArgTypes>\n\tinline typename std::enable_if<return_type_is_void>::type bind_catchexcept_impl(util::FunctionPtr<ReturnType, BoundArgTypes..., ArgTypes...> f, BoundArgTypes... bound_args) {\n\t\tthis->fptr = [=](ArgTypes... args) -> ReturnType {\n\t\t\tf.ptr(bound_args..., args...);\n\t\t\ttranslate_exc_py_to_cpp();\n\t\t};\n\t}\n\n\n\t/**\n\t * Specialization for bind() with non-void return types.\n\t */\n\ttemplate <bool return_type_is_void, typename... BoundArgTypes>\n\tinline typename std::enable_if<not return_type_is_void>::type bind_catchexcept_impl(util::FunctionPtr<ReturnType, BoundArgTypes..., ArgTypes...> f, BoundArgTypes... bound_args) {\n\t\tthis->fptr = [=](ArgTypes... args) -> ReturnType {\n\t\t\tReturnType &&result = f.ptr(bound_args..., args...);\n\t\t\ttranslate_exc_py_to_cpp();\n\t\t\treturn result;\n\t\t};\n\t}\n\n\npublic:\n\t/**\n\t * Like bind, but does _not_ add an exception checker.\n\t */\n\ttemplate <typename... BoundArgTypes>\n\tvoid bind_noexcept(util::FunctionPtr<ReturnType, BoundArgTypes..., ArgTypes...> f, BoundArgTypes... bound_args) {\n\t\tthis->fptr = [=](ArgTypes... args) -> ReturnType {\n\t\t\treturn f.ptr(bound_args..., args...);\n\t\t};\n\t}\n\n\t// non-variadic aliases for bind, for use by Cython\n\tinline void bind0(ReturnType (*f)(ArgTypes...)) {\n\t\tthis->bind<>(\n\t\t\tutil::FunctionPtr<ReturnType, ArgTypes...>(f));\n\t}\n\n\tinline void bind_noexcept0(ReturnType (*f)(ArgTypes...)) {\n\t\tthis->bind_noexcept<>(\n\t\t\tutil::FunctionPtr<ReturnType, ArgTypes...>(f));\n\t}\n\n\ttemplate <typename BoundArgType0>\n\tinline void bind1(ReturnType (*f)(BoundArgType0, ArgTypes...), BoundArgType0 bound_arg0) {\n\t\tthis->bind<BoundArgType0>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, ArgTypes...>(f), bound_arg0);\n\t}\n\n\ttemplate <typename BoundArgType0>\n\tinline void bind_noexcept1(ReturnType (*f)(BoundArgType0, ArgTypes...), BoundArgType0 bound_arg0) {\n\t\tthis->bind_noexcept<BoundArgType0>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, ArgTypes...>(f), bound_arg0);\n\t}\n\n\ttemplate <typename BoundArgType0, typename BoundArgType1>\n\tinline void bind2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) {\n\t\tthis->bind<BoundArgType0, BoundArgType1>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, BoundArgType1, ArgTypes...>(f), bound_arg0, bound_arg1);\n\t}\n\n\ttemplate <typename BoundArgType0, typename BoundArgType1>\n\tinline void bind_noexcept2(ReturnType (*f)(BoundArgType0, BoundArgType1, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1) {\n\t\tthis->bind_noexcept<BoundArgType0, BoundArgType1>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, BoundArgType1, ArgTypes...>(f), bound_arg0, bound_arg1);\n\t}\n\n\ttemplate <typename BoundArgType0, typename BoundArgType1, typename BoundArgType2>\n\tinline void bind3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) {\n\t\tthis->bind<BoundArgType0, BoundArgType1, BoundArgType2>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...>(f), bound_arg0, bound_arg1, bound_arg2);\n\t}\n\n\ttemplate <typename BoundArgType0, typename BoundArgType1, typename BoundArgType2>\n\tinline void bind_noexcept3(ReturnType (*f)(BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...), BoundArgType0 bound_arg0, BoundArgType1 bound_arg1, BoundArgType2 bound_arg2) {\n\t\tthis->bind_noexcept<BoundArgType0, BoundArgType1, BoundArgType2>(\n\t\t\tutil::FunctionPtr<ReturnType, BoundArgType0, BoundArgType1, BoundArgType2, ArgTypes...>(f), bound_arg0, bound_arg1, bound_arg2);\n\t}\n\nprivate:\n\tstd::function<ReturnType(ArgTypes...)> fptr;\n};\n\n\n/*\n * Now follow the glorious, aforementioned aliases for Cython, and accompanying pxd declarations.\n */\n\n/*\n * No arguments.\n *\n * pxd:\n *\n * cppclass Func0[RT]:\n *     RT call() except +\n *\n *     void bind0                          (RT (*f)()              except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)()                       with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2)          with gil, BT0, BT1, BT2) except +\n */\ntemplate <typename RT>\nusing Func0 = Func<RT>;\n\n/*\n * One argument.\n *\n * pxd:\n *\n * cppclass Func1[RT, AT0]:\n *     RT call(AT0) except +\n *\n *     void bind0                          (RT (*f)(AT0)                except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0, AT0)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1, AT0)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)(AT0)                         with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0, AT0)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1, AT0)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0)          with gil, BT0, BT1, BT2) except +\n */\ntemplate <typename RT, typename AT0>\nusing Func1 = Func<RT, AT0>;\n\n/*\n * Two arguments.\n *\n * pxd:\n *\n * cppclass Func2[RT, AT0, AT1]:\n *     RT call(AT0, AT1) except +\n *\n *     void bind0                          (RT (*f)(AT0, AT1)                except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0, AT0, AT1)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)(AT0, AT1)                         with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0, AT0, AT1)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1)          with gil, BT0, BT1, BT2) except +\n */\n\ntemplate <typename RT, typename AT0, typename AT1>\nusing Func2 = Func<RT, AT0, AT1>;\n\n/*\n * Three arguments.\n *\n * pxd:\n *\n * cppclass Func3[RT, AT0, AT1, AT2]:\n *     RT call(AT0, AT1, AT2) except +\n *\n *     void bind0                          (RT (*f)(AT0, AT1, AT2)                except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0, AT0, AT1, AT2)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)(AT0, AT1, AT2)                         with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0, AT0, AT1, AT2)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2)          with gil, BT0, BT1, BT2) except +\n */\n\ntemplate <typename RT, typename AT0, typename AT1, typename AT2>\nusing Func3 = Func<RT, AT0, AT1, AT2>;\n\n/*\n * Four arguments.\n *\n * pxd:\n *\n * cppclass Func4[RT, AT0, AT1, AT2, AT3]:\n *     RT call(AT0, AT1, AT2, AT3) except +\n *\n *     void bind0                          (RT (*f)(AT0, AT1, AT2, AT3)                except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0, AT0, AT1, AT2, AT3)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2, AT3)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)(AT0, AT1, AT2, AT3)                         with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0, AT0, AT1, AT2, AT3)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2, AT3)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3)          with gil, BT0, BT1, BT2) except +\n */\n\ntemplate <typename RT, typename AT0, typename AT1, typename AT2, typename AT3>\nusing Func4 = Func<RT, AT0, AT1, AT2, AT3>;\n\n\n/*\n * Five arguments.\n *\n * pxd:\n *\n * cppclass Func5[RT, AT0, AT1, AT2, AT3, AT4]:\n *     RT call(AT0, AT1, AT2, AT3, AT4) except +\n *\n *     void bind0                          (RT (*f)(AT0, AT1, AT2, AT3, AT4)                except * with gil               ) except +\n *     void bind1          [BT0]           (RT (*f)(BT0, AT0, AT1, AT2, AT3, AT4)           except * with gil, BT0          ) except +\n *     void bind2          [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2, AT3, AT4)      except * with gil, BT0, BT1     ) except +\n *     void bind3          [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3, AT4) except * with gil, BT0, BT1, BT2) except +\n *\n *     void bind_noexcept0                 (RT (*f)(AT0, AT1, AT2, AT3, AT4)                         with gil               ) except +\n *     void bind_noexcept1 [BT0]           (RT (*f)(BT0, AT0, AT1, AT2, AT3, AT4)                    with gil, BT0          ) except +\n *     void bind_noexcept2 [BT0, BT1]      (RT (*f)(BT0, BT1, AT0, AT1, AT2, AT3, AT4)               with gil, BT0, BT1     ) except +\n *     void bind_noexcept3 [BT0, BT1, BT2] (RT (*f)(BT0, BT1, BT2, AT0, AT1, AT2, AT3, AT4)          with gil, BT0, BT1, BT2) except +\n */\n\ntemplate <typename RT, typename AT0, typename AT1, typename AT2, typename AT3, typename AT4>\nusing Func5 = Func<RT, AT0, AT1, AT2, AT3, AT4>;\n\n\n/**\n * For usage by \"Py Interface Functions\", i.e. empty global function pointers\n * in libopenage that get filled by Cython at initialization time.\n *\n * Use only for global objects that have associated symbol names (this is enforced).\n *\n * The interface for PyIfFunc is identical to that of Func, so we'll simply typedef\n * them to avoid the redundant pxd lines from above.\n *\n * pxd:\n *\n * ctypedef Func0 PyIfFunc0\n * ctypedef Func1 PyIfFunc1\n * ctypedef Func2 PyIfFunc2\n * ctypedef Func3 PyIfFunc3\n * ctypedef Func4 PyIfFunc4\n * ctypedef Func5 PyIfFunc5\n */\ntemplate <typename ReturnType, typename... ArgTypes>\nclass PyIfFunc : public Func<ReturnType, ArgTypes...> {\npublic:\n\tPyIfFunc() {\n\t\tadd_py_if_component(this, [=, this]() -> bool {\n\t\t\ttry {\n\t\t\t\tthis->check_fptr();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tcatch (Error &) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t});\n\t}\n\n\t~PyIfFunc() {\n\t\tdestroy_py_if_component(this);\n\t}\n\n\t// no copy construction!\n\tPyIfFunc(const PyIfFunc<ReturnType, ArgTypes...> &other) = delete;\n\tPyIfFunc(PyIfFunc<ReturnType, ArgTypes...> &&other) = delete;\n\tPyIfFunc &operator=(const PyIfFunc<ReturnType, ArgTypes...> &other) = delete;\n\tPyIfFunc &operator=(PyIfFunc<ReturnType, ArgTypes...> &&other) = delete;\n\n\t// but you may convert this to a regular Func object.\n\toperator const Func<ReturnType, ArgTypes...> &() const {\n\t\treturn static_cast<Func<ReturnType, ArgTypes...>>(this->fptr);\n\t}\n};\n\n\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/hacks.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"hacks.h\"\n\n/*\n * hacks.h is mainly for inclusion by Cython;\n *\n * This file exists to test whether hacks.h compiles properly.\n * Otherwise, Cython might incorrectly receive the blame for C++ errors.\n */\n"
  },
  {
    "path": "libopenage/pyinterface/hacks.h",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/*\n * This file contains a collection of hacks to work around particular Cython\n * bugs and/or features.\n *\n * This header file is automatically included when building Cython modules.\n *\n * Note that all issues with Cython should be reported upstream.\n * They usually get fixed pretty fast, and openage always uses the newest\n * version of Cython.\n */\n\n\n/**\n * Include exctranslate.h, which defines its own replacement for\n * __Pyx_CppExn2PyErr.\n */\n#include \"exctranslate.h\"\n"
  },
  {
    "path": "libopenage/pyinterface/pyexception.cpp",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n#include \"pyexception.h\"\n\n#include \"../log/log.h\"\n\nnamespace openage {\nnamespace pyinterface {\n\n\nvoid PyExceptionBacktrace::get_symbols(std::function<void (const error::backtrace_symbol *)> cb, bool reversed) const {\n\tif (reversed) {\n\t\tpyexception_bt_get_symbols.call(this->ref, cb);\n\t} else {\n\t\t// pyexception_bt_get_symbols gives us the symbols in reverse order, we'll have to use a std::vector\n\t\t// to flip it.\n\n\t\tstd::vector<error::backtrace_symbol> symbols;\n\t\tpyexception_bt_get_symbols.call(this->ref, [&symbols](const error::backtrace_symbol *symbol) {\n\t\t\tsymbols.emplace_back(*symbol);\n\t\t});\n\n\t\tfor (size_t idx = symbols.size(); idx-- > 0;) {\n\t\t\tcb(&symbols[idx]);\n\t\t}\n\t}\n}\n\n\nvoid PyException::init_backtrace() {\n\tthis->backtrace = std::make_shared<PyExceptionBacktrace>(this->py_obj.get_ref());\n}\n\n\nstd::string PyException::type_name() const {\n\tstd::string result;\n\n\tresult.append(this->py_obj.modulename());\n\tresult.push_back('.');\n\tresult.append(this->py_obj.classname());\n\n\treturn result;\n}\n\n\nPyIfFunc<void, PyObject *, Func<void, const error::backtrace_symbol *>> pyexception_bt_get_symbols;\n\n\n}} // openage::pyinterface\n"
  },
  {
    "path": "libopenage/pyinterface/pyexception.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n// pxd: from libcpp.string cimport string\n#include <string>\n// pxd: from libcpp.vector cimport vector\n#include <vector>\n\n// pxd: from libopenage.error.error cimport Error\n#include \"../error/error.h\"\n// pxd: from libopenage.error.backtrace cimport backtrace_symbol_constptr\n#include \"../error/backtrace.h\"\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc2, Func1\n#include \"functional.h\"\n// pxd: from libopenage.pyinterface.pyobject cimport PyObjectRef, PyObjectPtr\n#include \"pyobject.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\n\n\n/**\n * An implementation of the abstract Backtrace class that does not collect\n * Backtrace information itself, but instead determines the data on the fly\n * from a Python Exception object.\n */\nclass PyExceptionBacktrace : public error::Backtrace {\npublic:\n\t/**\n\t * ref is a raw reference to the associated PyObject.\n\t */\n\tPyExceptionBacktrace(PyObject *ref) :\n\t\tref{ref} {}\n\n\t/**\n\t * Accesses the associated Python exception object to translate the traceback as needed.\n\t */\n\tvoid get_symbols(std::function<void(const error::backtrace_symbol *)> cb, bool reversed) const override;\n\nprivate:\n\tPyObject *ref;\n};\n\n\n/**\n * Thrown by translate_exc_py_to_cpp() from exctranslate.h.\n *\n * These objects are constructed entirely empty, and designed to be filled\n * from describe_py_exception().\n *\n * pxd:\n *\n * cppclass PyException(Error):\n *     PyObjectRef py_obj\n *     void init_backtrace() except +\n */\nclass OAAPI PyException : public error::Error {\npublic:\n\t/**\n\t * Returns the type name of py_obj.\n\t */\n\tstd::string type_name() const override;\n\n\n\t/**\n\t * Reference to the Python exception object.\n\t */\n\tPyObjectRef py_obj;\n\n\n\t/**\n\t * Initializes this->backtrace with a PyExceptionBacktrace object\n\t * that points at this->py_obj->get_ref().\n\t */\n\tvoid init_backtrace();\n};\n\n\n// pxd: PyIfFunc2[void, PyObjectPtr, Func1[void, backtrace_symbol_constptr]] pyexception_bt_get_symbols\nextern OAAPI PyIfFunc<void, PyObject *, Func<void, const error::backtrace_symbol *>> pyexception_bt_get_symbols;\n\n\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/pyobject.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"pyobject.h\"\n\n#include <algorithm>\n#include <functional>\n\n\nnamespace openage {\nnamespace pyinterface {\n\n\nPyObjectRef::PyObjectRef() noexcept\n\t:\n\tref{nullptr} {}\n\n\nPyObjectRef::PyObjectRef(PyObject *ref)\n\t:\n\tref{ref} {\n\n\tpy_xincref.call(this->ref);\n}\n\n\nPyObjectRef::PyObjectRef(const PyObjectRef &other)\n\t:\n\tref{other.ref} {\n\n\tpy_xincref.call(this->ref);\n}\n\n\nPyObjectRef::PyObjectRef(PyObjectRef &&other) noexcept\n\t:\n\tref{other.ref} {\n\n\t// don't incref, because we simultaneously clear other.ref.\n\tother.ref = nullptr;\n}\n\n\nPyObjectRef &PyObjectRef::operator =(const PyObjectRef &other) {\n\tif (this->ref != nullptr) {\n\t\tpy_xdecref.call(this->ref);\n\t}\n\n\tthis->ref = other.ref;\n\tpy_xincref.call(this->ref);\n\n\treturn *this;\n}\n\n\nPyObjectRef &PyObjectRef::operator =(PyObjectRef &&other) {\n\tif (this->ref != nullptr) {\n\t\tpy_xdecref.call(this->ref);\n\t}\n\n\tthis->ref = other.ref;\n\n\t// don't incref, because we simultaneously clear other.ref.\n\tother.ref = nullptr;\n\n\treturn *this;\n}\n\n\n/**\n * Integer conversion.\n */\ntemplate <>\nPyObjectRef::PyObjectRef(int number)\n\t:\n\tPyObjectRef(py::integer(number)) {}\n\n\n/**\n * Bytes conversion.\n */\ntemplate <>\nPyObjectRef::PyObjectRef(const char *data)\n\t:\n\tPyObjectRef(py::bytes(data)) {}\n\n\n/**\n * String conversion.\n */\ntemplate <>\nPyObjectRef::PyObjectRef(const std::string &txt)\n\t:\n\tPyObjectRef(py::bytes(txt)) {}\n\n\nPyObjectRef::~PyObjectRef() {\n\tif (this->ref != nullptr) {\n\t\tpy_xdecref.call(this->ref);\n\t}\n}\n\n\nvoid PyObjectRef::set_ref(PyObject *ref) {\n\tif (this->ref != nullptr) [[unlikely]] {\n\t\tpy_xdecref.call(this->ref);\n\t}\n\n\tthis->ref = ref;\n\tpy_xincref.call(this->ref);\n}\n\n\nvoid PyObjectRef::set_ref_without_incrementing(PyObject *ref) {\n\tif (this->ref != nullptr) [[unlikely]] {\n\t\tpy_xdecref.call(this->ref);\n\t}\n\n\tthis->ref = ref;\n}\n\n\nstd::string PyObjectRef::str() const {\n\treturn py_str.call(this->ref);\n}\n\n\nstd::string PyObjectRef::repr() const {\n\treturn py_repr.call(this->ref);\n}\n\n\nstd::string PyObjectRef::bytes() const {\n\treturn py_bytes.call(this->ref);\n}\n\n\nint PyObjectRef::len() const {\n\treturn py_len.call(this->ref);\n}\n\n\nbool PyObjectRef::callable() const {\n\treturn py_callable.call(this->ref);\n}\n\n\nPyObjectRef PyObjectRef::call_impl(std::vector<PyObjectRef> &args) const {\n\tPyObjectRef result;\n\tif (args.empty()) {\n\t\tpy_call0.call(&result, this->ref);\n\t} else {\n\t\tstd::vector<PyObject *> py_args{args.size()};\n\t\tstd::transform(args.begin(), args.end(), py_args.begin(),\n\t\t               std::mem_fn(&PyObjectRef::get_ref));\n\t\tpy_calln.call(&result, this->ref, py_args);\n\t}\n\treturn result;\n}\n\n\nbool PyObjectRef::hasattr(const std::string &name) const {\n\treturn py_hasattr.call(this->ref, name);\n}\n\n\nPyObjectRef PyObjectRef::getattr(const std::string &name) const {\n\tPyObjectRef result;\n\tpy_getattr.call(&result, this->ref, name);\n\treturn result;\n}\n\n\nvoid PyObjectRef::setattr(const std::string &name, const PyObjectRef &attr) const {\n\tpy_setattr.call(this->ref, name, attr.get_ref());\n}\n\n\nbool PyObjectRef::to_bool() const {\n\treturn py_to_bool.call(this->ref);\n}\n\n\nint64_t PyObjectRef::to_int() const {\n\treturn py_to_int.call(this->ref);\n}\n\n\nbool PyObjectRef::isinstance(const PyObjectRef &type) const {\n\treturn py_isinstance.call(this->ref, type.get_ref());\n}\n\n\nvoid PyObjectRef::dir(const Func<void, std::string> &callback) const {\n\tpy_dir.call(this->ref, callback);\n}\n\n\nbool PyObjectRef::is(const PyObjectRef &other) const {\n\treturn this->ref == other.get_ref();\n}\n\n\nbool PyObjectRef::equals(const PyObjectRef &other) const {\n\treturn py_equals.call(this->ref, other.get_ref());\n}\n\n\nPyObjectRef PyObjectRef::eval(const std::string &expression) const {\n\tPyObjectRef result;\n\tpy_eval.call(this->ref, &result, expression);\n\treturn result;\n}\n\n\nvoid PyObjectRef::exec(const std::string &statement) const {\n\tpy_exec.call(this->ref, statement);\n}\n\n\nPyObjectRef PyObjectRef::get(const PyObjectRef &key) const {\n\tPyObjectRef result;\n\tpy_get.call(this->ref, &result, key.get_ref());\n\treturn result;\n}\n\n\nPyObjectRef PyObjectRef::get(const std::string &key) const {\n\tPyObjectRef keyobj = py::str(key);\n\treturn this->get(keyobj);\n}\n\n\nPyObjectRef PyObjectRef::get(int key) const {\n\tPyObjectRef keyobj = py::integer(key);\n\treturn this->get(keyobj);\n}\n\n\nbool PyObjectRef::in(const PyObjectRef &container) const {\n\treturn py_in.call(this->ref, container.get_ref());\n}\n\n\nPyObjectRef PyObjectRef::type() const {\n\tPyObjectRef result;\n\tpy_type.call(this->ref, &result);\n\treturn result;\n}\n\n\nstd::string PyObjectRef::modulename() const {\n\treturn py_modulename.call(this->ref);\n}\n\n\nstd::string PyObjectRef::classname() const {\n\treturn py_classname.call(this->ref);\n}\n\n\nstd::ostream &operator <<(std::ostream &os, const PyObjectRef &ref) {\n\tif (ref.get_ref() == nullptr) [[unlikely]] {\n\t\tos << \"PyObjectRef[null]\";\n\t} else {\n\t\tos << \"PyObjectRef[\" << ref.repr() << \"]\";\n\t}\n\n\treturn os;\n}\n\n\nPyIfFunc<void, PyObject *> py_xincref;\nPyIfFunc<void, PyObject *> py_xdecref;\n\nPyIfFunc<std::string, PyObject *> py_str;\nPyIfFunc<std::string, PyObject *> py_repr;\nPyIfFunc<std::string, PyObject *> py_bytes;\nPyIfFunc<int, PyObject *> py_len;\nPyIfFunc<bool, PyObject *> py_callable;\nPyIfFunc<void, PyObjectRef *, PyObject *> py_call0;\nPyIfFunc<void, PyObjectRef *, PyObject *, std::vector<PyObject *>&> py_calln;\nPyIfFunc<bool, PyObject *, std::string> py_hasattr;\nPyIfFunc<void, PyObjectRef *, PyObject *, std::string> py_getattr;\nPyIfFunc<void, PyObject *, std::string, PyObject *> py_setattr;\nPyIfFunc<bool, PyObject *, PyObject *> py_isinstance;\nPyIfFunc<bool, PyObject *> py_to_bool;\nPyIfFunc<int64_t, PyObject *> py_to_int;\nPyIfFunc<void, PyObject *, Func<void, std::string>> py_dir;\nPyIfFunc<bool, PyObject *, PyObject *> py_equals;\nPyIfFunc<void, PyObject *, std::string> py_exec;\nPyIfFunc<void, PyObject *, PyObjectRef *, std::string> py_eval;\nPyIfFunc<void, PyObject *, PyObjectRef *, PyObject *> py_get;\nPyIfFunc<bool, PyObject *, PyObject *> py_in;\nPyIfFunc<void, PyObject *, PyObjectRef *> py_type;\nPyIfFunc<std::string, PyObject *> py_modulename;\nPyIfFunc<std::string, PyObject *> py_classname;\n\nPyIfFunc<void, PyObjectRef *, const std::string&> py_builtin;\nPyIfFunc<void, PyObjectRef *, const std::string&> py_import;\nPyIfFunc<void, PyObjectRef *, const std::string&> py_createstr;\nPyIfFunc<void, PyObjectRef *, const std::string&> py_createbytes;\nPyIfFunc<void, PyObjectRef *, int> py_createint;\nPyIfFunc<void, PyObjectRef *> py_createdict;\nPyIfFunc<void, PyObjectRef *> py_createlist;\n\nPyObjectRef None;\nPyObjectRef True;\nPyObjectRef False;\n\n} // pyinterface\n\n\nnamespace py {\nusing namespace pyinterface;\n\n\nObj builtin(const std::string &name) {\n\tObj result;\n\tpy_builtin.call(&result, name);\n\treturn result;\n}\n\n\nObj import(const std::string &name) {\n\tObj result;\n\tpy_import.call(&result, name);\n\treturn result;\n}\n\n\nObj str(const std::string &value) {\n\tObj result;\n\tpy_createstr.call(&result, value);\n\treturn result;\n}\n\n\nObj bytes(const std::string &value) {\n\tObj result;\n\tpy_createbytes.call(&result, value);\n\treturn result;\n}\n\n\nObj integer(int value) {\n\tObj result;\n\tpy_createint.call(&result, value);\n\treturn result;\n}\n\n\nObj dict() {\n\tObj result;\n\tpy_createdict.call(&result);\n\treturn result;\n}\n\n\nObj list() {\n\tObj result;\n\tpy_createlist.call(&result);\n\treturn result;\n}\n\n\n} // py\n} // openage\n"
  },
  {
    "path": "libopenage/pyinterface/pyobject.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool as cppbool\n\n// pxd: from libc.stdint cimport int64_t\n#include <cstdint>\n// pxd: from libcpp.string cimport string\n#include <string>\n// pxd: from libcpp.vector cimport vector\n#include <vector>\n\n// pxd: from libopenage.pyinterface.defs cimport PyObject\n#include \"defs.h\"\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc1, PyIfFunc2, PyIfFunc3\n// pxd: from libopenage.pyinterface.functional cimport Func1\n#include \"functional.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\n\n\n/**\n * Holds a Python object, as a PyObject * pointer.\n *\n * Reference counting (Py_INCREF and Py_DECREF) is correctly performed\n * in the various copy/move constructors/operators.\n *\n * pxd:\n *\n * cppclass PyObjectRef:\n *     PyObjectRef() noexcept\n *     PyObjectRef(PyObject *ref) except +\n *\n *     PyObject *get_ref() noexcept\n *     void set_ref(PyObject *ref) except +\n *     void set_ref_without_incrementing(PyObject *ref) except +\n *     void clear_ref_without_decrementing() noexcept\n *\n *\n * ctypedef PyObjectRef PyObj\n *\n * ctypedef PyObjectRef *PyObjectRefPtr\n * ctypedef PyObject    *PyObjectPtr\n */\nclass OAAPI PyObjectRef {\npublic:\n\t/**\n\t * Initializes reference with nullptr.\n\t */\n\tPyObjectRef() noexcept;\n\n\t/**\n\t * Wraps a raw PyObject * pointer (calls Py_INCREF).\n\t */\n\tPyObjectRef(PyObject *ref);\n\n\t/**\n\t * Clones a PyObjectRef (calls Py_INCREF).\n\t */\n\tPyObjectRef(const PyObjectRef &other);\n\n\t/**\n\t * Move-constructs from an other PyObjectRef.\n\t */\n\tPyObjectRef(PyObjectRef &&other) noexcept;\n\n\t/**\n\t * Assigns from an other PyObjectRef\n\t * (calls Py_XDECREF on the old value, and Py_XINCREF on the new one).\n\t */\n\tPyObjectRef &operator=(const PyObjectRef &other);\n\n\t/**\n\t * Move-assigns from an other PyObject\n\t * (calls Py_XDECREF on the old value).\n\t */\n\tPyObjectRef &operator=(PyObjectRef &&other);\n\n\t/**\n\t * Destroys the object, calls Py_XDECREF.\n\t */\n\t~PyObjectRef();\n\n\t// operator == is not implemented because users might expect it to\n\t// implement either of .is() or .equals().\n\n\t/**\n\t * str(obj)\n\t */\n\tstd::string str() const;\n\n\t/**\n\t * repr(obj)\n\t */\n\tstd::string repr() const;\n\n\t/**\n\t * bytes(obj)\n\t */\n\tstd::string bytes() const;\n\n\t/**\n\t * len(obj)\n\t */\n\tint len() const;\n\n\t/**\n\t * callable(obj)\n\t */\n\tbool callable() const;\n\n\t/**\n\t * obj(args...)\n\t */\n\ttemplate <typename... Args>\n\tPyObjectRef call(Args... args) const {\n\t\t// this vector collects the function call arguments\n\t\tstd::vector<PyObjectRef> arg_objs{\n\t\t\tPyObjectRef(args)...};\n\n\t\treturn this->call_impl(arg_objs);\n\t}\n\nprivate:\n\t/**\n\t * obj(args)\n\t */\n\tPyObjectRef call_impl(std::vector<PyObjectRef> &args) const;\n\n\t/**\n\t * Primary template for PyObjectRef conversions.\n\t */\n\ttemplate <typename T>\n\texplicit PyObjectRef(T arg);\n\t// the specializations follow in the cpp file.\n\npublic:\n\t/**\n\t * getattr(obj, name)\n\t */\n\tPyObjectRef getattr(const std::string &name) const;\n\n\t/**\n\t * hasattr(obj, name)\n\t */\n\tbool hasattr(const std::string &name) const;\n\n\t/**\n\t * setattr(obj, name)\n\t */\n\tvoid setattr(const std::string &name, const PyObjectRef &attr) const;\n\n\t/**\n\t * isinstance(obj, type)\n\t */\n\tbool isinstance(const PyObjectRef &type) const;\n\n\t/**\n\t * bool(obj)\n\t */\n\tbool to_bool() const;\n\n\t/**\n\t * int(obj)\n\t */\n\tint64_t to_int() const;\n\n\t/**\n\t * for elem in dir(obj):\n\t *     callback(elem)\n\t */\n\tvoid dir(const Func<void, std::string> &callback) const;\n\n\t/**\n\t * obj is other\n\t */\n\tbool is(const PyObjectRef &other) const;\n\n\t/**\n\t * obj == other\n\t */\n\tbool equals(const PyObjectRef &other) const;\n\n\t/**\n\t * eval(expression, obj)\n\t */\n\tPyObjectRef eval(const std::string &expression) const;\n\n\t/**\n\t * exec(statement, obj)\n\t */\n\tvoid exec(const std::string &statement) const;\n\n\t/**\n\t * obj[key]\n\t */\n\tPyObjectRef get(const PyObjectRef &key) const;\n\n\t/**\n\t * obj[key]\n\t */\n\tPyObjectRef get(const std::string &key) const;\n\n\t/**\n\t * obj[key]\n\t */\n\tPyObjectRef get(int key) const;\n\n\t/**\n\t * obj in container\n\t */\n\tbool in(const PyObjectRef &container) const;\n\n\t/**\n\t * type(obj)\n\t */\n\tPyObjectRef type() const;\n\n\t/**\n\t * type(obj).__module__\n\t */\n\tstd::string modulename() const;\n\n\t/**\n\t * type(obj).__name__\n\t */\n\tstd::string classname() const;\n\n\nprivate:\n\t/**\n\t * Internal PyObject * for obj.\n\t */\n\tPyObject *ref;\n\n\npublic:\n\t/**\n\t * Provide the internal PyObject *.\n\t */\n\tPyObject *get_ref() const noexcept {\n\t\treturn this->ref;\n\t}\n\n\t/**\n\t * Implicit conversion to PyObject *.\n\t * Mainly for convenience to avoid all the get_ref() calls.\n\t */\n\tPyObject *operator()() const noexcept {\n\t\treturn this->ref;\n\t}\n\n\t/**\n\t * Like operator =(const PyObjectRef &other).\n\t */\n\tvoid set_ref(PyObject *ref);\n\n\t/**\n\t * Like set_ref, but does _NOT_ call PY_XINCREF.\n\t * Only use in special cases, if you know exactly what you're doing.\n\t */\n\tvoid set_ref_without_incrementing(PyObject *ref);\n\n\t/**\n\t * Clears the internal reference, but does _NOT_ call PY_XDECREF.\n\t * Only use in special cases, if you know exactly what you're doing.\n\t */\n\tvoid clear_ref_without_decrementing() noexcept {\n\t\tthis->ref = nullptr;\n\t}\n};\n\n\n/**\n * convenience alias\n */\nusing PyObj = PyObjectRef;\n\n\n/**\n * Stream operator for printing PyObjects\n */\nstd::ostream &operator<<(std::ostream &os, const PyObjectRef &ref);\n\n\n// now follow the various Python callbacks that implement all of the above,\n// and need to be installed by openage.cppinterface.pyobject.setup().\n\n// for use by the reference-counting constructors\n\n// pxd: PyIfFunc1[void, PyObjectPtr] py_xincref\nextern OAAPI PyIfFunc<void, PyObject *> py_xincref;\n// pxd: PyIfFunc1[void, PyObjectPtr] py_xdecref\nextern OAAPI PyIfFunc<void, PyObject *> py_xdecref;\n\n// for all of those member functions\n\n// pxd: PyIfFunc1[string, PyObjectPtr] py_str\nextern OAAPI PyIfFunc<std::string, PyObject *> py_str;\n// pxd: PyIfFunc1[string, PyObjectPtr] py_repr\nextern OAAPI PyIfFunc<std::string, PyObject *> py_repr;\n// pxd: PyIfFunc1[string, PyObjectPtr] py_bytes\nextern OAAPI PyIfFunc<std::string, PyObject *> py_bytes;\n// pxd: PyIfFunc1[int, PyObjectPtr] py_len\nextern OAAPI PyIfFunc<int, PyObject *> py_len;\n// pxd: PyIfFunc1[cppbool, PyObjectPtr] py_callable\nextern OAAPI PyIfFunc<bool, PyObject *> py_callable;\n// pxd: PyIfFunc2[void, PyObjectRefPtr, PyObjectPtr] py_call0\nextern OAAPI PyIfFunc<void, PyObjectRef *, PyObject *> py_call0;\n// pxd: PyIfFunc3[void, PyObjectRefPtr, PyObjectPtr, vector[PyObjectPtr]] py_calln\nextern OAAPI PyIfFunc<void, PyObjectRef *, PyObject *, std::vector<PyObject *> &> py_calln;\n// pxd: PyIfFunc2[cppbool, PyObjectPtr, string] py_hasattr\nextern OAAPI PyIfFunc<bool, PyObject *, std::string> py_hasattr;\n// pxd: PyIfFunc3[void, PyObjectRefPtr, PyObjectPtr, string] py_getattr\nextern OAAPI PyIfFunc<void, PyObjectRef *, PyObject *, std::string> py_getattr;\n// pxd: PyIfFunc3[void, PyObjectPtr, string, PyObjectPtr] py_setattr\nextern OAAPI PyIfFunc<void, PyObject *, std::string, PyObject *> py_setattr;\n// pxd: PyIfFunc2[cppbool, PyObjectPtr, PyObjectPtr] py_isinstance\nextern OAAPI PyIfFunc<bool, PyObject *, PyObject *> py_isinstance;\n// pxd: PyIfFunc1[cppbool, PyObjectPtr] py_to_bool\nextern OAAPI PyIfFunc<bool, PyObject *> py_to_bool;\n// pxd: PyIfFunc1[int64_t, PyObjectPtr] py_to_int\nextern OAAPI PyIfFunc<int64_t, PyObject *> py_to_int;\n// pxd: PyIfFunc2[void, PyObjectPtr, Func1[void, string]] py_dir\nextern OAAPI PyIfFunc<void, PyObject *, Func<void, std::string>> py_dir;\n// pxd: PyIfFunc2[cppbool, PyObjectPtr, PyObjectPtr] py_equals\nextern OAAPI PyIfFunc<bool, PyObject *, PyObject *> py_equals;\n// pxd: PyIfFunc2[void, PyObjectPtr, string] py_exec\nextern OAAPI PyIfFunc<void, PyObject *, std::string> py_exec;\n// pxd: PyIfFunc3[void, PyObjectPtr, PyObjectRefPtr, string] py_eval\nextern OAAPI PyIfFunc<void, PyObject *, PyObjectRef *, std::string> py_eval;\n// pxd: PyIfFunc3[void, PyObjectPtr, PyObjectRefPtr, PyObjectPtr] py_get\nextern OAAPI PyIfFunc<void, PyObject *, PyObjectRef *, PyObject *> py_get;\n// pxd: PyIfFunc2[cppbool, PyObjectPtr, PyObjectPtr] py_in\nextern OAAPI PyIfFunc<bool, PyObject *, PyObject *> py_in;\n// pxd: PyIfFunc2[void, PyObjectPtr, PyObjectRefPtr] py_type\nextern OAAPI PyIfFunc<void, PyObject *, PyObjectRef *> py_type;\n// pxd: PyIfFunc1[string, PyObjectPtr] py_modulename\nextern OAAPI PyIfFunc<std::string, PyObject *> py_modulename;\n// pxd: PyIfFunc1[string, PyObjectPtr] py_classname\nextern OAAPI PyIfFunc<std::string, PyObject *> py_classname;\n\n// pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_builtin\nextern OAAPI PyIfFunc<void, PyObjectRef *, const std::string &> py_builtin;\n// pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_import\nextern OAAPI PyIfFunc<void, PyObjectRef *, const std::string &> py_import;\n// pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_createstr\nextern OAAPI PyIfFunc<void, PyObjectRef *, const std::string &> py_createstr;\n// pxd: PyIfFunc2[void, PyObjectRefPtr, const string] py_createbytes\nextern OAAPI PyIfFunc<void, PyObjectRef *, const std::string &> py_createbytes;\n// pxd: PyIfFunc2[void, PyObjectRefPtr, int] py_createint\nextern OAAPI PyIfFunc<void, PyObjectRef *, int> py_createint;\n// pxd: PyIfFunc1[void, PyObjectRefPtr] py_createdict\nextern OAAPI PyIfFunc<void, PyObjectRef *> py_createdict;\n// pxd: PyIfFunc1[void, PyObjectRefPtr] py_createlist\nextern OAAPI PyIfFunc<void, PyObjectRef *> py_createlist;\n\n// pxd: PyObjectRef None\nextern OAAPI PyObjectRef None;\n// pxd: PyObjectRef True\nextern OAAPI PyObjectRef True;\n// pxd: PyObjectRef False\nextern OAAPI PyObjectRef False;\n\n} // namespace pyinterface\n\n\n/**\n * Convenience functions and types for the python interface.\n */\nnamespace py {\n\n/**\n * Python object reference.\n */\nusing Obj = pyinterface::PyObjectRef;\n\n\n/**\n * getattr(importlib.import_module(\"builtins\"), name)\n */\nObj builtin(const std::string &name);\n\n\n/**\n * importlib.import_module(name)\n */\nObj import(const std::string &name);\n\n\n/**\n * str(value);\n */\nObj str(const std::string &value);\n\n\n/**\n * bytes(value)\n */\nObj bytes(const std::string &value);\n\n\n/**\n * int(value)\n */\nObj integer(int value);\n\n\n/**\n * dict()\n */\nObj dict();\n\n\n/**\n * list()\n */\nObj list();\n\n\n/**\n * The None python object.\n */\nusing pyinterface::None;\n\n\n/**\n * The True python object.\n */\nusing pyinterface::True;\n\n\n/**\n * The False python object.\n */\nusing pyinterface::False;\n\n} // namespace py\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/pyobject_tests.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"pyobject_tests.h\"\n\n#include <unordered_set>\n\n#include \"pyobject.h\"\n#include \"pyexception.h\"\n#include \"../testing/testing.h\"\n\n\nnamespace openage {\nnamespace pyinterface {\nnamespace tests {\n\nvoid pyobject() {\n\tTESTEQUALS(py::str(\"foo\").repr(), \"'foo'\");\n\tTESTEQUALS(py::bytes(\"foo\").repr(), \"b'foo'\");\n\tTESTEQUALS(py::integer(1337).repr(), \"1337\");\n\n\tTESTEQUALS(py::None.repr(), \"None\");\n\tTESTEQUALS(py::builtin(\"None\").equals(py::None), true);\n\tTESTEQUALS(py::builtin(\"None\").is(py::None), true);\n\n\tTESTEQUALS(py::True.repr(), \"True\");\n\tTESTEQUALS(py::builtin(\"True\").equals(py::True), true);\n\tTESTEQUALS(py::builtin(\"True\").is(py::True), true);\n\tTESTEQUALS(py::True.to_bool(), true);\n\n\tTESTEQUALS(py::False.repr(), \"False\");\n\tTESTEQUALS(py::builtin(\"False\").equals(py::False), true);\n\tTESTEQUALS(py::builtin(\"False\").is(py::False), true);\n\tTESTEQUALS(py::False.to_bool(), false);\n\n\tpy::Obj dict;\n\tTESTNOEXCEPT(dict = py::dict());\n\tTESTEQUALS(dict.repr(), \"{}\");\n\tTESTEQUALS(py::builtin(\"dict\").call().equals(dict), true);\n\tTESTEQUALS(py::builtin(\"dict\").call().is(dict), false);\n\n\tpy::Obj deque;\n\tTESTNOEXCEPT(deque = py::import(\"collections\").getattr(\"deque\").call());\n\tTESTEQUALS(deque.repr(), \"deque([])\");\n\n\tTESTTHROWS(dict.exec(\"raise Exception()\"));\n\n\tTESTNOEXCEPT(dict.exec(\"x = []\"));\n\n\tTESTEQUALS(dict.get(\"x\").repr(), \"[]\");\n\tTESTEQUALS(py::str(\"x\").in(dict), true);\n\tTESTEQUALS(py::bytes(\"x\").in(dict), false);\n\n\tTESTNOEXCEPT(dict.exec(\n\t\t\"class A:\\n\"\n\t\t\"    def __del__(self):\\n\"\n\t\t\"        x.append(1)\\n\"\n\t\t\"    def __str__(self):\\n\"\n\t\t\"        return 'A'\\n\"\n\t\t\"    def __repr__(self):\\n\"\n\t\t\"        return 'A()'\\n\"\n\t\t\"    def __bytes__(self):\\n\"\n\t\t\"        return b'bytes-A()'\\n\"\n\t));\n\n\t// test what happens when a goes out of scope\n\t{\n\t\tpy::Obj a;\n\t\tTESTNOEXCEPT(a = dict.eval(\"A()\"));\n\t\tTESTEQUALS(a.repr(), \"A()\");\n\t\tTESTEQUALS(a.str(), \"A\");\n\t\tTESTEQUALS(a.bytes(), \"bytes-A()\");\n\t\tTESTNOEXCEPT(a.setattr(\"foo\", deque));\n\t\tTESTEQUALS(a.getattr(\"foo\").is(deque), true);\n\t\tTESTEQUALS(a.hasattr(\"foo\"), true);\n\t\tTESTEQUALS(a.hasattr(\"bar\"), false);\n\t}\n\n\t// the __del__ added a '1'\n\tpy::Obj x;\n\tTESTNOEXCEPT(x = dict.get(\"x\"));\n\tTESTEQUALS(x.repr(), \"[1]\");\n\tTESTEQUALS(x.len(), 1);\n\tTESTEQUALS(py::integer(1).in(x), true);\n\tTESTEQUALS(x.get(0).equals(py::integer(1)), true);\n\tTESTEQUALS(x.get(0).equals(py::integer(2)), false);\n\n\tTESTEQUALS(dict.isinstance(py::builtin(\"dict\")), true);\n\tTESTEQUALS(dict.isinstance(py::builtin(\"list\")), false);\n\n\tpy::Obj pop;\n\tTESTNOEXCEPT(pop = x.getattr(\"pop\"));\n\tTESTEQUALS(x.callable(), false);\n\tTESTEQUALS(pop.callable(), true);\n\tTESTEQUALS(pop.call().repr(), \"1\");\n\tTESTEQUALS(x.to_bool(), false);\n\tTESTEQUALS(dict.to_bool(), true);\n\n\tstd::unordered_set<std::string> dir;\n\tTESTNOEXCEPT(pop.dir([&](std::string s) {dir.insert(s);}));\n\tdir.find(\"__self__\") == dir.end() and TESTFAIL;\n\tdir.clear();\n\n\tTESTEQUALS(pop.getattr(\"__self__\").is(x), true);\n\tTESTEQUALS(x.equals(deque), false);\n\tTESTEQUALS(x.equals(py::builtin(\"list\").call()), true);\n\n\tTESTEQUALS(dict.type().is(py::builtin(\"dict\")), true);\n\tTESTEQUALS(dict.modulename(), \"builtins\");\n\tTESTEQUALS(dict.classname(), \"dict\");\n\n\t// btw, if I invoke a nonexisting member function on a (a.foob(b))\n\t// the python heap will corrupt in printing the \"has no member\" backtrace\n\tTESTNOEXCEPT(dict.exec(\n\t\t\"def mul(x, y):\\n\"\n\t\t\"    return x * y\\n\"\n\t\t\"\\n\"\n\t\t\"def strformat(a, b):\\n\"\n\t\t\"    return a.format(b)\\n\"\n\t));\n\n\tTESTEQUALS(\n\t\tdict.get(\"mul\").call(13, 37).to_int(),\n\t\t13 * 37\n\t);\n\n\tTESTEQUALS(\n\t\tdict.get(\"strformat\").call(py::str(\"stuff: {}\"), 1337).str(),\n\t\tpy::str(\"stuff: {}\").getattr(\"format\").call(1337).str()\n\t);\n}\n\n\nvoid pyobject_demo() {\n\tpy::Obj globals = py::dict();\n\n\tpy::Obj inputfunc = globals.eval(\"lambda: input('>>> ')\");\n\n\twhile (true) {\n\t\tpy::Obj input;\n\t\ttry {\n\t\t\tinput = inputfunc.call();\n\n\t\t} catch (PyException &exc) {\n\t\t\tif (exc.type_name() == \"builtins.EOFError\") {\n\t\t\t\tstd::cout << \"goodbye.\" << std::endl;\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tstd::cout << exc << std::endl;\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tglobals.exec(input.str());\n\t\t} catch (PyException &exc) {\n\t\t\tstd::cout << exc << std::endl;\n\t\t}\n\t}\n}\n\n\n}}} // openage::pyinterface::tests\n"
  },
  {
    "path": "libopenage/pyinterface/pyobject_tests.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage {\nnamespace pyinterface {\nnamespace tests {\n\nvoid pyobject();\nvoid pyobject_demo();\n\n} // namespace tests\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/pyinterface/setup.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"setup.h\"\n\n#include <map>\n#include <mutex>\n#include <set>\n#include <utility>\n\n#include \"../error/error.h\"\n#include \"../util/compiler.h\"\n\nnamespace openage::pyinterface {\n\n\n// no need for these global objects to have any linkage.\nnamespace {\n\n\nstruct state {\n\t// secures the state object\n\tstd::mutex lock;\n\n\t// holds the checker function for each component\n\tstd::map<void *, std::function<bool ()>> map;\n\n\t// holds the components that have been destroyed so far\n\tstd::set<void *> destroyed_components;\n\n\t// set to true once the check has run for the first time.\n\tbool check_performed;\n\n\t// returns the global singleton object\n\tstatic state &get() {\n\t\tstatic state val{{}, {}, {}, false};\n\t\treturn val;\n\t}\n};\n\n\n} // anonymous namespace\n\n\nvoid add_py_if_component(void *thisptr, std::function<bool ()> checker) {\n\tstate &checkers = state::get();\n\n\tstd::unique_lock<std::mutex> lock{checkers.lock};\n\n\t// enforce that the object has an associated symbol name.\n\tif (not util::is_symbol(thisptr)) [[unlikely]] {\n\t\tthrow Error(\n\t\t\tMSG(err) <<\n\t\t\t\"Can't instantiate py interface component as non-global object. \" <<\n\t\t\t\"Attempted instantiation at \" <<\n\t\t\tutil::symbol_name(thisptr) << \".\",\n\t\t\ttrue);\n\t}\n\n\tcheckers.map[thisptr] = std::move(checker);\n}\n\n\nvoid destroy_py_if_component(void *thisptr) {\n\tstate &checkers = state::get();\n\n\tstd::unique_lock<std::mutex> lock{checkers.lock};\n\n\tcheckers.destroyed_components.insert(thisptr);\n\tcheckers.map.erase(thisptr);\n}\n\n\nvoid check() {\n\tstate &checkers = state::get();\n\n\tstd::unique_lock<std::mutex> lock{checkers.lock};\n\n\tif (checkers.check_performed) {\n\t\tthrow Error(MSG(err) <<\n\t\t\t\"py interface initialization check has already been performed. \"\n\t\t\t\"fix your init code.\");\n\t}\n\n\tcheckers.check_performed = true;\n\n\tbool error_found = false;\n\tauto msg = MSG(err);\n\tmsg << \"Fix your init code: \";\n\n\tif (not checkers.destroyed_components.empty()) {\n\t\tmsg << std::endl <<\n\t\t\t\"Py interface components have been de-initialized before \"\n\t\t\t\"initialization had even been completed:\";\n\n\t\tfor (void *thisptr : checkers.destroyed_components) {\n\t\t\tmsg << std::endl << \"\\t\" << util::symbol_name(thisptr);\n\t\t}\n\n\t\terror_found = true;\n\t}\n\n\tcheckers.destroyed_components.clear();\n\n\tbool first = true;\n\tfor (auto &checker : checkers.map) {\n\t\tif (not checker.second()) {\n\t\t\tif (first) {\n\t\t\t\tmsg << std::endl <<\n\t\t\t\t\t\"The following components have not been initialized:\";\n\n\t\t\t\tfirst = false;\n\t\t\t\terror_found = true;\n\t\t\t}\n\n\t\t\tmsg << std::endl << \"\\t\" << util::symbol_name(checker.first);\n\t\t}\n\t}\n\n\tcheckers.map.clear();\n\n\tif (error_found) {\n\t\tthrow Error(msg);\n\t}\n}\n\n\n} // openage::pyinterface\n"
  },
  {
    "path": "libopenage/pyinterface/setup.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include <functional>\n\n#pragma once\n\n#include \"../util/compiler.h\"\n\n/*\n * Code for verifying that all components of the Python interface have been\n * properly initialized.\n *\n * A \"Python interface component\" is a global object that is supposed to\n * be initialized externally by openage.cppinterface.setup.setup().\n * Examples include PyIfFunc and PyIfObjRef objects.\n */\n\nnamespace openage {\nnamespace pyinterface {\n\n\n/**\n * Adds a Python interface component.\n * Usually, you'd call this from a component's constructor.\n *\n * thisptr is a pointer to the component object.\n *\n * checker is a function that must have defined behavior until\n * destroy_py_if_component has been called for thisptr.\n * It shall return true if the object has been properly initialized,\n * and shall not throw any exceptions.\n */\nvoid add_py_if_component(void *thisptr, std::function<bool()> checker);\n\n\n/**\n * Signals that an interface component that has been previously registered\n * via add_py_if_component is no longer valid, e.g. because its destructor\n * has been invoked.\n * Usually, you'd call this from a component's destructor.\n */\nvoid destroy_py_if_component(void *thisptr);\n\n\n/**\n * Checks whether all objects that have been registered via add_py_if_component\n * have been properly initialized.\n *\n * If not, or if components have been destroyed already, raises an exception\n * that contains the component names.\n *\n * May be called only once.\n *\n * pxd: void check() except +\n */\nOAAPI void check();\n\n\n} // namespace pyinterface\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tcolor.cpp\n    definitions.cpp\n\tgeometry.cpp\n\trender_factory.cpp\n    render_pass.cpp\n    render_target.cpp\n    renderable.cpp\n\trenderer.cpp\n\tshader_program.cpp\n\ttexture.cpp\n\ttexture_array.cpp\n\ttypes.cpp\n    uniform_buffer.cpp\n\tuniform_input.cpp\n\tutil.cpp\n\twindow.cpp\n\twindow_event_handler.cpp\n)\n\nadd_subdirectory(camera/)\nadd_subdirectory(demo/)\nadd_subdirectory(font/)\nadd_subdirectory(gui/)\nadd_subdirectory(resources/)\nadd_subdirectory(stages/)\n\nif(OPENGL_FOUND)\n    add_subdirectory(opengl/)\nendif()\n\nif(VULKAN_FOUND)\n    add_subdirectory(vulkan/)\nendif()\n"
  },
  {
    "path": "libopenage/renderer/camera/CMakeLists.txt",
    "content": "add_sources(libopenage\n    boundaries.cpp\n\tcamera.cpp\n    definitions.cpp\n    frustum_2d.cpp\n\tfrustum_3d.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/camera/boundaries.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"boundaries.h\"\n\n\nnamespace openage::renderer::camera {\n\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/boundaries.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage::renderer::camera {\n\n/**\n * Defines boundaries for the camera's view.\n */\nstruct CameraBoundaries {\n\t/// The minimum boundary for the camera's X-coordinate.\n\tfloat x_min;\n\t/// The maximum boundary for the camera's X-coordinate.\n\tfloat x_max;\n\t/// The minimum boundary for the camera's Y-coordinate.\n\tfloat y_min;\n\t/// The maximum boundary for the camera's Y-coordinate.\n\tfloat y_max;\n\t/// The minimum boundary for the camera's Z-coordinate.\n\tfloat z_min;\n\t/// The maximum boundary for the camera's Z-coordinate.\n\tfloat z_max;\n};\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/camera.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"camera.h\"\n\n#include <array>\n#include <numbers>\n#include <string>\n#include <utility>\n\n#include \"coord/pixel.h\"\n#include \"coord/scene.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/buffer_info.h\"\n\n\nnamespace openage::renderer::camera {\n\nCamera::Camera(const std::shared_ptr<Renderer> &renderer,\n               util::Vector2s viewport_size) :\n\tscene_pos{Eigen::Vector3f(0.0f, 10.0f, 0.0f)},\n\tviewport_size{viewport_size},\n\tzoom{DEFAULT_ZOOM},\n\tmax_zoom_out{DEFAULT_MAX_ZOOM_OUT},\n\tdefault_zoom_ratio{DEFAULT_ZOOM_RATIO},\n\tmoved{true},\n\tzoom_changed{true},\n\tview{Eigen::Matrix4f::Identity()},\n\tproj{Eigen::Matrix4f::Identity()} {\n\tthis->look_at_scene(Eigen::Vector3f(0.0f, 0.0f, 0.0f));\n\n\tthis->init_uniform_buffer(renderer);\n\n\tlog::log(INFO << \"Created new camera at position \"\n\t              << \"(\" << this->scene_pos[0]\n\t              << \", \" << this->scene_pos[1]\n\t              << \", \" << this->scene_pos[2] << \")\");\n}\n\nCamera::Camera(const std::shared_ptr<Renderer> &renderer,\n               util::Vector2s viewport_size,\n               Eigen::Vector3f scene_pos,\n               float zoom,\n               float max_zoom_out,\n               float default_zoom_ratio) :\n\tscene_pos{scene_pos},\n\tviewport_size{viewport_size},\n\tzoom{zoom},\n\tmax_zoom_out{max_zoom_out},\n\tdefault_zoom_ratio{default_zoom_ratio},\n\tmoved{true},\n\tzoom_changed{true},\n\tviewport_changed{true},\n\tview{Eigen::Matrix4f::Identity()},\n\tproj{Eigen::Matrix4f::Identity()} {\n\tthis->init_uniform_buffer(renderer);\n\n\tlog::log(INFO << \"Created new camera at position \"\n\t              << \"(\" << this->scene_pos[0]\n\t              << \", \" << this->scene_pos[1]\n\t              << \", \" << this->scene_pos[2] << \")\");\n}\n\nvoid Camera::look_at_scene(Eigen::Vector3f scene_pos) {\n\tauto new_pos = calc_look_at(scene_pos);\n\tthis->move_to(new_pos);\n}\n\nvoid Camera::look_at_coord(coord::scene3 coord_pos) {\n\t// convert coord pos to scene pos\n\tauto scene_pos = coord_pos.to_world_space();\n\tthis->look_at_scene(scene_pos);\n}\n\nvoid Camera::move_to(Eigen::Vector3f scene_pos, const CameraBoundaries &camera_boundaries) {\n\tscene_pos[0] = std::clamp(scene_pos[0], camera_boundaries.x_min, camera_boundaries.x_max);\n\tscene_pos[1] = std::clamp(scene_pos[1], camera_boundaries.y_min, camera_boundaries.y_max);\n\tscene_pos[2] = std::clamp(scene_pos[2], camera_boundaries.z_min, camera_boundaries.z_max);\n\n\tthis->scene_pos = scene_pos;\n\tthis->moved = true;\n}\n\nvoid Camera::move_rel(Eigen::Vector3f direction, float delta, const CameraBoundaries &camera_boundaries) {\n\tthis->move_to(this->scene_pos + (direction * delta), camera_boundaries);\n}\n\nvoid Camera::set_zoom(float zoom) {\n\tif (zoom < Camera::MAX_ZOOM_IN) {\n\t\tzoom = Camera::MAX_ZOOM_IN;\n\t}\n\telse if (zoom > this->max_zoom_out) {\n\t\tzoom = this->max_zoom_out;\n\t}\n\n\tthis->zoom = zoom;\n\tthis->zoom_changed = true;\n}\n\nvoid Camera::zoom_in(float zoom_delta) {\n\tauto zoom = this->zoom - zoom_delta;\n\tthis->set_zoom(zoom);\n}\n\nvoid Camera::zoom_out(float zoom_delta) {\n\tauto zoom = this->zoom + zoom_delta;\n\tthis->set_zoom(zoom);\n}\n\nvoid Camera::resize(size_t width, size_t height) {\n\tthis->viewport_size = util::Vector2s(width, height);\n\tthis->viewport_changed = true;\n}\n\nfloat Camera::get_zoom() const {\n\treturn this->zoom;\n}\n\nconst Eigen::Matrix4f &Camera::get_view_matrix() {\n\tif (not this->moved) {\n\t\treturn this->view;\n\t}\n\n\tauto direction = CAM_DIRECTION.normalized();\n\n\tEigen::Vector3f eye = this->scene_pos;\n\tEigen::Vector3f center = this->scene_pos + direction; // look in the direction of the camera\n\tEigen::Vector3f up = CAM_UP;\n\n\tEigen::Vector3f f = center - eye;\n\tf.normalize();\n\tEigen::Vector3f s = f.cross(up);\n\ts.normalize();\n\tEigen::Vector3f u = s.cross(f);\n\n\t// View matrix\n\t// |  s[0]  s[1]  s[2] -dot(s,eye) |\n\t// |  u[0]  u[1]  u[2] -dot(u,eye) |\n\t// | -f[0] -f[1] -f[2]  dot(f,eye) |\n\t// |   0     0     0        1      |\n\tEigen::Matrix4f mat = Eigen::Matrix4f::Identity();\n\tmat(0, 0) = s[0];\n\tmat(0, 1) = s[1];\n\tmat(0, 2) = s[2];\n\tmat(1, 0) = u[0];\n\tmat(1, 1) = u[1];\n\tmat(1, 2) = u[2];\n\tmat(2, 0) = -f[0];\n\tmat(2, 1) = -f[1];\n\tmat(2, 2) = -f[2];\n\tmat(0, 3) = -s.dot(eye);\n\tmat(1, 3) = -u.dot(eye);\n\tmat(2, 3) = f.dot(eye);\n\n\t// Cache matrix for subsequent calls\n\tthis->view = mat;\n\tthis->moved = false;\n\n\treturn this->view;\n}\n\nconst Eigen::Matrix4f &Camera::get_projection_matrix() {\n\tif (not(this->zoom_changed or this->viewport_changed)) {\n\t\treturn this->proj;\n\t}\n\n\t// get center of viewport as the focus point\n\tfloat halfwidth = this->viewport_size[0] / 2;\n\tfloat halfheight = this->viewport_size[1] / 2;\n\n\t// get zoom level\n\tfloat real_zoom = this->get_real_zoom_factor();\n\n\t// zoom by narrowing or widening viewport around focus point.\n\t// narrow viewport => zoom in (projected area gets *smaller* while screen size stays the same)\n\t// widen viewport => zoom out (projected area gets *bigger* while screen size stays the same)\n\tfloat left = -(real_zoom * halfwidth);\n\tfloat right = real_zoom * halfwidth;\n\tfloat bottom = -(real_zoom * halfheight);\n\tfloat top = real_zoom * halfheight;\n\n\tEigen::Matrix4f mat = Eigen::Matrix4f::Identity();\n\tmat(0, 0) = 2.0f / (right - left);\n\tmat(1, 1) = 2.0f / (top - bottom);\n\tmat(2, 2) = -2.0f / (DEFAULT_FAR_DISTANCE - DEFAULT_NEAR_DISTANCE); // clip near and far planes (TODO: necessary?)\n\tmat(0, 3) = -(right + left) / (right - left);\n\tmat(1, 3) = -(top + bottom) / (top - bottom);\n\tmat(2, 3) = -(DEFAULT_FAR_DISTANCE + DEFAULT_NEAR_DISTANCE)\n\t            / (DEFAULT_FAR_DISTANCE - DEFAULT_NEAR_DISTANCE); // clip near and far planes (TODO: necessary?)\n\n\t// Cache matrix for subsequent calls\n\tthis->proj = mat;\n\tthis->zoom_changed = false;\n\tthis->viewport_changed = false;\n\n\treturn this->proj;\n}\n\nconst util::Vector2s &Camera::get_viewport_size() const {\n\treturn this->viewport_size;\n}\n\nEigen::Vector3f Camera::get_input_pos(const coord::input &coord) const {\n\t// calculate up (u) and right (s) vectors for camera\n\t// these define the camera plane in 3D space that the input\n\t// coord exists on\n\tauto direction = CAM_DIRECTION.normalized();\n\tEigen::Vector3f eye = this->scene_pos;\n\tEigen::Vector3f center = this->scene_pos + direction;\n\tEigen::Vector3f up = CAM_UP;\n\n\tEigen::Vector3f f = center - eye;\n\tf.normalize();\n\tEigen::Vector3f s = f.cross(up);\n\ts.normalize();\n\tEigen::Vector3f u = s.cross(f);\n\tu.normalize();\n\n\t// offsets are adjusted by zoom\n\t// this is the same calculation as for the projection matrix\n\tfloat halfwidth = this->viewport_size[0] / 2;\n\tfloat halfheight = this->viewport_size[1] / 2;\n\tfloat real_zoom = this->get_real_zoom_factor();\n\n\t// calculate x and y offset on the camera plane relative to the camera position\n\tfloat x = +(2.0f * coord.x / this->viewport_size[0] - 1) * (halfwidth * real_zoom);\n\tfloat y = -(2.0f * coord.y / this->viewport_size[1] - 1) * (halfheight * real_zoom);\n\n\t// calculate the absolutive position of the input coordinates on the camera plane\n\tEigen::Vector3f input_pos = this->scene_pos + s * x + u * y;\n\n\treturn input_pos;\n}\n\nconst std::shared_ptr<renderer::UniformBuffer> &Camera::get_uniform_buffer() const {\n\treturn this->uniform_buffer;\n}\n\nconst Frustum2d Camera::get_frustum_2d() {\n\treturn {this->viewport_size,\n\t        this->get_view_matrix(),\n\t        this->get_projection_matrix(),\n\t        this->get_zoom()};\n}\n\nconst Frustum3d Camera::get_frustum_3d() const {\n\treturn {this->viewport_size,\n\t        DEFAULT_NEAR_DISTANCE,\n\t        DEFAULT_FAR_DISTANCE,\n\t        this->scene_pos,\n\t        CAM_DIRECTION,\n\t        CAM_UP,\n\t        this->get_real_zoom_factor()};\n}\n\nvoid Camera::init_uniform_buffer(const std::shared_ptr<Renderer> &renderer) {\n\tresources::UBOInput view_input{\"view\", resources::ubo_input_t::M4F32};\n\tresources::UBOInput proj_input{\"proj\", resources::ubo_input_t::M4F32};\n\tresources::UBOInput inv_zoom_input{\"inv_zoom\", resources::ubo_input_t::F32};\n\tresources::UBOInput inv_viewport_size{\"inv_viewport_size\", resources::ubo_input_t::V2F32};\n\tauto ubo_info = resources::UniformBufferInfo{\n\t\tresources::ubo_layout_t::STD140,\n\t\t{view_input, proj_input, inv_zoom_input, inv_viewport_size}};\n\tthis->uniform_buffer = renderer->add_uniform_buffer(ubo_info);\n}\n\nEigen::Vector3f Camera::calc_look_at(Eigen::Vector3f target) {\n\tif (target[1] > this->scene_pos[1]) {\n\t\t// TODO: camera can't look at a position that's\n\t\t//       higher than it's own position\n\t}\n\n\t// TODO: Although the below method should be faster, calculating and adding the direction\n\t//       vector from scene_pos to new_pos may be easier to understand\n\t//       i.e. new_pos = scene_pos + b/sin(30) * direction_vec\n\n\t// due to the fixed angle, the centered scene position\n\t// and the new camera position form a right triangle.\n\t//\n\t//           c - + new camera pos\n\t//          -    |b\n\t// center +------+\n\t//            a\n\t//\n\t// we can calculate the new camera position via the offset a\n\t// using the angle and length of side b.\n\tauto y_delta = this->scene_pos[1] - target[1];    // b (vertical distance)\n\tauto xz_distance = y_delta * std::numbers::sqrt3; // a (horizontal distance); a = b * (cos(30°) / sin(30°))\n\n\t// get x and z offsets\n\t// the camera is pointed diagonally to the negative x and z axis\n\t// a is the length of the diagonal from camera.xz to scene_pos.xz\n\t// so the x and z offest are sides of a square with the same diagonal\n\tauto side_length = xz_distance / std::numbers::sqrt2;\n\treturn Eigen::Vector3f(\n\t\ttarget[0] + side_length,\n\t\tthis->scene_pos[1], // height unchanged\n\t\ttarget[2] + side_length);\n}\n\ninline float Camera::get_real_zoom_factor() const {\n\treturn 0.5f * this->default_zoom_ratio * this->zoom;\n}\n\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/camera.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cmath>\n#include <cstddef>\n#include <limits>\n#include <memory>\n#include <optional>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/pixel.h\"\n#include \"coord/scene.h\"\n#include \"util/vector.h\"\n\n#include \"renderer/camera/boundaries.h\"\n#include \"renderer/camera/definitions.h\"\n#include \"renderer/camera/frustum_2d.h\"\n#include \"renderer/camera/frustum_3d.h\"\n\n\nnamespace openage::renderer {\nclass Renderer;\nclass UniformBuffer;\n\nnamespace camera {\n\n/**\n * Camera for selecting what part of the ingame world is displayed.\n *\n * The camera uses orthographic projection as it is primarily used for\n * 2D rendering.\n *\n * TODO: Vulkan version.\n */\nclass Camera {\npublic:\n\t/**\n\t * Create a new camera for the renderer.\n\t *\n\t * The camera uses default values. Its centered on the origin of the scene (0.0f, 0.0f, 0.0f)\n\t * and has a zoom level of 1.0f.\n\t *\n\t * @param renderer openage renderer instance.\n\t * @param viewport_size Initial viewport size of the camera (width x height).\n\t */\n\tCamera(const std::shared_ptr<Renderer> &renderer,\n\t       util::Vector2s viewport_size);\n\n\t/**\n\t * Create a new camera for the renderer.\n\t *\n\t * @param renderer openage renderer instance.\n\t * @param viewport_size Viewport size of the camera (width x height).\n\t * @param scene_pos Position of the camera in the scene.\n\t * @param zoom Zoom level of the camera (defaults to 1.0f).\n\t * @param max_zoom_out Maximum zoom out level (defaults to 64.0f).\n\t * @param default_zoom_ratio Default zoom level calibration (defaults to (1.0f / 49)).\n\t */\n\tCamera(const std::shared_ptr<Renderer> &renderer,\n\t       util::Vector2s viewport_size,\n\t       Eigen::Vector3f scene_pos,\n\t       float zoom = DEFAULT_ZOOM,\n\t       float max_zoom_out = DEFAULT_MAX_ZOOM_OUT,\n\t       float default_zoom_ratio = DEFAULT_ZOOM_RATIO);\n\t~Camera() = default;\n\n\t/**\n\t * Move the camera so that the center of its viewpoint points\n\t * to the given position in the 3D scene.\n\t *\n\t * @param scene_pos Position in the 3D scene that the camera should center on.\n\t */\n\tvoid look_at_scene(Eigen::Vector3f scene_pos);\n\n\t/**\n\t * Move the camera so that the center of its viewpoint points\n\t * to the given ingame coordinates.\n\t *\n\t * @param scene_pos Position of the ingame coordinates that the camera should center on.\n\t */\n\tvoid look_at_coord(coord::scene3 coord_pos);\n\n\t/**\n\t * Move the camera position in the direction of a given vector.\n\t *\n\t * @param scene_pos New 3D position of the camera in the scene.\n\t * @param camera_boundaries 3D boundaries for the camera.\n\t */\n\tvoid move_to(Eigen::Vector3f scene_pos, const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES);\n\n\t/**\n\t * Move the camera position in the direction of a given vector taking the\n\t * camera boundaries into account.\n\t *\n\t * @param direction Direction vector. Added to the current position.\n\t * @param delta Delta for controlling the amount by which the camera is moved. The\n\t *              value is multiplied with the directional vector before its applied to\n\t *              the positional vector.\n\t * @param camera_boundaries 3D boundaries for the camera.\n\t */\n\tvoid move_rel(Eigen::Vector3f direction, float delta = 1.0f, const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES);\n\n\t/**\n\t * Set the zoom level of the camera. Values smaller than 1.0f let the\n\t * camera zoom in, values greater than 1.0f let the camera zoom out.\n\t *\n\t * The max zoom in value is 0.05f. Passing values lower than that\n\t * will just set the zoom level to max zoom.\n\t *\n\t * For incremental zooming, use the \\p zoom_in() and \\p zoom_out()\n\t * methods.\n\t *\n\t * @param zoom New zoom level.\n\t */\n\tvoid set_zoom(float zoom);\n\n\t/**\n\t * Zoom into the scene.\n\t *\n\t * Decreases the current zoom level.\n\t *\n\t * @param zoom_delta How far the camera should zoom in.\n\t */\n\tvoid zoom_in(float zoom_delta);\n\n\t/**\n\t * Zoom out of the scene.\n\t *\n\t * Increases the current zoom level.\n\t *\n\t * @param zoom_delta How far the camera should zoom out.\n\t */\n\tvoid zoom_out(float zoom_delta);\n\n\t/**\n\t * Resize the camera viewport.\n\t *\n\t * @param width New width of the viewport.\n\t * @param height New height of the viewport.\n\t */\n\tvoid resize(size_t width, size_t height);\n\n\t/**\n\t * Get the current zoom level of the camera.\n\t *\n\t * Determines the scale of rendered objects.\n\t *\n\t * @return Zoom level.\n\t */\n\tfloat get_zoom() const;\n\n\t/**\n\t * Get the view matrix for this camera.\n\t *\n\t * @return Camera view matrix.\n\t */\n\tconst Eigen::Matrix4f &get_view_matrix();\n\n\t/**\n\t * Get the projection matrix for this camera.\n\t *\n\t * @return Camera projection matrix.\n\t */\n\tconst Eigen::Matrix4f &get_projection_matrix();\n\n\t/**\n\t * Get the size of the camera viewport.\n\t *\n\t * @return Viewport size as a 2D vector (x, y).\n\t */\n\tconst util::Vector2s &get_viewport_size() const;\n\n\t/**\n\t * Get the corresponding 3D position of a 2D input coordinate in the viewport.\n\t * The position is on the plane created by the camera's orthographic projection.\n\t *\n\t * This may be used to get the 3D position of a mouse click and subsequent\n\t * ray casting calculations.\n\t *\n\t * @param coord 2D input coordinate in the viewport.\n\t *\n\t * @return Position of the input in the 3D scene.\n\t */\n\tEigen::Vector3f get_input_pos(const coord::input &coord) const;\n\n\t/**\n\t * Get the uniform buffer for this camera.\n\t *\n\t * @return Uniform buffer.\n\t */\n\tconst std::shared_ptr<renderer::UniformBuffer> &get_uniform_buffer() const;\n\n\t/**\n\t * Get a 2D frustum object for this camera.\n\t *\n\t * @return Frustum object.\n\t */\n\tconst Frustum2d get_frustum_2d();\n\n\t/**\n\t * Get a 3D frustum object for this camera.\n\t *\n\t * @return Frustum object.\n\t */\n\tconst Frustum3d get_frustum_3d() const;\n\n\nprivate:\n\t/**\n\t * Create the uniform buffer for the camera.\n\t *\n\t * @param renderer openage renderer instance.\n\t */\n\tvoid init_uniform_buffer(const std::shared_ptr<Renderer> &renderer);\n\n\t/**\n\t * Calculates the camera's position needed to center its view on the given target.\n\t *\n\t * @param target The target position in the 3D scene the camera should focus on.\n\t */\n\tEigen::Vector3f calc_look_at(Eigen::Vector3f target);\n\n\t/**\n\t * Get the zoom factor applied to the camera projection.\n\t *\n\t * The zoom factor is calculated as\n\t *\n\t *     zoom * zoom_ratio * 0.5f\n\t *\n\t * Note that this zoom factor should NOT be used for sprite scaling, but\n\t * only for 3D projection matrix calculations. For sprite scaling, use\n\t * \\p get_zoom() .\n\t *\n\t * @return Zoom factor for projection.\n\t */\n\tinline float get_real_zoom_factor() const;\n\n\t/**\n\t * Position in the 3D scene.\n\t */\n\tEigen::Vector3f scene_pos;\n\n\t/**\n\t * Size of the camera viewport.\n\t */\n\tutil::Vector2s viewport_size;\n\n\t/**\n\t * Zoom level.\n\t *\n\t * 0.0f < z < 1.0f => zoom in\n\t * z = 1.0f        => default view\n\t * z > 1.0f        => zoom out\n\t */\n\tfloat zoom;\n\n\t/**\n\t * Maximum possible zoom in level.\n\t *\n\t * Has to be above 0.0f, otherwise we get zero division errors.\n\t */\n\tstatic constexpr float MAX_ZOOM_IN = 0.005f;\n\n\t/**\n\t * Maximum possible zoom out level.\n\t *\n\t * This can be set per camera.\n\t */\n\tfloat max_zoom_out;\n\n\t/**\n\t * Modifier that controls what the default zoom level (zoom = 1.0f)\n\t * looks like. Essentially, this value is also a zoom that is pre-applied\n\t * before other calculations.\n\t *\n\t * This value is important for calibrating the default zoom to match\n\t * the pixel size of assets. For example, terrain in AoE2 has a height\n\t * of 49 pixels, while in the renderer scene the height is always 1.0.\n\t * Setting \\p default_zoom_ratio to 1 / 49 ensure that the default zoom\n\t * level matches the pixel ration of the original game.\n\t */\n\tfloat default_zoom_ratio;\n\n\t/**\n\t * Flag set when the camera is moved.\n\t *\n\t * If true, the view matrix needs to be recalculated.\n\t */\n\tbool moved;\n\n\t/**\n\t * Flag set when the camera zoom is changed.\n\t *\n\t * If true, the projection matrix needs to be recalculated.\n\t */\n\tbool zoom_changed;\n\n\t/**\n\t * Flag set when the camera viewport is resized.\n\t *\n\t * If true, the projection matrix needs to be recalculated.\n\t */\n\tbool viewport_changed;\n\n\t/**\n\t * Current view matrix for the camera.\n\t *\n\t * Cached because it may be requested many times.\n\t */\n\tEigen::Matrix4f view;\n\n\t/**\n\t * Current projection matrix for the camera.\n\t *\n\t * Cached because it may be requested many times.\n\t */\n\tEigen::Matrix4f proj;\n\n\t/**\n\t * Uniform buffer for the camera matrices.\n\t */\n\tstd::shared_ptr<renderer::UniformBuffer> uniform_buffer;\n};\n\n} // namespace camera\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/camera/definitions.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"definitions.h\"\n\nnamespace openage::renderer::camera {\n\n// this file is intentionally empty\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/definitions.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <eigen3/Eigen/Dense>\n#include <limits>\n\n#include \"renderer/camera/boundaries.h\"\n\nnamespace openage::renderer::camera {\n\n/**\n * Camera direction (= where it looks at).\n * Uses a dimetric perspective like in AoE with the (fixed) angles\n *    yaw   = -135 degrees\n *    pitch = -30 degrees\n */\nstatic const Eigen::Vector3f CAM_DIRECTION{\n\t-1 * (std::sqrt(6.f) / 4),\n\t-0.5f,\n\t-1 * (std::sqrt(6.f) / 4),\n};\n\n/**\n * Camera up vector.\n */\nstatic const Eigen::Vector3f CAM_UP{0.0f, 1.0f, 0.0f};\n\n/**\n * Default near distance of the camera.\n *\n * Determines how close objects can be to the camera before they are not rendered anymore.\n */\nstatic constexpr float DEFAULT_NEAR_DISTANCE = 0.01f;\n\n/**\n * Default far distance of the camera.\n *\n * Determines how far objects can be from the camera before they are not rendered anymore.\n */\nstatic constexpr float DEFAULT_FAR_DISTANCE = 100.0f;\n\n/**\n * Default zoom level of the camera.\n */\nstatic constexpr float DEFAULT_ZOOM = 1.0f;\n\n/**\n * Maximum zoom out level of the camera.\n */\nstatic constexpr float DEFAULT_MAX_ZOOM_OUT = 64.0f;\n\n/**\n * Default zoom ratio.\n *\n * This adjusts the zoom level of the camera to the size of sprites in the game,\n * so that 1 pixel in a sprite == 1 pixel on the screen.\n *\n * 1.0f / 49 is the default value for the AoE2 sprites.\n */\nstatic constexpr float DEFAULT_ZOOM_RATIO = 1.0f / 49;\n\nstatic constexpr CameraBoundaries DEFAULT_CAM_BOUNDARIES{\n\tstd::numeric_limits<float>::lowest(),\n\tstd::numeric_limits<float>::max(),\n\tstd::numeric_limits<float>::lowest(),\n\tstd::numeric_limits<float>::max(),\n\tstd::numeric_limits<float>::lowest(),\n\tstd::numeric_limits<float>::max()};\n\n/**\n * Constant values for the camera bounds (based on current fix terrain grid of 20x20).\n * TODO: Make boundaries dynamic based on map size.\n */\nstatic constexpr float X_BOUND_MIN = 12.25f;\nstatic constexpr float X_BOUND_MAX = 32.25f;\nstatic constexpr float Y_BOUND_MIN = 0.0f;\nstatic constexpr float Y_BOUND_MAX = 20.0f;\nstatic constexpr float Z_BOUND_MIN = -8.25f;\nstatic constexpr float Z_BOUND_MAX = 12.25f;\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/frustum_2d.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"frustum_2d.h\"\n\n\nnamespace openage::renderer::camera {\n\nFrustum2d::Frustum2d(const util::Vector2s &viewport_size,\n                     const Eigen::Matrix4f &view_matrix,\n                     const Eigen::Matrix4f &projection_matrix,\n                     const float zoom) {\n\tthis->update(viewport_size, view_matrix, projection_matrix, zoom);\n}\n\nvoid Frustum2d::update(const util::Vector2s &viewport_size,\n                       const Eigen::Matrix4f &view_matrix,\n                       const Eigen::Matrix4f &projection_matrix,\n                       const float zoom) {\n\tthis->pixel_size_ndc = {2.0f / viewport_size[0], 2.0f / viewport_size[1]};\n\tthis->inv_zoom_factor = 1.0f / zoom;\n\n\t// calculate the transformation matrix\n\tthis->transform_matrix = projection_matrix * view_matrix;\n}\n\nbool Frustum2d::in_frustum(const Eigen::Vector3f &scene_pos,\n                           const Eigen::Matrix4f &model_matrix,\n                           const float scalefactor,\n                           const util::Vector4i &boundaries) const {\n\t// calculate the position of the scene object in screen space\n\tEigen::Vector4f clip_pos = this->transform_matrix * model_matrix * scene_pos.homogeneous();\n\tfloat x_ndc = clip_pos[0];\n\tfloat y_ndc = clip_pos[1];\n\n\tfloat zoom_scale = scalefactor * this->inv_zoom_factor;\n\n\t// Scale the boundaries by the zoom factor and the pixel size\n\tfloat left_bound = boundaries[0] * zoom_scale * this->pixel_size_ndc[0];\n\tfloat right_bound = boundaries[1] * zoom_scale * this->pixel_size_ndc[0];\n\tfloat top_bound = boundaries[2] * zoom_scale * this->pixel_size_ndc[1];\n\tfloat bottom_bound = boundaries[3] * zoom_scale * this->pixel_size_ndc[1];\n\n\t// check if the object boundaries are inside the frustum\n\tif (x_ndc - left_bound >= 1.0f) {\n\t\treturn false;\n\t}\n\tif (x_ndc + right_bound <= -1.0f) {\n\t\treturn false;\n\t}\n\tif (y_ndc + top_bound <= -1.0f) {\n\t\treturn false;\n\t}\n\tif (y_ndc - bottom_bound >= 1.0f) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/frustum_2d.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <eigen3/Eigen/Dense>\n\n#include \"util/vector.h\"\n\nnamespace openage::renderer::camera {\n\n/**\n * Frustum for culling objects outside of a camera view in 2D screen space.\n * This frustum object should be used for sprite culling as sprites do not exist in 3D world space.\n * They are directly projected and reszied to the screen space.\n *\n * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling)\n * in the renderer.\n */\nclass Frustum2d {\npublic:\n\t/**\n\t * Create a new 2D frustum.\n\t *\n\t * @param viewport_size Size of the camera viewport (width x height).\n\t * @param view_matrix View matrix of the camera.\n\t * @param projection_matrix Projection matrix of the camera.\n\t * @param zoom Zoom of the camera.\n\t */\n\tFrustum2d(const util::Vector2s &viewport_size,\n\t          const Eigen::Matrix4f &view_matrix,\n\t          const Eigen::Matrix4f &projection_matrix,\n\t          const float zoom);\n\n\t/**\n\t * Update the frustum with new camera parameters.\n\t *\n\t * @param viewport_size Size of the camera viewport (width x height).\n\t * @param view_matrix View matrix of the camera.\n\t * @param projection_matrix Projection matrix of the camera.\n\t * @param zoom Zoom of the camera.\n\t */\n\tvoid update(const util::Vector2s &viewport_size,\n\t            const Eigen::Matrix4f &view_matrix,\n\t            const Eigen::Matrix4f &projection_matrix,\n\t            const float zoom);\n\n\t/**\n\t * Check if a scene object is inside the frustum.\n\t *\n\t * @param scene_pos 3D scene coordinates.\n\t * @param model_matrix Model matrix of the object.\n\t * @param scalefactor Scale factor of the animation.\n\t * @param boundaries Boundaries of the animation (in pixels): left, right, top, bottom.\n\t *\n\t * @return true if the object is inside the frustum, false otherwise.\n\t */\n\tbool in_frustum(const Eigen::Vector3f &scene_pos,\n\t                const Eigen::Matrix4f &model_matrix,\n\t                const float scalefactor,\n\t                const util::Vector4i &boundaries) const;\n\nprivate:\n\t/**\n\t * Camera transformation matrix.\n\t *\n\t * Pre-calculated from view and projection matrix.\n\t */\n\tEigen::Matrix4f transform_matrix;\n\n\t/**\n\t * Size of a pixel (width x height) in clip space.\n\t *\n\t * Uses normalized device coordinates (NDC) for the pixel size.\n\t */\n\tEigen::Vector2f pixel_size_ndc;\n\n\t/**\n\t * Zoom factor of the camera.\n\t */\n\tfloat inv_zoom_factor;\n};\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/frustum_3d.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"frustum_3d.h\"\n\n\nnamespace openage::renderer::camera {\n\nFrustum3d::Frustum3d(const util::Vector2s &viewport_size,\n                     const float near_distance,\n                     const float far_distance,\n                     const Eigen::Vector3f &camera_pos,\n                     const Eigen::Vector3f &cam_direction,\n                     const Eigen::Vector3f &up_direction,\n                     const float zoom_factor) {\n\tthis->update(viewport_size,\n\t             near_distance,\n\t             far_distance,\n\t             camera_pos,\n\t             cam_direction,\n\t             up_direction,\n\t             zoom_factor);\n}\n\nvoid Frustum3d::update(const util::Vector2s &viewport_size,\n                       const float near_distance,\n                       const float far_distance,\n                       const Eigen::Vector3f &camera_pos,\n                       const Eigen::Vector3f &cam_direction,\n                       const Eigen::Vector3f &up_direction,\n                       const float zoom_factor) {\n\t// offsets are adjusted by zoom\n\t// this is the same calculation as for the projection matrix\n\tfloat halfscreenwidth = viewport_size[0] / 2;\n\tfloat halfscreenheight = viewport_size[1] / 2;\n\n\tfloat halfwidth = halfscreenwidth * zoom_factor;\n\tfloat halfheight = halfscreenheight * zoom_factor;\n\n\tEigen::Vector3f direction = cam_direction.normalized();\n\tEigen::Vector3f eye = camera_pos;\n\tEigen::Vector3f center = camera_pos + direction;\n\n\t// calculate up (u) and right (s) vectors for camera\n\t// these define the camera plane in 3D space that the input\n\tEigen::Vector3f f = center - eye;\n\tf.normalize();\n\tEigen::Vector3f s = f.cross(up_direction);\n\ts.normalize();\n\tEigen::Vector3f u = s.cross(f);\n\tu.normalize();\n\n\t// calculate distance of the camera position to the near and far plane\n\tEigen::Vector3f near_position = camera_pos + direction * near_distance;\n\tEigen::Vector3f far_position = camera_pos + direction * far_distance;\n\n\t// The frustum is a cuboid box with 8 points defining it (4 on the near plane, 4 on the far plane)\n\tEigen::Vector3f near_top_left = near_position - s * halfwidth + u * halfheight;\n\tEigen::Vector3f near_top_right = near_position + s * halfwidth + u * halfheight;\n\tEigen::Vector3f near_bottom_left = near_position - s * halfwidth - u * halfheight;\n\tEigen::Vector3f near_bottom_right = near_position + s * halfwidth - u * halfheight;\n\n\tEigen::Vector3f far_top_left = far_position - s * halfwidth + u * halfheight;\n\t// Eigen::Vector3f far_top_right = far_position + s * halfwidth + u * halfheight; // unused\n\tEigen::Vector3f far_bottom_left = far_position - s * halfwidth - u * halfheight;\n\tEigen::Vector3f far_bottom_right = far_position + s * halfwidth - u * halfheight;\n\n\t// The near and far planes are easiest to compute, as they should be in the direction of the camera\n\tthis->near_face_normal = cam_direction.normalized();\n\tthis->far_face_normal = -1.0f * cam_direction.normalized();\n\n\t// The distance of the plane from the origin is the dot product of the normal and a point on the plane\n\tthis->near_face_distance = this->near_face_normal.dot(near_position);\n\tthis->far_face_distance = this->far_face_normal.dot(far_position);\n\n\t// Each left, right, top, and bottom plane are defined by three points on the plane\n\tthis->left_face_normal = (near_bottom_left - near_top_left).cross(far_bottom_left - near_bottom_left);\n\tthis->right_face_normal = (far_bottom_right - near_bottom_right).cross(near_bottom_right - near_top_right);\n\tthis->top_face_normal = (near_top_right - near_top_left).cross(near_top_left - far_top_left);\n\tthis->bottom_face_normal = (near_bottom_left - far_bottom_left).cross(near_bottom_right - near_bottom_left);\n\n\t// for orthographic projection, the normal of left/right should equal -s/s\n\t// and the normal of top/bottom should equal u/-u\n\tthis->left_face_normal.normalize();\n\tthis->right_face_normal.normalize();\n\tthis->top_face_normal.normalize();\n\tthis->bottom_face_normal.normalize();\n\n\t// calculate the distance of the planes to the origin\n\tthis->left_face_distance = this->left_face_normal.dot(near_bottom_left);\n\tthis->right_face_distance = this->right_face_normal.dot(far_bottom_right);\n\tthis->top_face_distance = this->top_face_normal.dot(near_top_right);\n\tthis->bottom_face_distance = this->bottom_face_normal.dot(near_bottom_left);\n}\n\nbool Frustum3d::in_frustum(const Eigen::Vector3f &pos) const {\n\t// For each plane, if a point is behind one of the frustum planes, it is not within the frustum\n\tfloat distance;\n\n\tdistance = this->top_face_normal.dot(pos) - this->top_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\tdistance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\tdistance = this->left_face_normal.dot(pos) - this->left_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\tdistance = this->right_face_normal.dot(pos) - this->right_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\tdistance = this->far_face_normal.dot(pos) - this->far_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\tdistance = this->bottom_face_normal.dot(pos) - this->bottom_face_distance;\n\tif (distance < 0) {\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/camera/frustum_3d.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <eigen3/Eigen/Dense>\n\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::camera {\n\n/**\n * Frustum for culling objects outside of a camera viewcone in 3D space. The frustum\n * is defined by 6 planes (top, bottom, right, left, far, near) that define the boundaries\n * of the viewing volume of the camera.\n *\n * Used for frustum culling (https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling)\n * in the renderer.\n *\n * As the openage camera uses orthographic projection, the frustum is a box, i.e. plane opposite\n * to each other are parallel.\n */\nclass Frustum3d {\npublic:\n\t/**\n\t * Create a new frustum.\n\t *\n\t * @param viewport_size Size of the viewport (width x height).\n\t * @param near_distance Near distance of the frustum.\n\t * @param far_distance Far distance of the frustum.\n\t * @param camera_pos Scene position of the camera.\n\t * @param cam_direction Direction the camera is looking at.\n\t * @param up_direction Up direction of the camera.\n\t * @param zoom_factor Zoom factor of the camera.\n\t */\n\tFrustum3d(const util::Vector2s &viewport_size,\n\t          const float near_distance,\n\t          const float far_distance,\n\t          const Eigen::Vector3f &camera_pos,\n\t          const Eigen::Vector3f &cam_direction,\n\t          const Eigen::Vector3f &up_direction,\n\t          const float zoom_factor);\n\n\t/**\n\t * Update this frustum with the new camera parameters.\n\t *\n\t * @param viewport_size Size of the viewport (width x height).\n\t * @param near_distance Near distance of the frustum.\n\t * @param far_distance Far distance of the frustum.\n\t * @param camera_pos Scene position of the camera.\n\t * @param cam_direction Direction the camera is looking at.\n\t * @param up_direction Up direction of the camera.\n\t * @param zoom_factor Zoom factor of the camera.\n\t */\n\tvoid update(const util::Vector2s &viewport_size,\n\t            const float near_distance,\n\t            const float far_distance,\n\t            const Eigen::Vector3f &camera_pos,\n\t            const Eigen::Vector3f &cam_direction,\n\t            const Eigen::Vector3f &up_direction,\n\t            const float zoom_factor);\n\n\t/**\n\t * Check whether a point in the scene is inside the frustum.\n\t *\n\t * @param scene_pos 3D scene coordinates.\n\t *\n\t * @return true if the point is inside the frustum, else false.\n\t */\n\tbool in_frustum(const Eigen::Vector3f &scene_pos) const;\n\nprivate:\n\t/**\n\t * Normal vector of the top face.\n\t */\n\tEigen::Vector3f top_face_normal;\n\n\t/**\n\t * Normal vector of the bottom face.\n\t */\n\tEigen::Vector3f bottom_face_normal;\n\n\t/**\n\t * Normal vector of the right face.\n\t */\n\tEigen::Vector3f right_face_normal;\n\n\t/**\n\t * Normal vector of the left face.\n\t */\n\tEigen::Vector3f left_face_normal;\n\n\t/**\n\t * Normal vector of the far face.\n\t */\n\tEigen::Vector3f far_face_normal;\n\n\t/**\n\t * Normal vector of the near face.\n\t */\n\tEigen::Vector3f near_face_normal;\n\n\t/**\n\t * Shortest distance from the top face to the scene origin.\n\t */\n\tfloat top_face_distance;\n\n\t/**\n\t * Shortest distance from the bottom face to the scene origin.\n\t */\n\tfloat bottom_face_distance;\n\n\t/**\n\t * Shortest distance from the right face to the scene origin.\n\t */\n\tfloat right_face_distance;\n\n\t/**\n\t * Shortest distance from the left face to the scene origin.\n\t */\n\tfloat left_face_distance;\n\n\t/**\n\t * Shortest distance from the far face to the scene origin.\n\t */\n\tfloat far_face_distance;\n\n\t/**\n\t * Shortest distance from the near face to the scene origin.\n\t */\n\tfloat near_face_distance;\n};\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/color.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"color.h\"\n\nnamespace openage {\nnamespace renderer {\n\nColor::Color() :\n\tr{0},\n\tg{0},\n\tb{0},\n\ta{255} {\n\t// Empty\n}\n\nColor::Color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) :\n\tr{r},\n\tg{g},\n\tb{b},\n\ta{a} {\n\t// Empty\n}\n\nbool Color::operator==(const Color &other) const {\n\treturn this->r == other.r && this->g == other.g && this->b == other.b && this->a == other.a;\n}\n\nbool Color::operator!=(const Color &other) const {\n\treturn !operator==(other);\n}\n\nColor Colors::WHITE = {255, 255, 255, 255};\nColor Colors::BLACK = {0, 0, 0, 255};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/color.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\nnamespace openage {\nnamespace renderer {\n\nclass Color {\npublic:\n\tColor();\n\n\tColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a);\n\n\tbool operator==(const Color &other) const;\n\n\tbool operator!=(const Color &other) const;\n\n\tuint8_t r;\n\tuint8_t g;\n\tuint8_t b;\n\tuint8_t a;\n};\n\nclass Colors {\npublic:\n\tstatic Color WHITE;\n\tstatic Color BLACK;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/definitions.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"definitions.h\"\n\nnamespace openage::renderer {\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/definitions.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <limits>\n\n#include \"coord/scene.h\"\n\n\n/**\n * Hardcoded definitions for parameters used in the renderer.\n *\n * May be moved to configuration files in the future.\n */\nnamespace openage::renderer {\n\n/**\n * Origin point of the 3D scene.\n */\nconstexpr coord::scene3 SCENE_ORIGIN = coord::scene3{0, 0, 0};\n\n/**\n * Maximum priority value for a render pass layer.\n */\nstatic constexpr int64_t LAYER_PRIORITY_MAX = std::numeric_limits<int64_t>::max();\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/demo/CMakeLists.txt",
    "content": "add_sources(libopenage\n    demo_0.cpp\n    demo_1.cpp\n    demo_2.cpp\n    demo_3.cpp\n    demo_4.cpp\n    demo_5.cpp\n    demo_6.cpp\n    demo_7.cpp\n    stresstest_0.cpp\n    stresstest_1.cpp\n\ttests.cpp\n    util.cpp\n)\n\npxdgen(\n\ttests.h\n)\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_0.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_0.h\"\n\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/shader_program.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_0(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\topengl::GlWindow window(\"openage renderer test\", settings);\n\tauto renderer = window.make_renderer();\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for copying the framebuffer in pass 2. */\n\tauto display_vshader_file = (shaderdir / \"demo_0_display.vert.glsl\").open();\n\tauto display_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file.read());\n\tdisplay_vshader_file.close();\n\n\tauto display_fshader_file = (shaderdir / \"demo_0_display.frag.glsl\").open();\n\tauto display_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file.read());\n\tdisplay_fshader_file.close();\n\n\tauto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tRenderable display_stuff{\n\t\tdisplay_shader->create_empty_input(),\n\t\tquad,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tif (not check_uniform_completeness({display_stuff})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\tauto pass = renderer->add_render_pass({display_stuff}, renderer->get_display_target());\n\n\twhile (not window.should_close()) {\n\t\trenderer->render(pass);\n\t\twindow.update();\n\t\tqtapp->process_events();\n\n\t\trenderer->check_error();\n\t}\n\twindow.close();\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_0.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Show the window creation functionality of the level 1 renderer:\n *     - Window creation\n *     - Loading shaders\n *     - Creating a render pass\n *     - Creating a renderable from a mesh\n *\n * @param path Path to the project rootdir.\n */\nvoid renderer_demo_0(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_1.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_1.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <epoxy/gl.h>\n#include <QMouseEvent>\n\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/texture.h\"\n#include \"util/math_constants.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_1(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\topengl::GlWindow window(\"openage renderer test\", settings);\n\tauto renderer = window.make_renderer();\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for individual objects in pass 1. */\n\tauto obj_vshader_file = (shaderdir / \"demo_1_obj.vert.glsl\").open();\n\tauto obj_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_vshader_file.read());\n\tobj_vshader_file.close();\n\n\tauto obj_fshader_file = (shaderdir / \"demo_1_obj.frag.glsl\").open();\n\tauto obj_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_fshader_file.read());\n\tobj_fshader_file.close();\n\n\t/* Shader for copying the framebuffer in pass 2. */\n\tauto display_vshader_file = (shaderdir / \"demo_1_display.vert.glsl\").open();\n\tauto display_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file.read());\n\tdisplay_vshader_file.close();\n\n\tauto display_fshader_file = (shaderdir / \"demo_1_display.frag.glsl\").open();\n\tauto display_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file.read());\n\tdisplay_fshader_file.close();\n\n\tauto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tauto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n\n\t/* Texture for the clickable objects. */\n\tauto tex = resources::Texture2dData(path / \"/assets/test/textures/gaben.png\");\n\tauto gltex = renderer->add_texture(tex);\n\n\tauto transform1 = Eigen::Affine3f::Identity();\n\ttransform1.prescale(Eigen::Vector3f(0.4f, 0.2f, 1.0f));\n\ttransform1.prerotate(Eigen::AngleAxisf(30.0f * math::PI / 180.0f, Eigen::Vector3f::UnitX()));\n\ttransform1.pretranslate(Eigen::Vector3f(-0.4f, -0.6f, 0.0f));\n\n\t/* Clickable objects on the screen consist of a transform (matrix which places each object\n\t * in the correct location), an identifier and a reference to the texture. */\n\tauto obj1_unifs = obj_shader->new_uniform_input(\n\t\t\"mv\",\n\t\ttransform1.matrix(),\n\t\t\"u_id\",\n\t\t1u,\n\t\t\"tex\",\n\t\tgltex);\n\n\tauto transform2 = Eigen::Affine3f::Identity();\n\ttransform2.prescale(Eigen::Vector3f(0.3f, 0.1f, 1.0f));\n\ttransform2.prerotate(Eigen::AngleAxisf(50.0f * math::PI / 180.0f, Eigen::Vector3f::UnitZ()));\n\n\tauto transform3 = transform2;\n\n\ttransform2.pretranslate(Eigen::Vector3f(0.3f, 0.1f, 0.3f));\n\n\tauto obj2_unifs = obj_shader->new_uniform_input(\n\t\t\"mv\",\n\t\ttransform2.matrix(),\n\t\t\"u_id\",\n\t\t2u,\n\t\t// TODO bug: this tex input spills over to all the other uniform inputs!\n\t\t\"tex\",\n\t\tgltex);\n\n\ttransform3.prerotate(Eigen::AngleAxisf(90.0f * math::PI / 180.0f, Eigen::Vector3f::UnitZ()));\n\ttransform3.pretranslate(Eigen::Vector3f(0.3f, 0.1f, 0.5f));\n\n\tauto obj3_unifs = obj_shader->new_uniform_input(\n\t\t\"mv\",\n\t\ttransform3.matrix(),\n\t\t\"u_id\",\n\t\t3u,\n\t\t\"tex\",\n\t\tgltex);\n\n\t/* The objects are using built-in quadrilateral geometry. */\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tRenderable obj1{\n\t\tobj1_unifs,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tRenderable obj2{\n\t\tobj2_unifs,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\tRenderable obj3{\n\t\tobj3_unifs,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\t/* Make a framebuffer for the first render pass to draw into. The framebuffer consists of a color texture\n\t * to be copied onto the back buffer in pass 2, as well as an id texture which will contain the object ids\n\t * which we can later read in order to determine which object was clicked. The depth texture is required,\n\t * but mostly irrelevant in this case. */\n\tauto size = window.get_size();\n\tauto color_texture = renderer->add_texture(resources::Texture2dInfo(size[0], size[1], resources::pixel_format::rgba8));\n\tauto id_texture = renderer->add_texture(resources::Texture2dInfo(size[0], size[1], resources::pixel_format::r32ui));\n\tauto depth_texture = renderer->add_texture(resources::Texture2dInfo(size[0], size[1], resources::pixel_format::depth24));\n\tauto fbo = renderer->create_texture_target({color_texture, id_texture, depth_texture});\n\n\t/* Make an object to update the projection matrix in pass 1 according to changes in the screen size.\n\t * Because uniform values are preserved across objects using the same shader in a single render pass,\n\t * it is sufficient to set it once at the beginning of the pass. To do this, we use an object with no\n\t * geometry, which will set the uniform but not render anything. */\n\tauto proj_unif = obj_shader->new_uniform_input();\n\tRenderable proj_update{\n\t\tproj_unif,\n\t\tnullptr,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass1 = renderer->add_render_pass(\n\t\t{proj_update, obj1, obj2, obj3},\n\t\tfbo);\n\n\t/* Make an object encompassing the entire screen for the second render pass. The object\n\t * will be textured with the color output of pass 1, effectively copying its framebuffer. */\n\tauto color_texture_unif = display_shader->new_uniform_input(\"color_texture\", color_texture);\n\tRenderable display_obj{\n\t\tcolor_texture_unif,\n\t\tquad,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target());\n\n\tif (not check_uniform_completeness({obj1, obj2, obj3, proj_update, display_obj})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\t/* Data retrieved from the object index texture. */\n\tresources::Texture2dData id_texture_data = id_texture->into_data();\n\tbool texture_data_valid = false;\n\n\t// TODO(Vtec234): move these into the renderer\n\tglDepthFunc(GL_LEQUAL);\n\tglDepthRange(0.0, 1.0);\n\tglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n\n\twindow.add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tif (ev.type() == QEvent::MouseButtonRelease) {\n\t\t\tauto qpos = ev.position();\n\t\t\tssize_t x = qpos.x();\n\t\t\tssize_t y = qpos.y();\n\n\t\t\tlog::log(INFO << \"Clicked at location (\" << x << \", \" << y << \")\");\n\t\t\tif (not texture_data_valid) {\n\t\t\t\tid_texture_data = id_texture->into_data();\n\t\t\t\ttexture_data_valid = true;\n\t\t\t}\n\t\t\tauto id = id_texture_data.read_pixel<uint32_t>(x, y);\n\t\t\tlog::log(INFO << \"Id-texture-value at location: \" << id);\n\t\t\tif (id == 0) {\n\t\t\t\tlog::log(INFO << \"Clicked at non existent object\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog::log(INFO << \"Object number \" << id << \" clicked.\");\n\t\t\t}\n\t\t}\n\t});\n\n\twindow.add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\t/* Calculate a projection matrix for the new screen size. */\n\t\tfloat aspectRatio = float(w) / float(h);\n\t\tfloat xScale = 1.0 / aspectRatio;\n\n\t\tEigen::Matrix4f pmat;\n\t\tpmat << xScale, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1;\n\n\t\tproj_unif->update(\"proj\", pmat);\n\n\t\t/* Create a new FBO with the right dimensions. */\n\t\tcolor_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::rgba8));\n\t\tid_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::r32ui));\n\t\tdepth_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::depth24));\n\t\tfbo = renderer->create_texture_target({color_texture, id_texture, depth_texture});\n\n\t\tcolor_texture_unif->update(\"color_texture\", color_texture);\n\n\t\ttexture_data_valid = false;\n\t\tpass1->set_target(fbo);\n\t});\n\n\twhile (not window.should_close()) {\n\t\trenderer->render(pass1);\n\t\trenderer->render(pass2);\n\t\twindow.update();\n\t\tqtapp->process_events();\n\n\t\trenderer->check_error();\n\t}\n\twindow.close();\n}\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_1.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Shows the level 1 renderer's ability to create textured renderable objects and\n * allow basic interaction with them via mouse/key callbacks:\n *     - Window creation\n *     - Loading shaders\n *     - Creating multiple render passes\n *     - Creating multiple renderables for a render pass\n *     - Configuring and setting uniforms for renderables\n *     - Changing uniforms via mouse/key callbacks\n *\n * @param path Path to project rootdir.\n */\nvoid renderer_demo_1(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_2.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_2.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <epoxy/gl.h>\n#include <QMouseEvent>\n\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/parser/parse_sprite.h\"\n#include \"renderer/resources/parser/parse_texture.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/texture.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_2(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\topengl::GlWindow window(\"openage renderer test\", settings);\n\tauto renderer = window.make_renderer();\n\n\t/* Load texture file standalone. */\n\tauto tex_path = path / \"assets/test/textures/test_texture.texture\";\n\tauto tex_info = renderer::resources::parser::parse_texture_file(tex_path);\n\n\tlog::log(INFO << \"Loaded texture \" << tex_path.resolve_native_path());\n\tlog::log(INFO << \"  imagefile: \" << tex_info.get_image_path().value().resolve_native_path());\n\tlog::log(INFO << \"  size:\");\n\tlog::log(INFO << \"    width: \" << tex_info.get_size().first);\n\tlog::log(INFO << \"    height: \" << tex_info.get_size().second);\n\tlog::log(INFO << \"  pxformat:\");\n\tlog::log(INFO << \"    data size: \" << tex_info.get_data_size()\n\t              << \" (\"\n\t              << tex_info.get_size().first << \" x \"\n\t              << tex_info.get_size().second << \" x \"\n\t              << renderer::resources::pixel_size(tex_info.get_format())\n\t              << \")\");\n\tlog::log(INFO << \"    cbits: \"\n\t              << \"(currently unused)\");\n\tlog::log(INFO << \"  subtex count: \" << tex_info.get_subtex_count());\n\n\t/* Load animation file using the texture. */\n\tauto sprite_path = path / \"assets/test/textures/test_animation.sprite\";\n\tauto sprite_info = renderer::resources::parser::parse_sprite_file(sprite_path);\n\n\tlog::log(INFO << \"Loaded animation \" << sprite_path.resolve_native_path());\n\tlog::log(INFO << \"  texture count: \" << sprite_info.get_texture_count());\n\tfor (size_t i = 0; i < sprite_info.get_texture_count(); ++i) {\n\t\tlog::log(INFO << \"    texture \" << i << \": \"\n\t\t              << sprite_info.get_texture(i)->get_image_path().value().resolve_native_path());\n\t}\n\n\tlog::log(INFO << \"  scalefactor: \" << sprite_info.get_scalefactor());\n\tlog::log(INFO << \"  layer count: \" << sprite_info.get_layer_count());\n\tfor (size_t i = 0; i < sprite_info.get_layer_count(); ++i) {\n\t\tauto layer_info = sprite_info.get_layer(i);\n\t\tlog::log(INFO << \"    layer \" << i << \":\");\n\t\tlog::log(INFO << \"      position: \" << layer_info.get_position());\n\t\tlog::log(INFO << \"      time per frame: \" << layer_info.get_time_per_frame());\n\t\tlog::log(INFO << \"      replay delay: \" << layer_info.get_replay_delay());\n\t\tlog::log(INFO << \"      angle count: \" << layer_info.get_angle_count());\n\n\t\tfor (size_t i = 0; i < layer_info.get_angle_count(); ++i) {\n\t\t\tauto angle_info = layer_info.get_angle(i);\n\t\t\tlog::log(INFO << \"      angle \" << i << \":\");\n\t\t\tlog::log(INFO << \"        degree: \" << angle_info->get_angle_start());\n\t\t\tlog::log(INFO << \"        frame count: \" << angle_info->get_frame_count());\n\t\t\tlog::log(INFO << \"        mirrored: \" << angle_info->is_mirrored());\n\n\t\t\tif (angle_info->is_mirrored()) {\n\t\t\t\tlog::log(INFO << \"        mirrored angle: \"\n\t\t\t\t              << angle_info->get_mirrored_angle().get()->get_angle_start());\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfor (size_t i = 0; i < angle_info->get_frame_count(); ++i) {\n\t\t\t\t\tauto frame_info = angle_info->get_frame(i);\n\t\t\t\t\tlog::log(INFO << \"        frame \" << i << \":\");\n\t\t\t\t\tlog::log(INFO << \"          texture idx: \" << frame_info->get_texture_idx());\n\t\t\t\t\tlog::log(INFO << \"          subtexture idx: \" << frame_info->get_subtexture_idx());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/* Display the subtextures using the meta information */\n\tlog::log(INFO << \"Loading shaders...\");\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for individual objects in pass 1. */\n\tauto obj_vshader_file = (shaderdir / \"demo_2_obj.vert.glsl\").open();\n\tauto obj_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_vshader_file.read());\n\tobj_vshader_file.close();\n\n\tauto obj_fshader_file = (shaderdir / \"demo_2_obj.frag.glsl\").open();\n\tauto obj_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_fshader_file.read());\n\tobj_fshader_file.close();\n\n\t/* Shader for copying the framebuffer in pass 2. */\n\tauto display_vshader_file = (shaderdir / \"demo_2_display.vert.glsl\").open();\n\tauto display_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file.read());\n\tdisplay_vshader_file.close();\n\n\tauto display_fshader_file = (shaderdir / \"demo_2_display.frag.glsl\").open();\n\tauto display_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file.read());\n\tdisplay_fshader_file.close();\n\n\tauto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tauto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n\n\tauto window_size = window.get_size();\n\n\t/* Load texture image using the metafile. */\n\tlog::log(INFO << \"Loading texture image...\");\n\tauto tex = resources::Texture2dData(*sprite_info.get_texture(0));\n\tauto gltex = renderer->add_texture(tex);\n\n\t/* Read location of the subtexture in the texture image */\n\tsize_t subtexture_index = 0;\n\tauto subtex_size = tex.get_info().get_subtex_info(subtexture_index).get_size();\n\tauto [s_left, s_right, s_top, s_bottom] = tex.get_info().get_subtexture_coordinates(subtexture_index);\n\tEigen::Vector4f subtex_coords{s_left, s_right, s_top, s_bottom};\n\n\t/* Upscale subtexture for better visibility */\n\tauto tex_size = tex_info.get_size();\n\tunsigned char upscale_factor = 3;\n\tfloat scale_x = upscale_factor * (float)subtex_size[1] / tex_size.first;\n\tfloat scale_y = upscale_factor * (float)subtex_size[0] / tex_size.second;\n\tauto transform1 = Eigen::Affine3f::Identity();\n\ttransform1.prescale(Eigen::Vector3f(scale_y,\n\t                                    scale_x,\n\t                                    1.0f));\n\n\t/* Pass uniforms to the shaders.\n\t    mv          : The upscaling matrix\n\t    offset_tile : Subtexture coordinates (as floats relative to texture image size)\n\t    u_id        : Identifier\n\t    tex         : OpenGL texture reference\n\t*/\n\tauto obj1_unifs = obj_shader->new_uniform_input(\n\t\t\"mv\",\n\t\ttransform1.matrix(),\n\t\t\"offset_tile\",\n\t\tsubtex_coords,\n\t\t\"u_id\",\n\t\t1u,\n\t\t\"tex\",\n\t\tgltex);\n\n\t/* The objects are using built-in quadrilateral geometry. */\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tRenderable obj1{\n\t\tobj1_unifs,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\t/* Make a framebuffer for the first render pass to draw into. The framebuffer consists of a color texture\n\t * to be copied onto the back buffer in pass 2, as well as an id texture which will contain the object ids\n\t * which we can later read in order to determine which object was clicked. The depth texture is required,\n\t * but mostly irrelevant in this case. */\n\tauto color_texture = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                    window_size[1],\n\t                                                                    resources::pixel_format::rgba8));\n\tauto id_texture = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                 window_size[1],\n\t                                                                 resources::pixel_format::r32ui));\n\tauto depth_texture = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                    window_size[1],\n\t                                                                    resources::pixel_format::depth24));\n\tauto fbo = renderer->create_texture_target({color_texture, id_texture, depth_texture});\n\n\t/* Make an object to update the projection matrix in pass 1 according to changes in the screen size.\n\t * Because uniform values are preserved across objects using the same shader in a single render pass,\n\t * it is sufficient to set it once at the beginning of the pass. To do this, we use an object with no\n\t * geometry, which will set the uniform but not render anything. */\n\tauto proj_unif = obj_shader->new_uniform_input();\n\tRenderable proj_update{\n\t\tproj_unif,\n\t\tnullptr,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass1 = renderer->add_render_pass(\n\t\t{proj_update, obj1},\n\t\tfbo);\n\n\t/* Make an object encompassing the entire screen for the second render pass. The object\n\t * will be textured with the color output of pass 1, effectively copying its framebuffer. */\n\tauto color_texture_unif = display_shader->new_uniform_input(\"color_texture\", color_texture);\n\tRenderable display_obj{\n\t\tcolor_texture_unif,\n\t\tquad,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target());\n\n\tif (not check_uniform_completeness({proj_update, obj1, display_obj})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\t/* Data retrieved from the object index texture. */\n\tresources::Texture2dData id_texture_data = id_texture->into_data();\n\tbool texture_data_valid = false;\n\n\t// TODO(Vtec234): move these into the renderer\n\tglDepthFunc(GL_LEQUAL);\n\tglDepthRange(0.0, 1.0);\n\tglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n\n\t/* Register callbacks */\n\tlog::log(INFO << \"Register callbacks...\");\n\n\twindow.add_mouse_button_callback([&](const QMouseEvent &ev) {\n\t\tif (ev.type() == QEvent::MouseButtonRelease) {\n\t\t\tauto qpos = ev.position();\n\t\t\tssize_t x = qpos.x();\n\t\t\tssize_t y = qpos.y();\n\n\t\t\tlog::log(INFO << \"Clicked at location (\" << x << \", \" << y << \")\");\n\t\t\tif (not texture_data_valid) {\n\t\t\t\tid_texture_data = id_texture->into_data();\n\t\t\t\ttexture_data_valid = true;\n\t\t\t}\n\t\t\tauto id = id_texture_data.read_pixel<uint32_t>(x, y);\n\t\t\tlog::log(INFO << \"Id-texture-value at location: \" << id);\n\t\t\tif (id == 0) {\n\t\t\t\tlog::log(INFO << \"Clicked at non existent object\");\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog::log(INFO << \"Object number \" << id << \" clicked.\");\n\t\t\t}\n\t\t}\n\t});\n\n\twindow.add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\t/* Calculate a projection matrix for the new screen size. */\n\t\tfloat aspectRatio = float(w) / float(h);\n\t\tfloat xScale = 1.0 / aspectRatio;\n\n\t\tEigen::Matrix4f pmat;\n\t\tpmat << xScale, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1;\n\n\t\tproj_unif->update(\"proj\", pmat);\n\n\t\t/* Create a new FBO with the right dimensions. */\n\t\tcolor_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::rgba8));\n\t\tid_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::r32ui));\n\t\tdepth_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::depth24));\n\t\tfbo = renderer->create_texture_target({color_texture, id_texture, depth_texture});\n\n\t\tcolor_texture_unif->update(\"color_texture\", color_texture);\n\n\t\ttexture_data_valid = false;\n\t\tpass1->set_target(fbo);\n\t});\n\n\t/* Iterate through subtextures with left/right arrows */\n\twindow.add_key_callback([&](const QKeyEvent &ev) {\n\t\tif (ev.type() == QEvent::KeyRelease) {\n\t\t\tif (ev.key() == Qt::Key_Right) {\n\t\t\t\tlog::log(INFO << \"Key pressed (Right arrow)\");\n\n\t\t\t\t++subtexture_index;\n\t\t\t\tif (subtexture_index >= tex.get_info().get_subtex_count()) {\n\t\t\t\t\tsubtexture_index = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (ev.key() == Qt::Key_Left) {\n\t\t\t\tlog::log(INFO << \"Key pressed (Left arrow)\");\n\t\t\t\tif (subtexture_index == 0) {\n\t\t\t\t\tsubtexture_index = tex.get_info().get_subtex_count() - 1;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t--subtexture_index;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog::log(INFO << \"Selected subtexture: \" << subtexture_index);\n\n\t\t\t/* Rescale the transformation matrix. */\n\t\t\ttex_size = tex_info.get_size();\n\t\t\tsubtex_size = tex.get_info().get_subtex_info(subtexture_index).get_size();\n\t\t\tscale_x = upscale_factor * (float)subtex_size[1] / tex_size.first;\n\t\t\tscale_y = upscale_factor * (float)subtex_size[0] / tex_size.second;\n\n\t\t\ttransform1 = Eigen::Affine3f::Identity();\n\t\t\ttransform1.prescale(Eigen::Vector3f(scale_y,\n\t\t\t                                    scale_x,\n\t\t\t                                    1.0f));\n\n\t\t\tobj1_unifs->update(\"mv\", transform1.matrix());\n\n\t\t\t/* Pass the new subtexture coordinates. */\n\t\t\tauto [s_left, s_right, s_top, s_bottom] = tex.get_info().get_subtexture_coordinates(subtexture_index);\n\t\t\tEigen::Vector4f subtex_coords{s_left, s_right, s_top, s_bottom};\n\n\t\t\tobj1_unifs->update(\"offset_tile\", subtex_coords);\n\t\t}\n\t});\n\tlog::log(INFO << \"Success!\");\n\n\tlog::log(INFO << \"Instructions:\");\n\tlog::log(INFO << \"  1. Click on the texture pixels with LEFT MOUSE BUTTON to get the object ID\");\n\tlog::log(INFO << \"  2. Press LEFT/RIGHT ARROW to cycle through available subtextures\");\n\n\twhile (not window.should_close()) {\n\t\tqtapp->process_events();\n\n\t\trenderer->render(pass1);\n\t\trenderer->render(pass2);\n\t\twindow.update();\n\n\t\trenderer->check_error();\n\t}\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_2.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Show loading of .sprite and .texture meta information files and display\n * of subtextures from a texture atlas:\n *     - Window creation\n *     - Loading shaders\n *     - Loading resource files (.sprite and .texture)\n *     - Rendering of subtextures from a texture atlas (analogue to demo 1)\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_demo_2(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_3.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_3.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <QKeyEvent>\n\n#include \"coord/tile.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_factory.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/stages/camera/manager.h\"\n#include \"renderer/stages/screen/render_stage.h\"\n#include \"renderer/stages/skybox/render_stage.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"renderer/stages/terrain/render_stage.h\"\n#include \"renderer/stages/world/render_entity.h\"\n#include \"renderer/stages/world/render_stage.h\"\n#include \"renderer/uniform_buffer.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_3(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\tauto window = std::make_shared<opengl::GlWindow>(\"openage renderer test\", settings);\n\tauto renderer = window->make_renderer();\n\n\t// Clock required by world renderer for timing animation frames\n\t// (we never advance time in this demo though, so it has no significance)\n\tauto clock = std::make_shared<time::Clock>();\n\n\t// Camera\n\t// our viewport into the game world\n\tauto camera = std::make_shared<renderer::camera::Camera>(renderer, window->get_size());\n\twindow->add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\tcamera->resize(w, h);\n\t});\n\n\t// The camera manager handles camera movement and zooming\n\t// it is updated each frame before the render stages\n\tauto cam_manager = std::make_shared<renderer::camera::CameraManager>(camera);\n\n\t// Set boundaries for camera movement in the scene\n\t// this restricts camera movement to the area defined by the boundaries\n\t// i.e. the map terrain in this case\n\tcam_manager->set_camera_boundaries(\n\t\tcamera::CameraBoundaries{\n\t\t\t12.25f,\n\t\t\t22.25f,\n\t\t\t0.0f,\n\t\t\t20.0f,\n\t\t\t2.25f,\n\t\t\t12.25f});\n\n\t// Render stages\n\t// every stage use a different subrenderer that manages renderables,\n\t// shaders, textures & more.\n\tstd::vector<std::shared_ptr<RenderPass>>\n\t\trender_passes{};\n\n\t// TODO: Make this optional for subrenderers?\n\tauto asset_manager = std::make_shared<renderer::resources::AssetManager>(\n\t\trenderer,\n\t\tpath[\"assets\"][\"test\"]);\n\n\t// Renders the background\n\tauto skybox_renderer = std::make_shared<renderer::skybox::SkyboxRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tskybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color\n\n\t// Renders the terrain in 3D\n\tauto terrain_renderer = std::make_shared<renderer::terrain::TerrainRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Renders units/buildings/other objects\n\tauto world_renderer = std::make_shared<renderer::world::WorldRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Store the render passes of the renderers\n\t// The order is important as its also the order in which they\n\t// are rendered and drawn onto the screen.\n\trender_passes.push_back(skybox_renderer->get_render_pass());\n\trender_passes.push_back(terrain_renderer->get_render_pass());\n\trender_passes.push_back(world_renderer->get_render_pass());\n\n\t// Final output on screen has its own subrenderer\n\t// It takes the outputs of all previous render passes\n\t// and blends them together\n\tauto screen_renderer = std::make_shared<renderer::screen::ScreenRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\tfor (auto &pass : render_passes) {\n\t\ttargets.push_back(pass->get_target());\n\t}\n\tscreen_renderer->set_render_targets(targets);\n\n\trender_passes.push_back(screen_renderer->get_render_pass());\n\n\twindow->add_resize_callback([&](size_t, size_t, double /*scale*/) {\n\t\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\t\tfor (size_t i = 0; i < render_passes.size() - 1; ++i) {\n\t\t\ttargets.push_back(render_passes[i]->get_target());\n\t\t}\n\t\tscreen_renderer->set_render_targets(targets);\n\t});\n\n\t// Create some entities to populate the scene\n\tauto render_factory = std::make_shared<RenderFactory>(terrain_renderer, world_renderer);\n\n\t// Fill a 10x10 terrain grid with height values\n\tauto terrain_size = util::Vector2s{10, 10};\n\tstd::vector<std::pair<terrain::RenderEntity::terrain_elevation_t, std::string>> tiles{};\n\ttiles.reserve(terrain_size[0] * terrain_size[1]);\n\tfor (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) {\n\t\ttiles.emplace_back(0.0f, \"./textures/test_terrain.terrain\");\n\t}\n\n\t// Create entity for terrain rendering\n\tauto terrain0 = render_factory->add_terrain_render_entity(terrain_size,\n\t                                                          coord::tile_delta{0, 0});\n\n\t// Create \"test bumps\" in the terrain to check if rendering works\n\ttiles[11].first = 1.0f;\n\ttiles[23].first = 2.3f;\n\ttiles[42].first = 4.2f;\n\ttiles[69].first = 6.9f; // nice\n\n\t// A hill\n\ttiles[55].first = 3.0f; // center\n\ttiles[45].first = 2.0f; // bottom left slope\n\ttiles[35].first = 1.0f;\n\ttiles[56].first = 1.0f; // bottom right slope (little steeper)\n\ttiles[65].first = 2.0f; // top right slope\n\ttiles[75].first = 1.0f;\n\ttiles[54].first = 2.0f; // top left slope\n\ttiles[53].first = 1.0f;\n\n\t// send the terrain data to the terrain renderer\n\tterrain0->update(terrain_size, tiles);\n\n\t// World entities\n\tauto world0 = render_factory->add_world_render_entity();\n\tworld0->update(0, coord::phys3(3.0f, 3.0f, 0.0f), \"./textures/test_gaben.sprite\");\n\n\t// should behind gaben and caught by depth test\n\tauto world1 = render_factory->add_world_render_entity();\n\tworld1->update(1, coord::phys3(4.0f, 1.0f, 0.0f), \"./textures/test_missing.sprite\");\n\n\tauto world2 = render_factory->add_world_render_entity();\n\tworld2->update(2, coord::phys3(1.0f, 3.0f, 0.0f), \"./textures/test_missing.sprite\");\n\n\t// Zoom in/out with mouse wheel\n\twindow->add_mouse_wheel_callback([&](const QWheelEvent &ev) {\n\t\tauto delta = ev.angleDelta().y() / 120;\n\n\t\t// zoom_frame updates the camera zoom level in the next drawn frame\n\t\tif (delta < 0) {\n\t\t\tcam_manager->zoom_frame(renderer::camera::ZoomDirection::OUT, 0.05f);\n\t\t}\n\t\telse if (delta > 0) {\n\t\t\tcam_manager->zoom_frame(renderer::camera::ZoomDirection::IN, 0.05f);\n\t\t}\n\t});\n\n\t// Move around the scene with WASD\n\twindow->add_key_callback([&](const QKeyEvent &ev) {\n\t\tif (ev.type() == QEvent::KeyPress) {\n\t\t\tauto key = ev.key();\n\n\t\t\t// move_frame moves the camera in the specified direction in the next drawn frame\n\t\t\tswitch (key) {\n\t\t\tcase Qt::Key_W: { // forward\n\t\t\t\tcam_manager->move_frame(renderer::camera::MoveDirection::FORWARD, 0.5f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_A: { // left\n\t\t\t\tcam_manager->move_frame(renderer::camera::MoveDirection::LEFT, 0.5f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_S: { // back\n\t\t\t\tcam_manager->move_frame(renderer::camera::MoveDirection::BACKWARD, 0.5f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_D: { // right\n\t\t\t\tcam_manager->move_frame(renderer::camera::MoveDirection::RIGHT, 0.5f);\n\t\t\t} break;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t});\n\n\tlog::log(INFO << \"Instructions:\");\n\tlog::log(INFO << \"  1. Move the camera with WASD\");\n\tlog::log(INFO << \"  2. Zoom in and out with MOUSE WHEEL\");\n\n\twhile (not window->should_close()) {\n\t\tqtapp->process_events();\n\n\t\t// update the camera matrices\n\t\tcam_manager->update();\n\n\t\t// Update the renderables of the subrenderers\n\t\t// camera zoom/position changes are also handled in here\n\t\tterrain_renderer->update();\n\t\tworld_renderer->update();\n\n\t\t// Draw everything\n\t\tfor (auto &pass : render_passes) {\n\t\t\trenderer->render(pass);\n\t\t}\n\n\t\trenderer->check_error();\n\n\t\t// Display final output on screen\n\t\twindow->update();\n\t}\n\twindow->close();\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_3.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Show off the render stages in the level 2 renderer and the camera\n * system.\n *     - Window creation\n *     - Creating a camera\n *     - Initializing the level 2 render stages: skybox, terrain, world, screen\n *     - Adding renderables to the render stages via the render factory\n *     - Moving camera with mouse/keyboard callbacks\n *\n * @param path Path to the project rootdir.\n */\nvoid renderer_demo_3(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_4.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_4.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <QKeyEvent>\n\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/frame_timing.h\"\n#include \"renderer/resources/parser/parse_sprite.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/shader_program.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::renderer::tests {\nvoid renderer_demo_4(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\topengl::GlWindow window(\"openage renderer test\", settings);\n\tauto renderer = window.make_renderer();\n\n\t/* Clock for timed display */\n\tauto clock = time::Clock();\n\n\t/* Controls whether animations are use \"real\" time for frame timings (if true)\n\t   or the actual simulation time (if false). When simulation time is used,\n\t   the animation speed increases with the clock speed.*/\n\tbool real_time_animation = true;\n\n\t/* Load animation file */\n\tauto sprite_path = path / \"assets/test/textures/test_animation.sprite\";\n\tauto sprite_info = renderer::resources::parser::parse_sprite_file(sprite_path);\n\n\t/* Display the subtextures using the meta information */\n\tlog::log(INFO << \"Loading shaders...\");\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for individual objects in pass 1. */\n\tauto obj_vshader_file = (shaderdir / \"demo_4_obj.vert.glsl\").open();\n\tauto obj_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_vshader_file.read());\n\tobj_vshader_file.close();\n\n\tauto obj_fshader_file = (shaderdir / \"demo_4_obj.frag.glsl\").open();\n\tauto obj_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_fshader_file.read());\n\tobj_fshader_file.close();\n\n\t/* Shader for copying the framebuffer in pass 2. */\n\tauto display_vshader_file = (shaderdir / \"demo_4_display.vert.glsl\").open();\n\tauto display_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file.read());\n\tdisplay_vshader_file.close();\n\n\tauto display_fshader_file = (shaderdir / \"demo_4_display.frag.glsl\").open();\n\tauto display_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file.read());\n\tdisplay_fshader_file.close();\n\n\tauto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tauto display_shader = renderer->add_shader({display_vshader_src, display_fshader_src});\n\n\tauto window_size = window.get_size();\n\n\t/* Load texture image using the metafile. */\n\tlog::log(INFO << \"Loading texture image...\");\n\tauto tex = resources::Texture2dData(*sprite_info.get_texture(0));\n\tauto gltex = renderer->add_texture(tex);\n\n\t/* Read location of the first subtexture in the texture image */\n\tsize_t subtexture_index = 0;\n\tauto subtex_size = tex.get_info().get_subtex_info(subtexture_index).get_size();\n\tauto [s_left, s_right, s_top, s_bottom] = tex.get_info().get_subtexture_coordinates(subtexture_index);\n\tEigen::Vector4f subtex_coords{s_left, s_right, s_top, s_bottom};\n\n\t/* Upscale subtexture for better visibility */\n\tauto tex_size = sprite_info.get_texture(0)->get_size();\n\tunsigned char upscale_factor = 3;\n\tfloat scale_x = upscale_factor * (float)subtex_size[1] / tex_size.first;\n\tfloat scale_y = upscale_factor * (float)subtex_size[0] / tex_size.second;\n\tauto transform1 = Eigen::Affine3f::Identity();\n\ttransform1.prescale(Eigen::Vector3f(scale_y,\n\t                                    scale_x,\n\t                                    1.0f));\n\n\t/* Pass uniforms to the shaders.\n\t    mv          : The upscaling matrix\n\t    offset_tile : Subtexture coordinates (as floats relative to texture image size)\n\t    u_id        : Identifier\n\t    tex         : OpenGL texture reference\n\t*/\n\tauto obj1_unifs = obj_shader->new_uniform_input(\n\t\t\"mv\",\n\t\ttransform1.matrix(),\n\t\t\"offset_tile\",\n\t\tsubtex_coords,\n\t\t\"u_id\",\n\t\t1u,\n\t\t\"tex\",\n\t\tgltex);\n\n\t/* The objects are using built-in quadrilateral geometry. */\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tRenderable obj1{\n\t\tobj1_unifs,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\t/* Make a framebuffer for the first render pass to draw into. The framebuffer consists of a color texture\n\t * to be copied onto the back buffer in pass 2, as well as an id texture which will contain the object ids\n\t * which we can later read in order to determine which object was clicked. The depth texture is required,\n\t * but mostly irrelevant in this case. */\n\tauto color_texture = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                    window_size[1],\n\t                                                                    resources::pixel_format::rgba8));\n\tauto fbo = renderer->create_texture_target({color_texture});\n\n\t/* Make an object to update the projection matrix in pass 1 according to changes in the screen size.\n\t * Because uniform values are preserved across objects using the same shader in a single render pass,\n\t * it is sufficient to set it once at the beginning of the pass. To do this, we use an object with no\n\t * geometry, which will set the uniform but not render anything. */\n\tauto proj_unif = obj_shader->new_uniform_input();\n\tRenderable proj_update{\n\t\tproj_unif,\n\t\tnullptr,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass1 = renderer->add_render_pass(\n\t\t{proj_update, obj1},\n\t\tfbo);\n\n\t/* Make an object encompassing the entire screen for the second render pass. The object\n\t * will be textured with the color output of pass 1, effectively copying its framebuffer. */\n\tauto color_texture_unif = display_shader->new_uniform_input(\"color_texture\", color_texture);\n\tRenderable display_obj{\n\t\tcolor_texture_unif,\n\t\tquad,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto pass2 = renderer->add_render_pass({display_obj}, renderer->get_display_target());\n\n\tif (not check_uniform_completeness({proj_update, obj1, display_obj})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\twindow.add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\t/* Calculate a projection matrix for the new screen size. */\n\t\tfloat aspectRatio = float(w) / float(h);\n\t\tfloat xScale = 1.0 / aspectRatio;\n\n\t\tEigen::Matrix4f pmat;\n\t\tpmat << xScale, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, 1, 0,\n\t\t\t0, 0, 0, 1;\n\n\t\tproj_unif->update(\"proj\", pmat);\n\n\t\t/* Create a new FBO with the right dimensions. */\n\t\tcolor_texture = renderer->add_texture(resources::Texture2dInfo(w, h, resources::pixel_format::rgba8));\n\t\tfbo = renderer->create_texture_target({color_texture});\n\n\t\tcolor_texture_unif->update(\"color_texture\", color_texture);\n\n\t\tpass1->set_target(fbo);\n\t});\n\n\t// Control simulation clock\n\twindow.add_key_callback([&](const QKeyEvent &ev) {\n\t\tif (ev.type() == QEvent::KeyRelease) {\n\t\t\tauto key = ev.key();\n\n\t\t\tswitch (key) {\n\t\t\tcase Qt::Key_Space: {\n\t\t\t\tif (clock.get_state() == time::ClockState::RUNNING) {\n\t\t\t\t\tclock.pause();\n\t\t\t\t\tlog::log(INFO << \"Stopped simulation at \" << clock.get_time() << \" (real = \" << clock.get_real_time() << \")\");\n\t\t\t\t}\n\t\t\t\telse if (clock.get_state() == time::ClockState::PAUSED) {\n\t\t\t\t\tclock.resume();\n\t\t\t\t\tlog::log(INFO << \"Resumed simulation at \" << clock.get_time() << \" (real = \" << clock.get_real_time() << \")\");\n\t\t\t\t}\n\t\t\t} break;\n\t\t\tcase Qt::Key_Return: {\n\t\t\t\treal_time_animation = not real_time_animation;\n\t\t\t\tif (real_time_animation) {\n\t\t\t\t\tlog::log(INFO << \"Animation speed switched to REAL time\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tlog::log(INFO << \"Animation speed switched to SIMULATION time\");\n\t\t\t\t}\n\t\t\t} break;\n\t\t\tcase Qt::Key_Minus: {\n\t\t\t\tclock.set_speed(clock.get_speed() - 0.5);\n\t\t\t\tlog::log(INFO << \"Decreased clock speed to: \" << clock.get_speed());\n\t\t\t} break;\n\t\t\tcase Qt::Key_Plus: {\n\t\t\t\tclock.set_speed(clock.get_speed() + 0.5);\n\t\t\t\tlog::log(INFO << \"Increased clock speed to: \" << clock.get_speed());\n\t\t\t} break;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t});\n\n\tlog::log(INFO << \"Instructions:\");\n\tlog::log(INFO << \"  1. Press SPACE to pause/resume simulation clock\");\n\tlog::log(INFO << \"  2. Press PLUS and MINUS keys to increase/decrease simulation speed\");\n\tlog::log(INFO << \"  3. Press RETURN to toggle between real time and simulation time for animation speed\");\n\n\t/* Get layer and angle that we want to display. For this texture, there is only\n\t   one layer and one angle. */\n\tsize_t frame_idx = 0;\n\tauto layer = sprite_info.get_layer(0);\n\tauto angle = layer.get_angle(0);\n\tauto timing = layer.get_frame_timing();\n\n\tclock.start();\n\twhile (not window.should_close()) {\n\t\tqtapp->process_events();\n\n\t\t/* Check simulation time to see if we have to switch to the next frame. */\n\t\tclock.update_time();\n\t\tsize_t timed_frame_idx = 0;\n\t\tif (real_time_animation) {\n\t\t\t// use real time for animation speed\n\t\t\ttimed_frame_idx = timing->get_frame(clock.get_real_time(), 0);\n\t\t}\n\t\telse {\n\t\t\t// use simulation time for animation speed (which is potentially sped up/slowed down)\n\t\t\ttimed_frame_idx = timing->get_frame(clock.get_time(), 0);\n\t\t}\n\n\t\t/* Update uniforms if new frame should be displayed */\n\t\tif (timed_frame_idx != frame_idx) {\n\t\t\tframe_idx = timed_frame_idx;\n\n\t\t\t/* Get index of texture and subtexture where the frame's pixels are located */\n\t\t\tauto frame_info = angle->get_frame(frame_idx);\n\t\t\t// auto tex_idx = frame_info->get_texture_idx();  // already loaded as 'tex'\n\t\t\tauto subtex_idx = frame_info->get_subtexture_idx();\n\n\t\t\t/* Rescale the transformation matrix. */\n\t\t\ttex_size = tex.get_info().get_size();\n\t\t\tsubtex_size = tex.get_info().get_subtex_info(subtex_idx).get_size();\n\t\t\tscale_x = upscale_factor * (float)subtex_size[1] / tex_size.first;\n\t\t\tscale_y = upscale_factor * (float)subtex_size[0] / tex_size.second;\n\n\t\t\ttransform1 = Eigen::Affine3f::Identity();\n\t\t\ttransform1.prescale(Eigen::Vector3f(scale_y,\n\t\t\t                                    scale_x,\n\t\t\t                                    1.0f));\n\n\t\t\tobj1_unifs->update(\"mv\", transform1.matrix());\n\n\t\t\t/* Pass the new subtexture coordinates. */\n\t\t\tauto [s_left, s_right, s_top, s_bottom] = tex.get_info().get_subtexture_coordinates(subtex_idx);\n\t\t\tEigen::Vector4f subtex_coords{s_left, s_right, s_top, s_bottom};\n\n\t\t\tobj1_unifs->update(\"offset_tile\", subtex_coords);\n\n\t\t\t/* Log subtex index and time */\n\t\t\tlog::log(INFO << \"Switch to subtex: \" << timed_frame_idx);\n\t\t\tlog::log(INFO << \"Time: \" << clock.get_time() << \" (real = \" << clock.get_real_time() << \")\");\n\t\t}\n\n\t\trenderer->render(pass1);\n\t\trenderer->render(pass2);\n\t\twindow.update();\n\n\t\trenderer->check_error();\n\t}\n\twindow.close();\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_4.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Show the timing of animation keyframes with a simulation clock.\n *     - Window creation\n *     - Loading shaders\n *     - Creating the simulation clock\n *     - Loading .sprite and .texture files and accessing subtextures\n *     - Update of animation keyframes via shader uniforms\n *     - Switching to correct animation keyframe by using the clock.\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_demo_4(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_5.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_5.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <epoxy/gl.h>\n#include <QMouseEvent>\n\n#include \"renderer/camera/camera.h\"\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/buffer_info.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/uniform_buffer.h\"\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_5(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\topengl::GlWindow window(\"openage renderer test\", settings);\n\tauto renderer = window.make_renderer();\n\tauto size = window.get_size();\n\n\t// Camera setup\n\tauto camera = std::make_shared<renderer::camera::Camera>(renderer, size);\n\twindow.add_resize_callback([&](size_t w, size_t h, double /*scale*/) {\n\t\tcamera->resize(w, h);\n\t});\n\n\t/* Display the subtextures using the meta information */\n\tlog::log(INFO << \"Loading shaders...\");\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for individual objects in pass 1. */\n\tauto obj_vshader_file = (shaderdir / \"demo_5_obj.vert.glsl\").open();\n\tauto obj_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_vshader_file.read());\n\tobj_vshader_file.close();\n\n\tauto obj_fshader_file = (shaderdir / \"demo_5_obj.frag.glsl\").open();\n\tauto obj_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_fshader_file.read());\n\tobj_fshader_file.close();\n\n\tauto obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\n\t// object texture\n\tauto tex = resources::Texture2dData(path / \"/assets/test/textures/test_terrain.png\");\n\tauto gltex = renderer->add_texture(tex);\n\n\t// object vertices\n\t// in the scene this is just a very large square that gets rendered like\n\t// a diamond terrain shape\n\t// clang-format off\n\tstd::vector<float> verts{\n\t\t0.0f,   0.0f,   0.0f, -1.0f, -1.0f, // bottom left\n\t\t-10.0f, 0.0f,   0.0f, -1.0f,  1.0f, // bottom right\n\t\t0.0f,   0.0f, -10.0f,  1.0f, -1.0f, // top left\n\t\t-10.0f, 0.0f, -10.0f,  1.0f,  1.0f, // top right\n\t};\n\t// clang-format on\n\n\t// create the mesh info\n\tresources::VertexInputInfo info{\n\t\t{resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32},\n\t\tresources::vertex_layout_t::AOS,\n\t\tresources::vertex_primitive_t::TRIANGLE_STRIP};\n\n\tauto const vert_data_size = verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data(vert_data_size);\n\tstd::memcpy(vert_data.data(), verts.data(), vert_data_size);\n\n\tresources::MeshData meshdata{std::move(vert_data), info};\n\n\t// create a geometry object from the mesh data\n\tauto geometry = renderer->add_mesh_geometry(meshdata);\n\n\t// create a uniform storage for the shader\n\tauto transform_unifs = obj_shader->create_empty_input();\n\n\t// create the renderable object from the geometry and the uniforms\n\tRenderable terrain_obj{\n\t\ttransform_unifs,\n\t\tgeometry,\n\t};\n\n\t// create the render pass\n\t// this directly renders outputs the rendered object to the screen\n\tauto pass = renderer->add_render_pass({terrain_obj}, renderer->get_display_target());\n\n\t// create the uniform buffer for the camera matrices\n\t// and bind it to the shader\n\t// this sets the binding point of the shader to the bindng point of the buffer\n\tresources::UBOInput view_input{\"view\", resources::ubo_input_t::M4F32};\n\tresources::UBOInput proj_input{\"proj\", resources::ubo_input_t::M4F32};\n\tauto ubo_info = resources::UniformBufferInfo{resources::ubo_layout_t::STD140, {view_input, proj_input}};\n\tauto uniform_buffer = renderer->add_uniform_buffer(ubo_info);\n\tobj_shader->bind_uniform_buffer(\"cam\", uniform_buffer);\n\n\t// create a uniform buffer storage\n\t// pass the camera view and projection matrices to the buffer storage\n\t// this does not update the buffer yet, it only prepares the data before the upload\n\tauto unif_in = uniform_buffer->new_uniform_input(\n\t\t\"view\",\n\t\tcamera->get_view_matrix(),\n\t\t\"proj\",\n\t\tcamera->get_projection_matrix());\n\n\t// pass the uniform storage to the buffer object\n\t// this uploads the updates to the buffer on the GPU\n\tuniform_buffer->update_uniforms(unif_in);\n\n\t// set the non-buffer uniforma for the object\n\tEigen::Matrix4f model = Eigen::Matrix4f::Identity();\n\ttransform_unifs->update(\n\t\t\"model\",\n\t\tmodel,\n\t\t\"tex\",\n\t\tgltex);\n\n\tif (not check_uniform_completeness({terrain_obj})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\t// Move around the scene with WASD\n\twindow.add_key_callback([&](const QKeyEvent &ev) {\n\t\tbool cam_update = false;\n\t\tif (ev.type() == QEvent::KeyPress) {\n\t\t\tauto key = ev.key();\n\n\t\t\tswitch (key) {\n\t\t\tcase Qt::Key_W: { // forward\n\t\t\t\tcamera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.5f);\n\t\t\t\tcam_update = true;\n\n\t\t\t\tlog::log(INFO << \"Camera moved forward.\");\n\t\t\t} break;\n\t\t\tcase Qt::Key_A: { // left\n\t\t\t\t// half the speed because the relationship between forward/back and\n\t\t\t\t// left/right is 1:2 in our ortho projection.\n\t\t\t\tcamera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.25f);\n\t\t\t\tcam_update = true;\n\n\t\t\t\tlog::log(INFO << \"Camera moved left.\");\n\t\t\t} break;\n\t\t\tcase Qt::Key_S: { // back\n\t\t\t\tcamera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.5f);\n\t\t\t\tcam_update = true;\n\n\t\t\t\tlog::log(INFO << \"Camera moved back.\");\n\t\t\t} break;\n\t\t\tcase Qt::Key_D: { // right\n\t\t\t\t// half the speed because the relationship between forward/back and\n\t\t\t\t// left/right is 1:2 in our ortho projection.\n\t\t\t\tcamera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.25f);\n\t\t\t\tcam_update = true;\n\n\t\t\t\tlog::log(INFO << \"Camera moved right.\");\n\t\t\t} break;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (cam_update) {\n\t\t\t// update the uniform buffer only if the camera has moved\n\t\t\tunif_in->update(\"view\", camera->get_view_matrix());\n\t\t\tuniform_buffer->update_uniforms(unif_in);\n\n\t\t\tlog::log(INFO << \"Uniform buffer updated.\");\n\t\t}\n\t});\n\n\twhile (not window.should_close()) {\n\t\tqtapp->process_events();\n\n\t\trenderer->render(pass);\n\t\twindow.update();\n\n\t\trenderer->check_error();\n\t}\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_5.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Show the usage of uniform buffers in the renderer.\n *     - Window creation\n *     - Loading shaders\n *     - Vertex data and mesh creation\n *     - Uniform buffer assignment\n *     - Uniform buffer update\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_demo_5(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_6.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"demo_6.h\"\n\n#include <QKeyEvent>\n\n#include \"curve/continuous.h\"\n#include \"curve/segmented.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/camera/frustum_2d.h\"\n#include \"renderer/camera/frustum_3d.h\"\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/animation/layer_info.h\"\n#include \"renderer/resources/parser/parse_sprite.h\"\n#include \"renderer/resources/parser/parse_terrain.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/texture.h\"\n#include \"renderer/uniform_buffer.h\"\n#include \"time/clock.h\"\n#include \"util/path.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_6(const util::Path &path) {\n\tauto render_mgr = RenderManagerDemo6{path};\n\n\t// Create render objects\n\tauto renderables_2d = render_mgr.create_2d_obj();\n\tauto renderables_3d = render_mgr.create_3d_obj();\n\tauto renderables_frame = render_mgr.create_frame_obj();\n\n\t// Add objects to the render passes\n\trender_mgr.obj_2d_pass->add_renderables(std::move(renderables_2d));\n\trender_mgr.obj_3d_pass->add_renderables({renderables_3d});\n\trender_mgr.frame_pass->add_renderables(std::move(renderables_frame));\n\n\t// Move the camera with the WASD keys\n\t// This is where we will also update the frustum for the 2D objects\n\trender_mgr.window->add_key_callback([&](const QKeyEvent &ev) {\n\t\tif (ev.type() == QEvent::KeyPress) {\n\t\t\tauto key = ev.key();\n\n\t\t\t// move_frame moves the camera in the specified direction in the next drawn frame\n\t\t\tswitch (key) {\n\t\t\tcase Qt::Key_W: { // forward\n\t\t\t\trender_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), 0.2f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_A: { // left\n\t\t\t\trender_mgr.camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), 0.1f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_S: { // back\n\t\t\t\trender_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), 0.2f);\n\t\t\t} break;\n\t\t\tcase Qt::Key_D: { // right\n\t\t\t\trender_mgr.camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), 0.1f);\n\t\t\t} break;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Update the camera uniform buffer\n\t\t\tauto new_cam_unifs = render_mgr.camera->get_uniform_buffer()->new_uniform_input(\n\t\t\t\t\"view\",\n\t\t\t\trender_mgr.camera->get_view_matrix(),\n\t\t\t\t\"proj\",\n\t\t\t\trender_mgr.camera->get_projection_matrix());\n\t\t\trender_mgr.camera->get_uniform_buffer()->update_uniforms(new_cam_unifs);\n\n\t\t\t/* Generate the frustum for the 2D objects */\n\n\t\t\t// Copy the camera used for drawing\n\t\t\t//\n\t\t\t// In this demo, we will manipulate the frustum camera\n\t\t\t// to create a slightly smaller frustum for the 2D objects\n\t\t\t// to show the effect of frustum culling.\n\t\t\t//\n\t\t\t// In the real renderer, the normal camera would be used.\n\t\t\tauto frustum_camera = *render_mgr.camera;\n\n\t\t\t// Downsize the frustum to 70% of the camera size\n\t\t\tfloat frustum_factor = 0.7f;\n\t\t\tauto frustum_cam_size = util::Vector2s{render_mgr.camera->get_viewport_size()[0] * frustum_factor,\n\t\t\t                                       render_mgr.camera->get_viewport_size()[1] * frustum_factor};\n\t\t\tfrustum_camera.resize(frustum_cam_size[0], frustum_cam_size[1]);\n\n\t\t\t// Get a 2D frustum object\n\t\t\tauto frustum_2d = frustum_camera.get_frustum_2d();\n\t\t\tfrustum_2d.update(frustum_camera.get_viewport_size(),\n\t\t\t                  frustum_camera.get_view_matrix(),\n\t\t\t                  frustum_camera.get_projection_matrix(),\n\t\t\t                  frustum_camera.get_zoom());\n\n\t\t\t// Check if the 2D scene objects are in the frustum\n\t\t\t// and update the renderables in the 2D render pass\n\t\t\tauto renderables_2d = render_mgr.create_2d_obj();\n\t\t\tstd::vector<renderer::Renderable> renderables_in_frustum{};\n\t\t\tfor (size_t i = 0; i < render_mgr.obj_2d_positions.size(); ++i) {\n\t\t\t\tauto pos = render_mgr.obj_2d_positions.at(i);\n\t\t\t\tbool in_frustum = frustum_2d.in_frustum(pos.to_world_space(),\n\t\t\t\t                                        Eigen::Matrix4f::Identity(),\n\t\t\t\t                                        render_mgr.animation_2d_info.get_scalefactor(),\n\t\t\t\t                                        render_mgr.animation_2d_info.get_max_bounds());\n\t\t\t\tif (in_frustum) {\n\t\t\t\t\t// Only add objects that are in the frustum\n\t\t\t\t\trenderables_in_frustum.push_back(renderables_2d.at(i));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Clear the renderables in the 2D render pass\n\t\t\t// and add ONLY the renderables that are inside the frustum\n\t\t\trender_mgr.obj_2d_pass->clear_renderables();\n\t\t\trender_mgr.obj_2d_pass->add_renderables(std::move(renderables_in_frustum));\n\t\t}\n\t});\n\n\t// Draw everything\n\trender_mgr.run();\n}\n\nRenderManagerDemo6::RenderManagerDemo6(const util::Path &path) :\n\tpath{path} {\n\tthis->setup();\n}\n\nvoid RenderManagerDemo6::run() {\n\twhile (not window->should_close()) {\n\t\tthis->qtapp->process_events();\n\n\t\t// Draw everything\n\t\trenderer->render(this->obj_3d_pass);\n\t\trenderer->render(this->obj_2d_pass);\n\t\trenderer->render(this->frame_pass);\n\t\trenderer->render(this->display_pass);\n\n\t\t// Display final output on screen\n\t\tthis->window->update();\n\t}\n}\n\nconst std::vector<renderer::Renderable> RenderManagerDemo6::create_2d_obj() {\n\tstd::vector<renderer::Renderable> renderables;\n\tfor (size_t i = 0; i < this->obj_2d_positions.size(); ++i) {\n\t\t// Create renderable for 2D animation\n\t\tauto scale = this->animation_2d_info.get_scalefactor();\n\t\tauto angle = this->obj_2d_angles.at(i);\n\t\tauto frame_info = this->animation_2d_info.get_layer(0).get_angle(angle)->get_frame(0);\n\t\tauto tex_id = frame_info->get_texture_idx();\n\t\tauto subtex_id = frame_info->get_subtexture_idx();\n\t\tauto subtex = this->animation_2d_info.get_texture(tex_id)->get_subtex_info(subtex_id);\n\t\tauto subtex_size = subtex.get_size();\n\t\tEigen::Vector2f subtex_size_vec{\n\t\t\tstatic_cast<float>(subtex_size[0]),\n\t\t\tstatic_cast<float>(subtex_size[1])};\n\t\tauto anchor_params = subtex.get_anchor_params();\n\t\tauto anchor_params_vec = Eigen::Vector2f{\n\t\t\tstatic_cast<float>(anchor_params[0]),\n\t\t\tstatic_cast<float>(anchor_params[1])};\n\t\tauto tile_params = subtex.get_subtex_coords();\n\t\tauto scene_pos = this->obj_2d_positions.at(i);\n\t\tauto animation_2d_unifs = this->obj_2d_shader->new_uniform_input(\n\t\t\t\"obj_world_position\",\n\t\t\tscene_pos.to_world_space(),\n\t\t\t\"scale\",\n\t\t\tscale,\n\t\t\t\"subtex_size\",\n\t\t\tsubtex_size_vec,\n\t\t\t\"anchor_offset\",\n\t\t\tanchor_params_vec,\n\t\t\t\"tex\",\n\t\t\tthis->obj_2d_texture,\n\t\t\t\"tile_params\",\n\t\t\ttile_params);\n\t\tauto quad = this->renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\t\tRenderable animation_2d_obj{\n\t\t\tanimation_2d_unifs,\n\t\t\tquad,\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t};\n\n\t\trenderables.push_back(animation_2d_obj);\n\t}\n\n\treturn renderables;\n}\n\nconst renderer::Renderable RenderManagerDemo6::create_3d_obj() {\n\t// Create renderable for terrain\n\tauto terrain_unifs = this->obj_3d_shader->new_uniform_input(\n\t\t\"tex\",\n\t\tthis->obj_3d_texture);\n\n\tstd::vector<coord::scene3> terrain_pos{};\n\tterrain_pos.push_back({-25, -25, 0});\n\tterrain_pos.push_back({25, -25, 0});\n\tterrain_pos.push_back({-25, 25, 0});\n\tterrain_pos.push_back({25, 25, 0});\n\tstd::vector<float> terrain_verts{};\n\tfor (size_t i = 0; i < terrain_pos.size(); ++i) {\n\t\tauto scene_pos = terrain_pos.at(i).to_world_space();\n\t\tterrain_verts.push_back(scene_pos[0]);\n\t\tterrain_verts.push_back(scene_pos[1]);\n\t\tterrain_verts.push_back(scene_pos[2]);\n\t\tterrain_verts.push_back(0.0f + i / 2);\n\t\tterrain_verts.push_back(0.0f + i % 2);\n\t}\n\tauto vert_info = resources::VertexInputInfo{\n\t\t{resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32},\n\t\tresources::vertex_layout_t::AOS,\n\t\tresources::vertex_primitive_t::TRIANGLE_STRIP,\n\t};\n\tstd::vector<uint8_t> vert_data(terrain_verts.size() * sizeof(float));\n\tstd::memcpy(vert_data.data(), terrain_verts.data(), vert_data.size());\n\tauto terrain_mesh = resources::MeshData{std::move(vert_data), vert_info};\n\tauto terrain_geometry = this->renderer->add_mesh_geometry(terrain_mesh);\n\tRenderable terrain_obj{\n\t\tterrain_unifs,\n\t\tterrain_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\treturn terrain_obj;\n}\n\nconst std::vector<renderer::Renderable> RenderManagerDemo6::create_frame_obj() {\n\tstd::vector<renderer::Renderable> renderables;\n\tfor (auto scene_pos : this->obj_2d_positions) {\n\t\t// Create renderables for frame around sprites\n\t\tstd::array<float, 8> frame_verts{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f};\n\t\tstd::vector<uint8_t> frame_vert_data(frame_verts.size() * sizeof(float));\n\t\tstd::memcpy(frame_vert_data.data(), frame_verts.data(), frame_vert_data.size());\n\t\tauto frame_vert_info = resources::VertexInputInfo{\n\t\t\t{resources::vertex_input_t::V2F32},\n\t\t\tresources::vertex_layout_t::AOS,\n\t\t\tresources::vertex_primitive_t::LINE_LOOP,\n\t\t};\n\t\tauto frame_mesh = resources::MeshData{std::move(frame_vert_data), frame_vert_info};\n\t\tauto frame_geometry = this->renderer->add_mesh_geometry(frame_mesh);\n\n\t\tauto scale = this->animation_2d_info.get_scalefactor();\n\t\tauto max_frame_size = this->animation_2d_info.get_max_size();\n\t\tauto frame_size = Eigen::Vector2f{\n\t\t\tstatic_cast<float>(max_frame_size[0]),\n\t\t\tstatic_cast<float>(max_frame_size[1])};\n\t\tauto frame_unifs = this->frame_shader->new_uniform_input(\n\t\t\t\"obj_world_position\",\n\t\t\tscene_pos.to_world_space(),\n\t\t\t\"scale\",\n\t\t\tscale,\n\t\t\t\"frame_size\",\n\t\t\tframe_size,\n\t\t\t\"incol\",\n\t\t\tEigen::Vector4f{0.0f, 0.0f, 1.0f, 1.0f});\n\t\tRenderable frame_obj{\n\t\t\tframe_unifs,\n\t\t\tframe_geometry,\n\t\t\ttrue,\n\t\t\ttrue,\n\t\t};\n\n\t\trenderables.push_back(frame_obj);\n\t}\n\n\t// Create renderable for frustum frame\n\tstd::array<float, 8> frame_verts{-0.7f, -0.7f, -0.7f, 0.7f, 0.7f, 0.7f, 0.7f, -0.7f};\n\tstd::vector<uint8_t> frame_vert_data(frame_verts.size() * sizeof(float));\n\tstd::memcpy(frame_vert_data.data(), frame_verts.data(), frame_vert_data.size());\n\tauto frame_vert_info = resources::VertexInputInfo{\n\t\t{resources::vertex_input_t::V2F32},\n\t\tresources::vertex_layout_t::AOS,\n\t\tresources::vertex_primitive_t::LINE_LOOP,\n\t};\n\tauto frame_mesh = resources::MeshData{std::move(frame_vert_data), frame_vert_info};\n\tauto frame_geometry = this->renderer->add_mesh_geometry(frame_mesh);\n\n\tauto frame_unifs = this->frustum_shader->new_uniform_input(\n\t\t\"incol\",\n\t\tEigen::Vector4f{1.0f, 0.0f, 0.0f, 1.0f});\n\tRenderable frame_obj{\n\t\tframe_unifs,\n\t\tframe_geometry,\n\t\ttrue,\n\t\ttrue,\n\t};\n\trenderables.push_back(frame_obj);\n\n\treturn renderables;\n}\n\nvoid RenderManagerDemo6::setup() {\n\tthis->qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\t// Create the window and renderer\n\twindow_settings settings;\n\tsettings.width = 1024;\n\tsettings.height = 768;\n\tsettings.debug = true;\n\tthis->window = std::make_shared<opengl::GlWindow>(\"openage renderer test\", settings);\n\tthis->renderer = window->make_renderer();\n\n\tthis->load_shaders();\n\tthis->load_assets();\n\tthis->create_camera();\n\tthis->create_render_passes();\n}\n\nvoid RenderManagerDemo6::load_shaders() {\n\t// Shader\n\tauto shaderdir = this->path / \"assets\" / \"test\" / \"shaders\";\n\n\t/* Shader for 3D objects*/\n\tauto obj_vshader_file = (shaderdir / \"demo_6_3d.vert.glsl\").open();\n\tauto obj_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_vshader_file.read());\n\tobj_vshader_file.close();\n\n\tauto obj_fshader_file = (shaderdir / \"demo_6_3d.frag.glsl\").open();\n\tauto obj_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_fshader_file.read());\n\tobj_fshader_file.close();\n\n\t/* Shader for 2D animations */\n\tauto obj_2d_vshader_file = (shaderdir / \"demo_6_2d.vert.glsl\").open();\n\tauto obj_2d_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tobj_2d_vshader_file.read());\n\tobj_2d_vshader_file.close();\n\n\tauto obj_2d_fshader_file = (shaderdir / \"demo_6_2d.frag.glsl\").open();\n\tauto obj_2d_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tobj_2d_fshader_file.read());\n\tobj_2d_fshader_file.close();\n\n\t/* Shader for frames */\n\tauto frame_vshader_file = (shaderdir / \"demo_6_2d_frame.vert.glsl\").open();\n\tauto frame_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tframe_vshader_file.read());\n\tframe_vshader_file.close();\n\n\tauto frame_fshader_file = (shaderdir / \"demo_6_2d_frame.frag.glsl\").open();\n\tauto frame_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tframe_fshader_file.read());\n\tframe_fshader_file.close();\n\n\t/* Shader for frustrum frame */\n\tauto frustum_vshader_file = (shaderdir / \"demo_6_2d_frustum_frame.vert.glsl\").open();\n\tauto frustum_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tfrustum_vshader_file.read());\n\tfrustum_vshader_file.close();\n\n\t// Use the same fragment shader as the frame shader\n\tauto frustum_fshader_src = frame_fshader_src;\n\n\t/* Shader for rendering to the screen */\n\tauto display_vshader_file = (shaderdir / \"demo_6_display.vert.glsl\").open();\n\tauto display_vshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tdisplay_vshader_file.read());\n\tdisplay_vshader_file.close();\n\n\tauto display_fshader_file = (shaderdir / \"demo_6_display.frag.glsl\").open();\n\tauto display_fshader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tdisplay_fshader_file.read());\n\tdisplay_fshader_file.close();\n\n\t// Create shader programs\n\tthis->obj_3d_shader = this->renderer->add_shader({obj_vshader_src, obj_fshader_src});\n\tthis->obj_2d_shader = this->renderer->add_shader({obj_2d_vshader_src, obj_2d_fshader_src});\n\tthis->frame_shader = this->renderer->add_shader({frame_vshader_src, frame_fshader_src});\n\tthis->frustum_shader = this->renderer->add_shader({frustum_vshader_src, frustum_fshader_src});\n\tthis->display_shader = this->renderer->add_shader({display_vshader_src, display_fshader_src});\n}\n\nvoid RenderManagerDemo6::load_assets() {\n\t// Load assets for 2D objects\n\tauto animation_2d_path = this->path / \"assets\" / \"test\" / \"textures\" / \"test_tank.sprite\";\n\tthis->animation_2d_info = resources::parser::parse_sprite_file(animation_2d_path);\n\n\tauto tex_info = this->animation_2d_info.get_texture(0);\n\tauto tex_data = resources::Texture2dData{*tex_info};\n\tthis->obj_2d_texture = this->renderer->add_texture(tex_data);\n\n\t// Load assets for 3D objects\n\tauto terrain_path = this->path / \"assets\" / \"test\" / \"textures\" / \"test_terrain.terrain\";\n\tthis->terrain_3d_info = resources::parser::parse_terrain_file(terrain_path);\n\n\tauto terrain_tex_info = this->terrain_3d_info.get_texture(0);\n\tauto terrain_tex_data = resources::Texture2dData{*terrain_tex_info};\n\tthis->obj_3d_texture = this->renderer->add_texture(terrain_tex_data);\n}\n\nvoid RenderManagerDemo6::create_camera() {\n\t// Camera\n\tthis->camera = std::make_shared<renderer::camera::Camera>(renderer, window->get_size());\n\tthis->camera->set_zoom(2.0f);\n\n\t// Bind the camera uniform buffer to the shaders\n\tobj_3d_shader->bind_uniform_buffer(\"camera\", this->camera->get_uniform_buffer());\n\tobj_2d_shader->bind_uniform_buffer(\"camera\", this->camera->get_uniform_buffer());\n\tframe_shader->bind_uniform_buffer(\"camera\", this->camera->get_uniform_buffer());\n\n\t// Update the camera uniform buffer\n\tauto camera_unifs = camera->get_uniform_buffer()->new_uniform_input(\n\t\t\"view\",\n\t\tthis->camera->get_view_matrix(),\n\t\t\"proj\",\n\t\tthis->camera->get_projection_matrix(),\n\t\t\"inv_zoom\",\n\t\t1.0f / this->camera->get_zoom());\n\tauto viewport_size = this->camera->get_viewport_size();\n\tEigen::Vector2f viewport_size_vec{\n\t\t1.0f / static_cast<float>(viewport_size[0]),\n\t\t1.0f / static_cast<float>(viewport_size[1])};\n\tcamera_unifs->update(\"inv_viewport_size\", viewport_size_vec);\n\tthis->camera->get_uniform_buffer()->update_uniforms(camera_unifs);\n}\n\nvoid RenderManagerDemo6::create_render_passes() {\n\t// Create render passes\n\tauto window_size = window->get_size();\n\tauto color_texture0 = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                     window_size[1],\n\t                                                                     resources::pixel_format::rgba8));\n\tauto fbo0 = renderer->create_texture_target({color_texture0});\n\tthis->obj_3d_pass = renderer->add_render_pass({}, fbo0);\n\n\tauto color_texture1 = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                     window_size[1],\n\t                                                                     resources::pixel_format::rgba8));\n\tauto fbo1 = renderer->create_texture_target({color_texture1});\n\tthis->obj_2d_pass = renderer->add_render_pass({}, fbo1);\n\n\tauto color_texture2 = renderer->add_texture(resources::Texture2dInfo(window_size[0],\n\t                                                                     window_size[1],\n\t                                                                     resources::pixel_format::rgba8));\n\tauto fbo2 = renderer->create_texture_target({color_texture2});\n\tthis->frame_pass = renderer->add_render_pass({}, fbo2);\n\n\t// Create render pass for rendering to screen\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tauto color_texture0_unif = display_shader->new_uniform_input(\"color_texture\", color_texture0);\n\tRenderable display_obj_3d{\n\t\tcolor_texture0_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto color_texture1_unif = display_shader->new_uniform_input(\"color_texture\", color_texture1);\n\tRenderable display_obj_2d{\n\t\tcolor_texture1_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tauto color_texture2_unif = display_shader->new_uniform_input(\"color_texture\", color_texture2);\n\tRenderable display_obj_frame{\n\t\tcolor_texture2_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\tthis->display_pass = renderer->add_render_pass(\n\t\t{display_obj_3d, display_obj_2d, display_obj_frame},\n\t\trenderer->get_display_target());\n\n\tif (not check_uniform_completeness({display_obj_3d, display_obj_2d, display_obj_frame})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_6.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"coord/scene.h\"\n#include \"renderer/renderable.h\"\n#include \"renderer/resources/animation/animation_info.h\"\n#include \"renderer/resources/terrain/terrain_info.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::renderer {\nclass RenderPass;\nclass Renderer;\nclass ShaderProgram;\nclass Texture2d;\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace gui {\nclass GuiApplicationWithLogger;\n}\n\nnamespace opengl {\nclass GlWindow;\n}\n\nnamespace tests {\n\n/**\n * Show the usage of frustum culling in the renderer.\n *     - Window creation\n *     - Loading shaders\n *     - Creating a camera\n *     - 2D and 3D frustum retrieval\n *     - Manipulating the frustum\n *     - Rendering objects with frustum culling\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_demo_6(const util::Path &path);\n\n\n/**\n * Render manager that handles drawing, object creation, etc.\n */\nclass RenderManagerDemo6 {\npublic:\n\tRenderManagerDemo6(const util::Path &path);\n\tvoid run();\n\n\t/* Create objects to render */\n\n\t/// Create 2D objects (sprites)\n\tconst std::vector<renderer::Renderable> create_2d_obj();\n\t/// Create 3D objects (terrain)\n\tconst renderer::Renderable create_3d_obj();\n\t/// Create frames around 2D objects. These represents the boundaries of the objects\n\t/// that are used by the frustum culling algorithm.\n\tconst std::vector<renderer::Renderable> create_frame_obj();\n\n\t/// Qt application\n\tstd::shared_ptr<gui::GuiApplicationWithLogger> qtapp;\n\n\t/// OpenGL window\n\tstd::shared_ptr<opengl::GlWindow> window;\n\n\t/// Camera\n\tstd::shared_ptr<camera::Camera> camera;\n\n\t/// Render passes\n\tstd::shared_ptr<renderer::RenderPass> obj_2d_pass;\n\tstd::shared_ptr<renderer::RenderPass> obj_3d_pass;\n\tstd::shared_ptr<renderer::RenderPass> frame_pass;\n\tstd::shared_ptr<renderer::RenderPass> display_pass;\n\n\t/// 2D object (sprite) positions\n\tconst std::array<coord::scene3, 5> obj_2d_positions = {\n\t\tcoord::scene3{0, 0, 0},\n\t\tcoord::scene3{-4, -4, 0},\n\t\tcoord::scene3{4, 4, 0},\n\t\tcoord::scene3{-2, 3, 0},\n\t\tcoord::scene3{3, -2, 0},\n\t};\n\n\t/// Rendered angles of the 2D objects\n\tconst std::array<size_t, 5> obj_2d_angles = {0, 1, 2, 3, 4};\n\n\t/// Animation and texture information\n\tresources::Animation2dInfo animation_2d_info;\n\tresources::TerrainInfo terrain_3d_info;\n\nprivate:\n\t/// Setup everything necessary for rendering.\n\tvoid setup();\n\n\t/// Load shaders, assets, create camera, and render passes.\n\tvoid load_shaders();\n\tvoid load_assets();\n\tvoid create_camera();\n\tvoid create_render_passes();\n\n\t/// Directory path\n\tutil::Path path;\n\n\t/// Renderer\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/// Shaders\n\tstd::shared_ptr<renderer::ShaderProgram> obj_2d_shader;\n\tstd::shared_ptr<renderer::ShaderProgram> obj_3d_shader;\n\tstd::shared_ptr<renderer::ShaderProgram> frame_shader;\n\tstd::shared_ptr<renderer::ShaderProgram> frustum_shader;\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n\n\t/// Textures for the rendered objects (2D and 3D)\n\tstd::shared_ptr<renderer::Texture2d> obj_2d_texture;\n\tstd::shared_ptr<renderer::Texture2d> obj_3d_texture;\n};\n\n} // namespace tests\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_7.cpp",
    "content": "// Copyright 2025-2025 the openage authors. See copying.md for legal info.\n\n#include \"demo_7.h\"\n\n#include \"util/path.h\"\n\n#include \"renderer/demo/util.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/shader_template.h\"\n#include \"renderer/shader_program.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo_7(const util::Path &path) {\n\t// Basic setup\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\twindow_settings settings;\n\tsettings.width = 800;\n\tsettings.height = 600;\n\tsettings.debug = true;\n\n\topengl::GlWindow window(\"Shader Commands Demo\", settings);\n\tauto renderer = window.make_renderer();\n\n\tauto shaderdir = path / \"assets\" / \"test\" / \"shaders\";\n\n\t// Initialize shader template\n\tresources::ShaderTemplate frag_template(shaderdir / \"demo_7_shader_command.frag.glsl\");\n\n\t// Load snippets from a snippet directory\n\tfrag_template.load_snippets(shaderdir / \"demo_7_snippets\");\n\n\tauto vert_shader_file = (shaderdir / \"demo_7_shader_command.vert.glsl\").open();\n\tauto vert_shader_src = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_src = frag_template.generate_source();\n\n\tauto shader = renderer->add_shader({vert_shader_src, frag_shader_src});\n\n\t// Create a simple quad for rendering\n\tauto quad = renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\n\tauto uniforms = shader->new_uniform_input(\"time\", 0.0f);\n\n\tRenderable display_obj{\n\t\tuniforms,\n\t\tquad,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tif (not check_uniform_completeness({display_obj})) {\n\t\tlog::log(WARN << \"Uniforms not complete.\");\n\t}\n\n\tauto pass = renderer->add_render_pass({display_obj}, renderer->get_display_target());\n\n\t// Main loop\n\tfloat time = 0.0f;\n\twhile (not window.should_close()) {\n\t\ttime += 0.016f;\n\t\tuniforms->update(\"time\", time);\n\n\t\trenderer->render(pass);\n\t\twindow.update();\n\t\tqtapp->process_events();\n\n\t\trenderer->check_error();\n\t}\n\twindow.close();\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/demo_7.h",
    "content": "// Copyright 2025-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Demonstrate the shader template system for shader generation.\n *     - Window creation\n *     - Create a shader template\n *     - Load shader snippets (command) from files\n *     - Generate shader sources from the template\n *     - Creating a render pass\n *     - Creating a renderable from a mesh\n *\n * @param path Path to the project rootdir.\n */\nvoid renderer_demo_7(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/stresstest_0.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"stresstest_0.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/tile.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_factory.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/stages/camera/manager.h\"\n#include \"renderer/stages/screen/render_stage.h\"\n#include \"renderer/stages/skybox/render_stage.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"renderer/stages/terrain/render_stage.h\"\n#include \"renderer/stages/world/render_entity.h\"\n#include \"renderer/stages/world/render_stage.h\"\n#include \"renderer/uniform_buffer.h\"\n#include \"time/clock.h\"\n#include \"util/fps.h\"\n\n\nnamespace openage::renderer::tests {\n\nvoid renderer_stresstest_0(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\twindow_settings settings;\n\tsettings.width = 1024;\n\tsettings.height = 768;\n\tsettings.vsync = false;\n\tsettings.debug = true;\n\tauto window = std::make_shared<opengl::GlWindow>(\"openage renderer test\", settings);\n\tauto renderer = window->make_renderer();\n\n\t// Clock required by world renderer for timing animation frames\n\tauto clock = std::make_shared<time::Clock>();\n\n\t// Camera\n\t// our viewport into the game world\n\tauto camera = std::make_shared<renderer::camera::Camera>(renderer,\n\t                                                         window->get_size(),\n\t                                                         Eigen::Vector3f{17.0f, 10.0f, 7.0f});\n\tauto cam_unifs = camera->get_uniform_buffer()->create_empty_input();\n\tcam_unifs->update(\n\t\t\"view\",\n\t\tcamera->get_view_matrix(),\n\t\t\"proj\",\n\t\tcamera->get_projection_matrix(),\n\t\t\"inv_zoom\",\n\t\t1.0f / camera->get_zoom());\n\tauto viewport_size = camera->get_viewport_size();\n\tEigen::Vector2f viewport_size_vec{\n\t\t1.0f / static_cast<float>(viewport_size[0]),\n\t\t1.0f / static_cast<float>(viewport_size[1])};\n\tcam_unifs->update(\"inv_viewport_size\", viewport_size_vec);\n\tcamera->get_uniform_buffer()->update_uniforms(cam_unifs);\n\n\t// Render stages\n\t// every stage use a different subrenderer that manages renderables,\n\t// shaders, textures & more.\n\tstd::vector<std::shared_ptr<RenderPass>>\n\t\trender_passes{};\n\n\t// TODO: Make this optional for subrenderers?\n\tauto asset_manager = std::make_shared<renderer::resources::AssetManager>(\n\t\trenderer,\n\t\tpath[\"assets\"][\"test\"]);\n\n\t// Renders the background\n\tauto skybox_renderer = std::make_shared<renderer::skybox::SkyboxRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tskybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color\n\n\t// Renders the terrain in 3D\n\tauto terrain_renderer = std::make_shared<renderer::terrain::TerrainRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Renders units/buildings/other objects\n\tauto world_renderer = std::make_shared<renderer::world::WorldRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Store the render passes of the renderers\n\t// The order is important as its also the order in which they\n\t// are rendered and drawn onto the screen.\n\trender_passes.push_back(skybox_renderer->get_render_pass());\n\trender_passes.push_back(terrain_renderer->get_render_pass());\n\trender_passes.push_back(world_renderer->get_render_pass());\n\n\t// Final output on screen has its own subrenderer\n\t// It takes the outputs of all previous render passes\n\t// and blends them together\n\tauto screen_renderer = std::make_shared<renderer::screen::ScreenRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\tfor (auto &pass : render_passes) {\n\t\ttargets.push_back(pass->get_target());\n\t}\n\tscreen_renderer->set_render_targets(targets);\n\n\twindow->add_resize_callback([&](size_t, size_t, double /*scale*/) {\n\t\t// Acquire the render targets for all previous passes\n\t\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\t\tfor (size_t i = 0; i < render_passes.size() - 1; ++i) {\n\t\t\ttargets.push_back(render_passes[i]->get_target());\n\t\t}\n\t\tscreen_renderer->set_render_targets(targets);\n\t});\n\n\trender_passes.push_back(screen_renderer->get_render_pass());\n\n\t// Create some entities to populate the scene\n\tauto render_factory = std::make_shared<RenderFactory>(terrain_renderer, world_renderer);\n\n\t// Fill a 10x10 terrain grid with height values\n\tauto terrain_size = util::Vector2s{10, 10};\n\tstd::vector<std::pair<terrain::RenderEntity::terrain_elevation_t, std::string>> tiles{};\n\ttiles.reserve(terrain_size[0] * terrain_size[1]);\n\tfor (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) {\n\t\ttiles.emplace_back(0.0f, \"./textures/test_terrain.terrain\");\n\t}\n\n\t// Create entity for terrain rendering\n\tauto terrain0 = render_factory->add_terrain_render_entity(terrain_size,\n\t                                                          coord::tile_delta{0, 0});\n\n\t// send the terrain data to the terrain renderer\n\tterrain0->update(terrain_size, tiles);\n\n\t// World entities\n\tstd::vector<std::shared_ptr<renderer::world::RenderEntity>> render_entities{};\n\tauto add_world_entity = [&](const coord::phys3 initial_pos,\n\t                            const time::time_t time) {\n\t\tconst auto animation_path = \"./textures/test_tank_mirrored.sprite\";\n\n\t\tauto position = curve::Continuous<coord::phys3>{nullptr, 0, \"\", nullptr, coord::phys3(0, 0, 0)};\n\t\tposition.set_insert(time, initial_pos);\n\t\tposition.set_insert(time + 1, initial_pos + coord::phys3_delta{0, 3, 0});\n\t\tposition.set_insert(time + 2, initial_pos + coord::phys3_delta{3, 6, 0});\n\t\tposition.set_insert(time + 3, initial_pos + coord::phys3_delta{6, 6, 0});\n\t\tposition.set_insert(time + 4, initial_pos + coord::phys3_delta{9, 3, 0});\n\t\tposition.set_insert(time + 5, initial_pos + coord::phys3_delta{9, 0, 0});\n\t\tposition.set_insert(time + 6, initial_pos + coord::phys3_delta{6, -3, 0});\n\t\tposition.set_insert(time + 7, initial_pos + coord::phys3_delta{3, -3, 0});\n\t\tposition.set_insert(time + 8, initial_pos);\n\n\t\tauto angle = curve::Segmented<coord::phys_angle_t>{nullptr, 0};\n\t\tangle.set_insert(time, coord::phys_angle_t::from_int(315));\n\t\tangle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(315), coord::phys_angle_t::from_int(270));\n\t\tangle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(225));\n\t\tangle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(180));\n\t\tangle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(180), coord::phys_angle_t::from_int(135));\n\t\tangle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(90));\n\t\tangle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(90), coord::phys_angle_t::from_int(45));\n\t\tangle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(0));\n\t\tangle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(315));\n\n\t\tauto entity = render_factory->add_world_render_entity();\n\t\tentity->update(render_entities.size(),\n\t\t               position,\n\t\t               angle,\n\t\t               animation_path,\n\t\t               time);\n\t\trender_entities.push_back(entity);\n\t};\n\n\t// Stop after 500 entities\n\tsize_t entity_limit = 500;\n\n\tclock->start();\n\n\tutil::FrameCounter timer;\n\n\tadd_world_entity(coord::phys3(0.0f, 3.0f, 0.0f), clock->get_time());\n\ttime::time_t next_entity = clock->get_real_time() + 0.1;\n\twhile (render_entities.size() <= entity_limit) {\n\t\t// Print FPS\n\t\ttimer.frame();\n\t\tstd::cout\n\t\t\t<< \"Entities: \" << render_entities.size()\n\t\t\t<< \" -- \"\n\t\t\t<< \"FPS: \" << timer.fps << \"\\r\" << std::flush;\n\n\t\tqtapp->process_events();\n\n\t\t// Advance time\n\t\tclock->update_time();\n\t\tauto current_time = clock->get_real_time();\n\t\tif (current_time > next_entity) {\n\t\t\tadd_world_entity(coord::phys3(0.0f, 3.0f, 0.0f), clock->get_time());\n\t\t\tnext_entity = current_time + 0.1;\n\t\t}\n\n\t\t// Update the renderables of the subrenderers\n\t\tterrain_renderer->update();\n\t\tworld_renderer->update();\n\n\t\t// Draw everything\n\t\tfor (auto &pass : render_passes) {\n\t\t\trenderer->render(pass);\n\t\t}\n\n\t\trenderer->check_error();\n\n\t\t// Display final output on screen\n\t\twindow->update();\n\t}\n\n\tclock->stop();\n\tlog::log(MSG(info) << \"Stopped after rendering \" << render_entities.size() << \" entities\");\n\n\twindow->close();\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/stresstest_0.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::tests {\n\n/**\n * Stresstest for the renderer.\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_stresstest_0(const util::Path &path);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/stresstest_1.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"stresstest_1.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/tile.h\"\n#include \"renderer/camera/camera.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/render_factory.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/stages/camera/manager.h\"\n#include \"renderer/stages/screen/render_stage.h\"\n#include \"renderer/stages/skybox/render_stage.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"renderer/stages/terrain/render_stage.h\"\n#include \"renderer/stages/world/render_entity.h\"\n#include \"renderer/stages/world/render_stage.h\"\n#include \"renderer/uniform_buffer.h\"\n#include \"time/clock.h\"\n#include \"util/fps.h\"\n\nnamespace openage::renderer::tests {\nvoid renderer_stresstest_1(const util::Path &path) {\n\tauto qtapp = std::make_shared<gui::GuiApplicationWithLogger>();\n\n\t// Create the window and renderer\n\twindow_settings settings;\n\tsettings.width = 1024;\n\tsettings.height = 768;\n\tsettings.vsync = false;\n\tsettings.debug = true;\n\tauto window = std::make_shared<opengl::GlWindow>(\"openage renderer test\", settings);\n\tauto renderer = window->make_renderer();\n\n\t// Clock required by world renderer for timing animation frames\n\tauto clock = std::make_shared<time::Clock>();\n\n\t// Camera\n\t// our viewport into the game world\n\t// on this one, enable frustum culling\n\tauto camera = std::make_shared<renderer::camera::Camera>(renderer,\n\t                                                         window->get_size(),\n\t                                                         Eigen::Vector3f{17.0f, 10.0f, 7.0f},\n\t                                                         1.f,\n\t                                                         64.f,\n\t                                                         1.f / 49.f);\n\tauto cam_unifs = camera->get_uniform_buffer()->create_empty_input();\n\tcam_unifs->update(\n\t\t\"view\",\n\t\tcamera->get_view_matrix(),\n\t\t\"proj\",\n\t\tcamera->get_projection_matrix(),\n\t\t\"inv_zoom\",\n\t\t1.0f / camera->get_zoom());\n\tauto viewport_size = camera->get_viewport_size();\n\tEigen::Vector2f viewport_size_vec{\n\t\t1.0f / static_cast<float>(viewport_size[0]),\n\t\t1.0f / static_cast<float>(viewport_size[1])};\n\tcam_unifs->update(\"inv_viewport_size\", viewport_size_vec);\n\tcamera->get_uniform_buffer()->update_uniforms(cam_unifs);\n\n\t// Render stages\n\t// every stage use a different subrenderer that manages renderables,\n\t// shaders, textures & more.\n\tstd::vector<std::shared_ptr<RenderPass>>\n\t\trender_passes{};\n\n\t// TODO: Make this optional for subrenderers?\n\tauto asset_manager = std::make_shared<renderer::resources::AssetManager>(\n\t\trenderer,\n\t\tpath[\"assets\"][\"test\"]);\n\n\t// Renders the background\n\tauto skybox_renderer = std::make_shared<renderer::skybox::SkyboxRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tskybox_renderer->set_color(1.0f, 0.5f, 0.0f, 1.0f); // orange color\n\n\t// Renders the terrain in 3D\n\tauto terrain_renderer = std::make_shared<renderer::terrain::TerrainRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Renders units/buildings/other objects\n\tauto world_renderer = std::make_shared<renderer::world::WorldRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tcamera,\n\t\tpath[\"assets\"][\"shaders\"],\n\t\tasset_manager,\n\t\tclock);\n\n\t// Enable frustum culling\n\trenderer::world::WorldRenderStage::ENABLE_FRUSTUM_CULLING = true;\n\n\t// Store the render passes of the renderers\n\t// The order is important as its also the order in which they\n\t// are rendered and drawn onto the screen.\n\trender_passes.push_back(skybox_renderer->get_render_pass());\n\trender_passes.push_back(terrain_renderer->get_render_pass());\n\trender_passes.push_back(world_renderer->get_render_pass());\n\n\t// Final output on screen has its own subrenderer\n\t// It takes the outputs of all previous render passes\n\t// and blends them together\n\tauto screen_renderer = std::make_shared<renderer::screen::ScreenRenderStage>(\n\t\twindow,\n\t\trenderer,\n\t\tpath[\"assets\"][\"shaders\"]);\n\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\tfor (auto &pass : render_passes) {\n\t\ttargets.push_back(pass->get_target());\n\t}\n\tscreen_renderer->set_render_targets(targets);\n\n\twindow->add_resize_callback([&](size_t, size_t, double /*scale*/) {\n\t\t// Acquire the render targets for all previous passes\n\t\tstd::vector<std::shared_ptr<renderer::RenderTarget>> targets{};\n\t\tfor (size_t i = 0; i < render_passes.size() - 1; ++i) {\n\t\t\ttargets.push_back(render_passes[i]->get_target());\n\t\t}\n\t\tscreen_renderer->set_render_targets(targets);\n\t});\n\n\trender_passes.push_back(screen_renderer->get_render_pass());\n\n\t// Create some entities to populate the scene\n\tauto render_factory = std::make_shared<RenderFactory>(terrain_renderer, world_renderer);\n\n\t// Fill a 10x10 terrain grid with height values\n\tauto terrain_size = util::Vector2s{10, 10};\n\tstd::vector<std::pair<terrain::RenderEntity::terrain_elevation_t, std::string>> tiles{};\n\ttiles.reserve(terrain_size[0] * terrain_size[1]);\n\tfor (size_t i = 0; i < terrain_size[0] * terrain_size[1]; ++i) {\n\t\ttiles.emplace_back(0.0f, \"./textures/test_terrain.terrain\");\n\t}\n\n\t// Create entity for terrain rendering\n\tauto terrain0 = render_factory->add_terrain_render_entity(terrain_size,\n\t                                                          coord::tile_delta{0, 0});\n\n\t// send the terrain data to the terrain renderer\n\tterrain0->update(terrain_size, tiles);\n\n\tstd::vector<std::shared_ptr<renderer::world::RenderEntity>> render_entities{};\n\tauto add_world_entity = [&](const coord::phys3 initial_pos,\n\t                            const time::time_t time) {\n\t\tconst auto animation_path = \"./textures/test_tank_mirrored.sprite\";\n\n\t\tauto position = curve::Continuous<coord::phys3>{nullptr, 0, \"\", nullptr, coord::phys3(0, 0, 0)};\n\t\tposition.set_insert(time, initial_pos);\n\t\tposition.set_insert(time + 1, initial_pos + coord::phys3_delta{5, 0, 0});\n\t\tposition.set_insert(time + 2, initial_pos + coord::phys3_delta{12, -2, 0});\n\t\tposition.set_insert(time + 3, initial_pos + coord::phys3_delta{5, 5, 0});\n\t\tposition.set_insert(time + 4, initial_pos + coord::phys3_delta{12, 12, 0});\n\t\tposition.set_insert(time + 5, initial_pos + coord::phys3_delta{5, 9, 0});\n\t\tposition.set_insert(time + 6, initial_pos + coord::phys3_delta{-2, 12, 0});\n\t\tposition.set_insert(time + 7, initial_pos + coord::phys3_delta{0, 5, 0});\n\t\tposition.set_insert(time + 8, initial_pos);\n\n\t\tauto angle = curve::Segmented<coord::phys_angle_t>{nullptr, 0};\n\t\tangle.set_insert(time, coord::phys_angle_t::from_int(225));\n\t\tangle.set_insert_jump(time + 1, coord::phys_angle_t::from_int(225), coord::phys_angle_t::from_int(210));\n\t\tangle.set_insert_jump(time + 2, coord::phys_angle_t::from_int(210), coord::phys_angle_t::from_int(0));\n\t\tangle.set_insert_jump(time + 3, coord::phys_angle_t::from_int(0), coord::phys_angle_t::from_int(270));\n\t\tangle.set_insert_jump(time + 4, coord::phys_angle_t::from_int(270), coord::phys_angle_t::from_int(60));\n\t\tangle.set_insert_jump(time + 5, coord::phys_angle_t::from_int(60), coord::phys_angle_t::from_int(45));\n\t\tangle.set_insert_jump(time + 6, coord::phys_angle_t::from_int(45), coord::phys_angle_t::from_int(120));\n\t\tangle.set_insert_jump(time + 7, coord::phys_angle_t::from_int(120), coord::phys_angle_t::from_int(135));\n\t\tangle.set_insert_jump(time + 8, coord::phys_angle_t::from_int(135), coord::phys_angle_t::from_int(225));\n\n\t\tauto entity = render_factory->add_world_render_entity();\n\t\tentity->update(render_entities.size(),\n\t\t               position,\n\t\t               angle,\n\t\t               animation_path,\n\t\t               time);\n\t\trender_entities.push_back(entity);\n\t};\n\n\t// Stop after 1000 entities\n\tsize_t entity_limit = 1000;\n\n\tclock->start();\n\n\tutil::FrameCounter timer;\n\n\ttime::time_t next_entity = clock->get_real_time();\n\twhile (render_entities.size() <= entity_limit) {\n\t\t// Print FPS\n\t\ttimer.frame();\n\t\tstd::cout\n\t\t\t<< \"Entities: \" << render_entities.size()\n\t\t\t<< \" -- \"\n\t\t\t<< \"FPS: \" << timer.fps << \"\\r\" << std::flush;\n\n\t\tqtapp->process_events();\n\n\t\t// Advance time\n\t\tclock->update_time();\n\t\tauto current_time = clock->get_real_time();\n\t\tif (current_time > next_entity) {\n\t\t\tadd_world_entity(coord::phys3(0.5, 0.5, 0.0f), clock->get_time());\n\t\t\tnext_entity = current_time + 0.05;\n\t\t}\n\n\t\t// Update the renderables of the subrenderers\n\t\tterrain_renderer->update();\n\t\tworld_renderer->update();\n\n\t\t// Draw everything\n\t\tfor (auto &pass : render_passes) {\n\t\t\trenderer->render(pass);\n\t\t}\n\n\t\trenderer->check_error();\n\n\t\t// Display final output on screen\n\t\twindow->update();\n\t}\n\n\tclock->stop();\n\tlog::log(MSG(info) << \"Stopped after rendering \" << render_entities.size() << \" entities\");\n\n\twindow->close();\n}\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/stresstest_1.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"util/path.h\"\n\nnamespace openage::renderer::tests {\n\n/**\n * Stresstest for the renderer's frustum culling feature.\n *\n * @param path Path to the openage asset directory.\n */\nvoid renderer_stresstest_1(const util::Path &path);\n\n}\n"
  },
  {
    "path": "libopenage/renderer/demo/tests.cpp",
    "content": "// Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n#include \"tests.h\"\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"renderer/demo/demo_0.h\"\n#include \"renderer/demo/demo_1.h\"\n#include \"renderer/demo/demo_2.h\"\n#include \"renderer/demo/demo_3.h\"\n#include \"renderer/demo/demo_4.h\"\n#include \"renderer/demo/demo_5.h\"\n#include \"renderer/demo/demo_6.h\"\n#include \"renderer/demo/demo_7.h\"\n#include \"renderer/demo/stresstest_0.h\"\n#include \"renderer/demo/stresstest_1.h\"\n\nnamespace openage::renderer::tests {\n\nvoid renderer_demo(int demo_id, const util::Path &path) {\n\tswitch (demo_id) {\n\tcase 0:\n\t\trenderer_demo_0(path);\n\t\tbreak;\n\n\tcase 1:\n\t\trenderer_demo_1(path);\n\t\tbreak;\n\n\tcase 2:\n\t\trenderer_demo_2(path);\n\t\tbreak;\n\n\tcase 3:\n\t\trenderer_demo_3(path);\n\t\tbreak;\n\n\tcase 4:\n\t\trenderer_demo_4(path);\n\t\tbreak;\n\n\tcase 5:\n\t\trenderer_demo_5(path);\n\t\tbreak;\n\n\tcase 6:\n\t\trenderer_demo_6(path);\n\t\tbreak;\n\n\tcase 7:\n\t\trenderer_demo_7(path);\n\t\tbreak;\n\n\tdefault:\n\t\tlog::log(MSG(err) << \"Unknown renderer demo requested: \" << demo_id << \".\");\n\t\tbreak;\n\t}\n}\n\nOAAPI void renderer_stresstest(int demo_id, const util::Path &path) {\n\tswitch (demo_id) {\n\tcase 0:\n\t\trenderer_stresstest_0(path);\n\t\tbreak;\n\tcase 1:\n\t\trenderer_stresstest_1(path);\n\t\tbreak;\n\tdefault:\n\t\tlog::log(MSG(err) << \"Unknown renderer stresstest requested: \" << demo_id << \".\");\n\t\tbreak;\n\t}\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/tests.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../../util/compiler.h\"\n// pxd: from libopenage.util.path cimport Path\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n} // namespace util\n\nnamespace renderer::tests {\n\n// pxd: void renderer_demo(int demo_id, Path path) except +\nOAAPI void renderer_demo(int demo_id, const util::Path &path);\n\n// pxd: void renderer_stresstest(int demo_id, Path path) except +\nOAAPI void renderer_stresstest(int demo_id, const util::Path &path);\n\n} // namespace renderer::tests\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/demo/util.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"util.h\"\n\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage::renderer::tests {\n\nbool check_uniform_completeness(const std::vector<Renderable> &renderables) {\n\t// Iterate over each renderable object\n\tfor (const auto &renderable : renderables) {\n\t\tif (renderable.uniform and not renderable.uniform->is_complete()) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/demo/util.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"renderer/renderable.h\"\n\nnamespace openage::renderer::tests {\n\n// Macro for debugging OpenGL state.\n#define DBG_GL(txt) \\\n\tprintf(\"before %s\\n\", txt); \\\n\topengl::GlContext::check_error(); \\\n\tprintf(\"after %s\\n\", txt);\n\n/**\n * Check if all uniform values for the given renderables have been set.\n *\n * @param renderables The list of renderable objects to check.\n * @return true if all uniforms have been set, false otherwise.\n */\nbool check_uniform_completeness(const std::vector<Renderable> &renderables);\n\n} // namespace openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/font/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tfont.cpp\n\tfont_manager.cpp\n\tglyph_atlas.cpp\n\n\ttests.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/font/font.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"font.h\"\n\n#include <ft2build.h>\n#include FT_FREETYPE_H\n#include <harfbuzz/hb.h>\n#include <harfbuzz/hb-ft.h>\n\n#include <utility>\n\n#include \"../../error/error.h\"\n#include \"../../util/compiler.h\"\n\nnamespace openage::renderer {\n\nfont_description::font_description(std::string font_file,\n                                   unsigned int size,\n                                   font_direction direction,\n                                   std::string language,\n                                   std::string script)\n\t:\n\tfont_file{std::move(font_file)},\n\tsize{size},\n\tdirection{direction},\n\tlanguage{std::move(language)},\n\tscript{std::move(script)} {\n\t// Empty\n}\n\nfont_description::font_description(const char *family, const char *style, unsigned int size)\n\t:\n\tfont_description{FontManager::get_font_filename(family, style), size} {\n\t// Empty\n}\n\nfont_description &font_description::operator=(const font_description &other) {\n\tif (this != &other) {\n\t\tthis->font_file = other.font_file;\n\t\tthis->size = other.size;\n\t\tthis->direction = other.direction;\n\t\tthis->language = other.language;\n\t\tthis->script = other.script;\n\t}\n\n\treturn *this;\n}\n\nfont_description::font_description(const font_description &other)\n\t:\n\tfont_file{other.font_file},\n\tsize{other.size},\n\tdirection{other.direction},\n\tlanguage{other.language},\n\tscript{other.script} {\n\t// Empty\n}\n\nbool font_description::operator==(const font_description &other) const {\n\treturn this->font_file == other.font_file &&\n\t\tthis->size == other.size &&\n\t\tthis->direction == other.direction &&\n\t\tthis->language == other.language &&\n\t\tthis->script == other.script;\n}\n\nbool font_description::operator!=(const font_description &other) const {\n\treturn !(*this == other);\n}\n\nstatic hb_direction_t get_hb_font_direction(const font_description &description) {\n\tswitch(description.direction) {\n\tcase font_direction::left_to_right:\n\t\treturn HB_DIRECTION_LTR;\n\tcase font_direction::right_to_left:\n\t\treturn HB_DIRECTION_RTL;\n\tcase font_direction::top_to_bottom:\n\t\treturn HB_DIRECTION_TTB;\n\tcase font_direction::bottom_to_top:\n\t\treturn HB_DIRECTION_BTT;\n\tdefault:\n\t\treturn HB_DIRECTION_INVALID;\n\t}\n}\n\nstatic hb_language_t get_hb_font_language(const font_description &description) {\n\treturn hb_language_from_string(description.language.c_str(), -1);\n}\n\nstatic hb_script_t get_hb_font_script(const font_description &description) {\n\treturn hb_script_from_string(description.script.c_str(), -1);\n}\n\nFont::Font(const font_description &description)\n\t:\n\tdescription{description},\n\tfreetype_library{nullptr},\n\thb_font{nullptr} {\n\n\tthis->freetype_library = std::make_unique<FreeTypeLibrary>();\n\tthis->initialize(this->freetype_library->ft_library);\n}\n\nFont::Font(FontManager *font_manager, const font_description &description)\n\t:\n\tdescription{description},\n\tfreetype_library{nullptr},\n\thb_font{nullptr} {\n\n\tthis->initialize(font_manager->get_ft_library());\n}\n\nvoid Font::initialize(FT_Library ft_library) {\n\tFT_Face ft_face;\n\tif (FT_New_Face(ft_library, this->description.font_file.c_str(), 0, &ft_face)) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Failed to create font from \" << this->description.font_file);\n\t}\n\tif (FT_Set_Char_Size(ft_face, 0, this->description.size * FREETYPE_UNIT, 72, 72)) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Failed to set font face size to \" << this->description.size);\n\t}\n\n\thb_ft_font_create_referenced(ft_face);\n\t// lambda with static_cast to help gcc understand this is ok\n\tthis->hb_font = hb_ft_font_create(ft_face, [] (void *user_data) -> void {\n\t\tFT_Done_Face(static_cast<FT_FaceRec_ *>(user_data));\n\t});\n}\n\nFont::~Font() {\n\t// Destroy HarfBuzz font\n\t// HarfBuzz will take care of destroying the FT_Face instance\n\tif (this->hb_font) {\n\t\thb_font_destroy(this->hb_font);\n\t\tthis->hb_font = nullptr;\n\t}\n}\n\nfloat Font::get_ascender() const {\n\tFT_Face ft_face = hb_ft_font_get_face(this->hb_font);\n\treturn static_cast<float>(ft_face->size->metrics.ascender)/FREETYPE_UNIT;\n}\n\nfloat Font::get_descender() const {\n\tFT_Face ft_face = hb_ft_font_get_face(this->hb_font);\n\treturn static_cast<float>(ft_face->size->metrics.descender)/FREETYPE_UNIT;\n}\n\nfloat Font::get_line_height() const {\n\tFT_Face ft_face = hb_ft_font_get_face(this->hb_font);\n\n\tif (FT_IS_SCALABLE(ft_face)) {\n\t\tfloat units_to_pixels_conversion = static_cast<float>(ft_face->size->metrics.y_ppem)/ft_face->units_per_EM;\n\t\treturn (ft_face->bbox.yMax - ft_face->bbox.yMin) * units_to_pixels_conversion;\n\t}\n\n\treturn static_cast<float>(ft_face->size->metrics.height)/FREETYPE_UNIT;\n}\n\nfloat Font::get_horizontal_kerning(codepoint_t left_glyph, codepoint_t right_glyph) const {\n\tint kerning = hb_font_get_glyph_h_kerning(this->hb_font, left_glyph, right_glyph);\n\treturn static_cast<float>(kerning)/FREETYPE_UNIT;\n}\n\nfloat Font::get_advance_width(const std::string &text) const {\n\thb_buffer_t *buffer = hb_buffer_create();\n\thb_buffer_set_direction(buffer, get_hb_font_direction(this->description));\n\thb_buffer_set_script(buffer, get_hb_font_script(this->description));\n\thb_buffer_set_language(buffer, get_hb_font_language(this->description));\n\thb_buffer_add_utf8(buffer, text.c_str(), text.length(), 0, text.length());\n\thb_shape(this->hb_font, buffer, nullptr, 0);\n\n\tfloat advance_width = 0.0f;\n\n\tunsigned int glyph_count = 0;\n\thb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buffer, &glyph_count);\n\thb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions(buffer, nullptr);\n\tfor (unsigned int i = 0; i < glyph_count; i++) {\n\t\tadvance_width += static_cast<float>(glyph_pos[i].x_advance)/FREETYPE_UNIT;\n\n\t\tif (i > 0) {\n\t\t\tcodepoint_t glyph = glyph_info[i].codepoint;\n\t\t\tcodepoint_t previous_glyph = glyph_info[i - 1].codepoint;\n\t\t\tadvance_width += this->get_horizontal_kerning(previous_glyph, glyph);\n\t\t}\n\t}\n\n\thb_buffer_destroy(buffer);\n\n\treturn advance_width;\n}\n\nstd::vector<codepoint_t> Font::get_glyphs(const std::string &text) const {\n\thb_buffer_t *buffer = hb_buffer_create();\n\thb_buffer_set_direction(buffer, get_hb_font_direction(this->description));\n\thb_buffer_set_script(buffer, get_hb_font_script(this->description));\n\thb_buffer_set_language(buffer, get_hb_font_language(this->description));\n\thb_buffer_add_utf8(buffer, text.c_str(), text.length(), 0, text.length());\n\thb_shape(this->hb_font, buffer, nullptr, 0);\n\n\tunsigned int glyph_count = 0;\n\thb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos(buffer, &glyph_count);\n\n\tstd::vector<codepoint_t> glyphs;\n\tglyphs.resize(glyph_count);\n\tfor (unsigned int i = 0; i < glyph_count; i++) {\n\t\tglyphs[i] = glyph_info[i].codepoint;\n\t}\n\n\thb_buffer_destroy(buffer);\n\n\treturn glyphs;\n}\n\nstd::unique_ptr<unsigned char[]> Font::load_glyph(codepoint_t codepoint, Glyph &glyph) const {\n\tFT_Face ft_face = hb_ft_font_get_face(this->hb_font);\n\tif (FT_Load_Glyph(ft_face, codepoint, FT_LOAD_DEFAULT | FT_LOAD_NO_HINTING | FT_LOAD_RENDER)) [[unlikely]] {\n\t\treturn nullptr;\n\t}\n\n\tglyph.codepoint = codepoint;\n\tglyph.x_offset = ft_face->glyph->bitmap_left;\n\tglyph.y_offset = ft_face->glyph->bitmap_top;\n\tglyph.width = ft_face->glyph->bitmap.width;\n\tglyph.height = ft_face->glyph->bitmap.rows;\n\tglyph.x_advance = static_cast<float>(ft_face->glyph->advance.x)/FREETYPE_UNIT;\n\tglyph.y_advance = static_cast<float>(ft_face->glyph->advance.y)/FREETYPE_UNIT;\n\n\tauto glyph_data = std::make_unique<unsigned char[]>(glyph.width * glyph.height);\n\tfor (size_t i = 0; i < glyph.height; i++) {\n\t\tmemcpy(\n\t\t\t&glyph_data[i * glyph.width],\n\t\t\tft_face->glyph->bitmap.buffer + (glyph.height - i - 1) * glyph.width,\n\t\t\tglyph.width * sizeof(unsigned char)\n\t\t);\n\t}\n\treturn glyph_data;\n}\n\n} // openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/font/font.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <typeinfo>\n#include <vector>\n\n#include \"../../util/hash.h\"\n#include \"font_manager.h\"\n\n// Forward Declarations of HarfBuzz stuff!\nstruct hb_font_t;\n\nnamespace openage {\nnamespace renderer {\n\nconstexpr int FREETYPE_UNIT = 64;\n\nusing codepoint_t = unsigned int;\n\n/**\n * Holds info about a single glyph.\n */\nclass Glyph {\npublic:\n\tcodepoint_t codepoint; //!< Glyph's codepoint.\n\tint x_offset;          //!< Horizontal distance from origin (current pen position) to glyph's leftmost boundary.\n\tint y_offset;          //!< Vertical distance from the baseline to glyph's topmost boundary.\n\tunsigned int width;    //!< Width of the glyph.\n\tunsigned int height;   //!< Height of the glyph.\n\tfloat x_advance;       //!< Advance width of the glyph.\n\tfloat y_advance;       //!< Advance height of the glyph.\n};\n\n/**\n * Enumeration of the possible font directions.\n */\nenum class font_direction {\n\tleft_to_right,\n\tright_to_left,\n\ttop_to_bottom,\n\tbottom_to_top\n};\n\n/**\n * Description for a font.\n *\n * An instance of font_description is capable of uniquely distinguishing a font.\n */\nstruct font_description {\n\tstd::string font_file;    //!< Path to the font's file.\n\tunsigned int size;        //!< Points size of the font.\n\tfont_direction direction; //!< The direction of the font.\n\tstd::string language;     //!< Language of the font.\n\tstd::string script;       //!< The font's script.\n\n\t/**\n\t * Constructs a font_description instance.\n\t *\n\t * @param font_file: The path to fon't font.\n\t * @param size: The size of the font in points.\n\t * @param direction: The direction of font.\n\t * @param language: The font's language.\n\t * @param script: The font's script.\n\t */\n\tfont_description(std::string font_file,\n\t                 unsigned int size,\n\t                 font_direction direction = font_direction::left_to_right,\n\t                 std::string language = \"en\",\n\t                 std::string script = \"Latin\");\n\n\t/**\n\t * Constructs a font_description instance.\n\t *\n\t * This constructor uses fontconfig to determine a font file for the specified font family and style.\n\t *\n\t * @param family: The font family.\n\t * @param style: The font style.\n\t * @param size: The size of the font in points.\n\t */\n\tfont_description(const char *family, const char *style, unsigned int size);\n\n\tfont_description(const font_description &other);\n\n\tfont_description &operator=(const font_description &other);\n\n\tbool operator==(const font_description &other) const;\n\n\tbool operator!=(const font_description &other) const;\n};\n\nclass Font {\npublic:\n\t/**\n\t * Create a font instance from the description.\n\t *\n\t * @param description: the font description.\n\t */\n\tFont(const font_description &description);\n\n\t/**\n\t * This constructor is used by the font manager to create a\n\t * new font instance from the description.\n\t *\n\t * @param font_manager: The font manager.\n\t * @param description: the font description.\n\t */\n\tFont(FontManager *font_manager, const font_description &description);\n\n\tvirtual ~Font();\n\n\t/**\n\t * Get the typographic ascender of font.\n\t *\n\t * @returns The ascender of font.\n\t */\n\tfloat get_ascender() const;\n\n\t/**\n\t * Get the typographic descender of font.\n\t *\n\t * @returns The descender of font.\n\t */\n\tfloat get_descender() const;\n\n\t/**\n\t * Get the line spacing of font.\n\t *\n\t * @returns The font's line height.\n\t */\n\tfloat get_line_height() const;\n\n\t/**\n\t * Get the kerning adjustment between two glyphs.\n\t *\n\t * @param left_glyph: The first glyph.\n\t * @param right_glyph: The next glyph.\n\t * @returns The kerning adjustment.\n\t */\n\tfloat get_horizontal_kerning(codepoint_t left_glyph, codepoint_t right_glyph) const;\n\n\t/**\n\t * Get the advance width of a particular string.\n\t *\n\t * @param text: The string for which the width is to be determined.\n\t * @returns The advance width of the specified string.\n\t */\n\tfloat get_advance_width(const std::string &text) const;\n\n\t/**\n\t * Get the list of glyphs for a particular string.\n\t *\n\t * @param text: the string for which the glyphs are to be retrieved.\n\t * @returns The list of the glyphs.\n\t */\n\tstd::vector<codepoint_t> get_glyphs(const std::string &text) const;\n\n\t/**\n\t * Load a particular glyph's info and retrieves the glyph's bitmap data.\n\t *\n\t * @param codepoint: The glyph.\n\t * @param glyph: The glyph's info is loaded in to this object.\n\t * @returns The glyph's bitmap data.\n\t */\n\tstd::unique_ptr<unsigned char[]> load_glyph(codepoint_t codepoint, Glyph &glyph) const;\n\nprivate:\n\t/**\n\t * Initializes the font's face and creates a harfbuzz font instance.\n\t *\n\t * @param ft_library: The freetype library that should be used to load the font's face.\n\t */\n\tvoid initialize(FT_Library ft_library);\n\npublic:\n\t/**\n\t * The description of the font.\n\t * @see font_description\n\t */\n\tfont_description description;\n\nprivate:\n\t// Font's created without the use of FontManager are standalone and have their own FreeTypeLibrary instance\n\tstd::unique_ptr<FreeTypeLibrary> freetype_library;\n\n\t// The HarfBuzz font instance that drives the operations of this font\n\thb_font_t *hb_font;\n};\n\n} // namespace renderer\n} // namespace openage\n\nnamespace std {\n\ntemplate <>\nstruct hash<openage::renderer::font_description> {\n\tsize_t operator()(const openage::renderer::font_description &fd) const {\n\t\tsize_t hash = std::hash<std::type_index>()(std::type_index(typeid(openage::renderer::font_description)));\n\t\thash = openage::util::hash_combine(hash, std::hash<std::string>()(fd.font_file));\n\t\thash = openage::util::hash_combine(hash, std::hash<unsigned int>()(fd.size));\n\t\treturn hash;\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/renderer/font/font_manager.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"font_manager.h\"\n\n#include <fontconfig/fontconfig.h>\n\n#include \"../../log/log.h\"\n#include \"font.h\"\n\n\nnamespace openage::renderer {\n\nstd::string FontManager::get_font_filename(const char *family, const char *style) {\n\t// Initialize fontconfig\n\tif (!FcInit()) {\n\t\tthrow Error{ERR << \"Failed to initialize fontconfig.\"};\n\t}\n\n\tFcPattern *font_pattern = FcPatternBuild(nullptr, FC_FAMILY, FcTypeString, family, nullptr);\n\tFcPatternBuild(font_pattern, FC_STYLE, FcTypeString, style, nullptr);\n\n\tFcChar8 *query_string = FcNameUnparse(font_pattern);\n\tlog::log(DBG << \"Font queried: \" << query_string);\n\tfree(query_string);\n\n\t// tell fontconfig to find the best match\n\tFcResult font_match_result;\n\tFcPattern *font_match = FcFontMatch(nullptr, font_pattern, &font_match_result);\n\n\t// get attibute FC_FILE (= filename) of best-matched font\n\tFcChar8 *font_filename_tmp;\n\tif (FcPatternGetString(font_match, FC_FILE, 0, &font_filename_tmp) != FcResultMatch) {\n\t\tthrow Error(ERR << \"Fontconfig could not provide font \" << family << \" \" << style);\n\t}\n\n\tstd::string font_filename{reinterpret_cast<char *>(font_filename_tmp)};\n\n\tlog::log(DBG << \"Font file: \" << font_filename);\n\n\t// deinitialize fontconfig.\n\tFcPatternDestroy(font_match);\n\tFcPatternDestroy(font_pattern);\n\tFcFini();\n\n\treturn font_filename;\n}\n\nFontManager::FontManager() {\n\t// Empty\n}\n\nFontManager::~FontManager() {\n\t// Empty\n}\n\nFT_Library FontManager::get_ft_library() {\n\treturn this->library.ft_library;\n}\n\nFont *FontManager::get_font(const char *family, const char *style, unsigned int size) {\n\tstd::string font_filename = FontManager::get_font_filename(family, style);\n\treturn this->get_font(font_filename.c_str(), size);\n}\n\nFont *FontManager::get_font(const char *font_file, unsigned int size) {\n\t// Check if the font was already created\n\tfont_description fd{font_file, size};\n\tsize_t key = std::hash<font_description>()(fd);\n\tauto it = this->fonts.find(key);\n\tif (it != this->fonts.end()) {\n\t\treturn it->second.get();\n\t}\n\n\t// Create a new font\n\tstd::unique_ptr<Font> font = std::make_unique<Font>(this, fd);\n\tauto inserted = this->fonts.emplace(key, std::move(font));\n\treturn inserted.first->second.get();\n}\n\n} // openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/font/font_manager.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <ft2build.h>\n#include FT_FREETYPE_H\n#include <memory>\n#include <string>\n#include <unordered_map>\n\n#include \"../../error/error.h\"\n\nnamespace openage {\nnamespace renderer {\n\nclass FreeTypeLibrary {\npublic:\n\tFT_Library ft_library;\n\npublic:\n\tFreeTypeLibrary() {\n\t\tif (FT_Init_FreeType(&this->ft_library) != 0) {\n\t\t\tthrow Error{MSG(err) << \"Failed to initialize freetype library.\"};\n\t\t}\n\t}\n\n\t~FreeTypeLibrary() {\n\t\tFT_Done_FreeType(this->ft_library);\n\t}\n\n\tFreeTypeLibrary(const FreeTypeLibrary &copy) = delete;\n\n\tFreeTypeLibrary &operator=(const FreeTypeLibrary &copy) = delete;\n\n\tFreeTypeLibrary(FreeTypeLibrary &&other) = delete;\n\n\tFreeTypeLibrary &operator=(FreeTypeLibrary &&other) = delete;\n};\n\nclass Font;\n\nclass FontManager {\npublic:\n\t/**\n\t * Gets the filepath of a particular font family and style.\n\t *\n\t * @param family: The font family.\n\t * @param style: The font style.\n\t * @returns The path to font's file.\n\t */\n\tstatic std::string get_font_filename(const char *family, const char *style);\n\npublic:\n\tFontManager();\n\n\t~FontManager();\n\n\t/**\n\t * Get the freetype library instance.\n\t */\n\tFT_Library get_ft_library();\n\n\t/**\n\t * Retrieves a font.\n\t *\n\t * @param family: The font family.\n\t * @param style: The font style.\n\t * @param size: The size of the font in points.\n\t * @returns The pointer to font instance.\n\t */\n\tFont *get_font(const char *family, const char *style, unsigned int size);\n\n\t/**\n\t * Retrieves a font.\n\t *\n\t * @param font_file: The path to font's file.\n\t * @param size: The size of the font in points.\n\t * @returns The pointer to font instance.\n\t */\n\tFont *get_font(const char *font_file, unsigned int size);\n\nprivate:\n\t// The freetype library instance\n\tFreeTypeLibrary library;\n\n\t// Font cache. the hash of font's description is used as the key\n\tstd::unordered_map<size_t, std::unique_ptr<Font>> fonts;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/font/glyph_atlas.cpp",
    "content": "// Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n#include \"glyph_atlas.h\"\n\n#include <algorithm>\n#include <functional>\n#include <typeindex>\n#include <typeinfo>\n#include <cstring>\n#include <epoxy/gl.h>\n\n#include \"../../error/error.h\"\n#include \"../../util/hash.h\"\n\nnamespace openage::renderer {\n\nGlyphAtlas::Shelf::Shelf(int y_position, int width, int height)\n\t:\n\ty_position{y_position},\n\twidth{width},\n\theight{height},\n\tremaining_width{width} {\n\t// Empty\n}\n\nbool GlyphAtlas::Shelf::check_fits(int required_width, int required_height) {\n\tif (required_width > this->remaining_width || required_height > this->height) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nint GlyphAtlas::Shelf::reserve(int required_width, int/* required_height*/) {\n\tint x_position = this->width - this->remaining_width;\n\tthis->remaining_width -= required_width;\n\treturn x_position;\n}\n\nGlyphAtlas::GlyphAtlas(int width, int height)\n\t:\n\twidth{width},\n\theight{height},\n\tis_dirty{false},\n\tdirty_area{width, height, 0, 0},\n\ttexture_id{0} {\n\n\tthis->buffer = std::unique_ptr<unsigned char>(new unsigned char[this->width * this->height]);\n\tmemset(this->buffer.get(), 0, this->width * this->height);\n\n\tglGenTextures(1, &this->texture_id);\n\tglBindTexture(GL_TEXTURE_2D, this->texture_id);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n\n\tglTexImage2D(GL_TEXTURE_2D, 0, GL_RED, this->width, this->height, 0, GL_RED, GL_UNSIGNED_BYTE, this->buffer.get());\n}\n\nGlyphAtlas::~GlyphAtlas() {\n\tif (this->texture_id) {\n\t\tglDeleteTextures(1, &this->texture_id);\n\t}\n}\n\nvoid GlyphAtlas::bind(int unit) {\n\tglActiveTexture(GL_TEXTURE0 + unit);\n\tglBindTexture(GL_TEXTURE_2D, this->texture_id);\n\n\tif (this->is_dirty) {\n\t\t// We update the entire width of the atlas because of our buffer memory alignment\n\t\tglTexSubImage2D(GL_TEXTURE_2D, 0,\n\t\t                0, this->dirty_area.y1, this->width, this->dirty_area.y2 - this->dirty_area.y1,\n\t\t                GL_RED, GL_UNSIGNED_BYTE, this->buffer.get() + this->width * this->dirty_area.y1);\n\n\t\tthis->is_dirty = false;\n\t\tthis->dirty_area = {this->width, this->height, 0, 0};\n\t}\n}\n\nGlyphAtlas::Entry GlyphAtlas::get(Font *font, codepoint_t codepoint) {\n\tsize_t key = this->get_cache_key(font, codepoint);\n\tauto it = this->glyphs.find(key);\n\tif (it != this->glyphs.end()) {\n\t\treturn it->second;\n\t}\n\n\tGlyph glyph;\n\tstd::unique_ptr<unsigned char[]> image = font->load_glyph(codepoint, glyph);\n\treturn this->set(key, glyph, image.get());\n}\n\nsize_t GlyphAtlas::get_cache_key(Font *font, codepoint_t codepoint) const {\n\tsize_t hash = std::hash<std::type_index>()(std::type_index(typeid(openage::renderer::GlyphAtlas)));\n\thash = openage::util::hash_combine(hash, std::hash<font_description>()(font->description));\n\thash = openage::util::hash_combine(hash, std::hash<codepoint_t>()(codepoint));\n\treturn hash;\n}\n\nGlyphAtlas::Entry GlyphAtlas::set(size_t key, const Glyph &glyph, const unsigned char *image) {\n\t// Give the glyphs a 1px padding in the atlas\n\tint required_width = glyph.width + 1;\n\tint required_height = glyph.height + 1;\n\n\tif (this->shelves.empty()) {\n\t\tthis->shelves.emplace_back(0, this->width, required_height);\n\t}\n\n\tfor (auto &shelf : this->shelves) {\n\t\tif (shelf.check_fits(required_width, required_height)) {\n\t\t\tint x_pos = shelf.reserve(required_width, required_height);\n\t\t\tint y_pos = shelf.y_position;\n\n\t\t\t// Create a new entry and insert it\n\t\t\tGlyphAtlas::Entry entry;\n\t\t\tentry.glyph = glyph;\n\t\t\tentry.u0 = static_cast<float>(x_pos)/this->width;\n\t\t\tentry.v0 = static_cast<float>(y_pos)/this->height;\n\t\t\tentry.u1 = static_cast<float>(x_pos + glyph.width)/this->width;\n\t\t\tentry.v1 = static_cast<float>(y_pos + glyph.height)/this->height;\n\n\t\t\tfor (unsigned int i = 0; i < glyph.height; i++) {\n\t\t\t\tmemcpy(\n\t\t\t\t\tthis->buffer.get() + ((y_pos + i) * this->width + x_pos),\n\t\t\t\t\timage + (i * glyph.width),\n\t\t\t\t\tglyph.width * sizeof(unsigned char)\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis->update_dirty_area(x_pos, y_pos, glyph.width, glyph.height);\n\n\t\t\tthis->glyphs.emplace(key, entry);\n\t\t\treturn entry;\n\t\t}\n\t}\n\n\tGlyphAtlas::Shelf last_shelf = this->shelves.back();\n\tif (this->height > (last_shelf.y_position + last_shelf.height + required_height)) {\n\t\tthis->shelves.emplace_back(last_shelf.y_position + last_shelf.height, this->width, required_height);\n\n\t\treturn this->set(key, glyph, image);\n\t}\n\n\t// TODO: figure out a clean way to resize the atlas\n\tthrow Error(MSG(err) << \"Not enough space in the atlas\");\n}\n\nvoid GlyphAtlas::update_dirty_area(int x, int y, int width, int height) {\n\tthis->is_dirty = true;\n\tthis->dirty_area = {\n\t\tstd::min(this->dirty_area.x1, x),\n\t\tstd::min(this->dirty_area.y1, y),\n\t\tstd::max(this->dirty_area.x2, x + width),\n\t\tstd::max(this->dirty_area.y2, y + height)};\n}\n\n} // openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/font/glyph_atlas.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <unordered_map>\n#include <vector>\n\n#include \"font.h\"\n\nnamespace openage {\nnamespace renderer {\n\n/**\n * A glyph atlas is used to pack and manage several font glyphs in to a single OpenGL texture.\n *\n * A single glyph atlas can be used to stored glyphs from multiple fonts.\n *\n * Currently, the glyph atlas uses a naive \"Shelf First-Fit\" algorithm based on the article\n * \"A Thousand Ways to Pack the Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing\"\n * by Jukka Jylänki. The article can be found at http://clb.demon.fi/files/RectangleBinPack.pdf\n *\n * This is a very basic algorithm. If there is a need for a more complex packing requirement, the\n * glyph atlas can be easily modified.\n */\nclass GlyphAtlas {\npublic:\n\t/**\n\t * Datastructure for a single atlas entry\n\t */\n\tclass Entry {\n\tpublic:\n\t\tGlyph glyph; //!< The Glyph.\n\t\tfloat u0;    //!< The bottom-left texture coordinate in u-axis.\n\t\tfloat v0;    //!< The bottom-left texture coordinate in v-axis.\n\t\tfloat u1;    //!< The top-right texture coordinate in u-axis.\n\t\tfloat v1;    //!< The top-right texture coordinate in v-axis.\n\t};\n\npublic:\n\t/**\n\t * Creates a glyph atlas with the specified width and height.\n\t *\n\t * Also, creates a OpenGL texture of the same width and height. The contents of this\n\t * glyph atlas is automatically synchronized to the texture (when you bind the texture).\n\t *\n\t * @param width: The width of glyph atlas\n\t * @param height: The height of glyph atlas\n\t */\n\tGlyphAtlas(int width = 1024, int height = 1024);\n\n\tvirtual ~GlyphAtlas();\n\n\t/**\n\t * Binds the OpenGL texture of this glyph atlas at the specified unit.\n\t *\n\t * @param unit: the texture unit.\n\t */\n\tvoid bind(int unit = 0);\n\n\t/**\n\t * Retrieves the atlas entry for a specified font and glyph.\n\t *\n\t * If the particular entry does not exist in the atlas, the glyph atlas requests the font to provide\n\t * the glyph info. The provided info along with the glyph's bitmap data is used to create a new\n\t * cached entry. This entry is then returned.\n\t *\n\t * @param font: The font\n\t * @param codepoint: The glyph whose atlas entry must be retrieved\n\t * @returns The atlas entry.\n\t */\n\tGlyphAtlas::Entry get(Font *font, codepoint_t codepoint);\n\nprivate:\n\tsize_t get_cache_key(Font *font, codepoint_t codepoint) const;\n\n\tGlyphAtlas::Entry set(size_t key, const Glyph &glyph, const unsigned char *image);\n\n\tvoid update_dirty_area(int x, int y, int width, int height);\n\nprivate:\n\tclass Shelf {\n\tpublic:\n\t\tShelf(int position, int width, int height);\n\n\t\tbool check_fits(int required_width, int required_height);\n\n\t\tint reserve(int required_width, int required_height);\n\n\tprivate:\n\t\tfriend class GlyphAtlas;\n\n\t\tint y_position;\n\t\tint width;\n\t\tint height;\n\t\tint remaining_width;\n\t};\n\n\tstruct dirty_rect {\n\t\tint x1; // Bottom-left x-coord\n\t\tint y1; // Bottom-left y-coord\n\t\tint x2; // Top-right y-coord\n\t\tint y2; // Top-right y-coord\n\t};\n\n\t// The width of the glyph atlas\n\tint width;\n\n\t// The height of the glyph atlas\n\tint height;\n\n\t// Flag indicating if any part of the glyph atlas was updated after previous flush to OpenGL texture\n\tbool is_dirty;\n\n\t// The area in the glyph atlas that was updated after the previous flush to OpenGL texture\n\t// This is used in optimizing the amount of data pushed to the texture\n\tdirty_rect dirty_area;\n\n\t// The bitmap image data of all glyphs\n\tstd::unique_ptr<unsigned char> buffer;\n\n\t// The OpenGL texture handle\n\tunsigned int texture_id;\n\n\t// Cache of all entries stored in this glyph atlas.\n\t// A combination of font and the glyph's codepoint is used as the cache key.\n\tstd::unordered_map<size_t, GlyphAtlas::Entry> glyphs;\n\n\t// List of shelves currently used in the atlas\n\tstd::vector<GlyphAtlas::Shelf> shelves;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/font/tests.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"../../testing/testing.h\"\n\n#include \"font_manager.h\"\n#include \"font.h\"\n\nnamespace openage {\nnamespace renderer {\nnamespace tests {\n\nvoid font_manager_test_get_font() {\n\tFontManager font_manager;\n\n\t// FontManager should provide different font instances for different sizes of same font face.\n\tFont *font1 = font_manager.get_font(\"DejaVu Serif\", \"Book\", 12);\n\tFont *font2 = font_manager.get_font(\"DejaVu Serif\", \"Book\", 20);\n\t(font1 != font2) or TESTFAIL;\n\n\t// FontManager should provide the cached font instance\n\tFont *font3 = font_manager.get_font(\"DejaVu Serif\", \"Book\", 12);\n\t(font3 == font1) or TESTFAIL;\n}\n\nvoid font_manager() {\n\tfont_manager_test_get_font();\n}\n\nvoid font_test_font_description() {\n\t// Test default direction, language, script of font_description\n\tfont_description fd1{\"DejaVu Serif\", \"Book\", 12};\n\t(fd1.direction == font_direction::left_to_right) or TESTFAIL;\n\t(fd1.language == \"en\") or TESTFAIL;\n\t(fd1.script == \"Latin\") or TESTFAIL;\n\n\t// Equality check of two different font_description instances\n\tfont_description fd2{\"DejaVu Serif\", \"Book\", 20};\n\t(fd1 != fd2) or TESTFAIL;\n\n\t// a font_description instance constructed from family,style,size should be equal to another with same font file.\n\tfont_description fd3{fd1.font_file, 12};\n\t(fd3 == fd1) or TESTFAIL;\n\n\t// Equality check with copy constructor and assignment operator\n\tfont_description fd4 = fd3;\n\tfont_description fd5{fd4};\n\t(fd4 == fd3) or TESTFAIL;\n\t(fd5 == fd3) or TESTFAIL;\n\n\t// Equality check with different font direction, language, script\n\tfd3.direction = font_direction::right_to_left;\n\tfd4.language = \"fr\";\n\tfd5.script = \"Deva\";\n\t(fd3 != fd1) or TESTFAIL;\n\t(fd4 != fd1) or TESTFAIL;\n\t(fd5 != fd1) or TESTFAIL;\n}\n\nvoid font() {\n\tfont_test_font_description();\n}\n\n}}} // openage::renderer::tests\n"
  },
  {
    "path": "libopenage/renderer/geometry.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"geometry.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nGeometry::Geometry(geometry_t type)\n\t: type(type) {}\n\ngeometry_t Geometry::get_type() const {\n\treturn this->type;\n}\n\nvoid Geometry::update_verts(const std::vector<uint8_t>& verts) {\n\tthis->update_verts_offset(verts, 0);\n}\n\n}} //openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/geometry.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <vector>\n\n\nnamespace openage {\nnamespace renderer {\n\n/// The type of geometry.\nenum class geometry_t {\n\t/// This passes 4 vertices with undefined positions to the shader.\n\t/// The shader has to set the positions itself (e.g. using gl_VertexID in OpenGL).\n\tbufferless_quad,\n\t/// This passes valid geometry defined by a mesh to the shader.\n\tmesh,\n};\n\n/// A class representing geometry to be passed to a draw call.\nclass Geometry {\npublic:\n\tvirtual ~Geometry() = default;\n\n\t/// Returns the type of this geometry.\n\tgeometry_t get_type() const;\n\n\t/// In a meshed geometry, updates the vertex data. The size and type of the vertex data has to be the same as before.\n\t/// If the mesh is indexed, indices will stay the same.\n\t/// @throws if there is a size mismatch between the new and old vertex data\n\tvoid update_verts(std::vector<uint8_t> const &verts);\n\n\t/// In a meshed geometry, updates the vertex data starting from the offset-th vertex. The type of the vertex\n\t/// data has to be the same as it was on initializing the geometry. The size plus the offset cannot exceed the\n\t/// previous size of the vertex data. If the mesh is indexed, indices will stay the same.\n\t/// @throws if there is a size mismatch between the new and old vertex data\n\tvirtual void update_verts_offset(std::vector<uint8_t> const &verts, size_t offset) = 0;\n\nprotected:\n\t/// Initialize the geometry to a given type.\n\texplicit Geometry(geometry_t type);\n\nprivate:\n\tgeometry_t type;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/gui/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tgui.cpp\n)\n\nadd_subdirectory(\"guisys\")\nadd_subdirectory(\"integration\")\n\nadd_sources(libopenage\n\t${QTGUI_SOURCES}\n)\n"
  },
  {
    "path": "libopenage/renderer/gui/gui.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"gui.h\"\n\n#include \"renderer/gui/guisys/public/gui_engine.h\"\n#include \"renderer/gui/guisys/public/gui_input.h\"\n#include \"renderer/gui/guisys/public/gui_renderer.h\"\n#include \"renderer/gui/integration/public/gui_application_with_logger.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/window.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::gui {\n\nGUI::GUI(std::shared_ptr<qtgui::GuiApplication> app,\n         std::shared_ptr<Window> window,\n         const util::Path &source,\n         const util::Path &rootdir,\n         const util::Path &assetdir,\n         const std::shared_ptr<Renderer> &renderer) :\n\tapplication{app},\n\tgui_renderer{std::make_shared<qtgui::GuiRenderer>(window)},\n\tgui_input{std::make_shared<qtgui::GuiInput>(gui_renderer)},\n\tengine{std::make_shared<qtgui::GuiQmlEngine>(gui_renderer)},\n\tsubtree{\n\t\tgui_renderer,\n\t\tengine,\n\t\tsource.resolve_native_path(),\n\t\trootdir.resolve_native_path()},\n\t// input{&gui_renderer, &game_logic_updater}\n    // image_provider_by_filename{\n    //\t&render_updater,\n    //\topenage::gui::GuiGameSpecImageProvider::Type::ByFilename},\n\trenderer{renderer} {\n\t// everything alright before we create the gui stuff?\n\trenderer::opengl::GlContext::check_error();\n\n\tauto size = window->get_size();\n\t// create the appropriate texture\n\tthis->resize(size[0], size[1]);\n\n\tutil::Path shaderdir = assetdir[\"shaders\"];\n\tthis->initialize_render_pass(size[0], size[1], shaderdir);\n\n\twindow->add_resize_callback([this](size_t width, size_t height, double /*scale*/) {\n\t\tthis->resize(width, height);\n\t});\n}\n\nstd::shared_ptr<qtgui::GuiInput> GUI::get_input_handler() const {\n\treturn this->gui_input;\n}\n\nstd::shared_ptr<renderer::RenderPass> GUI::get_render_pass() const {\n\treturn this->render_pass;\n}\n\nvoid GUI::initialize_render_pass(size_t width,\n                                 size_t height,\n                                 const util::Path &shaderdir) {\n\tauto id_vert_file = (shaderdir / \"identity.vert.glsl\").open();\n\tauto id_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tid_vert_file.read());\n\tid_vert_file.close();\n\n\tauto text_frag_file = (shaderdir / \"maptexture.frag.glsl\").open();\n\tauto maptex_frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\ttext_frag_file.read());\n\ttext_frag_file.close();\n\n\tauto quad = this->renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tauto maptex_shader = this->renderer->add_shader({id_shader_src, maptex_frag_shader_src});\n\n\t// GUI draw surface. gets drawn on top of the gameworld in the presenter.\n\tauto output_texture = this->renderer->add_texture(\n\t\tresources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tauto fbo = this->renderer->create_texture_target({output_texture});\n\n\tthis->texture_unif = maptex_shader->new_uniform_input(\"texture\", this->texture);\n\tRenderable display_obj{\n\t\tthis->texture_unif,\n\t\tquad,\n\t\ttrue,\n\t\ttrue,\n\t};\n\n\t// TODO: Rendering into the FBO is a bit redundant right now because we\n\t// just copy the GUI texture into the output texture\n\tthis->render_pass = renderer->add_render_pass({display_obj}, fbo);\n}\n\n\nvoid GUI::resize(size_t width, size_t height) {\n\t// TODO: this texture has to be bigger because of highdpi scaling\n\t//       width and height are not the real window pixel surface.\n\t// Hint: Use effectiveDevicePixelRatio() method\n\tthis->texture = this->renderer->add_texture(\n\t\tresources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\n\t// update new texture to gui renderer\n\tthis->gui_renderer->resize(width, height);\n\tthis->gui_renderer->set_texture(this->texture);\n\n\t// update the fbo size of the render pass\n\tif (this->render_pass) {\n\t\tauto output_texture = this->renderer->add_texture(\n\t\t\tresources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\t\tauto fbo = renderer->create_texture_target({output_texture});\n\n\t\t// pass the new texture to shader uniform\n\t\tthis->texture_unif->update(\"texture\", this->texture);\n\n\t\tthis->render_pass->set_target(fbo);\n\t}\n}\n\n\nvoid GUI::render() {\n\t// this->render_updater.process_callbacks();\n\n\tthis->gui_renderer->render();\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/gui.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"renderer/texture.h\"\n#include <memory>\n#include <string>\n\n#include \"renderer/gui/guisys/public/gui_subtree.h\"\n\nnamespace qtgui {\nclass GuiInput;\nclass GuiRenderer;\nclass GuiQmlEngine;\nclass GuiApplication;\n} // namespace qtgui\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer {\nclass RenderPass;\nclass Renderer;\nclass Window;\n\nclass UniformInput;\n\nnamespace gui {\n\n/**\n * Interface (= HUD) of the game.\n *\n * openage's GUI is Qt-based and uses QML definitions\n * to create buttons, etc. Qt draws the GUI into a texture\n * that is then drawn into the screen by our wn renderer.\n */\nclass GUI {\npublic:\n\texplicit GUI(std::shared_ptr<qtgui::GuiApplication> app,\n\t             std::shared_ptr<Window> window,\n\t             const util::Path &source,\n\t             const util::Path &rootdir,\n\t             const util::Path &assetdir,\n\t             const std::shared_ptr<Renderer> &renderer);\n\tvirtual ~GUI() = default;\n\n\t/**\n\t * Get the input handler of the GUI.\n\t *\n\t * @return Input handler of the GUI.\n\t */\n\tstd::shared_ptr<qtgui::GuiInput> get_input_handler() const;\n\n\t/**\n\t * Get the render pass of the GUI.\n\t *\n\t * The render pass has the GUI texture filled by Qt assigned as\n\t * a renderable.\n\t *\n\t * @return Render pass of the GUI.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass() const;\n\n\t/**\n\t * Render the GUI texture.\n\t */\n\tvoid render();\n\nprivate:\n\t/**\n\t * Create the render pass of the GUI.\n\t *\n\t * Called during initialization of the GUI.\n\t *\n\t * @param width Width of the GUI.\n\t * @param height Height of the GUI.\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(size_t width,\n\t                            size_t height,\n\t                            const util::Path &shaderdir);\n\n\t/**\n\t * Resize the GUI. This updates the GUI texture size and propagates\n\t * the changes to the GUI render pass.\n\t *\n\t * @param width New width of the GUI.\n\t * @param height New height of the GUI.\n\t */\n\tvoid resize(size_t width, size_t height);\n\n\t/**\n\t * Reference to the Qt GUI application singleton.\n\t */\n\tstd::shared_ptr<qtgui::GuiApplication> application;\n\n\t/**\n\t * Qt-based renderer for the GUI texture. Draws into\n\t * \\p gui_texture.\n\t */\n\tstd::shared_ptr<qtgui::GuiRenderer> gui_renderer;\n\n\t/**\n\t * Input handler for the GUI window\n\t */\n\tstd::shared_ptr<qtgui::GuiInput> gui_input;\n\n\t/**\n\t * Qt QML Engine wrapper.\n\t */\n\tstd::shared_ptr<qtgui::GuiQmlEngine> engine;\n\n\t/**\n\t * Manages Qt QML components and items.\n\t */\n\tqtgui::GuiSubtree subtree;\n\n\t/**\n\t * Reference to the openage renderer.\n\t * Used to fetch texture objects for the GUI texture.\n\t */\n\tstd::shared_ptr<Renderer> renderer;\n\n\t/**\n\t * Uniform input for the GUI texture handle.\n\t */\n\tstd::shared_ptr<renderer::UniformInput> texture_unif;\n\n\t/**\n\t * GUI texture handle. The GUI renderer ( \\p gui_renderer ) draws\n\t * into this texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> texture;\n\n\t/**\n\t * Render pass for the whole GUI. The GUI texture is attached to\n\t * this pass via a \\p renderer::resources::Renderable.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n};\n\n} // namespace gui\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/CMakeLists.txt",
    "content": "list(APPEND QT_SDL_SOURCES\n\t${CMAKE_CURRENT_SOURCE_DIR}/link/gui_item.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/link/gui_list_model.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/link/gui_property_map_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/link/gui_singleton_item.cpp\n)\n\nlist(APPEND QTGUI_SOURCES\n    ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/deferred_initial_constant_property_values.cpp\n    ${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/gui_live_reloader.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher_worker.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/livereload/recursive_directory_watcher.cpp\n)\n\nlist(APPEND QTGUI_SOURCES\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_application_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_ctx_setup.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_engine_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_input_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_renderer_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_rendering_setup_routines.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/gui_subtree_impl.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/private/opengl_debug_logger.cpp\n)\n\nlist(APPEND QTGUI_SOURCES\n\t${CMAKE_CURRENT_SOURCE_DIR}/public/gui_application.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/public/gui_engine.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/public/gui_input.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/public/gui_renderer.cpp\n\t${CMAKE_CURRENT_SOURCE_DIR}/public/gui_subtree.cpp\n)\n\nset(QTGUI_SOURCES ${QTGUI_SOURCES} PARENT_SCOPE)\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_item.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_item.h\"\n\n\nnamespace qtgui {\n\n\nQString name_tidier(const char *name) {\n\tQString cleaner_name = QString::fromLatin1(name);\n\tcleaner_name.remove(QRegularExpression(\"qtgui|PersistentCoreHolder\"));\n\treturn cleaner_name;\n}\n\nGuiItemQObject::GuiItemQObject(QObject *parent) :\n\tQObject{parent},\n\tGuiItemBase{} {\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_item.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cassert>\n#include <functional>\n#include <memory>\n#include <type_traits>\n#include <typeinfo>\n#include <utility>\n\n#include <QDebug>\n#include <QMetaType>\n#include <QObject>\n#include <QRegularExpression>\n\n#include \"renderer/gui/guisys/link/gui_item_link.h\"\n#include \"renderer/gui/guisys/link/qtgui_checked_static_cast.h\"\n#include \"renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h\"\n\n\nnamespace qtgui {\n\n/**\n * Cleans a text from unneeded content like \"qtgui\".\n */\nQString name_tidier(const char *name);\n\n\n/**\n * Helper to hide the type of the core.\n */\nclass PersistentCoreHolderBase {\npublic:\n\tvirtual ~PersistentCoreHolderBase() {\n\t}\n\n\t/**\n\t * Establish link from the core to the shell.\n\t * Core must have a public 'gui::GuiItemLink *gui_link` member.\n\t */\n\tvirtual void adopt_shell(GuiItemLink *link) = 0;\n};\n\ntemplate <typename T>\nclass PersistentCoreHolder : public PersistentCoreHolderBase {\npublic:\n\tPersistentCoreHolder(std::unique_ptr<T> core) :\n\t\tPersistentCoreHolderBase{},\n\t\tcore{std::move(core)} {\n\t}\n\n\texplicit PersistentCoreHolder() :\n\t\tPersistentCoreHolderBase{} {\n\t}\n\n\tvirtual void adopt_shell(GuiItemLink *link) override {\n\t\tthis->core->gui_link = link;\n\t}\n\n\tstd::unique_ptr<T> core;\n};\n\n\nclass GuiItemBase : public DeferredInitialConstantPropertyValues {\npublic:\n\tvirtual ~GuiItemBase() {\n\t}\n\nprivate:\n\tfriend class GuiLiveReloader;\n\tfriend class GuiSubtreeImpl;\n\n\ttemplate <typename>\n\tfriend class GuiItemMethods;\n\n\ttemplate <typename>\n\tfriend class GuiItemCoreInstantiator;\n\n\ttemplate <typename>\n\tfriend class GuiItemListModel;\n\n\t/**\n\t * Creates and returns a core object inside a holder.\n\t */\n\tstd::unique_ptr<PersistentCoreHolderBase> instantiate_core() {\n\t\treturn this->instantiate_core_func();\n\t}\n\n\t/**\n\t * Attaches to a core object.\n\t */\n\tvoid adopt_core(PersistentCoreHolderBase *holder, const QString &tag) {\n\t\tthis->do_adopt_core(holder, tag);\n\n\t\tif (this->on_core_adopted_func)\n\t\t\tthis->on_core_adopted_func();\n\n\t\tthis->on_core_adopted();\n\t}\n\n\tvoid do_adopt_core(PersistentCoreHolderBase *holder, const QString &tag) {\n\t\tthis->do_adopt_core_func(holder, tag);\n\t}\n\n\tvirtual void on_core_adopted() {\n\t}\n\nprotected:\n\tstd::function<std::unique_ptr<PersistentCoreHolderBase>()> instantiate_core_func;\n\tstd::function<void(PersistentCoreHolderBase *, const QString &)> do_adopt_core_func;\n\tstd::function<void()> on_core_adopted_func;\n};\n\ntemplate <typename T>\nclass GuiItemOrigin {\n\ttemplate <typename>\n\tfriend class GuiItemMethods;\n\n\ttemplate <typename>\n\tfriend class GuiItemCoreInstantiator;\n\n\t/**\n\t * Implementation object.\n\t */\n\tPersistentCoreHolder<typename Unwrap<T>::Type> *holder;\n};\n\n/**\n * Member function of the GuiPersistentItem\n */\ntemplate <typename T>\nclass GuiItemMethods {\npublic:\n#ifndef NDEBUG\n\tvirtual ~GuiItemMethods() {\n\t}\n#endif\n\n\t/**\n\t * Get core.\n\t */\n\ttemplate <typename U>\n\tU *get() const {\n\t\tif (checked_static_cast<T *>(this)->holder->core) {\n\t\t\tassert(checked_static_cast<U *>(checked_static_cast<T *>(this)->holder->core.get()));\n\t\t\treturn checked_static_cast<U *>(checked_static_cast<T *>(this)->holder->core.get());\n\t\t}\n\t\telse {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\n\t/**\n\t * Get core.\n\t */\n\ttemplate <typename U>\n\tU *get() {\n\t\tif (checked_static_cast<T *>(this)->holder->core) {\n\t\t\tassert(checked_static_cast<U *>(checked_static_cast<T *>(this)->holder->core.get()));\n\t\t\treturn checked_static_cast<U *>(checked_static_cast<T *>(this)->holder->core.get());\n\t\t}\n\t\telse {\n\t\t\treturn nullptr;\n\t\t}\n\t}\n\n\t/**\n\t * Invoke a function in the logic thread (the thread of the core of this object).\n\t */\n\ttemplate <typename F, typename... Args>\n\tvoid i(F f, Args &&...args) {\n\t\tGuiItemBase *base = checked_static_cast<GuiItemBase *>(checked_static_cast<T *>(this));\n\t}\n\nprotected:\n\t/**\n\t * Set property.\n\t */\n\ttemplate <typename F, typename P, typename A>\n\tvoid s(F f, P &p, A &&arg) {\n\t\tif (p != arg) {\n\t\t\tp = std::forward<A>(arg);\n\t\t\tthis->assign_while_catching_constant_properies_inits(f, p);\n\t\t}\n\t}\n\n\t/**\n\t * Set property even if it's the same.\n\t */\n\ttemplate <typename F, typename P, typename A>\n\tvoid sf(F f, P &p, A &&arg) {\n\t\tp = std::forward<A>(arg);\n\t\tthis->assign_while_catching_constant_properies_inits(f, p);\n\t}\n\nprivate:\n\t/**\n\t * Static QML properties assignments during the init phase are stored for a batch assignment at the end of the init phase.\n\t */\n\ttemplate <typename F, typename A>\n\tvoid assign_while_catching_constant_properies_inits(F f, A &&arg) {\n\t\tGuiItemBase *base = checked_static_cast<GuiItemBase *>(checked_static_cast<T *>(this));\n\n\t\tstatic_assert_about_unwrapping<F, decltype(unwrap(checked_static_cast<T *>(this))), decltype(unwrap_if_can(arg))>();\n\t}\n};\n\nclass GuiItemQObject : public QObject\n\t, public GuiItemBase\n\t, public GuiItemLink {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiItemQObject(QObject *parent = nullptr);\n};\n\ntemplate <typename T>\nclass GuiItemCoreInstantiator : public GuiItemMethods<T> {\npublic:\n\t/**\n\t * Sets up a factory for the type T.\n\t */\n\texplicit GuiItemCoreInstantiator(GuiItemBase *item_base) :\n\t\tGuiItemMethods<T>{} {\n\t\tusing namespace std::placeholders;\n\n\t\titem_base->instantiate_core_func = std::bind(&GuiItemCoreInstantiator::instantiate_core, this);\n\t\titem_base->do_adopt_core_func = std::bind(&GuiItemCoreInstantiator::do_adopt_core, this, _1, _2);\n\t}\n\nprivate:\n\t/**\n\t * Creates and returns a core object of type Unwrap<T>::Type inside a holder.\n\t */\n\tstd::unique_ptr<PersistentCoreHolderBase> instantiate_core() {\n\t\tT *origin = checked_static_cast<T *>(this);\n\n\t\tif (origin->holder) {\n\t\t\treturn std::unique_ptr<PersistentCoreHolderBase>();\n\t\t}\n\t\telse {\n\t\t\tauto core = std::make_unique<typename Unwrap<T>::Type>(origin);\n\t\t\treturn std::make_unique<typename std::remove_reference<decltype(*origin->holder)>::type>(std::move(core));\n\t\t}\n\t}\n\n\t/**\n\t * Attaches to a core object.\n\t */\n\tvoid do_adopt_core(PersistentCoreHolderBase *holder, const QString &tag) {\n\t\tassert(holder);\n\n\t\tT *origin = checked_static_cast<T *>(this);\n\n\t\tif (origin->holder) {\n\t\t\tqFatal(\"Error in QML code: GuiLiveReloader was asked to use same tag '%s' for multiple objects.\", qUtf8Printable(tag));\n\t\t}\n\t\telse {\n\t\t\tif (typeid(decltype(*origin->holder)) != typeid(*holder)) {\n\t\t\t\tqFatal(\n\t\t\t\t\t\"Error in QML code: GuiLiveReloader was asked \"\n\t\t\t\t\t\"to restore '%s' into different type '%s' \"\n\t\t\t\t\t\"using tag '%s'.\",\n\t\t\t\t\tqUtf8Printable(name_tidier(typeid(decltype(*origin->holder)).name())),\n\t\t\t\t\tqUtf8Printable(name_tidier(typeid(*holder).name())),\n\t\t\t\t\tqUtf8Printable(tag));\n\t\t\t}\n\t\t\telse {\n\t\t\t\torigin->holder = checked_static_cast<decltype(origin->holder)>(holder);\n\t\t\t\torigin->holder->adopt_shell(origin);\n\t\t\t}\n\t\t}\n\t}\n\n\tGuiItemCoreInstantiator(const GuiItemCoreInstantiator &) = delete;\n\tGuiItemCoreInstantiator &operator=(const GuiItemCoreInstantiator &) = delete;\n};\n\n/**\n * Base for shell QObjects that need corresponding cores to be kept alive across GUI reloads.\n *\n * These shell objects are destroyed by Qt during GUI reload.\n * Their cores are adopted by new shells identified by the 'LR.tag' in QML code.\n *\n * For each descendant the specializations of Wrap and Unwrap must be provided.\n * Corresponding cores must have public 'gui::GuiItemLink *gui_link` members.\n *\n * @tparam T type of the concrete shell class (pass the descendant class)\n */\ntemplate <typename T>\nclass GuiItem : public GuiItemOrigin<T>\n\t, public GuiItemCoreInstantiator<T> {\npublic:\n\t/**\n\t * Creates an empty QObject shell for a core that is wrappable into a type *T.\n\t */\n\texplicit GuiItem(GuiItemBase *item_base) :\n\t\tGuiItemOrigin<T>{},\n\t\tGuiItemCoreInstantiator<T>{item_base} {\n\t}\n};\n\ntemplate <typename T>\nclass GuiItemInterface : public GuiItemOrigin<T>\n\t, public GuiItemMethods<T> {\npublic:\n\texplicit GuiItemInterface() :\n\t\tGuiItemOrigin<T>{},\n\t\tGuiItemMethods<T>{} {\n\t}\n};\n\nnamespace {\nclass NullClass {\n\tNullClass() = delete;\n};\n} // namespace\n\n/**\n * Shadows inherited member functions.\n */\ntemplate <typename T>\nclass Shadow : public T {\npublic:\n\tShadow(QObject *parent = nullptr) :\n\t\tT{parent} {\n\t}\n\n\tvoid get(NullClass);\n\tvoid i(NullClass);\n\tvoid s(NullClass);\n\tvoid sf(NullClass);\n};\n\n/**\n * Switches the factory to production of the instances of the derived class.\n */\ntemplate <typename T, typename P>\nclass Inherits : public Shadow<T>\n\t, public GuiItemCoreInstantiator<P> {\npublic:\n\tusing Shadow<T>::get;\n\tusing GuiItemCoreInstantiator<P>::get;\n\tusing Shadow<T>::i;\n\tusing GuiItemCoreInstantiator<P>::i;\n\tusing Shadow<T>::s;\n\tusing GuiItemCoreInstantiator<P>::s;\n\tusing Shadow<T>::sf;\n\tusing GuiItemCoreInstantiator<P>::sf;\n\n\tInherits(QObject *parent = nullptr) :\n\t\tShadow<T>{parent},\n\t\tGuiItemCoreInstantiator<P>{this} {\n\t}\n\n\tvirtual ~Inherits() {\n\t}\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_item_link.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <type_traits>\n#include <utility>\n\n#include \"renderer/gui/guisys/link/qtgui_checked_static_cast.h\"\n\n\nnamespace qtgui {\n\n/**\n * Base for QObject wrapper of the domain-specific classes.\n */\nclass GuiItemLink {\n#ifndef NDEBUG\npublic:\n\tvirtual ~GuiItemLink() {\n\t}\n#endif\n};\n\n/**\n * If the core 'MyClass' has a shell 'MyClassLink' then 'Wrap<MyClass>' must have a 'using Type = MyClassLink'\n */\ntemplate <typename U>\nstruct Wrap {\n};\n\n/**\n * If the core 'MyClass' has a shell 'MyClassLink' then 'Unwrap<MyClassLink>' must have a 'using Type = MyClass'\n */\ntemplate <typename T>\nstruct Unwrap {\n};\n\ntemplate <typename T>\ntypename Unwrap<T>::Type *unwrap(T *t) {\n\treturn t ? t->template get<typename Unwrap<T>::Type>() : nullptr;\n}\n\ntemplate <typename T>\nconst typename Unwrap<T>::Type *unwrap(const T *t) {\n\treturn t ? t->template get<typename Unwrap<T>::Type>() : nullptr;\n}\n\ntemplate <typename U>\nconst typename Wrap<U>::Type *wrap(const U *u) {\n\treturn checked_static_cast<typename Wrap<U>::Type *>(u->gui_link);\n}\n\n/**\n * QML singletons should be unwrapped differently because several QML singletons can point to one C++ singleton/object. But not implementing that for now.\n */\nclass GuiSingletonItem;\n\ntemplate <typename U>\ntypename Wrap<U>::Type *wrap(U *u, typename std::enable_if<!std::is_base_of<GuiSingletonItem, typename Wrap<U>::Type>::value>::type * = nullptr) {\n\treturn u ? checked_static_cast<typename Wrap<U>::Type *>(u->gui_link) : nullptr;\n}\n\ntemplate <typename U>\ntypename Wrap<U>::Type *wrap(U *u, typename std::enable_if<std::is_base_of<GuiSingletonItem, typename Wrap<U>::Type>::value>::type * = nullptr) {\n\treturn u ? checked_static_cast<typename Wrap<U>::Type *>(u->gui_link) : nullptr;\n}\n\ntemplate <typename P>\nconstexpr P &&wrap_if_can(typename std::remove_reference<P>::type &&p) noexcept {\n\treturn std::forward<P>(p);\n}\n\ntemplate <typename T>\nT wrap_if_can(typename Unwrap<typename std::remove_pointer<T>::type>::Type *u) {\n\treturn wrap(u);\n}\n\ntemplate <typename P>\nP unwrap_if_can(P &p) {\n\treturn p;\n}\n\ntemplate <typename T, typename Unwrap<T>::Type * = nullptr>\ntypename Unwrap<T>::Type *unwrap_if_can(T *t) {\n\treturn unwrap(t);\n}\n\n/**\n * Checking that callable can be called with given argument types.\n */\nstruct can_call_test {\n\ttemplate <typename F, typename... Args>\n\tstatic decltype(std::declval<F>()(std::declval<Args>()...), std::true_type()) f(int);\n\n\ttemplate <typename F, typename... Args>\n\tstatic std::false_type f(...);\n};\n\n/**\n * Evaluates to true if callable can be called with given argument types.\n *\n * @tparam F callable\n * @tparam A arguments to test against the callable\n */\ntemplate <typename F, typename... Args>\nstruct can_call : decltype(can_call_test::f<F, Args...>(0)) {\n};\n\n/**\n * To print our own error message that warns about possible cause of the argument mismatch.\n *\n * Unwrapping uses type trait \"Unwrap\" that is defined in the header with corresponding type.\n * If the header is not included, then compiler considers it as a basic type and tries to pass it wrapped to a function that expects an unwrapped type.\n *\n * @tparam F callable\n * @tparam A arguments to test against the callable\n */\ntemplate <typename F, typename... Args>\nconstexpr void static_assert_about_unwrapping() {\n\tstatic_assert(can_call<F, Args...>{}, \"One of possible causes: if you're passing SomethingLink*, then don't forget to #include \\\"something_link.h\\\".\");\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_item_list_model.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"renderer/gui/guisys/link/gui_item.h\"\n#include \"renderer/gui/guisys/link/gui_list_model.h\"\n#include \"renderer/gui/guisys/link/gui_property_map_impl.h\"\n\n\nnamespace qtgui {\n\n/**\n * A shell object fir cores inherited from GuiPropertyMap\n *\n * Core can use a key-value interface while in QML it looks like a ListModel.\n */\ntemplate <typename T>\nclass GuiItemListModel : public GuiItem<T> {\npublic:\n\texplicit GuiItemListModel(GuiItemBase *item_base) :\n\t\tGuiItem<T>{item_base} {\n\t\titem_base->on_core_adopted_func = std::bind(&GuiItemListModel::on_core_adopted, this);\n\t}\n\nprivate:\n\tvoid on_core_adopted() {\n\t\tthis->establish_from_gui_propagation();\n\t\tthis->do_initial_to_gui_propagation();\n\t\tthis->establish_to_gui_propagation();\n\t}\n\n\tvoid establish_from_gui_propagation() {\n\t\tauto _this = checked_static_cast<T *>(this);\n\n\t\tQObject::connect(_this, &GuiListModel::changed_from_gui, [_this](const QByteArray &name, const QVariant &value) {\n\t\t\temit _this->game_logic_caller.in_game_logic_thread_blocking([&] {\n\t\t\t\tunwrap(_this)->impl->setProperty(name, value);\n\t\t\t});\n\t\t});\n\t}\n\n\tvoid establish_to_gui_propagation() {\n\t\tauto _this = checked_static_cast<T *>(this);\n\n\t\tauto properties = unwrap(_this)->impl.get();\n\t\tQObject::connect(properties, &GuiPropertyMapImpl::property_changed, _this, &GuiListModel::on_property_changed);\n\t}\n\n\tvoid do_initial_to_gui_propagation() {\n\t\tauto _this = checked_static_cast<T *>(this);\n\n\t\tauto properties = unwrap(_this)->impl.get();\n\t\tauto property_names = properties->dynamicPropertyNames();\n\n\t\tstd::vector<std::tuple<QByteArray, QVariant>> values;\n\t\tvalues.reserve(property_names.size());\n\n\t\tstd::transform(std::begin(property_names), std::end(property_names), std::back_inserter(values), [properties](const QByteArray &name) {\n\t\t\treturn std::make_tuple(name, properties->property(name));\n\t\t});\n\n\t\t_this->set(values);\n\t}\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_list_model.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_list_model.h\"\n\n#include <algorithm>\n\n\nnamespace qtgui {\n\nGuiListModel::GuiListModel(QObject *parent) :\n\tQAbstractListModel{parent} {\n}\n\nGuiListModel::~GuiListModel() = default;\n\nvoid GuiListModel::set(const std::vector<std::tuple<QByteArray, QVariant>> &values) {\n\tthis->beginResetModel();\n\tthis->values = values;\n\tthis->endResetModel();\n}\n\nvoid GuiListModel::on_property_changed(const QByteArray &name, const QVariant &value) {\n\tauto foundIt = std::find_if(std::begin(this->values), std::end(this->values), [&name](std::tuple<QByteArray, QVariant> &p) {\n\t\treturn std::get<QByteArray>(p) == name;\n\t});\n\n\tif (foundIt != std::end(this->values)) {\n\t\tauto &current_value = std::get<QVariant>(*foundIt);\n\n\t\tif (current_value != value) {\n\t\t\tcurrent_value = value;\n\n\t\t\tauto i = this->index(std::distance(std::begin(this->values), foundIt));\n\t\t\temit this->dataChanged(i, i, {Qt::EditRole});\n\t\t}\n\t}\n\telse {\n\t\tthis->beginInsertRows(QModelIndex(), this->values.size(), this->values.size());\n\t\tthis->values.emplace(std::end(this->values), name, value);\n\t\tthis->endInsertRows();\n\t}\n}\n\nint GuiListModel::rowCount(const QModelIndex &) const {\n\treturn values.size();\n}\n\nQt::ItemFlags GuiListModel::flags(const QModelIndex &index) const {\n\treturn Qt::ItemIsEditable | this->QAbstractListModel::flags(index);\n}\n\nQVariant GuiListModel::data(const QModelIndex &index, int role) const {\n\tswitch (role) {\n\tcase Qt::DisplayRole:\n\t\treturn std::get<QByteArray>(this->values[index.row()]);\n\n\tcase Qt::EditRole:\n\t\treturn std::get<QVariant>(this->values[index.row()]);\n\n\tdefault:\n\t\treturn QVariant{};\n\t}\n}\n\nbool GuiListModel::setData(const QModelIndex &index, const QVariant &value, int role) {\n\tif (role == Qt::EditRole) {\n\t\tauto &property = this->values[index.row()];\n\t\temit this->changed_from_gui(std::get<QByteArray>(property), std::get<QVariant>(property) = value);\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_list_model.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <tuple>\n#include <vector>\n\n#include <QAbstractListModel>\n\n#include \"renderer/gui/guisys/link/gui_item.h\"\n\n\nnamespace qtgui {\n\n/**\n * Adapts core that uses a property map to QAbstractListModel interface.\n */\nclass GuiListModel : public QAbstractListModel\n\t, public GuiItemBase\n\t, public GuiItemLink {\n\tQ_OBJECT\n\npublic:\n\tGuiListModel(QObject *parent = nullptr);\n\tvirtual ~GuiListModel();\n\n\tvoid set(const std::vector<std::tuple<QByteArray, QVariant>> &values);\n\npublic slots:\n\tvoid on_property_changed(const QByteArray &name, const QVariant &value);\n\nsignals:\n\tvoid changed_from_gui(const char *name, const QVariant &value);\n\nprivate:\n\tvirtual int rowCount(const QModelIndex &) const override;\n\tvirtual Qt::ItemFlags flags(const QModelIndex &index) const override;\n\tvirtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;\n\tvirtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;\n\n\tstd::vector<std::tuple<QByteArray, QVariant>> values;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_property_map_impl.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_property_map_impl.h\"\n\n#include <QDynamicPropertyChangeEvent>\n#include <QVariant>\n\n#include \"renderer/gui/guisys/link/qtgui_checked_static_cast.h\"\n\n\nnamespace qtgui {\n\nGuiPropertyMapImpl::GuiPropertyMapImpl() :\n\tQObject{} {\n}\n\nGuiPropertyMapImpl::~GuiPropertyMapImpl() = default;\n\nbool GuiPropertyMapImpl::event(QEvent *e) {\n\tif (e->type() == QEvent::DynamicPropertyChange) {\n\t\tauto property_name = checked_static_cast<QDynamicPropertyChangeEvent *>(e)->propertyName();\n\t\temit this->property_changed(property_name, this->property(property_name));\n\t\treturn true;\n\t}\n\n\treturn this->QObject::event(e);\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_property_map_impl.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QObject>\n\n\nnamespace qtgui {\n\nclass GuiPropertyMapImpl : public QObject {\n\tQ_OBJECT\n\npublic:\n\tGuiPropertyMapImpl();\n\tvirtual ~GuiPropertyMapImpl();\n\nsignals:\n\tvoid property_changed(const QByteArray &name, const QVariant &value);\n\nprivate:\n\tvirtual bool event(QEvent *e) override;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_singleton_item.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_singleton_item.h\"\n\n\nnamespace qtgui {\n\nGuiSingletonItem::GuiSingletonItem(QObject *parent) :\n\tQObject{parent},\n\tGuiItemLink{} {\n}\n\nGuiSingletonItem::~GuiSingletonItem() = default;\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/gui_singleton_item.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QObject>\n\n#include \"renderer/gui/guisys/link/gui_item_link.h\"\n\n\nnamespace qtgui {\n\nclass GuiSingletonItem : public QObject\n\t, public GuiItemLink {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiSingletonItem(QObject *parent = nullptr);\n\tvirtual ~GuiSingletonItem();\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/link/qtgui_checked_static_cast.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cassert>\n\n\nnamespace qtgui {\n\ntemplate <typename T, typename U>\nT checked_static_cast(U *u) {\n\tassert(dynamic_cast<T>(u));\n\treturn static_cast<T>(u);\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_application_impl.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_application_impl.h\"\n\n#include <cassert>\n#include <locale>\n\n#include <QSurfaceFormat>\n#include <QtDebug>\n#include <QtGlobal>\n\nnamespace qtgui {\n\nstd::weak_ptr<GuiApplicationImpl> GuiApplicationImpl::instance;\n\nstd::shared_ptr<GuiApplicationImpl> GuiApplicationImpl::get() {\n\tstd::shared_ptr<GuiApplicationImpl> candidate = GuiApplicationImpl::instance.lock();\n\n\tassert(!candidate || std::this_thread::get_id() == candidate->owner);\n\n\t// Ensure that OpenGL is used and not OpenGL ES.\n\t// This occurred in macos. See issue #1177 (PR #1179)\n\tif (!candidate) {\n\t\tQSurfaceFormat format;\n\t\tformat.setRenderableType(QSurfaceFormat::OpenGL);\n\t\tQSurfaceFormat::setDefaultFormat(format);\n\t}\n\n\treturn candidate ? candidate : std::shared_ptr<GuiApplicationImpl>{new GuiApplicationImpl};\n}\n\nGuiApplicationImpl::~GuiApplicationImpl() {\n\tassert(std::this_thread::get_id() == this->owner);\n}\n\nvoid GuiApplicationImpl::processEvents() {\n\tassert(std::this_thread::get_id() == this->owner);\n#ifndef __APPLE__\n\tthis->app.processEvents();\n#endif\n}\n\nnamespace {\nint argc = 1;\nchar arg[] = \"qtgui\";\nchar *argv = &arg[0];\n} // namespace\n\nGuiApplicationImpl::GuiApplicationImpl() :\n#ifndef NDEBUG\n\towner{std::this_thread::get_id()},\n#endif\n\tapp{argc, &argv} {\n\t// Set locale back to POSIX for the decimal point parsing (see qcoreapplication.html#locale-settings).\n\tstd::locale::global(std::locale().combine<std::numpunct<char>>(std::locale::classic()));\n\n\tqInfo() << \"Compiled with Qt\" << QT_VERSION_STR << \"and run with Qt\" << qVersion();\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_application_impl.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <thread>\n\n#include <QGuiApplication>\n\nnamespace qtgui {\n\n/**\n * Houses gui logic event queue.\n *\n * To launch it in a dedicated thread, use qtgui::GuiDedicatedThread instead.\n */\nclass GuiApplicationImpl {\npublic:\n\tstatic std::shared_ptr<GuiApplicationImpl> get();\n\n\t~GuiApplicationImpl();\n\n\tvoid processEvents();\n\nprivate:\n\tGuiApplicationImpl();\n\n\tGuiApplicationImpl(const GuiApplicationImpl &) = delete;\n\tGuiApplicationImpl &operator=(const GuiApplicationImpl &) = delete;\n\n#ifndef NDEBUG\n\tconst std::thread::id owner;\n#endif\n\n\tQGuiApplication app;\n\n\tstatic std::weak_ptr<GuiApplicationImpl> instance;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_ctx_setup.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_ctx_setup.h\"\n\n#include <cassert>\n\n#include <QOpenGLContext>\n#include <QOpenGLDebugLogger>\n#include <QOpenGLFramebufferObject>\n#include <QWindow>\n\n#include \"renderer/gui/guisys/private/opengl_debug_logger.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/window.h\"\n\nnamespace qtgui {\n\nCtxExtractionException::CtxExtractionException(const std::string &what_arg) :\n\tstd::runtime_error{what_arg} {\n}\n\nconst std::shared_ptr<QOpenGLContext> &CtxExtractionMode::get_ctx() const {\n\treturn this->ctx;\n}\n\nGlGuiRenderingContext::GlGuiRenderingContext(std::shared_ptr<openage::renderer::Window> window) :\n\tCtxExtractionMode{},\n\toffscreen_surface{},\n\twindow{window} {\n\tauto win = std::dynamic_pointer_cast<openage::renderer::opengl::GlWindow>(this->window);\n\tauto window_ctx = win->get_context()->get_raw_context();\n\tthis->ctx = std::make_shared<QOpenGLContext>();\n\tthis->ctx->setShareContext(window_ctx.get());\n\tthis->ctx->setFormat(window->get_qt_window()->requestedFormat());\n\tthis->ctx->create();\n\n\tthis->offscreen_surface.setFormat(this->ctx->format());\n\tthis->offscreen_surface.create();\n}\n\nvoid GlGuiRenderingContext::pre_render() {\n\t// Make the share context current, so Qt draw into its window.\n\tif (!this->ctx->makeCurrent(&this->offscreen_surface)) {\n\t\tassert(false);\n\t\treturn;\n\t}\n\n\t// Our renderer saves the current shader program loaded in OpenGL\n\t// to prevent unnecessary reloads. However, Qt loads its own shaders\n\t// when the GUI is rendered, so any shaders present in our own render\n\t// passes have to be reloaded. We do this by setting the current program to nullptr.\n\tauto win = std::dynamic_pointer_cast<openage::renderer::opengl::GlWindow>(this->window);\n\tauto window_ctx = win->get_context();\n\twindow_ctx->set_current_program(nullptr);\n}\n\nvoid GlGuiRenderingContext::post_render() {\n\tQOpenGLFramebufferObject::bindDefault();\n\tthis->ctx->functions()->glFlush();\n\n\t// switch back to the window context, so our own renderer can continue drawing\n\tthis->ctx->doneCurrent();\n\tauto window_ctx = std::dynamic_pointer_cast<openage::renderer::opengl::GlWindow>(this->window)->get_context()->get_raw_context();\n\tif (!window_ctx->makeCurrent(this->window->get_qt_window().get())) {\n\t\tassert(false);\n\t\treturn;\n\t}\n}\n\nQQuickGraphicsDevice GlGuiRenderingContext::get_device() {\n\treturn QQuickGraphicsDevice::fromOpenGLContext(this->ctx.get());\n}\n\nGuiUniqueRenderingContext::GuiUniqueRenderingContext(std::shared_ptr<openage::renderer::Window> window) :\n\tCtxExtractionMode{} {\n\tauto format = window->get_qt_window()->format();\n\tthis->ctx->setFormat(format);\n\tthis->ctx->create();\n\tassert(this->ctx->isValid());\n\n\tstd::shared_ptr<QWindow> w = window->get_qt_window();\n\n\tif (this->ctx->makeCurrent(w.get())) {\n\t\treturn;\n\t}\n\n\tthrow CtxExtractionException(\"adding GUI to the main rendering context failed\");\n}\n\nvoid GuiUniqueRenderingContext::pre_render() {\n}\n\nvoid GuiUniqueRenderingContext::post_render() {\n}\n\nQQuickGraphicsDevice GuiUniqueRenderingContext::get_device() {\n\treturn QQuickGraphicsDevice::fromOpenGLContext(this->ctx.get());\n}\n\nGuiSeparateRenderingContext::GuiSeparateRenderingContext() :\n\tCtxExtractionMode{} {\n\tthis->main_ctx.create();\n\tassert(this->main_ctx.isValid());\n\n\tauto context_debug_parameters = get_current_opengl_debug_parameters(this->main_ctx);\n\n\tthis->ctx->setFormat(this->main_ctx.format());\n\tthis->ctx->setShareContext(&this->main_ctx);\n\tthis->ctx->create();\n\tassert(this->ctx->isValid());\n\tassert(!(this->main_ctx.format().options() ^ this->ctx->format().options()).testFlag(QSurfaceFormat::DebugContext));\n\n\tthis->offscreen_surface.setFormat(this->ctx->format());\n\tthis->offscreen_surface.create();\n\n\tthis->pre_render();\n\tapply_opengl_debug_parameters(context_debug_parameters, *this->ctx);\n}\n\nGuiSeparateRenderingContext::~GuiSeparateRenderingContext() {\n\tthis->pre_render();\n\tthis->ctx_logger.reset();\n\tthis->post_render();\n}\n\nvoid GuiSeparateRenderingContext::pre_render() {\n\tif (!this->ctx->makeCurrent(&this->offscreen_surface)) {\n\t\tassert(false);\n\t\treturn;\n\t}\n}\n\nvoid GuiSeparateRenderingContext::post_render() {\n\tthis->make_current_back();\n}\n\nQQuickGraphicsDevice GuiSeparateRenderingContext::get_device() {\n\treturn QQuickGraphicsDevice::fromOpenGLContext(this->ctx.get());\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_ctx_setup.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <stdexcept>\n\n#include <QOffscreenSurface>\n#include <QOpenGLContext>\n#include <QQuickGraphicsDevice>\n\nQT_FORWARD_DECLARE_CLASS(QOpenGLDebugLogger)\n\nnamespace openage::renderer {\nclass Window;\n}\n\nnamespace qtgui {\n\nclass CtxExtractionException : public std::runtime_error {\npublic:\n\texplicit CtxExtractionException(const std::string &what_arg);\n};\n\n/**\n * Abstract base for the method of getting a Qt-usable context.\n */\nclass CtxExtractionMode {\npublic:\n\tvirtual ~CtxExtractionMode() {\n\t}\n\n\t/**\n\t * @return context that can be used by Qt\n\t */\n\tconst std::shared_ptr<QOpenGLContext> &get_ctx() const;\n\n\t/**\n\t * Function that must be called before rendering the GUI.\n\t */\n\tvirtual void pre_render() = 0;\n\n\t/**\n\t * Function that must be called after rendering the GUI.\n\t */\n\tvirtual void post_render() = 0;\n\n\t/**\n\t * Get a device object associated with the context.\n\t */\n\tvirtual QQuickGraphicsDevice get_device() = 0;\n\nprotected:\n\t/**\n\t * OpenGL Context for drawing. Should be acquired from the\n\t * displayed openage window.\n\t */\n\tstd::shared_ptr<QOpenGLContext> ctx;\n};\n\n/**\n * OpenGL Context manager for rendering the GUI.\n */\nclass GlGuiRenderingContext : public CtxExtractionMode {\npublic:\n\texplicit GlGuiRenderingContext(std::shared_ptr<openage::renderer::Window> window);\n\n\t/**\n\t * Makes the OpenGL context current to the offscreen surface.\n\t */\n\tvirtual void pre_render() override;\n\n\t/**\n\t * Indicate to Qt that OpenGL drawing is finished.\n\t */\n\tvirtual void post_render() override;\n\n\t/**\n\t * Get a Qt device object associated with the context.\n\t *\n\t * @return Device object of the OpenGL context.\n\t */\n\tvirtual QQuickGraphicsDevice get_device() override;\n\nprivate:\n\t/**\n\t * Surface that is needed to make the GUI context current.\n\t */\n\tQOffscreenSurface offscreen_surface;\n\n\t/**\n\t * TODO: Actual window (for switching back context).\n\t */\n\tstd::shared_ptr<openage::renderer::Window> window;\n};\n\n/**\n * Use the same context to render the GUI.\n */\nclass GuiUniqueRenderingContext : public CtxExtractionMode {\npublic:\n\texplicit GuiUniqueRenderingContext(std::shared_ptr<openage::renderer::Window> window);\n\n\tvirtual void pre_render() override;\n\tvirtual void post_render() override;\n\n\t/**\n\t * Get a device object associated with the context.\n\t */\n\tvirtual QQuickGraphicsDevice get_device() override;\n};\n\n/**\n * Create a separate context to render the GUI, make it shared with the main context.\n */\nclass GuiSeparateRenderingContext : public CtxExtractionMode {\npublic:\n\texplicit GuiSeparateRenderingContext();\n\tvirtual ~GuiSeparateRenderingContext();\n\n\tvirtual void pre_render() override;\n\tvirtual void post_render() override;\n\n\t/**\n\t * Get a device object associated with the context.\n\t */\n\tvirtual QQuickGraphicsDevice get_device() override;\n\nprivate:\n\t/**\n\t * GL context of the game\n\t */\n\tQOpenGLContext main_ctx;\n\n\t/**\n\t * GL debug logger of the GL context of the GUI\n\t */\n\tstd::unique_ptr<QOpenGLDebugLogger> ctx_logger;\n\n\t/**\n\t * Function to make the game context current\n\t */\n\tstd::function<void()> make_current_back;\n\n\t/**\n\t * Surface that is needed to make the GUI context current\n\t */\n\tQOffscreenSurface offscreen_surface;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_engine_impl.cpp",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#include \"gui_engine_impl.h\"\n\n#include <cassert>\n\n#include <QCoreApplication>\n#include <QDebug>\n#include <QQmlEngine>\n#include <QQuickWindow>\n\n#include \"renderer/gui/guisys/private/gui_renderer_impl.h\"\n#include \"renderer/gui/guisys/public/gui_engine.h\"\n\nnamespace qtgui {\n\nGuiQmlEngineImpl::GuiQmlEngineImpl(std::shared_ptr<GuiRenderer> renderer) :\n\tQObject{},\n\trenderer{},\n\tengine{std::make_shared<QQmlEngine>()} {\n\t// QThread *gui_thread = QCoreApplication::instance()->thread();\n\t// this->moveToThread(gui_thread);\n\t// this->engine.moveToThread(gui_thread);\n\t// this->watcher.moveToThread(gui_thread);\n\n\tassert(!this->engine->incubationController());\n\tthis->attach_to(renderer);\n\n\tQObject::connect(this,\n\t                 &GuiQmlEngineImpl::rootDirsPathsChanged,\n\t                 &this->watcher,\n\t                 &RecursiveDirectoryWatcher::rootDirsPathsChanged);\n\tQObject::connect(&this->watcher,\n\t                 &RecursiveDirectoryWatcher::changeDetected,\n\t                 this,\n\t                 &GuiQmlEngineImpl::onReload);\n}\n\nGuiQmlEngineImpl::~GuiQmlEngineImpl() = default;\n\nGuiQmlEngineImpl *GuiQmlEngineImpl::impl(GuiQmlEngine *engine) {\n\treturn engine->impl.get();\n}\n\nvoid GuiQmlEngineImpl::attach_to(std::shared_ptr<GuiRenderer> renderer) {\n\tthis->renderer = renderer;\n\tauto renderer_impl = GuiRendererImpl::impl(renderer.get());\n\tthis->engine->setIncubationController(renderer_impl->get_window()->incubationController());\n}\n\nstd::shared_ptr<QQmlEngine> GuiQmlEngineImpl::get_qml_engine() {\n\treturn this->engine;\n}\n\nvoid GuiQmlEngineImpl::onReload() {\n\tqDebug(\"reloading GUI\");\n\tthis->engine->clearComponentCache();\n\temit this->reload();\n}\n\nvoid GuiQmlEngineImpl::add_root_dir_path(const QString &root_dir_path) {\n\tthis->root_dirs_paths.push_back(root_dir_path);\n\temit this->rootDirsPathsChanged(this->root_dirs_paths);\n}\n\nvoid GuiQmlEngineImpl::remove_root_dir_path(const QString &root_dir_path) {\n\tif (this->root_dirs_paths.removeOne(root_dir_path))\n\t\temit this->rootDirsPathsChanged(this->root_dirs_paths);\n\telse\n\t\tqWarning() << \"Failed to remove path watched by ReloadableQmlEngine.\";\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_engine_impl.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include <QObject>\n#include <QStringList>\n\n#include \"renderer/gui/guisys/private/livereload/recursive_directory_watcher.h\"\n\nQT_FORWARD_DECLARE_CLASS(QQmlEngine)\n\nnamespace qtgui {\nclass GuiQmlEngine;\nclass GuiRenderer;\nclass GuiRendererImpl;\n\n/**\n * Qt QML engine wrapper.\n */\nclass GuiQmlEngineImpl : public QObject {\n\tQ_OBJECT\n\npublic:\n\t/**\n\t * Create a QmlEngine and attach it to a GUI renderer.\n\t *\n\t * @param renderer Renderer for drawing the GUI. The renderer holds a\n\t * \t\t\t\t   reference to an offscreen QQuickWindow that the QML\n\t *\t\t\t\t   components can be attached to.\n\t */\n\tGuiQmlEngineImpl(std::shared_ptr<GuiRenderer> renderer);\n\tvirtual ~GuiQmlEngineImpl();\n\n\tstatic GuiQmlEngineImpl *impl(GuiQmlEngine *engine);\n\n\t/**\n\t * Get the underlying QQmlEngine object.\n\t */\n\tstd::shared_ptr<QQmlEngine> get_qml_engine();\n\n\t/**\n\t * Add a path to the list of root dirs for GUI resources.\n\t *\n\t * @param root_dir_path Path to the resources.\n\t */\n\tvoid add_root_dir_path(const QString &root_dir_path);\n\n\t/**\n\t * Remove an existing path to the list of root dirs for GUI resources.\n\t *\n\t * @param root_dir_path Path to the resources.\n\t */\n\tvoid remove_root_dir_path(const QString &root_dir_path);\n\nsignals:\n\t/**\n\t * Qt signal emitted on reload.\n\t */\n\tvoid reload();\n\n\t/**\n\t * Qt signal emitted when the root dir paths are changed.\n\t */\n\tvoid rootDirsPathsChanged(const QStringList &);\n\npublic slots:\n\t/**\n\t * Qt slot for attaching a new GUI renderer.\n\t *\n\t * @param renderer Renderer for drawing the GUI.\n\t */\n\tvoid attach_to(std::shared_ptr<GuiRenderer> renderer);\n\n\t/**\n\t * Qt slot for reloading the GUI. Clears the component cache.\n\t */\n\tvoid onReload();\n\nprivate:\n\t/**\n\t * GUI renderer that draws the QML.\n\t */\n\tstd::shared_ptr<GuiRenderer> renderer;\n\n\t/**\n\t * Underlying Qt QML engine.\n\t */\n\tstd::shared_ptr<QQmlEngine> engine;\n\n\t/**\n\t * Watcher for changes to the root directories. Used for\n\t * live reloading the GUI if files are changed at runtime.\n\t */\n\tRecursiveDirectoryWatcher watcher;\n\n\t/**\n\t * Root directories for the QML assets.\n\t */\n\tQStringList root_dirs_paths;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_input_impl.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_input_impl.h\"\n\n#include <QCoreApplication>\n#include <QEvent>\n#include <QQuickWindow>\n\n#include \"renderer/gui/guisys/public/gui_renderer.h\"\n\nnamespace qtgui {\n\nGuiInputImpl::GuiInputImpl(const std::shared_ptr<GuiRenderer> &renderer) :\n\tQObject{},\n\twindow{renderer->get_window()} {\n}\n\n\nbool GuiInputImpl::process(const std::shared_ptr<QEvent> &event) {\n\tswitch (event->type()) {\n\tcase QEvent::MouseButtonPress:\n\tcase QEvent::MouseButtonRelease:\n\tcase QEvent::MouseMove:\n\tcase QEvent::MouseButtonDblClick: {\n\t\t// Quote from the QQuickRenderControl Example:\n\t\t// Use the constructor taking position and globalPosition. That puts position into the\n\t\t// event's position and scenePosition, and globalPosition into the event's globalPosition. This way\n\t\t// the scenePosition in e is ignored and is replaced by position. This is necessary\n\t\t// because QQuickWindow thinks of itself as a top-level window always.\n\t\tauto mouse_event = std::dynamic_pointer_cast<QMouseEvent>(event);\n\t\tQMouseEvent ev{mouse_event->type(),\n\t\t               mouse_event->position(),\n\t\t               mouse_event->globalPosition(),\n\t\t               mouse_event->button(),\n\t\t               mouse_event->buttons(),\n\t\t               mouse_event->modifiers()};\n\t\treturn QCoreApplication::instance()->sendEvent(this->window.get(), &ev);\n\t}\n\tcase QEvent::KeyPress:\n\tcase QEvent::KeyRelease:\n\t\treturn QCoreApplication::instance()->sendEvent(this->window.get(), event.get());\n\n\tcase QEvent::Wheel:\n\t\treturn QCoreApplication::instance()->sendEvent(this->window.get(), event.get());\n\n\tdefault:\n\t\tbreak;\n\t}\n\treturn false;\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_input_impl.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QObject>\n\nQ_FORWARD_DECLARE_OBJC_CLASS(QEvent);\nQ_FORWARD_DECLARE_OBJC_CLASS(QQuickWindow);\n\nnamespace qtgui {\n\nclass GuiRenderer;\n\n/**\n * Send QEvents from the main window system to the GUI subsystem.\n */\nclass GuiInputImpl : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiInputImpl(const std::shared_ptr<GuiRenderer> &renderer);\n\t~GuiInputImpl() = default;\n\n\t/**\n\t * Returns true if the event was accepted.\n\t */\n\tbool process(const std::shared_ptr<QEvent> &event);\n\nprivate:\n\tstd::shared_ptr<QQuickWindow> window;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_renderer_impl.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n// epoxy needs to be loaded before qt stuff\n#include \"renderer/opengl/texture.h\"\n\n#include \"gui_renderer_impl.h\"\n\n#include \"renderer/texture.h\"\n#include <cassert>\n\n#include <QCoreApplication>\n#include <QQuickGraphicsDevice>\n#include <QQuickRenderControl>\n#include <QQuickRenderTarget>\n// #include <QThread>\n\n#include \"log/log.h\"\n\n#include \"renderer/gui/guisys/public/gui_renderer.h\"\n#include \"renderer/window.h\"\n\n\nnamespace qtgui {\n\n\nGuiRendererImpl::GuiRendererImpl(std::shared_ptr<openage::renderer::Window> window) :\n\tQObject{},\n\tgui_context{window},\n\trender_control{} {\n\t// use of undocumented as of qt6.4 constructor\n\t// QQuickWindow(QQuickRenderControl *renderControl)\n\t// which associates the render controller with a qwindow.\n\tthis->target_window = std::make_unique<QQuickWindow>(&this->render_control);\n\n\t// same format as main window\n\tthis->target_window->setFormat(window->get_qt_window()->format());\n\n\t// make everything that's not part of the GUI transparent\n\tthis->target_window->setColor(Qt::transparent);\n\n\t// Add callbacks so that input events from the main window are propagated to the\n\t// target window's QML scene\n\t//\n\t// TODO: Currently, events are received by target_window, but have no effect in QML. Why?\n\t//\n\t// TODO: This solution leads to a 1 frame delay because Qt processing of the events sent here\n\t//       does not happen until the start of the next frame. Maybe we can forward them to the\n\t//       GUI earlier?\n\twindow->add_key_callback([this](const QKeyEvent &cb) {\n\t\tauto event = cb.clone();\n\t\t// TODO: Use notify() instead of sendEvent() to check if the event was processed\n\t\tQCoreApplication::sendEvent(this->target_window.get(), event);\n\t});\n\twindow->add_mouse_button_callback([this](const QMouseEvent &cb) {\n\t\t// Quote from the QQuickRenderControl Example:\n\t\t// Use the constructor taking position and globalPosition. That puts position into the\n\t\t// event's position and scenePosition, and globalPosition into the event's globalPosition. This way\n\t\t// the scenePosition in e is ignored and is replaced by position. This is necessary\n\t\t// because QQuickWindow thinks of itself as a top-level window always.\n\t\tQMouseEvent event{cb.type(),\n\t\t                  cb.position(),\n\t\t                  cb.globalPosition(),\n\t\t                  cb.button(),\n\t\t                  cb.buttons(),\n\t\t                  cb.modifiers()};\n\t\tQCoreApplication::sendEvent(this->target_window.get(), &event);\n\t});\n\twindow->add_mouse_wheel_callback([this](const QWheelEvent &cb) {\n\t\tauto event = cb.clone();\n\t\tQCoreApplication::sendEvent(this->target_window.get(), event);\n\t});\n\n\tauto size = window->get_qt_window()->size();\n\tthis->resize(size.width(), size.height());\n\n\tthis->gui_context.pre_render();\n\t// TODO: switch for vulkan support\n\tthis->target_window->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(this->gui_context.get_ctx().get()));\n\tthis->render_control.initialize();\n\n\tthis->gui_context.post_render();\n}\n\n\nvoid GuiRendererImpl::set_texture(const std::shared_ptr<openage::renderer::Texture2d> &texture) {\n\tthis->gui_context.pre_render();\n\tthis->texture = texture;\n\tauto tex_info = texture->get_info();\n\tQSize tex_size{tex_info.get_size().first, tex_info.get_size().second};\n\n\t// TODO switch here for vulkan surface draw support.\n\tauto tex_gl = std::dynamic_pointer_cast<openage::renderer::opengl::GlTexture2d>(texture);\n\tauto tex_gl_id = tex_gl->get_handle();\n\tthis->target_window->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(tex_gl_id, tex_size));\n\tthis->gui_context.post_render();\n}\n\n\nvoid GuiRendererImpl::render() {\n\tthis->gui_context.pre_render();\n\n\tthis->render_control.beginFrame();\n\tthis->render_control.polishItems();\n\tthis->render_control.sync();\n\tthis->render_control.render();\n\tthis->render_control.endFrame();\n\n\tthis->gui_context.post_render();\n}\n\nstd::shared_ptr<QQuickWindow> GuiRendererImpl::get_window() {\n\treturn this->target_window;\n}\n\nGuiRendererImpl *GuiRendererImpl::impl(GuiRenderer *renderer) {\n\treturn renderer->impl.get();\n}\n\nvoid GuiRendererImpl::resize(const size_t width, const size_t height) {\n\tthis->gui_context.pre_render();\n\tthis->target_window->setGeometry(0, 0, width, height);\n\temit this->resized(QSize(width, height));\n\tthis->gui_context.post_render();\n}\n\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_renderer_impl.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"renderer/texture.h\"\n\n#include <atomic>\n#include <condition_variable>\n#include <memory>\n#include <mutex>\n\n#include <QObject>\n#include <QOffscreenSurface>\n#include <QQuickRenderControl>\n#include <QQuickWindow>\n#include <QtGlobal>\n\n#include \"renderer/gui/guisys/private/gui_ctx_setup.h\"\n\nQT_FORWARD_DECLARE_CLASS(QOpenGLFramebufferObject)\n\nnamespace openage::renderer {\nclass Window;\n} // namespace openage::renderer\n\nnamespace qtgui {\n\nclass GuiRenderer;\n\n\n/**\n * Renderer for drawing a Qt QML GUI into an openage texture.\n *\n * The method ued in this class closely follows the QQuickRenderControl flow\n * provided by Qt. Essentially, we tell Qt to render its GUI into an offscreen\n * surface (our texture). See\n * https://doc.qt.io/qt-6/qquickrendercontrol.html#details\n * for more info.\n */\nclass GuiRendererImpl final : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiRendererImpl(std::shared_ptr<openage::renderer::Window> window);\n\t~GuiRendererImpl() = default;\n\n\tstatic GuiRendererImpl *impl(GuiRenderer *renderer);\n\n\t/**\n\t * Draw the QML GUI into the assigned GUI texture.\n\t */\n\tvoid render();\n\n\t/**\n\t * Adjust the offscreen surface's geometry.\n\t *\n\t * @param width Target width.\n\t * @param height Target height.\n\t */\n\tvoid resize(const size_t width, const size_t height);\n\n\t/**\n\t * Set the texture that the GUI is rendered into.\n\t */\n\tvoid set_texture(const std::shared_ptr<openage::renderer::Texture2d> &texture);\n\n\t/**\n\t * Get the offscreen window.\n\t */\n\tstd::shared_ptr<QQuickWindow> get_window();\n\nsignals:\n\t/**\n\t * Emitted when the off-screen gui window was resized through `resize`.\n\t */\n\tvoid resized(const QSize &size);\n\nprivate:\n\t/**\n\t * Rendering context.\n\t *\n\t * Beware: in the member order, put _before_ all context-affine resources (e.g. texture).\n\t * otherwise the context will be deleted before the resource in it.\n\t */\n\tGlGuiRenderingContext gui_context;\n\n\t/**\n\t * Object for sending render command to Qt.\n\t */\n\tQQuickRenderControl render_control;\n\n\t/**\n\t * Qt Window that represents the scene graph of the GUI.\n\t *\n\t * This window is never shown onscreen, it's just used by QQuickRenderControl\n\t * to control the rendering process. It draws into a texture\n\t * that we pass it via QQuickWindow::setRenderTarget().\n\t */\n\tstd::shared_ptr<QQuickWindow> target_window;\n\n\t/**\n\t * GUI texture that is targeted by the render control.\n\t */\n\tstd::shared_ptr<openage::renderer::Texture2d> texture;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_rendering_setup_routines.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_rendering_setup_routines.h\"\n\n#include <cassert>\n\n#include <QDebug>\n\n#include \"renderer/gui/guisys/private/gui_ctx_setup.h\"\n\nnamespace qtgui {\n\nGuiRenderingSetupRoutines::GuiRenderingSetupRoutines(std::shared_ptr<openage::renderer::Window> window) {\n\tthis->ctx_extraction_mode = std::make_unique<GlGuiRenderingContext>(window);\n\t// try {\n\t// \tthis->ctx_extraction_mode = std::make_unique<GuiUniqueRenderingContext>(window);\n\t// }\n\t// catch (const CtxExtractionException &) {\n\t// \tqInfo() << \"Falling back to separate render context for GUI\";\n\n\t// \ttry {\n\t// \t\tthis->ctx_extraction_mode = std::make_unique<GuiSeparateRenderingContext>();\n\t// \t}\n\t// \tcatch (const CtxExtractionException &) {\n\t// \t\tassert(false && \"setting up context for GUI failed\");\n\t// \t}\n\t// }\n}\n\nGuiRenderingSetupRoutines::~GuiRenderingSetupRoutines() = default;\n\nconst std::shared_ptr<QOpenGLContext> &GuiRenderingSetupRoutines::get_ctx() const {\n\treturn this->ctx_extraction_mode->get_ctx();\n}\n\nvoid GuiRenderingSetupRoutines::pre_render() {\n\tthis->ctx_extraction_mode->pre_render();\n}\n\nvoid GuiRenderingSetupRoutines::post_render() {\n\tthis->ctx_extraction_mode->post_render();\n}\n\nGuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines) :\n\trendering_setup_routines{&rendering_setup_routines} {\n\tthis->rendering_setup_routines->pre_render();\n}\n\nGuiRenderingCtxActivator::~GuiRenderingCtxActivator() {\n\tif (this->rendering_setup_routines)\n\t\tthis->rendering_setup_routines->post_render();\n}\n\nGuiRenderingCtxActivator::GuiRenderingCtxActivator(GuiRenderingCtxActivator &&o) :\n\trendering_setup_routines{o.rendering_setup_routines} {\n\to.rendering_setup_routines = nullptr;\n}\n\nGuiRenderingCtxActivator &GuiRenderingCtxActivator::operator=(GuiRenderingCtxActivator &&o) {\n\tthis->rendering_setup_routines = o.rendering_setup_routines;\n\to.rendering_setup_routines = nullptr;\n\treturn *this;\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_rendering_setup_routines.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QtGlobal>\n\nQT_FORWARD_DECLARE_CLASS(QOpenGLContext)\n\nnamespace openage::renderer {\nclass Window;\n}\n\nnamespace qtgui {\n\nclass CtxExtractionMode;\n\nclass GuiRenderingCtxActivator;\n\n/**\n * Returns a GL context usable by Qt classes.\n * Provides pre- and post-rendering functions to make the context usable for GUI rendering.\n */\nclass GuiRenderingSetupRoutines {\npublic:\n\texplicit GuiRenderingSetupRoutines(std::shared_ptr<openage::renderer::Window> window);\n\t~GuiRenderingSetupRoutines();\n\n\tconst std::shared_ptr<QOpenGLContext> &get_ctx() const;\n\nprivate:\n\tfriend class GuiRenderingCtxActivator;\n\tvoid pre_render();\n\tvoid post_render();\n\n\tstd::unique_ptr<CtxExtractionMode> ctx_extraction_mode;\n};\n\n/**\n * Prepares the context for rendering the GUI for one frame.\n * Activator must be destroyed as soon as the GUI has executed its frame render call.\n */\nclass GuiRenderingCtxActivator {\npublic:\n\texplicit GuiRenderingCtxActivator(GuiRenderingSetupRoutines &rendering_setup_routines);\n\t~GuiRenderingCtxActivator();\n\n\tGuiRenderingCtxActivator(GuiRenderingCtxActivator &&o);\n\tGuiRenderingCtxActivator &operator=(GuiRenderingCtxActivator &&o);\n\nprivate:\n\tGuiRenderingCtxActivator(const GuiRenderingCtxActivator &) = delete;\n\tGuiRenderingCtxActivator &operator=(const GuiRenderingCtxActivator &) = delete;\n\n\tGuiRenderingSetupRoutines *rendering_setup_routines;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_subtree_impl.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_subtree_impl.h\"\n#include \"gui_renderer_impl.h\"\n\n#include <cassert>\n#include <ciso646>\n\n#include <QCoreApplication>\n#include <QDebug>\n#include <QDir>\n#include <QFileInfo>\n#include <QQmlEngine>\n#include <QQuickItem>\n#include <QQuickWindow>\n#include <utility>\n\n#include \"renderer/gui/guisys/private/gui_engine_impl.h\"\n#include \"renderer/gui/guisys/private/gui_renderer_impl.h\"\n#include \"renderer/gui/guisys/public/gui_engine.h\"\n#include \"renderer/gui/guisys/public/gui_subtree.h\"\n\nnamespace qtgui {\n\n\nGuiSubtreeImpl::GuiSubtreeImpl(std::shared_ptr<GuiRenderer> renderer,\n                               std::shared_ptr<GuiQmlEngine> engine,\n                               const QString &source,\n                               const QString &rootdir) :\n\tQObject{},\n\trenderer{},\n\tengine{},\n\troot{} {\n\t// this->moveToThread(QCoreApplication::instance()->thread());\n\n\t// this->attach_to(GuiEventQueueImpl::impl(game_logic_updater));\n\tthis->attach_to(renderer);\n\tthis->attach_to(engine, rootdir);\n\n\t// Should now be initialized by the engine-attaching\n\tassert(this->root_component);\n\tthis->root_component->loadUrl(QUrl::fromLocalFile(source));\n\n\t// Need to queue the loading because some underlying game logic elements\n\t// require the loop to be running (maybe some things that are created after\n\t// the gui).\n\t// QMetaObject::invokeMethod(this->root_component.get(),\n\t//                           \"loadUrl\",\n\t//                           Qt::QueuedConnection,\n\t//                           Q_ARG(QUrl, QUrl::fromLocalFile(source)));\n}\n\nGuiSubtreeImpl::~GuiSubtreeImpl() = default;\n\nvoid GuiSubtreeImpl::onEngineReloaded() {\n\tconst QUrl source = this->root_component->url();\n\n\tthis->destroy_root();\n\n\tauto engine_impl = GuiQmlEngineImpl::impl(this->engine.get());\n\tthis->root_component = std::make_unique<QQmlComponent>(engine_impl->get_qml_engine().get());\n\n\tQObject::connect(\n\t\tthis->root_component.get(),\n\t\t&QQmlComponent::statusChanged,\n\t\tthis,\n\t\t&GuiSubtreeImpl::component_status_changed);\n\n\tthis->root_component->loadUrl(source);\n}\n\n// void GuiSubtreeImpl::attach_to(GuiEventQueueImpl *game_logic_updater) {\n// \tthis->game_logic_callback.moveToThread(game_logic_updater->get_thread());\n// }\n\nvoid GuiSubtreeImpl::attach_to(std::shared_ptr<GuiRenderer> renderer) {\n\tassert(renderer);\n\n\tauto renderer_impl = GuiRendererImpl::impl(renderer.get());\n\tif (this->renderer)\n\t\tQObject::disconnect(renderer_impl, nullptr, this, nullptr);\n\n\tthis->renderer = renderer;\n\n\tQObject::connect(\n\t\trenderer_impl,\n\t\t&GuiRendererImpl::resized,\n\t\tthis,\n\t\t&GuiSubtreeImpl::on_resized);\n\tthis->reparent_root();\n}\n\nvoid GuiSubtreeImpl::attach_to(std::shared_ptr<GuiQmlEngine> engine, const QString &rootdir) {\n\tthis->destroy_root();\n\tif (this->engine) {\n\t\t// disconnect the current engine\n\t\tauto engine_impl = GuiQmlEngineImpl::impl(this->engine.get());\n\t\tQObject::disconnect(\n\t\t\tengine_impl,\n\t\t\t&GuiQmlEngineImpl::reload,\n\t\t\tthis,\n\t\t\t&GuiSubtreeImpl::onEngineReloaded);\n\t\tif (not this->root_dir.isEmpty()) {\n\t\t\tengine_impl->remove_root_dir_path(this->root_dir);\n\t\t}\n\t}\n\n\tthis->engine = engine;\n\n\tauto engine_impl = GuiQmlEngineImpl::impl(engine.get());\n\tthis->root_component = std::make_unique<QQmlComponent>(engine_impl->get_qml_engine().get());\n\n\tQObject::connect(\n\t\tthis->root_component.get(),\n\t\t&QQmlComponent::statusChanged,\n\t\tthis,\n\t\t&GuiSubtreeImpl::component_status_changed);\n\n\tQObject::connect(\n\t\tengine_impl,\n\t\t&GuiQmlEngineImpl::reload,\n\t\tthis,\n\t\t&GuiSubtreeImpl::onEngineReloaded);\n\n\tthis->root_dir = rootdir;\n\tengine_impl->add_root_dir_path(this->root_dir);\n\n\t// this->root_component->moveToThread(QCoreApplication::instance()->thread());\n}\n\nvoid GuiSubtreeImpl::component_status_changed(QQmlComponent::Status status) {\n\tif (QQmlComponent::Error == status) {\n\t\tqCritical(\"%s\", qUtf8Printable(this->root_component->errorString()));\n\t\treturn;\n\t}\n\n\tif (QQmlComponent::Ready == status) {\n\t\tassert(!this->root);\n\n\t\tauto engine_impl = GuiQmlEngineImpl::impl(this->engine.get());\n\t\tauto root_context = engine_impl->get_qml_engine()->rootContext();\n\t\tthis->root = std::shared_ptr<QQuickItem>{\n\t\t\tqobject_cast<QQuickItem *>(this->root_component->beginCreate(root_context))};\n\t\tassert(this->root);\n\n\t\t// this->init_persistent_items();\n\n\t\tthis->root_component->completeCreate();\n\n\t\tthis->reparent_root();\n\t}\n}\n\nvoid GuiSubtreeImpl::on_resized(const QSize &size) {\n\tif (this->root)\n\t\tthis->root->setSize(size);\n}\n\nvoid GuiSubtreeImpl::reparent_root() {\n\tif (this->root) {\n\t\tauto renderer_impl = GuiRendererImpl::impl(this->renderer.get());\n\t\tauto window = renderer_impl->get_window();\n\n\t\tthis->root->setParentItem(window->contentItem());\n\t\tthis->root->setSize(QSize{window->width(), window->height()});\n\t\t// delegate key input events to root object\n\t\tthis->root->forceActiveFocus();\n\t}\n}\n\nvoid GuiSubtreeImpl::destroy_root() {\n\tif (this->root) {\n\t\tthis->root->setParent(nullptr);\n\t\tthis->root->setParentItem(nullptr);\n\t\tthis->root->deleteLater();\n\t\tthis->root = nullptr;\n\t}\n}\n\n// GuiEngineImplConnection::GuiEngineImplConnection() :\n// \tsubtree{},\n// \tengine{} {\n// }\n\n// GuiEngineImplConnection::GuiEngineImplConnection(GuiSubtreeImpl *subtree,\n//                                                  GuiEngineImpl *engine,\n//                                                  QString root_dir) :\n// \tsubtree{subtree},\n// \tengine{engine},\n// \troot_dir{std::move(root_dir)} {\n// \tassert(this->subtree);\n// \tassert(this->engine);\n\n// \tQObject::connect(\n// \t\tthis->engine,\n// \t\t&GuiEngineImpl::reload,\n// \t\tthis->subtree,\n// \t\t&GuiSubtreeImpl::onEngineReloaded);\n\n// \t// add the directory so it can be watched for changes.\n// \tthis->engine->add_root_dir_path(this->root_dir);\n// }\n\n// GuiEngineImplConnection::~GuiEngineImplConnection() {\n// \tthis->disconnect();\n// }\n\n// void GuiEngineImplConnection::disconnect() {\n// \tif (this->has_subtree()) {\n// \t\tassert(this->engine);\n\n// \t\tQObject::disconnect(\n// \t\t\tthis->engine,\n// \t\t\t&GuiEngineImpl::reload,\n// \t\t\tthis->subtree,\n// \t\t\t&GuiSubtreeImpl::onEngineReloaded);\n\n// \t\tif (not this->root_dir.isEmpty()) {\n// \t\t\tthis->engine->remove_root_dir_path(this->root_dir);\n// \t\t}\n// \t}\n// }\n\n// GuiEngineImplConnection::GuiEngineImplConnection(GuiEngineImplConnection &&cnx) noexcept\n// \t:\n// \tsubtree{cnx.subtree},\n// \tengine{cnx.engine} {\n// \tcnx.subtree = nullptr;\n// \tcnx.engine = nullptr;\n// }\n\n// GuiEngineImplConnection &GuiEngineImplConnection::operator=(GuiEngineImplConnection &&cnx) noexcept {\n// \tthis->disconnect();\n\n// \tthis->subtree = cnx.subtree;\n// \tthis->engine = cnx.engine;\n\n// \tcnx.subtree = nullptr;\n// \tcnx.engine = nullptr;\n\n// \treturn *this;\n// }\n\n// bool GuiEngineImplConnection::has_subtree() const {\n// \treturn this->subtree != nullptr;\n// }\n\n// QQmlContext *GuiEngineImplConnection::rootContext() const {\n// \tassert(this->subtree);\n// \tassert(this->engine);\n// \treturn this->engine->get_qml_engine()->rootContext();\n// }\n\n// QQmlEngine *GuiEngineImplConnection::get_qml_engine() const {\n// \tassert(this->subtree);\n// \tassert(this->engine);\n// \treturn this->engine->get_qml_engine();\n// }\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/gui_subtree_impl.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <vector>\n\n#include <QObject>\n#include <QQmlComponent>\n\nQT_FORWARD_DECLARE_CLASS(QQuickItem)\n\nnamespace qtgui {\n\nclass GuiRenderer;\nclass GuiQmlEngine;\n\nclass GuiSubtreeImpl : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiSubtreeImpl(std::shared_ptr<GuiRenderer> renderer,\n\t                        std::shared_ptr<GuiQmlEngine> engine,\n\t                        const QString &source,\n\t                        const QString &rootdir);\n\tvirtual ~GuiSubtreeImpl();\n\npublic slots:\n\tvoid onEngineReloaded();\n\n\t// void attach_to(GuiEventQueueImpl *game_logic_updater);\n\tvoid attach_to(std::shared_ptr<GuiRenderer> renderer);\n\tvoid attach_to(std::shared_ptr<GuiQmlEngine> engine, const QString &rootdir);\n\nprivate slots:\n\tvoid component_status_changed(QQmlComponent::Status status);\n\tvoid on_resized(const QSize &size);\n\nsignals:\n\tvoid process_game_logic_callback_blocking(const std::function<void()> &f);\n\nprivate:\n\tvoid reparent_root();\n\tvoid destroy_root();\n\n\tstd::shared_ptr<GuiRenderer> renderer;\n\tstd::shared_ptr<GuiQmlEngine> engine;\n\t// GuiLiveReloader reloader;\n\n\tstd::unique_ptr<QQmlComponent> root_component;\n\tstd::shared_ptr<QQuickItem> root;\n\tQString root_dir;\n};\n\n// class GuiEngineImplConnection {\n// public:\n// \tGuiEngineImplConnection();\n// \texplicit GuiEngineImplConnection(GuiSubtreeImpl *subtree,\n// \t                                 GuiEngineImpl *engine,\n// \t                                 QString root_dir);\n// \t~GuiEngineImplConnection();\n\n// \tGuiEngineImplConnection(GuiEngineImplConnection &&cnx) noexcept;\n// \tGuiEngineImplConnection &operator=(GuiEngineImplConnection &&cnx) noexcept;\n\n// \tbool has_subtree() const;\n\n// \tQQmlContext *rootContext() const;\n// \tQQmlEngine *get_qml_engine() const;\n\n// private:\n// \tGuiEngineImplConnection(const GuiEngineImplConnection &cnx) = delete;\n// \tGuiEngineImplConnection &operator=(const GuiEngineImplConnection &cnx) = delete;\n\n// \tvoid disconnect();\n\n// \tGuiSubtreeImpl *subtree;\n// \tGuiEngineImpl *engine;\n// \tQString root_dir;\n// };\n\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"deferred_initial_constant_property_values.h\"\n\nnamespace qtgui {\n\nDeferredInitialConstantPropertyValues::DeferredInitialConstantPropertyValues() :\n\tinit_over{} {\n}\n\nDeferredInitialConstantPropertyValues::~DeferredInitialConstantPropertyValues() = default;\n\nvoid DeferredInitialConstantPropertyValues::apply_static_properties() {\n\tfor (const auto &f : this->static_properties_assignments)\n\t\tf();\n\n\tthis->clear_static_properties();\n\tthis->init_over = true;\n}\n\nvoid DeferredInitialConstantPropertyValues::clear_static_properties() {\n\tthis->static_properties_assignments.clear();\n}\n\nbool DeferredInitialConstantPropertyValues::is_init_over() const {\n\treturn this->init_over;\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/deferred_initial_constant_property_values.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <vector>\n\nnamespace qtgui {\n\n/**\n * Stores static properties during initialization to be able to assign them after all 'liveReloadTag' properties are set.\n */\nclass DeferredInitialConstantPropertyValues {\npublic:\n\tDeferredInitialConstantPropertyValues();\n\n\t/**\n\t * Is virtual to be able to catch the error when non-persistent item uses liveReloadTag.\n\t */\n\tvirtual ~DeferredInitialConstantPropertyValues();\n\n\t/**\n\t * Execute saved static properties assignments (for newly created cores).\n\t */\n\tvoid apply_static_properties();\n\n\t/**\n\t * Clear saved static properties assignments (for cores that existed before the GUI reload).\n\t */\n\tvoid clear_static_properties();\n\n\tbool is_init_over() const;\n\nprotected:\n\tbool init_over;\n\tstd::vector<std::function<void()>> static_properties_assignments;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/gui_live_reloader.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_live_reloader.h\"\n\n#include <algorithm>\n#include <unordered_set>\n\n#include \"../../link/gui_item.h\"\n#include \"deferred_initial_constant_property_values.h\"\n\nnamespace qtgui {\n\nnamespace {\nconst int registration = qmlRegisterUncreatableType<GuiLiveReloaderAttachedPropertyProvider>(\"yay.sfttech.livereload\", 1, 0, \"LR\", \"LR is non-instantiable. It provides the 'LR.tag' attached property.\");\nconst int registration_property = qmlRegisterAnonymousType<GuiLiveReloaderAttachedProperty>(\"GuiLiveReloaderAttachedProperty\", 1);\n} // namespace\n\nGuiLiveReloaderAttachedProperty::GuiLiveReloaderAttachedProperty(QObject *object) :\n\tQObject{object} {\n\tQ_UNUSED(registration);\n\tQ_UNUSED(registration_property);\n\n\tif (!dynamic_cast<GuiItemBase *>(this->parent()))\n\t\tqFatal(\"Error in QML code: tried to set the 'LR.tag' to item with non-persistent type '%s'.\", object->metaObject()->className());\n}\n\nGuiItemBase *GuiLiveReloaderAttachedProperty::get_attachee() const {\n\tauto attachee = dynamic_cast<GuiItemBase *>(this->parent());\n\tassert(attachee);\n\n\treturn attachee;\n}\n\nQString GuiLiveReloaderAttachedProperty::get_tag() const {\n\tqWarning(\"Some QML code is enquiring about the '%s' 'LR.tag'. You must not rely on the tag value.\", qUtf8Printable(this->tag));\n\treturn this->tag;\n}\n\nvoid GuiLiveReloaderAttachedProperty::set_tag(const QString &tag) {\n\tif (this->get_attachee()->is_init_over())\n\t\tqCritical(\"Error in QML code: tried to change the '%s' 'LR.tag' to '%s'.\", qUtf8Printable(this->tag), qUtf8Printable(tag));\n\telse\n\t\tthis->tag = tag;\n}\n\nQString GuiLiveReloaderAttachedProperty::get_tag_for_init() const {\n\treturn this->tag;\n}\n\nGuiLiveReloaderAttachedProperty *GuiLiveReloaderAttachedPropertyProvider::qmlAttachedProperties(QObject *attachee) {\n\treturn new GuiLiveReloaderAttachedProperty(attachee);\n}\n\nvoid GuiLiveReloader::init_persistent_items(const QList<GuiLiveReloaderAttachedProperty *> &items) {\n\tstd::unordered_set<QString> stored_tags;\n\tstd::transform(std::begin(this->preservable), std::end(this->preservable), std::inserter(stored_tags, std::end(stored_tags)), [](const TagToPreservableMap::value_type &pair) { return pair.first; });\n\n\tfor (auto ap : items) {\n\t\tauto obj = ap->get_attachee();\n\t\tauto tag = ap->get_tag_for_init();\n\n\t\tif (tag.isEmpty()) {\n\t\t\tqFatal(\"Error in QML code: an item with underlying type of '%s' has no 'LR.tag' property.\", dynamic_cast<QObject *>(obj)->metaObject()->className());\n\t\t}\n\t\telse {\n\t\t\tauto found_it = this->preservable.find(tag);\n\n\t\t\tif (found_it != std::end(this->preservable)) {\n\t\t\t\tobj->adopt_core(found_it->second.get(), tag);\n\t\t\t\tobj->clear_static_properties();\n\t\t\t}\n\t\t\telse if (auto wrapped_core = obj->instantiate_core()) {\n\t\t\t\tobj->adopt_core(this->preservable.insert(found_it, std::make_pair(tag, std::move(wrapped_core)))->second.get(), tag);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tqCritical(\"Error in QML code: trying to add an object to the GuiLiveReloader multiple times. Second time was with tag '%s'.\", qUtf8Printable(tag));\n\t\t\t}\n\n\t\t\tstored_tags.erase(tag);\n\t\t}\n\t}\n\n\tfor (auto ap : items)\n\t\tap->get_attachee()->apply_static_properties();\n\n\tstd::for_each(std::begin(stored_tags), std::end(stored_tags), [](const QString &dangling) {\n\t\tqWarning(\"The '%s' 'LR.tag' hasn't found any recipients after GUI reload.\", qUtf8Printable(dangling));\n\t});\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/gui_live_reloader.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <tuple>\n#include <unordered_map>\n\n#include <QHash>\n#include <QList>\n#include <QStringList>\n#include <QtGlobal>\n#include <QtQml>\n\n// since qt 5.14, the std::hash of q* types are included in qt\n#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)\nnamespace std {\ntemplate <>\nstruct hash<QString> {\n\tsize_t operator()(const QString &val) const noexcept {\n\t\treturn qHash(val);\n\t}\n};\n} // namespace std\n#endif\n\nnamespace qtgui {\n\nclass GuiItemBase;\n\n/**\n * The 'LR.tag' attached property.\n */\nclass GuiLiveReloaderAttachedProperty : public QObject {\n\tQ_OBJECT\n\n\tQ_PROPERTY(QString tag READ get_tag WRITE set_tag)\n\npublic:\n\texplicit GuiLiveReloaderAttachedProperty(QObject *object);\n\n\tGuiItemBase *get_attachee() const;\n\n\tQString get_tag() const;\n\tvoid set_tag(const QString &tag);\n\n\tQString get_tag_for_init() const;\n\nprivate:\n\t/**\n\t * Tag that allows to connect newly created shells to corresponding cores after GUI reload.\n\t */\n\tQString tag;\n};\n\n/**\n * Non-instantiable type that is needed to use a 'LR.tag' attached property.\n */\nclass GuiLiveReloaderAttachedPropertyProvider : public QObject {\n\tQ_OBJECT\n\npublic:\n\tstatic GuiLiveReloaderAttachedProperty *qmlAttachedProperties(QObject *);\n};\n\nclass PersistentCoreHolderBase;\n\n/**\n * Stores objects that need to be kept alive across GUI reloads.\n */\nclass GuiLiveReloader {\npublic:\n\tvoid init_persistent_items(const QList<GuiLiveReloaderAttachedProperty *> &items);\n\nprivate:\n\tusing TagToPreservableMap = std::unordered_map<QString, std::unique_ptr<PersistentCoreHolderBase>>;\n\tTagToPreservableMap preservable;\n};\n\n} // namespace qtgui\n\nQML_DECLARE_TYPEINFO(qtgui::GuiLiveReloaderAttachedPropertyProvider, QML_HAS_ATTACHED_PROPERTIES)\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/recursive_directory_watcher.cpp",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#include \"recursive_directory_watcher.h\"\n\n#include <QEventLoop>\n#include <QSemaphore>\n\n#include \"renderer/gui/guisys/private/livereload/recursive_directory_watcher_worker.h\"\n\nnamespace qtgui {\n\nRecursiveDirectoryWatcher::RecursiveDirectoryWatcher(QObject *parent) :\n\tQObject{parent} {\n\tQSemaphore wait_worker_started;\n\n\tthis->worker = std::async(std::launch::async, [this, &wait_worker_started] {\n\t\tQEventLoop loop;\n\t\tQObject::connect(this, &RecursiveDirectoryWatcher::quit, &loop, &QEventLoop::quit);\n\n\t\tRecursiveDirectoryWatcherWorker watcher;\n\t\tQObject::connect(&watcher, &RecursiveDirectoryWatcherWorker::changeDetected, this, &RecursiveDirectoryWatcher::changeDetected);\n\t\tQObject::connect(this, &RecursiveDirectoryWatcher::rootDirsPathsChanged, &watcher, &RecursiveDirectoryWatcherWorker::onRootDirsPathsChanged);\n\n\t\twait_worker_started.release();\n\n\t\tloop.exec();\n\t});\n\n\twait_worker_started.acquire();\n}\n\nRecursiveDirectoryWatcher::~RecursiveDirectoryWatcher() {\n\temit this->quit();\n\tthis->worker.wait();\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/recursive_directory_watcher.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <future>\n\n#include <QObject>\n#include <QStringList>\n\nnamespace qtgui {\n\n/**\n * Emits a signal when anything changes in the directories.\n */\nclass RecursiveDirectoryWatcher : public QObject {\n\tQ_OBJECT\n\npublic:\n\texplicit RecursiveDirectoryWatcher(QObject *parent = nullptr);\n\tvirtual ~RecursiveDirectoryWatcher();\n\nsignals:\n\tvoid changeDetected();\n\tvoid rootDirsPathsChanged(const QStringList &);\n\tvoid quit();\n\nprivate:\n\tstd::future<void> worker;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/recursive_directory_watcher_worker.cpp",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#include \"recursive_directory_watcher_worker.h\"\n\n#include <algorithm>\n\n#include <QDebug>\n#include <QDirIterator>\n\nnamespace qtgui {\n\nnamespace {\nconst int batch_ms = 100;\n}\n\nRecursiveDirectoryWatcherWorker::RecursiveDirectoryWatcherWorker() :\n\tQObject{} {\n\tthis->batching_timer.setInterval(batch_ms);\n\tthis->batching_timer.setSingleShot(true);\n\tQObject::connect(&this->batching_timer, &QTimer::timeout, this, &RecursiveDirectoryWatcherWorker::changeDetected);\n\tQObject::connect(&this->batching_timer, &QTimer::timeout, this, &RecursiveDirectoryWatcherWorker::restartWatching);\n}\n\nvoid RecursiveDirectoryWatcherWorker::onRootDirsPathsChanged(const QStringList &root_dirs_paths) {\n\tif (this->root_dirs_paths != root_dirs_paths) {\n\t\tthis->root_dirs_paths = root_dirs_paths;\n\t\tthis->restartWatching();\n\t}\n}\n\nnamespace {\nQStringList collect_entries_to_watch(const QStringList &root_dirs_paths) {\n\tQStringList root_dirs_paths_no_duplicates = root_dirs_paths;\n\troot_dirs_paths_no_duplicates.removeDuplicates();\n\n\tQStringList entries_to_watch;\n\n\tstd::for_each(std::begin(root_dirs_paths_no_duplicates), std::end(root_dirs_paths_no_duplicates), [&entries_to_watch](const QString &root_dir_path) {\n\t\tQDirIterator it{root_dir_path, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks};\n\n\t\twhile (it.hasNext()) {\n\t\t\tentries_to_watch.append(it.next());\n\t\t}\n\t});\n\n\treturn entries_to_watch;\n}\n} // namespace\n\nvoid RecursiveDirectoryWatcherWorker::restartWatching() {\n\tthis->restart_watching(collect_entries_to_watch(this->root_dirs_paths));\n}\n\nvoid RecursiveDirectoryWatcherWorker::restart_watching(const QStringList &entries_to_watch) {\n\tthis->watcher.reset();\n\tthis->watcher = std::make_unique<QFileSystemWatcher>();\n\n\tQObject::connect(&*this->watcher, &QFileSystemWatcher::directoryChanged, this, &RecursiveDirectoryWatcherWorker::onEntryChanged);\n\n\tif (entries_to_watch.empty())\n\t\tqWarning() << \"RecursiveDirectoryWatcheWorker hasn't found any files to watch.\";\n\telse\n\t\tthis->watcher->addPaths(entries_to_watch);\n}\n\nvoid RecursiveDirectoryWatcherWorker::onEntryChanged() {\n\tif (!this->batching_timer.isActive())\n\t\tthis->batching_timer.start();\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/livereload/recursive_directory_watcher_worker.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QFileSystemWatcher>\n#include <QStringList>\n#include <QTimer>\n\nnamespace qtgui {\n\n/**\n * Emits a signal when anything changes in the directories.\n */\nclass RecursiveDirectoryWatcherWorker : public QObject {\n\tQ_OBJECT\n\npublic:\n\tRecursiveDirectoryWatcherWorker();\n\nsignals:\n\tvoid changeDetected();\n\npublic slots:\n\tvoid onRootDirsPathsChanged(const QStringList &root_dirs_paths);\n\tvoid restartWatching();\n\nprivate slots:\n\tvoid onEntryChanged();\n\nprivate:\n\tvoid restart_watching(const QStringList &entries_to_watch);\n\n\t/**\n\t * Actual watcher.\n\t * Its event processing and destruction has to be in the same separate thread.\n\t */\n\tstd::unique_ptr<QFileSystemWatcher> watcher;\n\n\t/**\n\t * Waits to glue multiple changes into one event.\n\t */\n\tQTimer batching_timer;\n\n\tQStringList root_dirs_paths;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/opengl_debug_logger.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"opengl_debug_logger.h\"\n\n#include <QOpenGLContext>\n#include <QOpenGLFunctions_4_4_Core>\n#include <QOpenGLVersionFunctionsFactory>\n\n#ifdef __APPLE__\n\t// from https://www.khronos.org/registry/OpenGL/api/GL/glext.h\n\t#define GL_DEBUG_CALLBACK_FUNCTION 0x8244\n\t#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242\n\t#define GL_DEBUG_TYPE_ERROR 0x824C\n\t#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E\n#endif\n\nnamespace qtgui {\n\ngl_debug_parameters get_current_opengl_debug_parameters(QOpenGLContext &current_source_context) {\n\tgl_debug_parameters params{};\n\n\tif (QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_4_4_Core>(&current_source_context))\n\t\tif ((params.is_debug = current_source_context.format().options().testFlag(QSurfaceFormat::DebugContext))) {\n\t\t\tglGetPointerv(GL_DEBUG_CALLBACK_FUNCTION, &params.callback);\n\t\t\tparams.synchronous = glIsEnabled(GL_DEBUG_OUTPUT_SYNCHRONOUS);\n\t\t}\n\n\treturn params;\n}\n\nvoid apply_opengl_debug_parameters(gl_debug_parameters params, QOpenGLContext &current_dest_context) {\n\tif (params.is_debug && params.callback) {\n\t\tif (auto functions = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_4_4_Core>(&current_dest_context)) {\n\t\t\tfunctions->initializeOpenGLFunctions();\n\n\t\t\tfunctions->glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_FALSE);\n\n\t\t\tfunctions->glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, nullptr, GL_TRUE);\n\t\t\tfunctions->glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, GL_DONT_CARE, 0, nullptr, GL_TRUE);\n\n\t\t\tfunctions->glDebugMessageCallback((GLDEBUGPROC)params.callback, nullptr);\n\n\t\t\tif (params.synchronous)\n\t\t\t\tglEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);\n\t\t}\n\t}\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/private/opengl_debug_logger.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QOpenGLFunctions>\n\nQT_FORWARD_DECLARE_CLASS(QOpenGLContext)\n\nnamespace qtgui {\nstruct gl_debug_parameters {\n\t/**\n\t * True if the GL context is a debug context\n\t */\n\tbool is_debug;\n\n\t/**\n\t * Function that GL context uses to report debug messages\n\t */\n\tGLvoid *callback;\n\n\t/**\n\t * True if debug callback calling method is chosen to be synchronous\n\t */\n\tbool synchronous;\n};\n\n/**\n * Get debugging settings of the current GL context\n *\n * @param current_source_context current GL context\n * @return debugging settings\n */\ngl_debug_parameters get_current_opengl_debug_parameters(QOpenGLContext &current_source_context);\n\n/**\n * Create a GL logger in the current GL context\n *\n * @param params debugging settings\n * @param current_dest_context current GL context to which parameters will be applied\n */\nvoid apply_opengl_debug_parameters(gl_debug_parameters params, QOpenGLContext &current_dest_context);\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_application.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include <utility>\n\n#include \"gui_application.h\"\n\n#include \"../private/gui_application_impl.h\"\n\nnamespace qtgui {\n\nGuiApplication::GuiApplication() :\n\tapplication{GuiApplicationImpl::get()} {\n}\n\nGuiApplication::GuiApplication(std::shared_ptr<GuiApplicationImpl> application) :\n\tapplication{application} {\n}\n\nGuiApplication::~GuiApplication() = default;\n\nvoid GuiApplication::process_events() {\n\tthis->application->processEvents();\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_application.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\nnamespace qtgui {\n\nclass GuiApplicationImpl;\n\n/**\n * Houses gui logic event queue.\n */\nclass GuiApplication {\npublic:\n\tGuiApplication();\n\tGuiApplication(std::shared_ptr<GuiApplicationImpl> application);\n\t~GuiApplication();\n\n\tvoid process_events();\n\nprivate:\n\tstd::shared_ptr<GuiApplicationImpl> application;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_engine.cpp",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#include \"renderer/gui/guisys/public/gui_engine.h\"\n\n#include \"renderer/gui/guisys/private/gui_engine_impl.h\"\n\nnamespace qtgui {\n\nGuiQmlEngine::GuiQmlEngine(std::shared_ptr<GuiRenderer> renderer) :\n\timpl{std::make_unique<GuiQmlEngineImpl>(renderer)} {\n}\n\nGuiQmlEngine::~GuiQmlEngine() = default;\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_engine.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include <QObject>\n\n// QT_FORWARD_DECLARE_CLASS(QQuickWindow)\n\nnamespace qtgui {\n\nclass GuiQmlEngineImpl;\nclass GuiRenderer;\n\n/**\n * Represents one QML execution environment.\n */\nclass GuiQmlEngine {\npublic:\n\texplicit GuiQmlEngine(std::shared_ptr<GuiRenderer> renderer);\n\t~GuiQmlEngine();\n\nprivate:\n\tfriend class GuiQmlEngineImpl;\n\tstd::unique_ptr<GuiQmlEngineImpl> impl;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_input.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_input.h\"\n\n#include \"renderer/gui/guisys/private/gui_input_impl.h\"\n\nnamespace qtgui {\n\nGuiInput::GuiInput(const std::shared_ptr<GuiRenderer> &renderer) :\n\timpl{std::make_unique<GuiInputImpl>(renderer)} {\n}\n\n\nbool GuiInput::process(const std::shared_ptr<QEvent> &event) {\n\treturn this->impl->process(event);\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_input.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QEvent>\n\n#include \"renderer/gui/guisys/private/gui_input_impl.h\"\n\nnamespace qtgui {\n\nclass GuiRenderer;\nclass GuiInputImpl;\n\n/**\n * Send QEvents from the main window system to the GUI subsystem.\n */\nclass GuiInput {\npublic:\n\texplicit GuiInput(const std::shared_ptr<GuiRenderer> &renderer);\n\t~GuiInput() = default;\n\n\t/**\n\t * Returns true if the event was accepted.\n\t */\n\tbool process(const std::shared_ptr<QEvent> &event);\n\nprivate:\n\tfriend class GuiInputImpl;\n\tstd::unique_ptr<GuiInputImpl> impl;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_renderer.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_renderer.h\"\n\n#include <QSize>\n\n#include \"renderer/gui/guisys/private/gui_renderer_impl.h\"\n\nnamespace qtgui {\n\nGuiRenderer::GuiRenderer(const std::shared_ptr<openage::renderer::Window> &window) :\n\timpl{std::make_unique<GuiRendererImpl>(window)} {\n}\n\nGuiRenderer::~GuiRenderer() = default;\n\nvoid GuiRenderer::render() {\n\tthis->impl->render();\n}\n\nvoid GuiRenderer::resize(int w, int h) {\n\tthis->impl->resize(w, h);\n}\n\nvoid GuiRenderer::set_texture(const std::shared_ptr<openage::renderer::Texture2d> &texture) {\n\tthis->impl->set_texture(texture);\n}\n\nstd::shared_ptr<QQuickWindow> GuiRenderer::get_window() {\n\treturn this->impl->get_window();\n}\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_renderer.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QObject>\n\nQT_FORWARD_DECLARE_CLASS(QQuickWindow)\n\nnamespace openage::renderer {\nclass Texture2d;\nclass Window;\n} // namespace openage::renderer\n\nnamespace qtgui {\n\nclass GuiRendererImpl;\n\n/**\n * Passes the native graphic context to Qt.\n */\nclass GuiRenderer {\npublic:\n\texplicit GuiRenderer(const std::shared_ptr<openage::renderer::Window> &window);\n\t~GuiRenderer();\n\n\tvoid render();\n\tvoid resize(int w, int h);\n\tvoid set_texture(const std::shared_ptr<openage::renderer::Texture2d> &texture);\n\tstd::shared_ptr<QQuickWindow> get_window();\n\nprivate:\n\tfriend class GuiRendererImpl;\n\tstd::unique_ptr<GuiRendererImpl> impl;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_subtree.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_subtree.h\"\n\n#include \"renderer/gui/guisys/private/gui_subtree_impl.h\"\n\nnamespace qtgui {\n\nGuiSubtree::GuiSubtree(std::shared_ptr<GuiRenderer> renderer,\n                       std::shared_ptr<GuiQmlEngine> engine,\n                       const std::string &source,\n                       const std::string &rootdir) :\n\timpl{std::make_unique<GuiSubtreeImpl>(\n\t\trenderer,\n\t\tengine,\n\t\tQString::fromStdString(source),\n\t\tQString::fromStdString(rootdir))} {}\n\nGuiSubtree::~GuiSubtree() = default;\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/guisys/public/gui_subtree.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n\nnamespace qtgui {\n\nclass GuiRenderer;\nclass GuiQmlEngine;\nclass GuiSubtreeImpl;\n\n\n/**\n * A root item that loads its code from source url.\n *\n * rootdir is the qml file root folder which is watched for changes.\n */\nclass GuiSubtree {\npublic:\n\texplicit GuiSubtree(std::shared_ptr<GuiRenderer> renderer,\n\t                    std::shared_ptr<GuiQmlEngine> engine,\n\t                    const std::string &source,\n\t                    const std::string &rootdir);\n\n\t~GuiSubtree();\n\nprivate:\n\tfriend class GuiSubtreeImpl;\n\tstd::unique_ptr<GuiSubtreeImpl> impl;\n};\n\n} // namespace qtgui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/CMakeLists.txt",
    "content": "add_subdirectory(\"private\")\nadd_subdirectory(\"public\")\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/CMakeLists.txt",
    "content": "add_sources(libopenage\n    gui_filled_texture_handles.cpp\n    gui_log.cpp\n    gui_make_standalone_subtexture.cpp\n    gui_standalone_subtexture.cpp\n    gui_texture.cpp\n    gui_texture_handle.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_filled_texture_handles.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_filled_texture_handles.h\"\n\n#include <utility>\n\n#include \"renderer/gui/integration/private/gui_texture_handle.h\"\n\n\nnamespace openage::renderer::gui {\n\nGuiFilledTextureHandles::GuiFilledTextureHandles() = default;\n\nGuiFilledTextureHandles::~GuiFilledTextureHandles() = default;\n\nvoid GuiFilledTextureHandles::add_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) {\n\tstd::unique_lock<std::mutex> lck{this->handles_mutex};\n\tthis->handles.emplace_back(id, requested_size, filled_handle);\n}\n\nvoid GuiFilledTextureHandles::free_texture_handle(SizedTextureHandle *filled_handle) {\n\tstd::unique_lock<std::mutex> lck{this->handles_mutex};\n\tthis->handles.erase(std::remove_if(std::begin(this->handles), std::end(this->handles), [filled_handle](const std::tuple<QString, QSize, SizedTextureHandle *> &handle) {\n\t\t\t\t\t\t\treturn std::get<SizedTextureHandle *>(handle) == filled_handle;\n\t\t\t\t\t\t}),\n\t                    std::end(this->handles));\n}\n\nvoid GuiFilledTextureHandles::fill_all_handles_with_texture(const TextureHandle &texture) {\n\tstd::unique_lock<std::mutex> lck{this->handles_mutex};\n\tstd::for_each(std::begin(this->handles), std::end(this->handles), [&texture](std::tuple<QString, QSize, SizedTextureHandle *> &handle) {\n\t\tauto filled_handle = std::get<SizedTextureHandle *>(handle);\n\t\t*filled_handle = {texture, textureSize(*filled_handle)};\n\t});\n}\n\nvoid GuiFilledTextureHandles::refresh_all_handles_with_texture(const std::function<void(const QString &, const QSize &, SizedTextureHandle *)> &refresher) {\n\tstd::unique_lock<std::mutex> lck{this->handles_mutex};\n\n\tstd::vector<SizedTextureHandle> refreshed_handles(this->handles.size());\n\n\tstd::transform(std::begin(this->handles), std::end(this->handles), std::begin(refreshed_handles), [refresher](const std::tuple<QString, QSize, SizedTextureHandle *> &handle) {\n\t\tSizedTextureHandle refreshed_handles;\n\t\trefresher(std::get<QString>(handle), std::get<QSize>(handle), &refreshed_handles);\n\t\treturn refreshed_handles;\n\t});\n\n\tfor (std::size_t i = 0, e = refreshed_handles.size(); i != e; ++i)\n\t\t*std::get<SizedTextureHandle *>(this->handles[i]) = refreshed_handles[i];\n}\n\nGuiFilledTextureHandleUser::GuiFilledTextureHandleUser(std::shared_ptr<GuiFilledTextureHandles> texture_handles, const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle) :\n\ttexture_handles{std::move(texture_handles)},\n\tfilled_handle{filled_handle} {\n\tthis->texture_handles->add_texture_handle(id, requested_size, filled_handle);\n}\n\nGuiFilledTextureHandleUser::~GuiFilledTextureHandleUser() {\n\t// Assume that the factory lives longer than the textures it has created.\n\ttexture_handles->free_texture_handle(filled_handle);\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_filled_texture_handles.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <mutex>\n#include <vector>\n\n#include <QSize>\n#include <QString>\n\nnamespace openage::renderer::gui {\n\nclass TextureHandle;\nclass SizedTextureHandle;\n\n/**\n * Stores pointers to game texture handlers given to the Qt.\n */\nclass GuiFilledTextureHandles {\npublic:\n\texplicit GuiFilledTextureHandles();\n\t~GuiFilledTextureHandles();\n\n\t/**\n\t * Memorizes pointer to provided handle object in order to be able to update it if the source changes.\n\t */\n\tvoid add_texture_handle(const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle);\n\tvoid free_texture_handle(SizedTextureHandle *filled_handle);\n\n\tvoid fill_all_handles_with_texture(const TextureHandle &texture);\n\tvoid refresh_all_handles_with_texture(const std::function<void(const QString &, const QSize &, SizedTextureHandle *)> &refresher);\n\nprivate:\n\t/**\n\t * Changing the texture handles underneath the QSGTexture to reflect the reload of the GameSpec.\n\t *\n\t * It's not a proper Qt usage, so the live reload of the game assets for the gui may break in future Qt releases.\n\t * When it breaks, this feature should be implemented via the recreation of the qml engine.\n\t */\n\tstd::vector<std::tuple<QString, QSize, SizedTextureHandle *>> handles;\n\tstd::mutex handles_mutex;\n};\n\nclass GuiFilledTextureHandleUser {\npublic:\n\tGuiFilledTextureHandleUser(std::shared_ptr<GuiFilledTextureHandles> texture_handles, const QString &id, const QSize &requested_size, SizedTextureHandle *filled_handle);\n\t~GuiFilledTextureHandleUser();\n\n\tGuiFilledTextureHandleUser(GuiFilledTextureHandleUser &&) noexcept = default;\n\nprivate:\n\tGuiFilledTextureHandleUser(const GuiFilledTextureHandleUser &) = delete;\n\tGuiFilledTextureHandleUser &operator=(const GuiFilledTextureHandleUser &) = delete;\n\n\tstd::shared_ptr<GuiFilledTextureHandles> texture_handles;\n\tSizedTextureHandle *filled_handle;\n};\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_log.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"../../integration/private/gui_log.h\"\n\n#include <QString>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::renderer::gui {\n\nvoid gui_log(QtMsgType type, const QMessageLogContext &context, const QString &msg) {\n\tlog::level msg_lvl;\n\n\tswitch (type) {\n\tcase QtDebugMsg:\n\t\tmsg_lvl = log::level::dbg;\n\t\tbreak;\n\tcase QtInfoMsg:\n\t\tmsg_lvl = log::level::info;\n\t\tbreak;\n\tcase QtWarningMsg:\n\t\tmsg_lvl = log::level::warn;\n\t\tbreak;\n\tcase QtCriticalMsg:\n\t\tmsg_lvl = log::level::err;\n\t\tbreak;\n\tcase QtFatalMsg:\n\t\tmsg_lvl = log::level::crit;\n\t\tbreak;\n\tdefault:\n\t\tmsg_lvl = log::level::warn;\n\t\tbreak;\n\t}\n\n\tlog::MessageBuilder builder{\n\t\tcontext.file != nullptr ? context.file : \"<unknown file>\",\n\t\tstatic_cast<unsigned>(context.line),\n\t\tcontext.function != nullptr ? context.function : \"<unknown function>\",\n\t\tmsg_lvl};\n\n\t// TODO: maybe it's not UTF-8\n\t// TODO: Qt should become a LogSource\n\tlog::log(builder << msg.toUtf8().data());\n\n\tif (type == QtFatalMsg)\n\t\tabort();\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_log.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QtGlobal>\n\nnamespace openage::renderer::gui {\n\nvoid gui_log(QtMsgType type, const QMessageLogContext &context, const QString &msg);\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_make_standalone_subtexture.h\"\n\n#include \"renderer/gui/integration/private/gui_standalone_subtexture.h\"\n\n\nnamespace openage::renderer::gui {\n\nstd::unique_ptr<QSGTexture> make_standalone_subtexture(GLuint id, const QSize &size) {\n\treturn std::make_unique<GuiStandaloneSubtexture>(id, size);\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_make_standalone_subtexture.h",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <epoxy/gl.h>\n\n#include <memory>\n\n#include <QSGTexture>\n\nnamespace openage::renderer::gui {\n\n/*\n * Reason for this is to resolve epoxy header clash that occur\n * when a header with QObject-derived (so, it's moc-ed) class\n * needs GL headers. Automoc can arrange headers in any order,\n * so the GL can be included through Qt first, because guisys\n * files aren't allowed to include epoxy at all, for example.\n */\nstd::unique_ptr<QSGTexture> make_standalone_subtexture(GLuint id, const QSize &size);\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_standalone_subtexture.cpp",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_standalone_subtexture.h\"\n\nnamespace openage::renderer::gui {\n\nGuiStandaloneSubtexture::GuiStandaloneSubtexture(GLuint id, const QSize &size) :\n\tid(id),\n\tsize(size) {\n}\n\nGuiStandaloneSubtexture::~GuiStandaloneSubtexture() {\n\tglDeleteTextures(1, &this->id);\n}\n\nvoid GuiStandaloneSubtexture::bind() {\n\tglBindTexture(GL_TEXTURE_2D, this->textureId());\n}\n\nqint64 GuiStandaloneSubtexture::comparisonKey() const {\n\treturn 0;\n}\n\nbool GuiStandaloneSubtexture::hasAlphaChannel() const {\n\t// assume 32bit textures\n\treturn true;\n}\n\nbool GuiStandaloneSubtexture::hasMipmaps() const {\n\treturn false;\n}\n\nbool GuiStandaloneSubtexture::isAtlasTexture() const {\n\treturn false;\n}\n\nint GuiStandaloneSubtexture::textureId() const {\n\treturn this->id;\n}\n\nQSize GuiStandaloneSubtexture::textureSize() const {\n\treturn this->size;\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_standalone_subtexture.h",
    "content": "// Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QSGTexture>\n#include <QtGui>\n\nnamespace openage::renderer::gui {\n\nclass GuiStandaloneSubtexture : public QSGTexture {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiStandaloneSubtexture(GLuint id, const QSize &size);\n\tvirtual ~GuiStandaloneSubtexture();\n\nprivate:\n\tvirtual qint64 comparisonKey() const override;\n\tvirtual bool hasAlphaChannel() const override;\n\tvirtual bool hasMipmaps() const override;\n\tvirtual bool isAtlasTexture() const override;\n\tvirtual QSize textureSize() const override;\n\n\t// TODO: Leftover from Qt5; not used in Qt6\n\tvirtual void bind();\n\tvirtual int textureId() const;\n\n\tconst GLuint id;\n\tconst QSize size;\n};\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_texture.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include <array>\n\n#include <QTransform>\n\n#include \"renderer/gui/integration/private/gui_make_standalone_subtexture.h\"\n#include \"renderer/gui/integration/private/gui_texture.h\"\n\n\nnamespace openage::renderer::gui {\n\nGuiTexture::GuiTexture(const SizedTextureHandle &texture_handle) :\n\tQSGTexture{},\n\ttexture_handle(texture_handle) {\n}\n\nGuiTexture::~GuiTexture() = default;\n\nvoid GuiTexture::bind() {\n\tglBindTexture(GL_TEXTURE_2D, this->textureId());\n}\n\nqint64 GuiTexture::comparisonKey() const {\n\t// TODO: Qt5 What does this do??????\n\treturn 0;\n}\n\nbool GuiTexture::hasAlphaChannel() const {\n\t// assume 32bit textures\n\treturn true;\n}\n\nbool GuiTexture::hasMipmaps() const {\n\treturn false;\n}\n\nbool GuiTexture::isAtlasTexture() const {\n\treturn openage::renderer::gui::isAtlasTexture(this->texture_handle);\n}\n\nQSGTexture *GuiTexture::removedFromAtlas(QRhiResourceUpdateBatch * /* resourceUpdates */ /* = nullptr */) const {\n\tif (this->isAtlasTexture()) {\n\t\treturn this->standalone.get();\n\t}\n\n\treturn nullptr;\n}\n\nQRectF GuiTexture::normalizedTextureSubRect() const {\n\treturn QSGTexture::normalizedTextureSubRect();\n}\n\nint GuiTexture::textureId() const {\n\treturn 0;\n}\n\nQSize GuiTexture::textureSize() const {\n\treturn openage::renderer::gui::textureSize(this->texture_handle);\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_texture.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QSGTexture>\n\n#include \"gui_texture_handle.h\"\n\nnamespace openage::renderer::gui {\n\nclass GuiTexture : public QSGTexture {\n\tQ_OBJECT\n\npublic:\n\texplicit GuiTexture(const SizedTextureHandle &texture_handle);\n\tvirtual ~GuiTexture();\n\nprivate:\n\tvirtual qint64 comparisonKey() const override;\n\tvirtual bool hasAlphaChannel() const override;\n\tvirtual bool hasMipmaps() const override;\n\tvirtual bool isAtlasTexture() const override;\n\tvirtual QSGTexture *removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdates = nullptr) const override;\n\tvirtual QRectF normalizedTextureSubRect() const override;\n\tvirtual QSize textureSize() const override;\n\n\t// TODO: Leftover from Qt5\n\tvoid bind();\n\tint textureId() const;\n\n\tconst SizedTextureHandle texture_handle;\n\tmutable std::unique_ptr<QSGTexture> standalone;\n};\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_texture_handle.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"gui_texture_handle.h\"\n\n#include <numeric>\n\n\nnamespace openage::renderer::gui {\n\nSizedTextureHandle::SizedTextureHandle() :\n\tTextureHandle{0},\n\tsize{} {\n}\n\nSizedTextureHandle::SizedTextureHandle(const TextureHandle &handle, const QSize &size) :\n\tTextureHandle(handle),\n\tsize{size} {\n}\n\nbool isAtlasTexture(const TextureHandle &texture_handle) {\n\treturn texture_handle.subid >= 0;\n}\n\nQSize textureSize(const SizedTextureHandle &texture_handle) {\n\treturn texture_handle.size;\n}\n\nQSize native_size(const TextureHandle & /* texture_handle */) {\n\treturn QSize(0, 0);\n}\n\nQSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requested_size) {\n\tconst QSize size = native_size(texture_handle);\n\n\tif (requested_size.isValid()) {\n\t\tconst QSize bounding_size(requested_size.width() > 0 ? requested_size.width() : size.width(), requested_size.height() > 0 ? requested_size.height() : size.height());\n\n\t\t// If requested_size.isEmpty() then the caller don't care how big one or two dimensions can grow.\n\t\treturn size.scaled(bounding_size, requested_size.isEmpty() && (requested_size.width() > size.width() || requested_size.height() > size.height()) ? Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio);\n\t}\n\telse {\n\t\treturn size;\n\t}\n}\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/private/gui_texture_handle.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QSize>\n\nnamespace openage::renderer::gui {\n\nclass TextureHandle {\npublic:\n\tint subid;\n};\n\nclass SizedTextureHandle : public TextureHandle {\npublic:\n\tSizedTextureHandle();\n\tSizedTextureHandle(const TextureHandle &handle, const QSize &size);\n\tQSize size;\n};\n\nbool isAtlasTexture(const TextureHandle &texture_handle);\nQSize textureSize(const SizedTextureHandle &texture_handle);\n\nQSize native_size(const TextureHandle &texture_handle);\n\n/**\n * The sourceSize property of the Image QML element ends up here for checking.\n *\n * @param requested_size sourceSize if used in Image QML element.\n * @return if !requested_size.isValid() or requested_size.isNull() then native size, otherwise fit in the requested_size (zero component in requested_size means unbounded).\n */\nQSize aspect_fit_size(const TextureHandle &texture_handle, const QSize &requested_size);\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/public/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tgui_application_with_logger.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/public/gui_application_with_logger.cpp",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#include \"gui_application_with_logger.h\"\n\n#include \"../../guisys/private/gui_application_impl.h\"\n#include \"../../integration/private/gui_log.h\"\n\nnamespace openage::renderer::gui {\n\nnamespace {\nstd::shared_ptr<qtgui::GuiApplicationImpl> create() {\n\tqInstallMessageHandler(gui_log);\n\treturn qtgui::GuiApplicationImpl::get();\n}\n}\n\nGuiApplicationWithLogger::GuiApplicationWithLogger()\n\t:\n\tGuiApplication{create()} {\n}\n\nGuiApplicationWithLogger::~GuiApplicationWithLogger() = default;\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/gui/integration/public/gui_application_with_logger.h",
    "content": "// Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../../guisys/public/gui_application.h\"\n\nnamespace openage::renderer::gui {\n\n/**\n * Houses gui logic event queue and attaches to game logger.\n */\nclass GuiApplicationWithLogger : public qtgui::GuiApplication {\npublic:\n\tGuiApplicationWithLogger();\n\t~GuiApplicationWithLogger();\n};\n\n} // namespace openage::renderer::gui\n"
  },
  {
    "path": "libopenage/renderer/opengl/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tbuffer.cpp\n\tcontext.cpp\n\tdebug.cpp\n\terror.cpp\n\tframebuffer.cpp\n\tgeometry.cpp\n\trender_pass.cpp\n\trender_target.cpp\n\trenderer.cpp\n\tshader.cpp\n    shader_data.cpp\n\tshader_program.cpp\n\tsimple_object.cpp\n\ttexture.cpp\n\ttexture_array.cpp\n    uniform_buffer.cpp\n\tuniform_input.cpp\n\tutil.cpp\n\tvertex_array.cpp\n\twindow.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/opengl/buffer.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"buffer.h\"\n\n#include \"../../error/error.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlBuffer::GlBuffer(const std::shared_ptr<GlContext> &context,\n                   size_t size,\n                   GLenum usage) :\n\tGlSimpleObject(context, [](GLuint handle) { glDeleteBuffers(1, &handle); }),\n\tsize(size) {\n\tGLuint handle;\n\tglGenBuffers(1, &handle);\n\tthis->handle = handle;\n\n\tthis->bind(GL_COPY_WRITE_BUFFER);\n\tglBufferData(GL_COPY_WRITE_BUFFER, size, nullptr, usage);\n}\n\nGlBuffer::GlBuffer(const std::shared_ptr<GlContext> &context,\n                   const uint8_t *data,\n                   size_t size,\n                   GLenum usage) :\n\tGlSimpleObject(context, [](GLuint handle) { glDeleteBuffers(1, &handle); }),\n\tsize(size) {\n\tGLuint handle;\n\tglGenBuffers(1, &handle);\n\tthis->handle = handle;\n\n\tthis->bind(GL_COPY_WRITE_BUFFER);\n\tglBufferData(GL_COPY_WRITE_BUFFER, size, data, usage);\n}\n\nsize_t GlBuffer::get_size() const {\n\treturn this->size;\n}\n\nvoid GlBuffer::upload_data(const uint8_t *data, size_t offset, size_t size) {\n\tif (offset + size > this->size) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Tried to upload more data to OpenGL buffer than can fit.\");\n\t}\n\n\tthis->bind(GL_COPY_WRITE_BUFFER);\n\tglBufferSubData(GL_COPY_WRITE_BUFFER, offset, size, data);\n}\n\nvoid GlBuffer::bind(GLenum target) const {\n\tif (!bool(this->handle)) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"OpenGL buffer has been moved out of.\");\n\t}\n\n\tglBindBuffer(target, *this->handle);\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/buffer.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"simple_object.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// Represents an OpenGL buffer of memory\n/// allocated on the GPU.\nclass GlBuffer final : public GlSimpleObject {\npublic:\n\t/// Creates an empty buffer of the specified size.\n\t/// Binds the GL_COPY_WRITE_BUFFER target.\n\tGlBuffer(const std::shared_ptr<GlContext> &context,\n\t         size_t size,\n\t         GLenum usage = GL_STATIC_DRAW);\n\n\t/// Creates a buffer of the specified size and fills it with the given data.\n\t/// Binds the GL_COPY_WRITE_BUFFER target.\n\tGlBuffer(const std::shared_ptr<GlContext> &context,\n\t         const uint8_t *data,\n\t         size_t size,\n\t         GLenum usage = GL_STATIC_DRAW);\n\n\t/// The size in bytes of this buffer.\n\tsize_t get_size() const;\n\n\t/// Uploads `size` bytes of new data starting at `offset`.\n\t/// `offset + size` has to be less than or equal to `get_size()`.\n\t/// Binds the GL_COPY_WRITE_BUFFER target.\n\tvoid upload_data(const uint8_t *data, size_t offset, size_t size);\n\n\t/// Bind this buffer to the specified GL target.\n\tvoid bind(GLenum target) const;\n\nprivate:\n\t/// The size in bytes of this buffer.\n\tsize_t size;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/context.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include <array>\n#include <epoxy/gl.h>\n\n#include \"context.h\"\n\n#include <QOffscreenSurface>\n#include <QOpenGLContext>\n#include <QOpenGLDebugLogger>\n#include <QWindow>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"renderer/opengl/debug.h\"\n\n\nnamespace openage::renderer::opengl {\n\n/// The first element is the lowest version we need, last is highest version we support.\nstatic constexpr std::array<std::pair<int, int>, 1> gl_versions = {{{3, 3}}}; // for now we don't need any higher versions\n\n/// Finds out the supported graphics functions and OpenGL version of the device.\ngl_context_spec GlContext::find_spec() {\n\tQSurfaceFormat test_format{};\n\ttest_format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);\n\ttest_format.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer);\n\ttest_format.setDepthBufferSize(24);\n\n\tfor (size_t i_ver = 0; i_ver < gl_versions.size(); ++i_ver) {\n\t\tQOpenGLContext test_context{};\n\n\t\ttest_format.setMajorVersion(gl_versions[i_ver].first);\n\t\ttest_format.setMinorVersion(gl_versions[i_ver].second);\n\n\t\ttest_context.setFormat(test_format);\n\t\ttest_context.create();\n\n\t\tif (!test_context.isValid()) {\n\t\t\tif (i_ver == 0) {\n\t\t\t\tthrow Error(MSG(err) << \"OpenGL version \"\n\t\t\t\t                     << gl_versions[0].first << \".\" << gl_versions[0].second\n\t\t\t\t                     << \" is not available. It is the minimal required version.\");\n\t\t\t}\n\n\t\t\ttest_format.setMajorVersion(gl_versions[i_ver - 1].first);\n\t\t\ttest_format.setMinorVersion(gl_versions[i_ver - 1].second);\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tQOpenGLContext test_context{};\n\ttest_context.setFormat(test_format);\n\ttest_context.create();\n\tif (!test_context.isValid()) {\n\t\tthrow Error(MSG(err) << \"Failed to create OpenGL context which previously succeeded. This should not happen!\");\n\t}\n\n\tQOffscreenSurface test_surface{};\n\ttest_surface.setFormat(test_format);\n\ttest_surface.create();\n\n\ttest_context.makeCurrent(&test_surface);\n\n\tgl_context_spec caps{};\n\n\t// Texture parameters\n\tGLint temp;\n\tglGetIntegerv(GL_MAX_TEXTURE_SIZE, &temp);\n\tcaps.max_texture_size = temp;\n\t// TODO maybe GL_MAX_TEXTURE_IMAGE_UNITS or maybe GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS\n\t// lol opengl\n\tglGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &temp);\n\tcaps.max_texture_slots = temp;\n\tglGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &temp);\n\tcaps.max_vertex_attributes = temp;\n\tglGetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &temp);\n\tcaps.max_uniform_locations = temp;\n\tglGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &temp);\n\tcaps.max_uniform_buffer_bindings = temp;\n\n\t// OpenGL version\n\tglGetIntegerv(GL_MAJOR_VERSION, &caps.major_version);\n\tglGetIntegerv(GL_MINOR_VERSION, &caps.minor_version);\n\n\treturn caps;\n}\n\nGlContext::GlContext(const std::shared_ptr<QWindow> &window,\n                     bool debug) :\n\twindow{window},\n\tlog_handler{} {\n\tthis->specs = find_spec();\n\tauto const &specs = this->specs;\n\n\tthis->uniform_buffer_bindings = std::vector<bool>(specs.max_uniform_buffer_bindings);\n\n\tif (debug) {\n\t\tthis->log_handler = std::make_shared<GlDebugLogHandler>();\n\t}\n\n\tthis->gl_context = std::make_shared<QOpenGLContext>();\n\tthis->gl_context->setFormat(this->window->requestedFormat());\n\tthis->gl_context->create();\n\tif (!this->gl_context->isValid()) {\n\t\tthrow Error(MSG(err) << \"OpenGL context creation failed.\");\n\t}\n\n\tthis->gl_context->makeCurrent(window.get());\n\n\tif (debug) {\n\t\t// Log handler requires a current context, so we start it after associating\n\t\t// it with the window.\n\t\tthis->log_handler->start();\n\t}\n\n\t// We still have to verify that our version of libepoxy supports this version of OpenGL.\n\tint epoxy_glv = specs.major_version * 10 + specs.minor_version;\n\tif (not epoxy_is_desktop_gl() or epoxy_gl_version() < epoxy_glv) {\n\t\tthrow Error(MSG(err) << \"The used version of libepoxy does not support OpenGL version \"\n\t\t                     << specs.major_version << \".\" << specs.minor_version);\n\t}\n\n\tlog::log(MSG(info) << \"Created OpenGL context version \" << specs.major_version << \".\" << specs.minor_version);\n\n\t// To quote the standard doc: 'The value gives a rough estimate of the\n\t// largest texture that the GL can handle'\n\t// -> wat?  anyways, we need at least 1024x1024.\n\tlog::log(MSG(dbg) << \"Maximum supported texture size: \"\n\t                  << specs.max_texture_size);\n\tif (specs.max_texture_size < 1024) {\n\t\tthrow Error(MSG(err) << \"Maximum supported texture size is too small: \"\n\t\t                     << specs.max_texture_size);\n\t}\n\n\tlog::log(MSG(dbg) << \"Maximum supported texture units: \"\n\t                  << specs.max_texture_slots);\n\tif (specs.max_texture_slots < 2) {\n\t\tthrow Error(MSG(err) << \"Your GPU doesn't have enough texture units: \"\n\t\t                     << specs.max_texture_slots);\n\t}\n}\n\nGlContext::GlContext(GlContext &&other) :\n\tgl_context(other.gl_context), specs(other.specs) {\n\tother.gl_context = nullptr;\n}\n\nGlContext &GlContext::operator=(GlContext &&other) {\n\tthis->gl_context = other.gl_context;\n\tthis->specs = other.specs;\n\tother.gl_context = nullptr;\n\n\treturn *this;\n}\n\nstd::shared_ptr<QOpenGLContext> GlContext::get_raw_context() const {\n\treturn this->gl_context;\n}\n\nGLuint GlContext::get_default_framebuffer_id() {\n\treturn this->gl_context->defaultFramebufferObject();\n}\n\ngl_context_spec GlContext::get_specs() const {\n\treturn this->specs;\n}\n\nvoid GlContext::check_error() {\n\tGLenum error_state = glGetError();\n\tif (error_state != GL_NO_ERROR) {\n\t\tconst char *msg = [=] {\n\t\t\t// generate error message\n\t\t\tswitch (error_state) {\n\t\t\tcase GL_INVALID_ENUM:\n\t\t\t\t// An unacceptable value is specified for an enumerated argument.\n\t\t\t\t// The offending command is ignored\n\t\t\t\t// and has no other side effect than to set the error flag.\n\t\t\t\treturn \"GL_INVALID_ENUM\";\n\t\t\tcase GL_INVALID_VALUE:\n\t\t\t\t// A numeric argument is out of range.\n\t\t\t\t// The offending command is ignored\n\t\t\t\t// and has no other side effect than to set the error flag.\n\t\t\t\treturn \"GL_INVALID_VALUE\";\n\t\t\tcase GL_INVALID_OPERATION:\n\t\t\t\t// The specified operation is not allowed in the current state.\n\t\t\t\t// The offending command is ignored\n\t\t\t\t// and has no other side effect than to set the error flag.\n\t\t\t\treturn \"GL_INVALID_OPERATION\";\n\t\t\tcase GL_INVALID_FRAMEBUFFER_OPERATION:\n\t\t\t\t// The framebuffer object is not complete. The offending command\n\t\t\t\t// is ignored and has no other side effect than to set the error flag.\n\t\t\t\treturn \"GL_INVALID_FRAMEBUFFER_OPERATION\";\n\t\t\tcase GL_OUT_OF_MEMORY:\n\t\t\t\t// There is not enough memory left to execute the command.\n\t\t\t\t// The state of the GL is undefined,\n\t\t\t\t// except for the state of the error flags,\n\t\t\t\t// after this error is recorded.\n\t\t\t\treturn \"GL_OUT_OF_MEMORY\";\n\t\t\tcase GL_STACK_UNDERFLOW:\n\t\t\t\t// An attempt has been made to perform an operation that would\n\t\t\t\t// cause an internal stack to underflow.\n\t\t\t\treturn \"GL_STACK_UNDERFLOW\";\n\t\t\tcase GL_STACK_OVERFLOW:\n\t\t\t\t// An attempt has been made to perform an operation that would\n\t\t\t\t// cause an internal stack to overflow.\n\t\t\t\treturn \"GL_STACK_OVERFLOW\";\n\t\t\tdefault:\n\t\t\t\t// unknown error state\n\t\t\t\treturn \"unknown error\";\n\t\t\t}\n\t\t}();\n\n\t\tthrow Error(\n\t\t\tMSG(err) << \"An OpenGL error has occured.\\n\\t\"\n\t\t\t\t\t << \"(\" << error_state << \"): \" << msg);\n\t}\n}\n\nvoid GlContext::set_vsync(bool on) {\n\tif (on) {\n\t\tthis->gl_context->format().setSwapInterval(1);\n\t}\n\telse {\n\t\tthis->gl_context->format().setSwapInterval(0);\n\t}\n}\n\n\nconst std::weak_ptr<GlShaderProgram> &GlContext::get_current_program() const {\n\treturn this->last_program;\n}\n\n\nvoid GlContext::set_current_program(const std::shared_ptr<GlShaderProgram> &prog) {\n\tthis->last_program = prog;\n}\n\nsize_t GlContext::get_uniform_buffer_binding() {\n\tfor (size_t i = 1; i < this->specs.max_uniform_buffer_bindings; ++i) {\n\t\tif (not this->uniform_buffer_bindings[i]) {\n\t\t\tthis->uniform_buffer_bindings[i] = true;\n\t\t\treturn i;\n\t\t}\n\t}\n\n\tthrow Error(MSG(err) << \"Cannot get free uniform buffer binding point: \"\n\t                     << \"No free uniform buffer binding points available.\");\n}\n\nvoid GlContext::free_uniform_buffer_binding(size_t binding_point) {\n\tif (binding_point >= this->specs.max_uniform_buffer_bindings) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Cannot free invalid uniform buffer binding point: \" << binding_point);\n\t}\n\tthis->uniform_buffer_bindings[binding_point] = false;\n}\n\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/context.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <QObject>\n\nQT_FORWARD_DECLARE_CLASS(QWindow)\nQT_FORWARD_DECLARE_CLASS(QOpenGLContext)\n\nnamespace openage::renderer::opengl {\n\nclass GlDebugLogHandler;\nclass GlShaderProgram;\n\n/**\n * Stores information about context capabilities and limitations.\n */\nstruct gl_context_spec {\n\t/// The maximum number of vertex attributes in a shader.\n\tsize_t max_vertex_attributes;\n\t/// The amount of texture units (GL_TEXTUREi) available.\n\tsize_t max_texture_slots;\n\t/// The maximum size of a single dimension of a texture.\n\tsize_t max_texture_size;\n\t/// The maximum number of uniform locations per shader.\n\tsize_t max_uniform_locations;\n\t/// The maximum number of binding points for uniform blocks\n\t/// in a single shader.\n\tsize_t max_uniform_buffer_bindings;\n\n\tint major_version;\n\tint minor_version;\n};\n\n/**\n * Manages an OpenGL context.\n */\nclass GlContext {\npublic:\n\t/**\n\t * Create a GL context for the given Qt window.\n\t *\n\t * @param window Window for the context. The context is made current to this window.\n\t * @param debug If true, enable OpenGL debug logging.\n\t */\n\texplicit GlContext(const std::shared_ptr<QWindow> &window,\n\t                   bool debug = false);\n\t~GlContext() = default;\n\n\t// It doesn't make sense to have more than one instance of the same context.\n\tGlContext(const GlContext &) = delete;\n\tGlContext &operator=(const GlContext &) = delete;\n\n\t/**\n\t * Move this context.\n\t *\n\t * We have to support moving to avoid a mess somewhere else.\n\t */\n\tGlContext(GlContext &&);\n\tGlContext &operator=(GlContext &&);\n\n\t/**\n\t * Get the underlying Qt OpenGL context object.\n\t *\n\t * @return Qt's OpenGL context object.\n\t */\n\tstd::shared_ptr<QOpenGLContext> get_raw_context() const;\n\n\t/**\n\t * Get the ID of the default framebuffer used for displaying to\n\t * the window.\n\t *\n\t * This value may change on every frame, so it should be called every\n\t * time the default framebuffer is bound.\n\t *\n\t * @return ID of the default (display) framebuffer.\n\t */\n\tunsigned int get_default_framebuffer_id();\n\n\t/**\n\t * Get the capabilities of this context.\n\t */\n\tgl_context_spec get_specs() const;\n\n\t/**\n\t * Activate or deactivate VSync for this context.\n\t *\n\t * TODO: This currently does not work at runtime. vsync must be set before\n\t * the QWindow is created.\n\t *\n\t * @param on \\p true to activate VSync, \\p false to deactivate.\n\t */\n\tvoid set_vsync(bool on);\n\n\t/**\n\t * Check whether the current GL context on this thread reported any errors.\n\t * If any OpenGL error is found, throw an exception.\n\t *\n\t * @throw openage::error::Error Error with OpenGL error type.\n\t */\n\tstatic void check_error();\n\n\t/**\n\t * Get the currently active shader program.\n\t *\n\t * If \\p nullptr is returned, the current shader program is unknown. There\n\t * may be an active shader loaded by external libraries or scripts.\n\t *\n\t * @return Currently active shader program.\n\t */\n\tconst std::weak_ptr<GlShaderProgram> &get_current_program() const;\n\n\t/**\n\t * Store the currently active shader program for this context.\n\t *\n\t * Note that this method does not load the shader in OpenGL. It is merely\n\t * a conveniance function so that the renderer can check which program\n\t * is currently used.\n\t *\n\t * @param prog Currently active shader program.\n\t */\n\tvoid set_current_program(const std::shared_ptr<GlShaderProgram> &prog);\n\n\t/**\n\t * Get a free uniform buffer binding point that is not bound to any buffer.\n\t *\n\t * The number of available binding points is limited by the OpenGL implementation.\n\t * When the context is created, there are \\p capabilities.max_uniform_buffer_bindings\n\t * free binding points available.\n\t *\n\t * @return Binding point ID.\n\t *\n\t * @throw Error if no binding point is available.\n\t */\n\tsize_t get_uniform_buffer_binding();\n\n\t/**\n\t * Free a buffer binding point, indicating that newly created buffers can use it.\n\t *\n\t * When calling this function, it must be ensured that the binding point is not\n\t * assigned to any buffer or shader. Otherwise, reassigning the binding point\n\t * can corrupt the uniform data.\n\t *\n\t * @param binding_point Binding point ID.\n\t *\n\t * @throw Error if the binding point is not valid.\n\t */\n\tvoid free_uniform_buffer_binding(size_t binding_point);\n\n\t/**\n\t * Find out the supported graphics functions and OpenGL version of the device.\n\t */\n\tstatic gl_context_spec find_spec();\n\nprivate:\n\t/**\n\t * Associated Qt window. Held here so the context remains active.\n\t */\n\tstd::shared_ptr<QWindow> window;\n\n\t/**\n\t * Logger for OpenGL debug messages.\n\t *\n\t * TODO: Make debug logging optional\n\t */\n\tstd::shared_ptr<GlDebugLogHandler> log_handler;\n\n\t/**\n\t * Pointer to Qt struct representing the GL context.\n\t */\n\tstd::shared_ptr<QOpenGLContext> gl_context;\n\n\t/**\n\t * Context capabilities, i.e. available OpenGL features and version.\n\t */\n\tgl_context_spec specs{};\n\n\t/**\n\t * The last-active shader program\n\t */\n\tstd::weak_ptr<GlShaderProgram> last_program;\n\n\t/**\n\t * Store the currently active binding points for uniform buffers.\n\t */\n\tstd::vector<bool> uniform_buffer_bindings;\n};\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/debug.cpp",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#include \"debug.h\"\n\n#include <QOpenGLDebugLogger>\n\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlDebugLogHandler::GlDebugLogHandler() :\n\tlogger{std::make_shared<QOpenGLDebugLogger>()} {\n}\n\nvoid GlDebugLogHandler::start() {\n\tthis->logger->initialize();\n\tQObject::connect(this->logger.get(),\n\t                 &QOpenGLDebugLogger::messageLogged,\n\t                 this,\n\t                 &GlDebugLogHandler::log);\n\tthis->logger->startLogging();\n}\n\nvoid GlDebugLogHandler::stop() {\n\tthis->logger->stopLogging();\n}\n\nvoid GlDebugLogHandler::log(const QOpenGLDebugMessage &debugMessage) {\n\tlog::log(MSG(dbg) << debugMessage.message().toStdString());\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/debug.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QObject>\n#include <QOpenGLDebugMessage>\n\nQT_FORWARD_DECLARE_CLASS(QOpenGLDebugLogger)\n\nnamespace openage::renderer::opengl {\n\nclass GlDebugLogHandler : public QObject {\n\tQ_OBJECT\n\npublic:\n\t/**\n\t * Create a new OpenGL debug logger.\n\t */\n\tGlDebugLogHandler();\n\t~GlDebugLogHandler() = default;\n\n\t/**\n\t * Start logging OpenGL debug messages.\n\t *\n\t * Logging requires a current OpenGL context. If no current context can\n\t * be found, no log messages will be generated.\n\t *\n\t * You may call this method multiple times to reattach Qt's logger if\n\t * the current context has changed.\n\t */\n\tvoid start();\n\n\t/**\n\t * Stop logging OpenGL debug messages.\n\t */\n\tvoid stop();\n\npublic slots:\n\t/**\n\t * Pass log messages to the openage logger.\n\t *\n\t * All messages are passed with DBG info level. This also means that the logger\n\t * does not exit on OpenGL errors.\n\t *\n\t * @param debugMessage Qt's log message object.\n\t */\n\tvoid log(const QOpenGLDebugMessage &debugMessage);\n\nprivate:\n\t/**\n\t * Logger for OpenGL debug messages\n\t */\n\tstd::shared_ptr<QOpenGLDebugLogger> logger;\n};\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/error.cpp",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#include \"error.h\"\n\n#include <epoxy/gl.h>\n\n#include \"error/error.h\"\n\nnamespace openage::renderer::opengl {\n\nvoid gl_check_error() {\n\tint glerrorstate = 0;\n\n\tglerrorstate = glGetError();\n\tif (glerrorstate != GL_NO_ERROR) {\n\t\tconst char *errormsg;\n\n\t\t//generate error message\n\t\tswitch (glerrorstate) {\n\t\tcase GL_INVALID_ENUM:\n\t\t\t// An unacceptable value is specified for an enumerated argument.\n\t\t\t// The offending command is ignored\n\t\t\t// and has no other side effect than to set the error flag.\n\t\t\terrormsg = \"invalid enum passed to opengl call\";\n\t\t\tbreak;\n\t\tcase GL_INVALID_VALUE:\n\t\t\t// A numeric argument is out of range.\n\t\t\t// The offending command is ignored\n\t\t\t// and has no other side effect than to set the error flag.\n\t\t\terrormsg = \"invalid value passed to opengl call\";\n\t\t\tbreak;\n\t\tcase GL_INVALID_OPERATION:\n\t\t\t// The specified operation is not allowed in the current state.\n\t\t\t// The offending command is ignored\n\t\t\t// and has no other side effect than to set the error flag.\n\t\t\terrormsg = \"invalid operation performed during some state\";\n\t\t\tbreak;\n\t\tcase GL_INVALID_FRAMEBUFFER_OPERATION:\n\t\t\t// The framebuffer object is not complete. The offending command\n\t\t\t// is ignored and has no other side effect than to set the error flag.\n\t\t\terrormsg = \"invalid framebuffer operation\";\n\t\t\tbreak;\n\t\tcase GL_OUT_OF_MEMORY:\n\t\t\t// There is not enough memory left to execute the command.\n\t\t\t// The state of the GL is undefined,\n\t\t\t// except for the state of the error flags,\n\t\t\t// after this error is recorded.\n\t\t\terrormsg = \"out of memory, wtf?\";\n\t\t\tbreak;\n\t\tcase GL_STACK_UNDERFLOW:\n\t\t\t// An attempt has been made to perform an operation that would\n\t\t\t// cause an internal stack to underflow.\n\t\t\terrormsg = \"stack underflow\";\n\t\t\tbreak;\n\t\tcase GL_STACK_OVERFLOW:\n\t\t\t// An attempt has been made to perform an operation that would\n\t\t\t// cause an internal stack to overflow.\n\t\t\terrormsg = \"stack overflow\";\n\t\t\tbreak;\n\t\tdefault:\n\t\t\t// unknown error state\n\t\t\terrormsg = \"unknown error\";\n\t\t}\n\t\tthrow Error(MSG(err) << \"OpenGL error state after running draw method: \"\n\t\t                     << glerrorstate << \"\\n\"\n\t\t                                        \"\\t\"\n\t\t                     << errormsg << \"\\n\"\n\t\t                     << \"Run the engine with --gl-debug to get more information.\");\n\t}\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/error.h",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage::renderer::opengl {\n\n/**\n * query the current opengl context for any errors.\n *\n * raises exceptions on any error.\n */\nvoid gl_check_error();\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/framebuffer.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"framebuffer.h\"\n\n#include \"log/log.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/texture.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlFramebuffer::GlFramebuffer(const std::shared_ptr<GlContext> &context) :\n\tGlSimpleObject(context,\n                   [](GLuint /*handle*/) {}),\n\ttype{gl_framebuffer_t::display} {\n\tlog::log(MSG(dbg) << \"Created OpenGL framebuffer with display target\");\n}\n\n// TODO the validity of this object is contingent\n// on its texture existing. use shared_ptr?\nGlFramebuffer::GlFramebuffer(const std::shared_ptr<GlContext> &context,\n                             std::vector<std::shared_ptr<GlTexture2d>> const &textures) :\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteFramebuffers(1, &handle); }),\n\ttype{gl_framebuffer_t::textures} {\n\tGLuint handle;\n\tglGenFramebuffers(1, &handle);\n\tthis->handle = handle;\n\n\tglBindFramebuffer(GL_FRAMEBUFFER, handle);\n\n\tstd::vector<GLenum> drawBuffers;\n\n\tif (textures.empty()) {\n\t\tthrow Error{ERR << \"At least 1 texture must be assigned to texture framebuffer.\"};\n\t}\n\n\tsize_t colorTextureCount = 0;\n\tfor (auto const &texture : textures) {\n\t\t// TODO figure out attachment points from pixel formats\n\t\tif (texture->get_info().get_format() == resources::pixel_format::depth24) {\n\t\t\tglFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture->get_handle(), 0);\n\t\t}\n\t\telse {\n\t\t\tauto attachmentPoint = GL_COLOR_ATTACHMENT0 + colorTextureCount++;\n\t\t\tglFramebufferTexture2D(GL_FRAMEBUFFER, attachmentPoint, GL_TEXTURE_2D, texture->get_handle(), 0);\n\t\t\tdrawBuffers.push_back(attachmentPoint);\n\t\t}\n\t}\n\n\tglDrawBuffers(drawBuffers.size(), drawBuffers.data());\n\n\tif (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {\n\t\tthrow Error(MSG(err) << \"Could not create OpenGL framebuffer.\");\n\t}\n\n\tlog::log(MSG(dbg) << \"Created OpenGL framebuffer with texture targets\");\n}\n\ngl_framebuffer_t GlFramebuffer::get_type() const {\n\treturn this->type;\n}\n\nvoid GlFramebuffer::bind_read() const {\n\tif (this->type == gl_framebuffer_t::textures) {\n\t\tglBindFramebuffer(GL_READ_FRAMEBUFFER, *this->handle);\n\t}\n\telse {\n\t\tglBindFramebuffer(GL_READ_FRAMEBUFFER, this->context->get_default_framebuffer_id());\n\t}\n}\n\nvoid GlFramebuffer::bind_write() const {\n\tif (this->type == gl_framebuffer_t::textures) {\n\t\tglBindFramebuffer(GL_DRAW_FRAMEBUFFER, *this->handle);\n\t}\n\telse {\n\t\tglBindFramebuffer(GL_DRAW_FRAMEBUFFER, this->context->get_default_framebuffer_id());\n\t}\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/framebuffer.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"renderer/opengl/simple_object.h\"\n\n\nnamespace openage::renderer::opengl {\n\nclass GlTexture2d;\n\n/**\n * The type of OpenGL framebuffer.\n */\nenum class gl_framebuffer_t {\n\t/**\n\t * The actual window. This is visible to the user after swapping front and back buffers.\n\t */\n\tdisplay,\n\t/**\n\t * A bunch of textures. These can be color texture, depth textures, etc.\n\t */\n\ttextures,\n};\n\n/**\n * Represents an OpenGL Framebuffer Object.\n * It is a collection of bitmap targets that can be drawn into\n * and read from.\n */\nclass GlFramebuffer final : public GlSimpleObject {\npublic:\n\t/**\n\t * Construct a framebuffer pointing at the default framebuffer - the window.\n\t *\n\t * Drawing into this framebuffer draws onto the screen.\n\t *\n\t * @param context OpenGL context used for drawing.\n\t */\n\tGlFramebuffer(const std::shared_ptr<GlContext> &context);\n\n\t/**\n\t * Construct a framebuffer pointing at the given textures.\n\t *\n\t * Texture are attached to points specific to their pixel format,\n\t * e.g. a depth texture will be set as the depth target.\n\t *\n\t * @param context OpenGL context used for drawing.\n\t * @param textures Textures targeted by the framebuffer. They are automatically\n\t *                 attached to the correct attachement points depending on their type.\n\t */\n\tGlFramebuffer(const std::shared_ptr<GlContext> &context,\n\t              std::vector<std::shared_ptr<GlTexture2d>> const &textures);\n\n\t/**\n\t * Get the type of this framebuffer.\n\t *\n\t * @return Framebuffer type.\n\t */\n\tgl_framebuffer_t get_type() const;\n\n\t/**\n\t * Bind this framebuffer to \\p GL_READ_FRAMEBUFFER.\n\t */\n\tvoid bind_read() const;\n\n\t/**\n\t * Bind this framebuffer to \\p GL_DRAW_FRAMEBUFFER.\n\t */\n\tvoid bind_write() const;\n\nprivate:\n\t/**\n\t * Type of this framebuffer.\n\t */\n\tgl_framebuffer_t type;\n};\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/geometry.cpp",
    "content": "// Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n#include \"geometry.h\"\n\n#include <epoxy/gl.h>\n\n#include \"../../datastructure/constexpr_map.h\"\n#include \"../../error/error.h\"\n\n#include \"lookup.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlGeometry::GlGeometry() :\n\tGeometry(geometry_t::bufferless_quad) {}\n\nGlGeometry::GlGeometry(const std::shared_ptr<GlContext> &context, const resources::MeshData &mesh) :\n\tGeometry(geometry_t::mesh) {\n\tGlBuffer verts_buf{context, mesh.get_data().data(), mesh.get_data().size()};\n\tGlVertexArray verts_array(context, verts_buf, mesh.get_info());\n\n\tthis->mesh = GlMesh{\n\t\tstd::move(verts_buf),\n\t\tstd::move(verts_array),\n\t\t{},\n\t\t{},\n\t\tmesh.get_data().size() / mesh.get_info().vert_size(),\n\t\tGL_PRIMITIVE.get(mesh.get_info().get_primitive()),\n\t};\n\n\tif (mesh.get_ids()) {\n\t\tthis->mesh->indices = GlBuffer{context, mesh.get_ids()->data(), mesh.get_ids()->size()};\n\t\tthis->mesh->index_type = GL_INDEX_TYPE.get(*mesh.get_info().get_index_type());\n\t\tthis->mesh->vert_count = mesh.get_ids()->size() / GL_INDEX_SIZE.get(*mesh.get_info().get_index_type());\n\t}\n}\n\nvoid GlGeometry::update_verts_offset(std::vector<uint8_t> const &verts, size_t offset) {\n\tif (this->get_type() != geometry_t::mesh) {\n\t\tthrow Error(MSG(err) << \"Cannot update vertex data for non-mesh GlGeometry.\");\n\t}\n\n\tif (verts.size() != this->mesh->vertices.get_size()) {\n\t\tthrow Error(MSG(err) << \"Size mismatch between old and new vertex data for GlGeometry.\");\n\t}\n\n\t// TODO support offset updating\n\tthis->mesh->vertices.upload_data(verts.data(), offset, verts.size());\n}\n\nvoid GlGeometry::draw() const {\n\tswitch (this->get_type()) {\n\tcase geometry_t::bufferless_quad:\n\t\t// any VAO must be bound before this call\n\t\tglDrawArrays(GL_TRIANGLE_STRIP, 0, 4);\n\t\tbreak;\n\n\tcase geometry_t::mesh: {\n\t\tauto const &mesh = *this->mesh;\n\t\tmesh.vao.bind();\n\n\t\tif (mesh.indices) {\n\t\t\t// TODO: Binding the EBO may not be necessary if the VAO is already bound.\n\t\t\tmesh.indices->bind(GL_ELEMENT_ARRAY_BUFFER);\n\n\t\t\tglDrawElements(mesh.primitive, mesh.vert_count, *mesh.index_type, nullptr);\n\t\t}\n\t\telse {\n\t\t\tglDrawArrays(mesh.primitive, 0, mesh.vert_count);\n\t\t}\n\n\t\tbreak;\n\t}\n\tdefault:\n\t\tthrow Error(MSG(err) << \"Unknown geometry type in GlGeometry.\");\n\t}\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/geometry.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n\n#include \"../geometry.h\"\n#include \"../resources/mesh_data.h\"\n\n#include \"buffer.h\"\n#include \"vertex_array.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// The OpenGL class representing geometry to be passed to a draw call.\nclass GlGeometry final : public Geometry {\npublic:\n\t/// The default constructor makes a quad.\n\tGlGeometry();\n\n\t/// Initialize a meshed geometry. Relatively costly, has to initialize GL buffers and copy vertex data.\n\texplicit GlGeometry(const std::shared_ptr<GlContext> &context, resources::MeshData const &);\n\n\t/// Executes a draw command for the geometry on the currently active context.\n\t/// Assumes bound and valid shader program and all other necessary state.\n\tvoid draw() const;\n\n\tvoid update_verts_offset(std::vector<uint8_t> const &, size_t) override;\n\nprivate:\n\t/// All the pieces of OpenGL state that represent a mesh.\n\tstruct GlMesh {\n\t\tGlBuffer vertices;\n\t\tGlVertexArray vao;\n\t\tstd::optional<GlBuffer> indices;\n\t\tstd::optional<GLenum> index_type;\n\t\tsize_t vert_count;\n\t\tGLenum primitive;\n\t};\n\n\t/// Data managing GPU memory and interpretation of mesh data.\n\t/// Only present if the type is a mesh.\n\tstd::optional<GlMesh> mesh;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/lookup.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n// Lookup tables for translating between OpenGL-specific values and generic renderer values,\n// as well as mapping things like type sizes within OpenGL.\n\n#pragma once\n\n#include <epoxy/gl.h>\n\n#include \"renderer/resources/buffer_info.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/texture_info.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// Input and output pixel formats from pixel_format.\nstatic constexpr auto GL_PIXEL_FORMAT = datastructure::create_const_map<resources::pixel_format, std::tuple<GLint, GLenum, GLenum>>(\n\t// TODO check correctness of formats here\n\tstd::pair(resources::pixel_format::r16ui, std::tuple(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_INT)),\n\tstd::pair(resources::pixel_format::r32ui, std::tuple(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT)),\n\tstd::pair(resources::pixel_format::rgb8, std::tuple(GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE)),\n\tstd::pair(resources::pixel_format::bgr8, std::tuple(GL_RGB8, GL_BGR, GL_UNSIGNED_BYTE)),\n\tstd::pair(resources::pixel_format::rgba8, std::tuple(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE)),\n\tstd::pair(resources::pixel_format::rgba8ui, std::tuple(GL_RGBA8UI, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE)),\n\tstd::pair(resources::pixel_format::depth24, std::tuple(GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE)));\n\n/// Sizes of various uniform/vertex input types in shaders.\nstatic constexpr auto GL_UNIFORM_TYPE_SIZE = datastructure::create_const_map<GLenum, size_t>(\n\tstd::pair(GL_FLOAT, 4),\n\tstd::pair(GL_FLOAT_VEC2, 8),\n\tstd::pair(GL_FLOAT_VEC3, 12),\n\tstd::pair(GL_FLOAT_VEC4, 16),\n\tstd::pair(GL_DOUBLE, 8),\n\tstd::pair(GL_DOUBLE_VEC2, 16),\n\tstd::pair(GL_DOUBLE_VEC3, 24),\n\tstd::pair(GL_DOUBLE_VEC4, 32),\n\tstd::pair(GL_INT, 4),\n\tstd::pair(GL_INT_VEC2, 8),\n\tstd::pair(GL_INT_VEC3, 12),\n\tstd::pair(GL_INT_VEC4, 16),\n\tstd::pair(GL_UNSIGNED_INT, 4),\n\tstd::pair(GL_UNSIGNED_INT_VEC2, 8),\n\tstd::pair(GL_UNSIGNED_INT_VEC3, 12),\n\tstd::pair(GL_UNSIGNED_INT_VEC4, 16),\n\tstd::pair(GL_BOOL, 1),\n\tstd::pair(GL_BOOL_VEC2, 2),\n\tstd::pair(GL_BOOL_VEC3, 3),\n\tstd::pair(GL_BOOL_VEC4, 4),\n\tstd::pair(GL_FLOAT_MAT2, 16),\n\tstd::pair(GL_FLOAT_MAT3, 36),\n\tstd::pair(GL_FLOAT_MAT4, 64),\n\tstd::pair(GL_SAMPLER_1D, 4),\n\tstd::pair(GL_SAMPLER_2D, 4),\n\tstd::pair(GL_SAMPLER_2D_ARRAY, 4),\n\tstd::pair(GL_SAMPLER_3D, 4),\n\tstd::pair(GL_SAMPLER_CUBE, 4));\n\n/// GL types with corresponding GLSL type strings.\nstatic constexpr auto GLSL_TYPE_NAME = datastructure::create_const_map<GLenum, const char *>(\n\tstd::pair(GL_FLOAT, \"float\"),\n\tstd::pair(GL_FLOAT_VEC2, \"vec2\"),\n\tstd::pair(GL_FLOAT_VEC3, \"vec3\"),\n\tstd::pair(GL_FLOAT_VEC4, \"vec4\"),\n\tstd::pair(GL_INT, \"int\"),\n\tstd::pair(GL_INT_VEC2, \"ivec2\"),\n\tstd::pair(GL_INT_VEC3, \"ivec3\"),\n\tstd::pair(GL_INT_VEC4, \"ivec4\"),\n\tstd::pair(GL_UNSIGNED_INT, \"uint\"),\n\tstd::pair(GL_UNSIGNED_INT_VEC2, \"uvec2\"),\n\tstd::pair(GL_UNSIGNED_INT_VEC3, \"uvec3\"),\n\tstd::pair(GL_UNSIGNED_INT_VEC4, \"uvec4\"),\n\tstd::pair(GL_BOOL, \"bool\"),\n\tstd::pair(GL_BOOL_VEC2, \"bvec2\"),\n\tstd::pair(GL_BOOL_VEC3, \"bvec3\"),\n\tstd::pair(GL_BOOL_VEC4, \"bvec4\"),\n\tstd::pair(GL_FLOAT_MAT2, \"mat2\"),\n\tstd::pair(GL_FLOAT_MAT3, \"mat3\"),\n\tstd::pair(GL_FLOAT_MAT4, \"mat4\"),\n\tstd::pair(GL_FLOAT_MAT2x3, \"mat2x3\"),\n\tstd::pair(GL_FLOAT_MAT2x4, \"mat2x4\"),\n\tstd::pair(GL_FLOAT_MAT3x2, \"mat3x2\"),\n\tstd::pair(GL_FLOAT_MAT3x4, \"mat3x4\"),\n\tstd::pair(GL_FLOAT_MAT4x2, \"mat4x2\"),\n\tstd::pair(GL_FLOAT_MAT4x3, \"mat4x3\"),\n\tstd::pair(GL_SAMPLER_1D, \"sampler1D\"),\n\tstd::pair(GL_SAMPLER_2D, \"sampler2D\"),\n\tstd::pair(GL_SAMPLER_3D, \"sampler3D\"),\n\tstd::pair(GL_SAMPLER_CUBE, \"samplerCube\"),\n\tstd::pair(GL_SAMPLER_1D_SHADOW, \"sampler1DShadow\"),\n\tstd::pair(GL_SAMPLER_2D_SHADOW, \"sampler2DShadow\"),\n\tstd::pair(GL_SAMPLER_1D_ARRAY, \"sampler1DArray\"),\n\tstd::pair(GL_SAMPLER_2D_ARRAY, \"sampler2DArray\"),\n\tstd::pair(GL_SAMPLER_1D_ARRAY_SHADOW, \"sampler1DArrayShadow\"),\n\tstd::pair(GL_SAMPLER_2D_ARRAY_SHADOW, \"sampler2DArrayShadow\"),\n\tstd::pair(GL_SAMPLER_2D_MULTISAMPLE, \"sampler2DMS\"),\n\tstd::pair(GL_SAMPLER_2D_MULTISAMPLE_ARRAY, \"sampler2DMSArray\"),\n\tstd::pair(GL_SAMPLER_CUBE_SHADOW, \"samplerCubeShadow\"),\n\tstd::pair(GL_SAMPLER_BUFFER, \"samplerBuffer\"),\n\tstd::pair(GL_SAMPLER_2D_RECT, \"sampler2DRect\"),\n\tstd::pair(GL_SAMPLER_2D_RECT_SHADOW, \"sampler2DRectShadow\"),\n\tstd::pair(GL_INT_SAMPLER_1D, \"isampler1D\"),\n\tstd::pair(GL_INT_SAMPLER_2D, \"isampler2D\"),\n\tstd::pair(GL_INT_SAMPLER_3D, \"isampler3D\"),\n\tstd::pair(GL_INT_SAMPLER_CUBE, \"isamplerCube\"),\n\tstd::pair(GL_INT_SAMPLER_1D_ARRAY, \"isampler1DArray\"),\n\tstd::pair(GL_INT_SAMPLER_2D_ARRAY, \"isampler2DArray\"),\n\tstd::pair(GL_INT_SAMPLER_2D_MULTISAMPLE, \"isampler2DMS\"),\n\tstd::pair(GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY, \"isampler2DMSArray\"),\n\tstd::pair(GL_INT_SAMPLER_BUFFER, \"isamplerBuffer\"),\n\tstd::pair(GL_INT_SAMPLER_2D_RECT, \"isampler2DRect\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_1D, \"usampler1D\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_2D, \"usampler2D\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_3D, \"usampler3D\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_CUBE, \"usamplerCube\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_1D_ARRAY, \"usampler2DArray\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_2D_ARRAY, \"usampler2DArray\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE, \"usampler2DMS\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY, \"usampler2DMSArray\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_BUFFER, \"usamplerBuffer\"),\n\tstd::pair(GL_UNSIGNED_INT_SAMPLER_2D_RECT, \"usampler2DRect\"));\n\n/// Generic vertex input types from GL types.\nstatic constexpr auto GL_VERT_IN_TYPE = datastructure::create_const_map<GLenum, resources::vertex_input_t>(\n\tstd::pair(GL_FLOAT, resources::vertex_input_t::F32),\n\tstd::pair(GL_FLOAT_VEC2, resources::vertex_input_t::V2F32),\n\tstd::pair(GL_FLOAT_VEC3, resources::vertex_input_t::V3F32),\n\tstd::pair(GL_FLOAT_MAT3, resources::vertex_input_t::M3F32));\n\n/// The type of a single element in a per-vertex attribute.\nstatic constexpr auto GL_VERT_IN_ELEM_TYPE = datastructure::create_const_map<resources::vertex_input_t, GLenum>(\n\tstd::pair(resources::vertex_input_t::F32, GL_FLOAT),\n\tstd::pair(resources::vertex_input_t::V2F32, GL_FLOAT),\n\tstd::pair(resources::vertex_input_t::V3F32, GL_FLOAT),\n\tstd::pair(resources::vertex_input_t::M3F32, GL_FLOAT));\n\n/// Mapping from generic primitive types to GL types.\nstatic constexpr auto GL_PRIMITIVE = datastructure::create_const_map<resources::vertex_primitive_t, GLenum>(\n\tstd::pair(resources::vertex_primitive_t::POINTS, GL_POINTS),\n\tstd::pair(resources::vertex_primitive_t::LINES, GL_LINES),\n\tstd::pair(resources::vertex_primitive_t::LINE_STRIP, GL_LINE_STRIP),\n\tstd::pair(resources::vertex_primitive_t::LINE_LOOP, GL_LINE_LOOP),\n\tstd::pair(resources::vertex_primitive_t::TRIANGLES, GL_TRIANGLES),\n\tstd::pair(resources::vertex_primitive_t::TRIANGLE_STRIP, GL_TRIANGLE_STRIP),\n\tstd::pair(resources::vertex_primitive_t::TRIANGLE_FAN, GL_TRIANGLE_FAN));\n\n/// Mapping from generic index types to GL types.\nstatic constexpr auto GL_INDEX_TYPE = datastructure::create_const_map<resources::index_t, GLenum>(\n\tstd::pair(resources::index_t::U8, GL_UNSIGNED_BYTE),\n\tstd::pair(resources::index_t::U16, GL_UNSIGNED_SHORT),\n\tstd::pair(resources::index_t::U32, GL_UNSIGNED_INT));\n\n/// Mapping from generic index types to their size in bytes.\nstatic constexpr auto GL_INDEX_SIZE = datastructure::create_const_map<resources::index_t, size_t>(\n\tstd::pair(resources::index_t::U8, 1),\n\tstd::pair(resources::index_t::U16, 2),\n\tstd::pair(resources::index_t::U32, 4));\n\nstatic constexpr auto GL_UBO_INPUT_TYPE = datastructure::create_const_map<resources::ubo_input_t, GLenum>(\n\tstd::pair(resources::ubo_input_t::F32, GL_FLOAT),\n\tstd::pair(resources::ubo_input_t::I32, GL_INT),\n\tstd::pair(resources::ubo_input_t::U32, GL_UNSIGNED_INT),\n\tstd::pair(resources::ubo_input_t::BOOL, GL_BOOL),\n\tstd::pair(resources::ubo_input_t::V2F32, GL_FLOAT_VEC2),\n\tstd::pair(resources::ubo_input_t::V3F32, GL_FLOAT_VEC3),\n\tstd::pair(resources::ubo_input_t::V4F32, GL_FLOAT_VEC4),\n\tstd::pair(resources::ubo_input_t::V2I32, GL_INT_VEC2),\n\tstd::pair(resources::ubo_input_t::V3I32, GL_INT_VEC3),\n\tstd::pair(resources::ubo_input_t::V4I32, GL_INT_VEC4),\n\tstd::pair(resources::ubo_input_t::V2U32, GL_UNSIGNED_INT_VEC2),\n\tstd::pair(resources::ubo_input_t::V3U32, GL_UNSIGNED_INT_VEC3),\n\tstd::pair(resources::ubo_input_t::V4U32, GL_UNSIGNED_INT_VEC4),\n\tstd::pair(resources::ubo_input_t::M4F32, GL_FLOAT_MAT4));\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/render_pass.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_pass.h\"\n\n#include \"log/log.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlRenderPass::GlRenderPass(std::vector<Renderable> &&renderables,\n                           const std::shared_ptr<RenderTarget> &target) :\n\tRenderPass(std::move(renderables), target),\n\tis_optimized(false) {\n}\n\nvoid GlRenderPass::set_renderables(std::vector<Renderable> &&renderables) {\n\tRenderPass::set_renderables(std::move(renderables));\n\tthis->is_optimized = false;\n}\n\nvoid GlRenderPass::add_renderables(std::vector<Renderable> &&renderables, int64_t priority) {\n\tRenderPass::add_renderables(std::move(renderables), priority);\n\tthis->is_optimized = false;\n}\n\nvoid GlRenderPass::add_renderables(Renderable &&renderable, int64_t priority) {\n\tRenderPass::add_renderables(std::move(renderable), priority);\n\tthis->is_optimized = false;\n}\n\nbool GlRenderPass::get_is_optimized() const {\n\treturn this->is_optimized;\n}\n\nvoid GlRenderPass::set_is_optimized(bool flag) {\n\tthis->is_optimized = flag;\n}\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/render_pass.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"renderer/render_pass.h\"\n#include \"renderer/renderable.h\"\n\n\nnamespace openage::renderer::opengl {\n\nclass GlRenderPass final : public RenderPass {\npublic:\n\tGlRenderPass(std::vector<Renderable> &&renderables,\n\t             const std::shared_ptr<RenderTarget> &target);\n\n\tvoid set_renderables(std::vector<Renderable> &&renderables);\n\tvoid add_renderables(std::vector<Renderable> &&renderables, int64_t priority = LAYER_PRIORITY_MAX);\n\tvoid add_renderables(Renderable &&renderable, int64_t priority = LAYER_PRIORITY_MAX);\n\n\tvoid set_is_optimized(bool);\n\tbool get_is_optimized() const;\n\nprivate:\n\t/// Whether the renderables order is optimised\n\tbool is_optimized;\n};\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/render_target.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_target.h\"\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"renderer/opengl/texture.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlRenderTarget::GlRenderTarget(const std::shared_ptr<GlContext> &context, size_t width, size_t height) :\n\ttype(gl_render_target_t::framebuffer),\n\tsize(width, height),\n\tframebuffer(context) {\n\tlog::log(MSG(dbg) << \"Created OpenGL render target for default framebuffer\");\n}\n\nGlRenderTarget::GlRenderTarget(const std::shared_ptr<GlContext> &context,\n                               const std::vector<std::shared_ptr<GlTexture2d>> &textures) :\n\ttype(gl_render_target_t::framebuffer),\n\tframebuffer({context, textures}),\n\ttextures(textures) {\n\t// TODO: Check if the textures are all the same size\n\tthis->size = this->textures.value().at(0)->get_info().get_size();\n\n\tlog::log(MSG(dbg) << \"Created OpenGL render target for textures\");\n}\n\nresources::Texture2dData GlRenderTarget::into_data() {\n\t// make sure the framebuffer is bound\n\tthis->bind_read();\n\n\tstd::vector<uint8_t> pxdata(this->size.first * this->size.second * 4);\n\tglReadPixels(0, 0, this->size.first, this->size.second, GL_RGBA, GL_UNSIGNED_BYTE, pxdata.data());\n\n\tresources::Texture2dInfo info{this->size.first,\n\t                              this->size.second,\n\t                              resources::pixel_format::rgba8};\n\treturn resources::Texture2dData{info, std::move(pxdata)};\n}\n\ngl_render_target_t GlRenderTarget::get_type() const {\n\treturn this->type;\n}\n\nstd::vector<std::shared_ptr<Texture2d>> GlRenderTarget::get_texture_targets() {\n\tstd::vector<std::shared_ptr<Texture2d>> textures{};\n\tif (this->framebuffer->get_type() == gl_framebuffer_t::display) {\n\t\treturn textures;\n\t}\n\t// else upcast pointers\n\tfor (auto tex : this->textures.value()) {\n\t\tauto new_ptr = dynamic_pointer_cast<Texture2d>(tex);\n\t\ttextures.push_back(new_ptr);\n\t}\n\treturn textures;\n}\n\nvoid GlRenderTarget::resize(size_t width, size_t height) {\n\tif (this->framebuffer->get_type() == gl_framebuffer_t::textures) {\n\t\tthrow Error{ERR << \"Render target with textured framebuffer should not be resized. \"\n\t\t                << \"Create a new one instead.\"};\n\t}\n\n\tthis->size = std::make_pair(width, height);\n}\n\nvoid GlRenderTarget::bind_write() const {\n\t// adjust the viewport to the target size\n\t// we have to do this because window and texture targets may have\n\t// different sizes\n\tglViewport(0, 0, size.first, size.second);\n\n\tthis->framebuffer->bind_write();\n}\n\nvoid GlRenderTarget::bind_read() const {\n\tthis->framebuffer->bind_read();\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/render_target.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n\n#include \"renderer/opengl/framebuffer.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/renderer.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nclass Texture2d;\n\nnamespace opengl {\n\nclass GlTexture2d;\n\n/**\n * The type of OpenGL render target.\n */\nenum class gl_render_target_t {\n\t/**\n\t * Render into a framebuffer.\n\t */\n\tframebuffer,\n\t// TODO renderbuffers mixed with textures\n};\n\n/**\n * Represents an OpenGL target that can be drawn into.\n * It can be either a framebuffer with texture attachements or the display (the window).\n */\nclass GlRenderTarget final : public RenderTarget {\npublic:\n\t/**\n\t * Construct a render target pointed at the default framebuffer - the window.\n\t *\n\t * @param context OpenGL context used for drawing.\n\t * @param width Current width of the window.\n\t * @param height Current height of the window.\n\t */\n\tGlRenderTarget(const std::shared_ptr<GlContext> &context,\n\t               size_t width,\n\t               size_t height);\n\n\t/**\n\t * Construct a render target pointing at the given textures.\n\t * Texture are attached to points specific to their pixel format,\n\t * e.g. a depth texture will be set as the depth target.\n\t *\n\t * @param context OpenGL context used for drawing.\n\t * @param textures Texture attachements.\n\t */\n\tGlRenderTarget(const std::shared_ptr<GlContext> &context,\n\t               std::vector<std::shared_ptr<GlTexture2d>> const &textures);\n\n\t/**\n\t * Get the pixels stored in the render target's buffer.\n\t *\n\t * @return Texture data with the image contents of the buffer.\n\t */\n\tresources::Texture2dData into_data() override;\n\n\t/**\n\t * Get the type of this render target.\n\t *\n\t * @return Render target type.\n\t */\n\tgl_render_target_t get_type() const;\n\n\t/**\n\t * Get the targeted textures.\n\t *\n\t * @return Textures drawn into by the render target.\n\t */\n\tstd::vector<std::shared_ptr<Texture2d>> get_texture_targets() override;\n\n\t/**\n\t * Resize the render target to the specified dimensions.\n\t *\n\t * This is used to scale the viewport to the correct size when\n\t * binding the render target with write access.\n\t *\n\t * @param width New width.\n\t * @param height New height.\n\t */\n\tvoid resize(size_t width, size_t height);\n\n\t/**\n\t * Bind this render target to be drawn into.\n\t */\n\tvoid bind_write() const;\n\n\t/**\n\t * Bind this render target to be read from.\n\t */\n\tvoid bind_read() const;\n\nprivate:\n\t/**\n\t * Type of this render target.\n\t */\n\tgl_render_target_t type;\n\n\t/**\n\t * Size of the window or the texture targets.\n\t */\n\tstd::pair<size_t, size_t> size;\n\n\t/**\n\t * For framebuffer target type, the framebuffer.\n\t */\n\tstd::optional<GlFramebuffer> framebuffer;\n\n\t/**\n\t * Target textures if the render target is a textured fbo.\n\t */\n\tstd::optional<std::vector<std::shared_ptr<GlTexture2d>>> textures;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/renderer.cpp",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#include \"renderer.h\"\n\n#include <algorithm>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/geometry.h\"\n#include \"renderer/opengl/lookup.h\"\n#include \"renderer/opengl/render_pass.h\"\n#include \"renderer/opengl/render_target.h\"\n#include \"renderer/opengl/shader_program.h\"\n#include \"renderer/opengl/texture.h\"\n#include \"renderer/opengl/uniform_buffer.h\"\n#include \"renderer/opengl/uniform_input.h\"\n#include \"renderer/opengl/vertex_array.h\"\n#include \"renderer/opengl/window.h\"\n#include \"renderer/resources/buffer_info.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlRenderer::GlRenderer(const std::shared_ptr<GlContext> &ctx,\n                       const util::Vector2s &viewport_size) :\n\tgl_context{ctx},\n\tdisplay{std::make_shared<GlRenderTarget>(ctx,\n                                             viewport_size[0],\n                                             viewport_size[1])},\n\tshared_quad_vao{std::make_shared<GlVertexArray>(ctx)} {\n\t// color used to clear the color buffers\n\tglClearColor(0.0f, 0.0f, 0.0f, 0.0f);\n\n\t// global GL alpha blending settings\n\t// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n\tglBlendFuncSeparate(\n\t\tGL_SRC_ALPHA,           // source (incoming) RGB factor\n\t\tGL_ONE_MINUS_SRC_ALPHA, // destination (underlying) RGB factor\n\t\tGL_ONE,                 // source (incoming) alpha factor\n\t\tGL_ONE_MINUS_SRC_ALPHA  // destination (underlying) alpha factor\n\t);\n\n\t// global GL depth testing settings\n\tglDepthFunc(GL_LEQUAL);\n\tglDepthRange(0.0, 1.0);\n\n\tlog::log(MSG(info) << \"Created OpenGL renderer\");\n}\n\nstd::shared_ptr<Texture2d> GlRenderer::add_texture(const resources::Texture2dData &data) {\n\treturn std::make_shared<GlTexture2d>(this->gl_context, data);\n}\n\nstd::shared_ptr<Texture2d> GlRenderer::add_texture(const resources::Texture2dInfo &info) {\n\treturn std::make_shared<GlTexture2d>(this->gl_context, info);\n}\n\nstd::shared_ptr<ShaderProgram> GlRenderer::add_shader(std::vector<resources::ShaderSource> const &srcs) {\n\treturn std::make_shared<GlShaderProgram>(this->gl_context, srcs);\n}\n\nstd::shared_ptr<Geometry> GlRenderer::add_mesh_geometry(resources::MeshData const &mesh) {\n\treturn std::make_shared<GlGeometry>(this->gl_context, mesh);\n}\n\nstd::shared_ptr<Geometry> GlRenderer::add_bufferless_quad() {\n\treturn std::make_shared<GlGeometry>();\n}\n\nstd::shared_ptr<RenderPass> GlRenderer::add_render_pass(std::vector<Renderable> renderables, const std::shared_ptr<RenderTarget> &target) {\n\treturn std::make_shared<GlRenderPass>(std::move(renderables), target);\n}\n\nstd::shared_ptr<RenderTarget> GlRenderer::create_texture_target(std::vector<std::shared_ptr<Texture2d>> const &textures) {\n\tstd::vector<std::shared_ptr<GlTexture2d>> gl_textures;\n\tgl_textures.reserve(textures.size());\n\tfor (auto tex : textures) {\n\t\tgl_textures.push_back(std::dynamic_pointer_cast<GlTexture2d>(tex));\n\t}\n\n\treturn std::make_shared<GlRenderTarget>(this->gl_context, gl_textures);\n}\n\nstd::shared_ptr<RenderTarget> GlRenderer::get_display_target() {\n\treturn this->display;\n}\n\nstd::shared_ptr<UniformBuffer> GlRenderer::add_uniform_buffer(resources::UniformBufferInfo const &info) {\n\tauto inputs = info.get_inputs();\n\tstd::vector<GlInBlockUniform> uniforms{};\n\tsize_t offset = 0;\n\tfor (auto const &input : inputs) {\n\t\tauto type = GL_UBO_INPUT_TYPE.get(input.type);\n\t\tauto size = resources::UniformBufferInfo::get_size(input, info.get_layout());\n\n\t\t// align offset to the size of the type\n\t\toffset += offset % size;\n\t\tuniforms.push_back(\n\t\t\tGlInBlockUniform{type,\n\t\t                     offset,\n\t\t                     resources::UniformBufferInfo::get_size(input, info.get_layout()),\n\t\t                     resources::UniformBufferInfo::get_stride_size(input.type, info.get_layout()),\n\t\t                     input.count,\n\t\t                     input.name});\n\n\t\toffset += size;\n\t}\n\n\treturn std::make_shared<GlUniformBuffer>(this->gl_context,\n\t                                         info.get_size(),\n\t                                         uniforms,\n\t                                         this->gl_context->get_uniform_buffer_binding());\n}\n\nstd::shared_ptr<UniformBuffer> GlRenderer::add_uniform_buffer(std::shared_ptr<ShaderProgram> const &prog,\n                                                              std::string const &block_name) {\n\tauto gl_prog = std::dynamic_pointer_cast<GlShaderProgram>(prog);\n\tauto block_def = gl_prog->get_uniform_block(block_name.c_str());\n\n\treturn std::make_shared<GlUniformBuffer>(this->gl_context,\n\t                                         block_def.data_size,\n\t                                         block_def.uniforms,\n\t                                         this->gl_context->get_uniform_buffer_binding());\n}\n\nresources::Texture2dData GlRenderer::display_into_data() {\n\tGLint params[4];\n\tglGetIntegerv(GL_VIEWPORT, params);\n\n\tGLint width = params[2];\n\tGLint height = params[3];\n\n\tresources::Texture2dInfo tex_info(width, height, resources::pixel_format::rgba8);\n\tstd::vector<uint8_t> data(tex_info.get_data_size());\n\n\tstd::static_pointer_cast<GlRenderTarget>(this->get_display_target())->bind_read();\n\tglPixelStorei(GL_PACK_ALIGNMENT, 4);\n\tglReadnPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, tex_info.get_data_size(), data.data());\n\n\tresources::Texture2dData img(std::move(tex_info), std::move(data));\n\treturn img.flip_y();\n}\n\nvoid GlRenderer::resize_display_target(size_t width, size_t height) {\n\tthis->display->resize(width, height);\n}\n\nvoid GlRenderer::optimize(const std::shared_ptr<GlRenderPass> &pass) {\n\tif (!pass->get_is_optimized()) {\n\t\tpass->sort([](const Renderable &a, const Renderable &b) {\n\t\t\tGLuint shader_a = std::dynamic_pointer_cast<GlShaderProgram>(\n\t\t\t\t\t\t\t\t  std::dynamic_pointer_cast<GlUniformInput>(a.uniform)->get_program())\n\t\t\t                      ->get_handle();\n\t\t\tGLuint shader_b = std::dynamic_pointer_cast<GlShaderProgram>(\n\t\t\t\t\t\t\t\t  std::dynamic_pointer_cast<GlUniformInput>(b.uniform)->get_program())\n\t\t\t                      ->get_handle();\n\t\t\treturn shader_a < shader_b;\n\t\t});\n\n\t\tpass->set_is_optimized(true);\n\t}\n}\n\nvoid GlRenderer::check_error() {\n\t// thanks for the global state, opengl!\n\tGlContext::check_error();\n}\n\nvoid GlRenderer::render(const std::shared_ptr<RenderPass> &pass) {\n\tauto gl_target = std::dynamic_pointer_cast<GlRenderTarget>(pass->get_target());\n\tgl_target->bind_write();\n\n\t// ensure that an (empty) VAO is bound before rendering geometry\n\t// a bound VAO is required to render bufferless quad geometries by OpenGL\n\t// see https://www.khronos.org/opengl/wiki/Vertex_Rendering#Causes_of_rendering_failure\n\tshared_quad_vao->bind();\n\n\tglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n\n\t// TODO: Option for face culling\n\t// glEnable(GL_CULL_FACE);\n\n\tauto gl_pass = std::dynamic_pointer_cast<GlRenderPass>(pass);\n\t// TODO: Optimization is disabled for now. Figure out how to do this without calling it every frame\n\t// GlRenderer::optimize(gl_pass);\n\n\t// render all objects in the pass\n\tconst auto &layers = gl_pass->get_layers();\n\tconst auto &renderables = gl_pass->get_renderables();\n\n\t// Draw by layers\n\tfor (size_t i = 0; i < layers.size(); i++) {\n\t\tconst auto &layer = layers[i];\n\t\tconst auto &objects = renderables[i];\n\n\t\tif (layer.clear_depth) {\n\t\t\tglClear(GL_DEPTH_BUFFER_BIT);\n\t\t}\n\n\t\tfor (auto const &obj : objects) {\n\t\t\tif (obj.alpha_blending) {\n\t\t\t\tglEnable(GL_BLEND);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tglDisable(GL_BLEND);\n\t\t\t}\n\n\t\t\tif (obj.depth_test) {\n\t\t\t\tglEnable(GL_DEPTH_TEST);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tglDisable(GL_DEPTH_TEST);\n\t\t\t}\n\n\t\t\tauto in = std::dynamic_pointer_cast<GlUniformInput>(obj.uniform);\n\t\t\tauto program = std::static_pointer_cast<GlShaderProgram>(in->get_program());\n\n\t\t\t// this also calls program->use()\n\t\t\tprogram->update_uniforms(in);\n\n\t\t\t// draw the geometry\n\t\t\tif (obj.geometry != nullptr) {\n\t\t\t\tauto geom = std::dynamic_pointer_cast<GlGeometry>(obj.geometry);\n\t\t\t\t// TODO read obj.blend + family\n\t\t\t\tgeom->draw();\n\t\t\t}\n\t\t}\n\t}\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/renderer.h",
    "content": "// Copyright 2017-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"renderer/renderer.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass RenderPass;\nclass ShaderProgram;\n\nnamespace opengl {\nclass GlContext;\nclass GlRenderPass;\nclass GlRenderTarget;\nclass GlVertexArray;\nclass GlWindow;\n\n/// The OpenGL specialization of the rendering interface.\nclass GlRenderer final : public Renderer {\npublic:\n\t/**\n\t * Create a new OpenGL renderer.\n\t *\n\t * @param ctx OpenGL context of the current window.\n\t * @param viewport_size Size of the window viewport.\n\t * \t\t\t\t\t\tMust be adjusted by scale for highDPI displays.\n\t */\n\tGlRenderer(const std::shared_ptr<GlContext> &ctx,\n\t           const util::Vector2s &viewport_size);\n\n\tstd::shared_ptr<Texture2d> add_texture(resources::Texture2dData const &) override;\n\tstd::shared_ptr<Texture2d> add_texture(resources::Texture2dInfo const &) override;\n\n\tstd::shared_ptr<ShaderProgram> add_shader(std::vector<resources::ShaderSource> const &) override;\n\n\tstd::shared_ptr<Geometry> add_mesh_geometry(resources::MeshData const &) override;\n\tstd::shared_ptr<Geometry> add_bufferless_quad() override;\n\n\tstd::shared_ptr<RenderPass> add_render_pass(std::vector<Renderable>, const std::shared_ptr<RenderTarget> &) override;\n\n\tstd::shared_ptr<RenderTarget> create_texture_target(std::vector<std::shared_ptr<Texture2d>> const &) override;\n\n\tstd::shared_ptr<RenderTarget> get_display_target() override;\n\n\tstd::shared_ptr<UniformBuffer> add_uniform_buffer(resources::UniformBufferInfo const &) override;\n\tstd::shared_ptr<UniformBuffer> add_uniform_buffer(std::shared_ptr<ShaderProgram> const &,\n\t                                                  std::string const &) override;\n\n\tresources::Texture2dData display_into_data() override;\n\n\tvoid resize_display_target(size_t width, size_t height);\n\n\tvoid check_error() override;\n\n\tvoid render(const std::shared_ptr<RenderPass> &) override;\n\nprivate:\n\t/// Optimize the render pass by reordering stuff\n\tstatic void optimize(const std::shared_ptr<GlRenderPass> &pass);\n\n\t/// The GL context.\n\tstd::shared_ptr<GlContext> gl_context;\n\n\t/// The main screen surface as a render target.\n\tstd::shared_ptr<GlRenderTarget> display;\n\n\t/// An empty vertex array object (VAO).\n\t///\n\t/// This VAO has to be bound at the start of a render pass to ensure\n\t/// that bufferless quad geometry can be drawn without errors. Drawing a\n\t/// bufferless quad requires any VAO to be bound\n\t/// see https://www.khronos.org/opengl/wiki/Vertex_Rendering#Causes_of_rendering_failure\n\tstd::shared_ptr<GlVertexArray> shared_quad_vao;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"shader.h\"\n\n#include \"../../datastructure/constexpr_map.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nstatic constexpr auto gl_shdr_type = datastructure::create_const_map<resources::shader_stage_t, GLenum>(\n\tstd::make_pair(resources::shader_stage_t::vertex, GL_VERTEX_SHADER),\n\tstd::make_pair(resources::shader_stage_t::geometry, GL_GEOMETRY_SHADER),\n\tstd::make_pair(resources::shader_stage_t::tesselation_control, GL_TESS_CONTROL_SHADER),\n\tstd::make_pair(resources::shader_stage_t::tesselation_evaluation, GL_TESS_EVALUATION_SHADER),\n\tstd::make_pair(resources::shader_stage_t::fragment, GL_FRAGMENT_SHADER));\n\nGlShader::GlShader(const std::shared_ptr<GlContext> &context,\n                   const resources::ShaderSource &src) :\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteShader(handle); }),\n\ttype(gl_shdr_type.get(src.get_stage())) {\n\tif (src.get_lang() != resources::shader_lang_t::glsl) {\n\t\tthrow Error(MSG(err) << \"Unsupported shader language passed to OpenGL renderer.\");\n\t}\n\n\t// allocate shader in opengl\n\tGLuint handle = glCreateShader(this->type);\n\tthis->handle = handle;\n\n\t// load shader source\n\tconst char *data = src.get_source().c_str();\n\tglShaderSource(handle, 1, &data, nullptr);\n\n\t// compile shader source\n\tglCompileShader(handle);\n\n\t// check compiliation result\n\tGLint status;\n\tglGetShaderiv(handle, GL_COMPILE_STATUS, &status);\n\n\tif (status != GL_TRUE) {\n\t\tGLint loglen;\n\t\tglGetShaderiv(handle, GL_INFO_LOG_LENGTH, &loglen);\n\n\t\tstd::vector<char> infolog(loglen);\n\t\tglGetShaderInfoLog(handle, loglen, nullptr, infolog.data());\n\n\t\tthrow Error(MSG(err) << \"Failed to compile shader:\\n\"\n\t\t                     << infolog.data());\n\t}\n}\n\nGLenum GlShader::get_type() const {\n\treturn this->type;\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../resources/shader_source.h\"\n#include \"simple_object.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// A single OpenGL shader stage.\nclass GlShader final : public GlSimpleObject {\npublic:\n\t/// Compiles the shader from the given source.\n\texplicit GlShader(const std::shared_ptr<GlContext> &context,\n\t                  const resources::ShaderSource &);\n\n\t/// Returns the stage of the rendering pipeline this shader defines.\n\tGLenum get_type() const;\n\nprivate:\n\t/// Which stage of the rendering pipeline this shader defines.\n\tGLenum type;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader_data.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"shader_data.h\"\n\n\nnamespace openage::renderer::opengl {\n\n// this file is intentionally empty\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader_data.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include <epoxy/gl.h>\n\nnamespace openage::renderer::opengl {\n/**\n * Represents a uniform in the default (shader) block, i.e. not within a named block.\n */\nstruct GlUniform {\n\t/**\n\t * Data type of the uniform for setting with glUniform.\n\t */\n\tGLenum type;\n\n\t/**\n\t * Location of the uniform for use with glUniform and glGetUniform.\n\t * NOT the same as the uniform index.\n\t */\n\tGLuint location;\n\n\t/**\n\t * Only used for sampler uniforms.\n\t *\n\t * Texture unit to which the sampler is bound.\n\t */\n\tstd::optional<GLuint> tex_unit;\n};\n\n/**\n * Represents a uniform in a named block.\n */\nstruct GlInBlockUniform {\n\tGLenum type;\n\n\t/**\n\t * Offset relative to the beginning of the block at which this uniform is placed.\n\t */\n\tsize_t offset;\n\n\t/**\n\t * The size in bytes of the whole uniform. If the uniform is an array,\n\t * the size of the whole array.\n\t */\n\tsize_t size;\n\n\t/**\n\t * Only relevant for arrays and matrices.\n\t * In arrays, specifies the distance between the start of each element.\n\t * In row-major matrices, specifies the distance between the start of each row.\n\t * In column-major matrices, specifies the distance between the start of each column.\n\t */\n\tsize_t stride;\n\n\t/**\n\t * Only relevant for arrays. The number of elements in the array.\n\t */\n\tsize_t count;\n\n\t/**\n\t * Name of the block uniform.\n\t */\n\tstd::string name;\n};\n\n/**\n * Represents a uniform block in a shader program.\n */\nstruct GlUniformBlock {\n\tGLuint index;\n\n\t/**\n\t * Size of the entire block. How uniforms are packed within depends\n\t * on the block layout and is described in corresponding GlUniforms.\n\t */\n\tsize_t data_size;\n\n\t/**\n\t * Maps uniform names within this block to their descriptions.\n\t */\n\tstd::vector<GlInBlockUniform> uniforms;\n\n\t/**\n\t * The binding point assigned to this block.\n\t */\n\tGLuint binding_point;\n};\n\n/**\n * Represents a per-vertex input to the shader program.\n */\nstruct GlVertexAttrib {\n\tGLenum type;\n\tGLint location;\n\t// TODO what is this?\n\tGLint size;\n};\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader_program.cpp",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#include \"shader_program.h\"\n\n#include <algorithm>\n#include <cstdio>\n#include <unordered_set>\n\n#include \"datastructure/constexpr_map.h\"\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/error.h\"\n#include \"renderer/opengl/geometry.h\"\n#include \"renderer/opengl/lookup.h\"\n#include \"renderer/opengl/shader.h\"\n#include \"renderer/opengl/texture.h\"\n#include \"renderer/opengl/uniform_buffer.h\"\n#include \"renderer/opengl/uniform_input.h\"\n#include \"renderer/opengl/util.h\"\n\n\nnamespace openage::renderer::opengl {\n\nstatic void check_program_status(GLuint program, GLenum what_to_check) {\n\tGLint status = GL_FALSE;\n\tglGetProgramiv(program, what_to_check, &status);\n\n\tGlContext::check_error();\n\n\tif (status != GL_TRUE) {\n\t\tconst char *what_str = [=] {\n\t\t\tswitch (what_to_check) {\n\t\t\tcase GL_LINK_STATUS:\n\t\t\t\treturn \"linking\";\n\t\t\tcase GL_VALIDATE_STATUS:\n\t\t\t\treturn \"validation\";\n\t\t\tcase GL_COMPILE_STATUS:\n\t\t\t\treturn \"compilation\";\n\t\t\tdefault:\n\t\t\t\treturn \"unknown shader creation task\";\n\t\t\t}\n\t\t}();\n\n\t\tGLint loglen = 0;\n\t\tglGetProgramiv(program, GL_INFO_LOG_LENGTH, &loglen);\n\n\t\tstd::vector<char> infolog(loglen);\n\t\tGLint real_loglen;\n\t\tglGetProgramInfoLog(program, loglen, &real_loglen, infolog.data());\n\n\t\tstd::string gl_message = \"no reason returned\";\n\t\tif (infolog.size() and strlen(infolog.data())) {\n\t\t\tgl_message = infolog.data();\n\t\t}\n\n\t\tthrow Error(ERR << \"OpenGL shader program \" << what_str << \" failed (len \" << loglen << \", real: \" << real_loglen << \"):\\n\"\n\t\t                << gl_message,\n\t\t            true);\n\t}\n}\n\nGlShaderProgram::GlShaderProgram(const std::shared_ptr<GlContext> &context,\n                                 const std::vector<resources::ShaderSource> &srcs) :\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteProgram(handle); }),\n\tvalidated(false) {\n\tconst gl_context_spec &caps = context->get_specs();\n\n\tGLuint handle = glCreateProgram();\n\tthis->handle = handle;\n\n\tstd::vector<GlShader> shaders;\n\tfor (auto const &src : srcs) {\n\t\tGlShader shader{context, src};\n\t\tglAttachShader(handle, shader.get_handle());\n\t\tshaders.push_back(std::move(shader));\n\t}\n\n\tglLinkProgram(handle);\n\tcheck_program_status(handle, GL_LINK_STATUS);\n\n\t// after linking we can delete the shaders\n\tfor (auto const &shdr : shaders) {\n\t\tglDetachShader(handle, shdr.get_handle());\n\t}\n\n\t// query program information\n\tGLint val;\n\tglGetProgramiv(handle, GL_ACTIVE_ATTRIBUTES, &val);\n\tsize_t attrib_count = val;\n\tglGetProgramiv(handle, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, &val);\n\tsize_t max_name_len = val;\n\tglGetProgramiv(handle, GL_ACTIVE_UNIFORMS, &val);\n\tsize_t unif_count = val;\n\tglGetProgramiv(handle, GL_ACTIVE_UNIFORM_MAX_LENGTH, &val);\n\tmax_name_len = std::max(size_t(val), max_name_len);\n\tglGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &val);\n\tsize_t unif_block_count = val;\n\tglGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH, &val);\n\tmax_name_len = std::max(size_t(val), max_name_len);\n\n\tstd::vector<char> name(max_name_len);\n\t// Indices of uniforms within named blocks.\n\tstd::unordered_set<GLuint> in_block_unifs;\n\n\tGLuint block_binding = 0;\n\n\t// Extract uniform block descriptions.\n\tfor (GLuint i_unif_block = 0; i_unif_block < unif_block_count; ++i_unif_block) {\n\t\tglGetActiveUniformBlockName(\n\t\t\thandle,\n\t\t\ti_unif_block,\n\t\t\tname.size(),\n\t\t\tnullptr,\n\t\t\tname.data());\n\n\t\tstd::string block_name(name.data());\n\n\t\tGLint data_size;\n\t\tglGetActiveUniformBlockiv(handle, i_unif_block, GL_UNIFORM_BLOCK_DATA_SIZE, &data_size);\n\n\t\tglGetActiveUniformBlockiv(handle, i_unif_block, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &val);\n\t\tstd::vector<GLint> uniform_indices(val);\n\t\tglGetActiveUniformBlockiv(handle, i_unif_block, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, uniform_indices.data());\n\n\t\tstd::vector<GlInBlockUniform> uniforms;\n\t\tfor (GLuint const i_unif : uniform_indices) {\n\t\t\tin_block_unifs.insert(i_unif);\n\n\t\t\tGLenum type;\n\t\t\tGLint offset, count, stride;\n\n\t\t\tglGetActiveUniform(\n\t\t\t\thandle,\n\t\t\t\ti_unif,\n\t\t\t\tname.size(),\n\t\t\t\tnullptr,\n\t\t\t\t&count,\n\t\t\t\t&type,\n\t\t\t\tname.data());\n\n\t\t\tglGetActiveUniformsiv(handle, 1, &i_unif, GL_UNIFORM_OFFSET, &offset);\n\t\t\tglGetActiveUniformsiv(handle, 1, &i_unif, GL_UNIFORM_ARRAY_STRIDE, &stride);\n\t\t\tif (stride == 0) {\n\t\t\t\t// The uniform is not an array, but it's declared in a named block and hence might\n\t\t\t\t// be a matrix whose stride we need to know.\n\t\t\t\tglGetActiveUniformsiv(handle, 1, &i_unif, GL_UNIFORM_MATRIX_STRIDE, &stride);\n\t\t\t}\n\n\t\t\t// We do not need to handle sampler types here like in the uniform loop below,\n\t\t\t// because named blocks cannot contain samplers.\n\n\t\t\tuniforms.push_back(\n\t\t\t\tGlInBlockUniform{\n\t\t\t\t\ttype,\n\t\t\t\t\tsize_t(offset),\n\t\t\t\t\tsize_t(count) * GL_UNIFORM_TYPE_SIZE.get(type),\n\t\t\t\t\tsize_t(stride),\n\t\t\t\t\tsize_t(count),\n\t\t\t\t\tstd::string(name.data())});\n\t\t}\n\n\t\t// ENSURE(block_binding < caps.max_uniform_buffer_bindings,\n\t\t//        \"Tried to create an OpenGL shader that uses more uniform blocks \"\n\t\t//            << \"than there are binding points (\" << caps.max_uniform_buffer_bindings\n\t\t//            << \" available).\");\n\t\t//\n\t\t// glUniformBlockBinding(handle, i_unif_block, block_binding);\n\t\t// block_binding += 1;\n\n\t\tthis->uniform_blocks.insert(std::make_pair(\n\t\t\tblock_name,\n\t\t\tGlUniformBlock{\n\t\t\t\ti_unif_block,\n\t\t\t\tsize_t(data_size),\n\t\t\t\tstd::move(uniforms),\n\t\t\t\tblock_binding}));\n\t}\n\n\tGLuint tex_unit_id = 0;\n\n\t// Extract information about uniforms in the default block.\n\n\t// the uniform ID is the index in the uniforms vector\n\tuniform_id_t unif_id = 0;\n\tfor (GLuint i_unif = 0; i_unif < unif_count; ++i_unif) {\n\t\tif (in_block_unifs.count(i_unif) == 1) {\n\t\t\t// Skip uniforms within named blocks.\n\t\t\tcontinue;\n\t\t}\n\n\t\tGLint count;\n\t\tGLenum type;\n\t\tglGetActiveUniform(\n\t\t\thandle,\n\t\t\ti_unif,\n\t\t\tname.size(),\n\t\t\tnullptr,\n\t\t\t&count,\n\t\t\t&type,\n\t\t\tname.data());\n\n\t\tGLuint loc = glGetUniformLocation(handle, name.data());\n\n\t\tthis->uniforms.push_back({type, loc, std::nullopt});\n\n\t\tthis->uniforms_by_name.insert(std::make_pair(\n\t\t\tname.data(),\n\t\t\tunif_id));\n\n\t\tif (type == GL_SAMPLER_2D) {\n\t\t\tENSURE(tex_unit_id < caps.max_texture_slots,\n\t\t\t       \"Tried to create an OpenGL shader that uses more texture sampler uniforms \"\n\t\t\t           << \"than there are texture unit slots (\" << caps.max_texture_slots << \" available).\");\n\n\t\t\tthis->uniforms[unif_id].tex_unit = tex_unit_id;\n\n\t\t\ttex_unit_id += 1;\n\t\t}\n\n\t\t// Increment uniform ID\n\t\tunif_id += 1;\n\t}\n\n\t// Resize the texture unit bindings\n\t// to number of texture units used by the shader\n\tthis->textures_per_texunits.resize(tex_unit_id);\n\n\t// Extract vertex attribute descriptions.\n\tfor (GLuint i_attrib = 0; i_attrib < attrib_count; ++i_attrib) {\n\t\tGLint size;\n\t\tGLenum type;\n\t\tglGetActiveAttrib(\n\t\t\thandle,\n\t\t\ti_attrib,\n\t\t\tname.size(),\n\t\t\tnullptr,\n\t\t\t&size,\n\t\t\t&type,\n\t\t\tname.data());\n\n\t\tthis->attribs.insert(std::make_pair(\n\t\t\tname.data(),\n\t\t\tGlVertexAttrib{\n\t\t\t\ttype,\n\t\t\t\tGLint(i_attrib),\n\t\t\t\tsize,\n\t\t\t}));\n\t}\n\n\tlog::log(MSG(info) << \"Created OpenGL shader program\");\n\n\tif (!this->uniform_blocks.empty()) {\n\t\tlog::log(MSG(dbg) << \"Uniform blocks: \");\n\t\tfor (auto const &pair : this->uniform_blocks) {\n\t\t\tlog::log(MSG(dbg) << \"(\" << pair.second.index << \") \" << pair.first\n\t\t\t                  << \" (size: \" << pair.second.data_size << \") {\");\n\t\t\tfor (auto const &unif : pair.second.uniforms) {\n\t\t\t\tlog::log(MSG(dbg) << \"\\t+\" << unif.offset\n\t\t\t\t                  << \" \" << unif.name << \": \"\n\t\t\t\t                  << GLSL_TYPE_NAME.get(unif.type));\n\t\t\t}\n\t\t\tlog::log(MSG(dbg) << \"}\");\n\t\t}\n\t}\n\n\tif (!this->uniforms.empty()) {\n\t\tlog::log(MSG(dbg) << \"Uniforms: \");\n\t\tfor (const auto &pair : this->uniforms_by_name) {\n\t\t\tconst auto &unif_info = this->uniforms[pair.second];\n\t\t\tlog::log(MSG(dbg) << \"(\" << unif_info.location << \") \"\n\t\t\t                  << pair.first << \": \"\n\t\t\t                  << GLSL_TYPE_NAME.get(unif_info.type));\n\t\t}\n\t}\n\n\tif (!this->attribs.empty()) {\n\t\tlog::log(MSG(dbg) << \"Vertex attributes: \");\n\t\tfor (auto const &pair : this->attribs) {\n\t\t\tlog::log(MSG(dbg) << \"(\" << pair.second.location << \") \" << pair.first << \": \"\n\t\t\t                  << GLSL_TYPE_NAME.get(pair.second.type));\n\t\t}\n\t}\n}\n\n\nvoid GlShaderProgram::use() {\n\t// the setup doesn't need to be done if this program is already active.\n\tif (this->in_use()) {\n\t\treturn;\n\t}\n\n\tif (!this->validated) {\n\t\t// TODO(Vtec234): validation depends on the context state, so this might be worth calling\n\t\t// more than once. However, once per frame is probably too much.\n\t\tglValidateProgram(*this->handle);\n\t\tcheck_program_status(*this->handle, GL_VALIDATE_STATUS);\n\n\t\tthis->validated = true;\n\t}\n\n\tglUseProgram(*this->handle);\n\n\t// store this program as the active one\n\tthis->context->set_current_program(\n\t\tstd::static_pointer_cast<GlShaderProgram>(\n\t\t\tthis->shared_from_this()));\n\n\tfor (size_t i = 0; i < this->textures_per_texunits.size(); ++i) {\n\t\tauto &tex_unit = this->textures_per_texunits[i];\n\t\tif (not tex_unit) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (not glIsTexture(*tex_unit)) {\n\t\t\t// By the time we use the shader again, the texture may have been deleted\n\t\t\t// but if it's fixed afterwards using update_uniforms, the render state\n\t\t\t// will still be fine\n\t\t\t// We can free the texture unit in this case\n\t\t\ttex_unit = std::nullopt;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// We have to bind the texture to their texture units here because\n\t\t// the texture unit bindings are global to the context. Each time\n\t\t// the shader switches, it is possible that some other shader overwrote\n\t\t// these, and since we want the uniform values to persist across update_uniforms\n\t\t// calls, we have to set them more often than just on update_uniforms.\n\t\tglActiveTexture(GL_TEXTURE0 + i);\n\t\tglBindTexture(GL_TEXTURE_2D, *tex_unit);\n\t}\n}\n\n\nbool GlShaderProgram::in_use() const {\n\treturn this->context->get_current_program().lock().get() == this;\n}\n\n\nvoid GlShaderProgram::update_uniforms(std::shared_ptr<GlUniformInput> const &unif_in) {\n\tENSURE(unif_in->get_program().get() == this, \"Uniform input passed to different shader than it was created with.\");\n\n\t// TODO: use glProgramUniform when we're on OpenGL 4.1\n\t// then we don't need to \"use\" and then call glUniform*\n\n\tif (not this->in_use()) {\n\t\tthis->use();\n\t}\n\n\tconst auto &update_offs = unif_in->update_offs;\n\tconst auto &used_uniforms = unif_in->used_uniforms;\n\tconst auto &uniforms = this->uniforms;\n\tuint8_t const *data = unif_in->update_data.data();\n\n\tsize_t unif_count = used_uniforms.size();\n\tfor (size_t i = 0; i < unif_count; ++i) {\n\t\tuniform_id_t unif_id = used_uniforms[i];\n\n\t\tconst auto &update_off = update_offs[unif_id];\n\t\tuint8_t const *ptr = data + update_off.offset;\n\n\t\tconst auto &unif = uniforms[unif_id];\n\t\tauto loc = unif.location;\n\n\t\tswitch (unif.type) {\n\t\tcase GL_INT:\n\t\t\tglUniform1i(loc, *reinterpret_cast<const GLint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_UNSIGNED_INT:\n\t\t\tglUniform1ui(loc, *reinterpret_cast<const GLuint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT:\n\t\t\tglUniform1f(loc, *reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_DOUBLE:\n\t\t\t// TODO requires an extension\n\t\t\tglUniform1d(loc, *reinterpret_cast<const double *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_BOOL:\n\t\t\tglUniform1ui(loc, *reinterpret_cast<const bool *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT_VEC2:\n\t\t\tglUniform2fv(loc, 1, reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT_VEC3:\n\t\t\tglUniform3fv(loc, 1, reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT_VEC4:\n\t\t\tglUniform4fv(loc, 1, reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_INT_VEC2:\n\t\t\tglUniform2iv(loc, 1, reinterpret_cast<const GLint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_INT_VEC3:\n\t\t\tglUniform3iv(loc, 1, reinterpret_cast<const GLint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_INT_VEC4:\n\t\t\tglUniform4iv(loc, 1, reinterpret_cast<const GLint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_UNSIGNED_INT_VEC2:\n\t\t\tglUniform2uiv(loc, 1, reinterpret_cast<const GLuint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_UNSIGNED_INT_VEC3:\n\t\t\tglUniform3uiv(loc, 1, reinterpret_cast<const GLuint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_UNSIGNED_INT_VEC4:\n\t\t\tglUniform4uiv(loc, 1, reinterpret_cast<const GLuint *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT_MAT3:\n\t\t\tglUniformMatrix3fv(loc, 1, GLboolean(false), reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_FLOAT_MAT4:\n\t\t\tglUniformMatrix4fv(loc, 1, GLboolean(false), reinterpret_cast<const float *>(ptr));\n\t\t\tbreak;\n\t\tcase GL_SAMPLER_2D: {\n\t\t\tENSURE(unif.tex_unit,\n\t\t\t       \"Tried to access texture unit for uniform that has no texture unit assigned.\");\n\t\t\tGLuint tex_unit_id = *unif.tex_unit;\n\n\t\t\tGLuint tex = *reinterpret_cast<const GLuint *>(ptr);\n\t\t\tglActiveTexture(GL_TEXTURE0 + tex_unit_id);\n\t\t\tglBindTexture(GL_TEXTURE_2D, tex);\n\t\t\t// TODO: maybe call this at a more appropriate position\n\t\t\tglUniform1i(loc, tex_unit_id);\n\t\t\tENSURE(tex_unit_id < this->textures_per_texunits.size(),\n\t\t\t       \"Tried to assign texture to non-existant texture unit at index \"\n\t\t\t           << tex_unit_id\n\t\t\t           << \" (max: \" << this->textures_per_texunits.size() << \").\");\n\t\t\tthis->textures_per_texunits[tex_unit_id] = tex;\n\t\t\tbreak;\n\t\t}\n\t\tdefault:\n\t\t\tthrow Error(MSG(err) << \"Tried to upload unknown uniform type to GL shader.\");\n\t\t}\n\t}\n}\n\nconst GlUniformBlock &GlShaderProgram::get_uniform_block(const char *name) const {\n\treturn this->uniform_blocks.at(name);\n}\n\nstd::map<size_t, resources::vertex_input_t> GlShaderProgram::vertex_attributes() const {\n\tstd::map<size_t, resources::vertex_input_t> attrib_map;\n\n\tfor (auto const &attr : this->attribs) {\n\t\tattrib_map[attr.second.location] = GL_VERT_IN_TYPE.get(attr.second.type);\n\t}\n\n\treturn attrib_map;\n}\n\nstd::shared_ptr<UniformInput> GlShaderProgram::new_unif_in() {\n\tauto in = std::make_shared<GlUniformInput>(this->shared_from_this());\n\n\treturn in;\n}\n\nuniform_id_t GlShaderProgram::get_uniform_id(const char *name) {\n\treturn this->uniforms_by_name.at(name);\n}\n\nconst std::vector<GlUniform> &GlShaderProgram::get_uniforms() const {\n\treturn this->uniforms;\n}\n\nconst std::unordered_map<std::string, GlUniformBlock> &GlShaderProgram::get_uniform_blocks() const {\n\treturn this->uniform_blocks;\n}\n\nbool GlShaderProgram::has_uniform(const char *name) {\n\treturn this->uniforms_by_name.contains(name);\n}\n\nvoid GlShaderProgram::bind_uniform_buffer(const char *block_name, std::shared_ptr<UniformBuffer> const &buffer) {\n\tENSURE(this->uniform_blocks.count(block_name) == 1,\n\t       \"Tried to set binding point for uniform block \" << block_name << \" that does not exist in the shader program.\");\n\n\tauto gl_buffer = std::dynamic_pointer_cast<GlUniformBuffer>(buffer);\n\tauto &block = this->uniform_blocks[block_name];\n\n\t// Check if the uniform buffer matches the block definition\n\tfor (auto const &unif : block.uniforms) {\n\t\tENSURE(gl_buffer->has_uniform(unif.name.c_str()),\n\t\t       \"Uniform buffer does not contain uniform '\" << unif.name << \"' required by block \" << block_name);\n\t}\n\n\tblock.binding_point = gl_buffer->get_binding_point();\n\tglUniformBlockBinding(*this->handle, block.index, block.binding_point);\n}\n\nvoid GlShaderProgram::set_unif(UniformInput &in,\n                               const char *unif,\n                               void const *val,\n                               size_t size,\n                               GLenum type) {\n\tauto uniform_id = this->uniforms_by_name.find(unif);\n\tENSURE(uniform_id != std::end(this->uniforms_by_name),\n\t       \"Tried to set uniform '\" << unif << \"' that does not exist in the shader program.\");\n\n\tthis->set_unif(in, uniform_id->second, val, size, type);\n}\n\nvoid GlShaderProgram::set_unif(UniformInput &in,\n                               uniform_id_t unif_id,\n                               void const *val,\n                               size_t size,\n                               GLenum type) {\n\tauto &unif_in = dynamic_cast<GlUniformInput &>(in);\n\n\tENSURE(unif_id < this->uniforms.size(),\n\t       \"Tried to set uniform '\" << unif_id << \"' that does not exist in the shader program.\");\n\n\tENSURE(unif_id < this->uniforms.size(),\n\t       \"Tried to set uniform with invalid ID \" << unif_id);\n\n\tauto const &unif_info = this->uniforms[unif_id];\n\tENSURE(type == unif_info.type,\n\t       \"Tried to set uniform '\" << unif_id << \"' to a value of the wrong type.\");\n\n\tauto &update_off = unif_in.update_offs[unif_id];\n\tauto offset = update_off.offset;\n\tmemcpy(unif_in.update_data.data() + offset, val, size);\n\tif (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before\n\t\tauto lower_bound = std::lower_bound(\n\t\t\tstd::begin(unif_in.used_uniforms),\n\t\t\tstd::end(unif_in.used_uniforms),\n\t\t\tunif_id);\n\t\tunif_in.used_uniforms.insert(lower_bound, unif_id);\n\t\tupdate_off.used = true;\n\t}\n}\n\nvoid GlShaderProgram::set_i32(UniformInput &in, const char *unif, int32_t val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_INT), GL_INT);\n}\n\nvoid GlShaderProgram::set_u32(UniformInput &in, const char *unif, uint32_t val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT);\n}\n\nvoid GlShaderProgram::set_f32(UniformInput &in, const char *unif, float val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT);\n}\n\nvoid GlShaderProgram::set_f64(UniformInput &in, const char *unif, double val) {\n\t// TODO requires extension\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE);\n}\n\nvoid GlShaderProgram::set_bool(UniformInput &in, const char *unif, bool val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_BOOL), GL_BOOL);\n}\n\nvoid GlShaderProgram::set_v2f32(UniformInput &in, const char *unif, Eigen::Vector2f const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3f32(UniformInput &in, const char *unif, Eigen::Vector3f const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4f32(UniformInput &in, const char *unif, Eigen::Vector4f const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4);\n}\n\nvoid GlShaderProgram::set_v2i32(UniformInput &in, const char *unif, Eigen::Vector2i const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3i32(UniformInput &in, const char *unif, Eigen::Vector3i const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4i32(UniformInput &in, const char *unif, Eigen::Vector4i const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4);\n}\n\nvoid GlShaderProgram::set_v2ui32(UniformInput &in, const char *unif, Eigen::Vector2<uint32_t> const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3ui32(UniformInput &in, const char *unif, Eigen::Vector3<uint32_t> const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4ui32(UniformInput &in, const char *unif, Eigen::Vector4<uint32_t> const &val) {\n\tthis->set_unif(in, unif, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4);\n}\n\nvoid GlShaderProgram::set_m4f32(UniformInput &in, const char *unif, Eigen::Matrix4f const &val) {\n\tthis->set_unif(in, unif, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4);\n}\n\nvoid GlShaderProgram::set_tex(UniformInput &in, const char *unif, std::shared_ptr<Texture2d> const &val) {\n\tauto tex = std::dynamic_pointer_cast<GlTexture2d>(val);\n\tGLuint handle = tex->get_handle();\n\tthis->set_unif(in, unif, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D);\n}\n\nvoid GlShaderProgram::set_i32(UniformInput &in, uniform_id_t id, int32_t val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_INT), GL_INT);\n}\n\nvoid GlShaderProgram::set_u32(UniformInput &in, uniform_id_t id, uint32_t val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT), GL_UNSIGNED_INT);\n}\n\nvoid GlShaderProgram::set_f32(UniformInput &in, uniform_id_t id, float val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT), GL_FLOAT);\n}\n\nvoid GlShaderProgram::set_f64(UniformInput &in, uniform_id_t id, double val) {\n\t// TODO requires extension\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_DOUBLE), GL_DOUBLE);\n}\n\nvoid GlShaderProgram::set_bool(UniformInput &in, uniform_id_t id, bool val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_BOOL), GL_BOOL);\n}\n\nvoid GlShaderProgram::set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC2), GL_FLOAT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC3), GL_FLOAT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_FLOAT_VEC4), GL_FLOAT_VEC4);\n}\n\nvoid GlShaderProgram::set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC2), GL_INT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC3), GL_INT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_INT_VEC4), GL_INT_VEC4);\n}\n\nvoid GlShaderProgram::set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2<uint32_t> const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC2), GL_UNSIGNED_INT_VEC2);\n}\n\nvoid GlShaderProgram::set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3<uint32_t> const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC3), GL_UNSIGNED_INT_VEC3);\n}\n\nvoid GlShaderProgram::set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4<uint32_t> const &val) {\n\tthis->set_unif(in, id, &val, get_uniform_type_size(GL_UNSIGNED_INT_VEC4), GL_UNSIGNED_INT_VEC4);\n}\n\nvoid GlShaderProgram::set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &val) {\n\tthis->set_unif(in, id, val.data(), get_uniform_type_size(GL_FLOAT_MAT4), GL_FLOAT_MAT4);\n}\n\nvoid GlShaderProgram::set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr<Texture2d> const &val) {\n\tauto tex = std::dynamic_pointer_cast<GlTexture2d>(val);\n\tGLuint handle = tex->get_handle();\n\tthis->set_unif(in, id, &handle, get_uniform_type_size(GL_SAMPLER_2D), GL_SAMPLER_2D);\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/shader_program.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n#include <vector>\n\n#include \"renderer/opengl/shader_data.h\"\n#include \"renderer/opengl/simple_object.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/shader_program.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nclass UniformInput;\n\nnamespace opengl {\n\nclass GlContext;\nclass GlUniformBuffer;\nclass GlUniformInput;\n\n/**\n * A handle to an OpenGL shader program.\n */\nclass GlShaderProgram final : public ShaderProgram\n\t, public GlSimpleObject {\npublic:\n\t/**\n\t * Tries to create a shader program from the given sources.\n\t * Throws an exception on compile/link errors.\n\t */\n\texplicit GlShaderProgram(const std::shared_ptr<GlContext> &,\n\t                         const std::vector<resources::ShaderSource> &);\n\n\t/**\n\t * Bind this program as the currently used one in the OpenGL context.\n\t */\n\tvoid use();\n\n\t/**\n\t * Check if this program is currently in use in the OpenGL context.\n\t *\n\t * @return true if the program is in use, false otherwise.\n\t */\n\tbool in_use() const;\n\n\t/**\n\t * Updates the uniform values with the given input specification.\n\t *\n\t * @param input The uniform input specification.\n\t */\n\tvoid update_uniforms(std::shared_ptr<GlUniformInput> const &unif_in);\n\n\t/**\n\t * Get the uniform block with the given name.\n\t *\n\t * @param block_name Name of the uniform block.\n\t *\n\t * @return Uniform block.\n\t */\n\tconst GlUniformBlock &get_uniform_block(const char *block_name) const;\n\n\t/**\n\t * Get the uniform ID for the given uniform name.\n\t *\n\t * @param name Name of the uniform in the shader code.\n\t *\n\t * @return ID of the uniform.\n\t */\n\tuniform_id_t get_uniform_id(const char *name) override;\n\n\t/**\n\t * Get the uniforms in the default block of the shader program.\n\t * This does not include uniforms in blocks.\n\t *\n\t * @return Uniforms in the shader program.\n\t */\n\tconst std::vector<GlUniform> &get_uniforms() const;\n\n\t/**\n\t * Get the map of uniform blocks in the shader program.\n\t *\n\t * @return Uniform blocks in the shader program.\n\t */\n\tconst std::unordered_map<std::string, GlUniformBlock> &get_uniform_blocks() const;\n\n\t/**\n\t * Check whether the shader program contains a uniform variable with the given name.\n\t *\n\t * @param name Name of the uniform in the shader code.\n\t *\n\t * @return true if the shader program contains the uniform, false otherwise.\n\t */\n\tbool has_uniform(const char *name) override;\n\n\t/**\n\t * Binds a uniform block in the shader program to the same binding point as\n\t * the given uniform buffer.\n\t *\n\t * @param buffer Uniform buffer to bind.\n\t * @param block_name Name of the uniform block in the shader program.\n\t */\n\tvoid bind_uniform_buffer(const char *block_name,\n\t                         std::shared_ptr<UniformBuffer> const &) override;\n\n\tstd::map<size_t, resources::vertex_input_t> vertex_attributes() const override;\n\nprotected:\n\tstd::shared_ptr<UniformInput> new_unif_in() override;\n\tvoid set_i32(UniformInput &in, const char *, int32_t) override;\n\tvoid set_u32(UniformInput &in, const char *, uint32_t) override;\n\tvoid set_f32(UniformInput &in, const char *, float) override;\n\tvoid set_f64(UniformInput &in, const char *, double) override;\n\tvoid set_bool(UniformInput &in, const char *, bool) override;\n\tvoid set_v2f32(UniformInput &in, const char *, Eigen::Vector2f const &) override;\n\tvoid set_v3f32(UniformInput &in, const char *, Eigen::Vector3f const &) override;\n\tvoid set_v4f32(UniformInput &in, const char *, Eigen::Vector4f const &) override;\n\tvoid set_v2i32(UniformInput &in, const char *, Eigen::Vector2i const &) override;\n\tvoid set_v3i32(UniformInput &in, const char *, Eigen::Vector3i const &) override;\n\tvoid set_v4i32(UniformInput &in, const char *, Eigen::Vector4i const &) override;\n\tvoid set_v2ui32(UniformInput &in, const char *, Eigen::Vector2<uint32_t> const &) override;\n\tvoid set_v3ui32(UniformInput &in, const char *, Eigen::Vector3<uint32_t> const &) override;\n\tvoid set_v4ui32(UniformInput &in, const char *, Eigen::Vector4<uint32_t> const &) override;\n\tvoid set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) override;\n\tvoid set_tex(UniformInput &in, const char *, std::shared_ptr<Texture2d> const &) override;\n\n\tvoid set_i32(UniformInput &in, uniform_id_t id, int32_t) override;\n\tvoid set_u32(UniformInput &in, uniform_id_t id, uint32_t) override;\n\tvoid set_f32(UniformInput &in, uniform_id_t id, float) override;\n\tvoid set_f64(UniformInput &in, uniform_id_t id, double) override;\n\tvoid set_bool(UniformInput &in, uniform_id_t id, bool) override;\n\tvoid set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &) override;\n\tvoid set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &) override;\n\tvoid set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &) override;\n\tvoid set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &) override;\n\tvoid set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &) override;\n\tvoid set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &) override;\n\tvoid set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2<uint32_t> const &) override;\n\tvoid set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3<uint32_t> const &) override;\n\tvoid set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4<uint32_t> const &) override;\n\tvoid set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &) override;\n\tvoid set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr<Texture2d> const &) override;\n\nprivate:\n\t/**\n\t * Set the uniform value via uniform name from a uniform input.\n\t *\n\t * This method should only be used for debugging and not for performance-critical code.\n\t * String lookups are much slower than using the uniform ID.\n\t * If performance is important, use the alternative \\p set_unif(..) implementation\n\t * that works on IDs instead.\n\t *\n\t * @param in Uniform input.\n\t * @param name Name of the uniform.\n\t * @param value Value to set.\n\t * @param size Size of the value (in bytes).\n\t * @param type Type of the value.\n\t */\n\tvoid set_unif(UniformInput &in,\n\t              const char *name,\n\t              void const *value,\n\t              size_t size,\n\t              GLenum type);\n\n\t/**\n\t * Set the uniform value via uniform ID from a uniform input.\n\t *\n\t * @param in Uniform input.\n\t * @param unif_id ID of the uniform.\n\t * @param value Value to set.\n\t * @param size Size of the value (in bytes).\n\t * @param type Type of the value.\n\t */\n\tvoid set_unif(UniformInput &in,\n\t              uniform_id_t unif_id,\n\t              void const *value,\n\t              size_t size,\n\t              GLenum type);\n\n\t/// Uniforms in the shader program. Contains only\n\t/// uniforms in the default block, i.e. not within named blocks.\n\tstd::vector<GlUniform> uniforms;\n\n\t/// Maps uniform names to their ID (the index in the uniform vector).\n\tstd::unordered_map<std::string, uniform_id_t> uniforms_by_name;\n\n\t/// Maps uniform block names to their descriptions.\n\tstd::unordered_map<std::string, GlUniformBlock> uniform_blocks;\n\n\t/// Maps per-vertex attribute names to their descriptions.\n\tstd::unordered_map<std::string, GlVertexAttrib> attribs;\n\n\t/// Store which texture handles are currently bound to the shader's texture units.\n\t/// A value of std::nullopt means the texture unit is unbound (no texture assigned).\n\tstd::vector<std::optional<GLuint>> textures_per_texunits;\n\n\t/// Whether this program has been validated.\n\tbool validated;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/simple_object.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"simple_object.h\"\n\n#include <utility>\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlSimpleObject::GlSimpleObject(const std::shared_ptr<GlContext> &context,\n                               std::function<void(GLuint)> &&delete_fun) :\n\tcontext{context},\n\tdelete_fun(std::move(delete_fun)) {}\n\nGlSimpleObject::GlSimpleObject(GlSimpleObject &&other) :\n\tcontext{std::move(other.context)},\n\thandle{other.handle},\n\tdelete_fun(std::move(other.delete_fun)) {\n\tother.handle = {};\n}\n\nGlSimpleObject &GlSimpleObject::operator=(GlSimpleObject &&other) {\n\tif (this->handle) {\n\t\tthis->delete_fun(*this->handle);\n\t}\n\n\tthis->context = std::move(other.context);\n\tthis->handle = other.handle;\n\tthis->delete_fun = std::move(other.delete_fun);\n\tother.handle = {};\n\n\treturn *this;\n}\n\nGlSimpleObject::~GlSimpleObject() {\n\tif (this->handle) {\n\t\tthis->delete_fun(*this->handle);\n\t}\n}\n\nGLuint GlSimpleObject::get_handle() const {\n\treturn *this->handle;\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/simple_object.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <optional>\n\n#include <epoxy/gl.h>\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nclass GlContext;\n\n\n/// A base class for all classes representing OpenGL Objects to inherit from.\n/// It allows moving the object, but not copying it through the copy constructor.\n/// It has unique_ptr-like semantics. It is called 'simple', because in the future\n/// we might want to add collections of objects and similar more advanced features.\nclass GlSimpleObject {\npublic:\n\t// Moving the representation is okay.\n\tGlSimpleObject(GlSimpleObject &&);\n\tGlSimpleObject &operator=(GlSimpleObject &&);\n\n\t// Generally, copying GL objects is costly and if we want to allow it,\n\t// we do so through an explicit copy() function.\n\tGlSimpleObject(GlSimpleObject const &) = delete;\n\tGlSimpleObject &operator=(GlSimpleObject const &) = delete;\n\n\t/// Uses delete_fun to destroy the underlying object,\n\t/// but only if the handle is present (hasn't been moved out).\n\tvirtual ~GlSimpleObject();\n\n\t/// Returns the handle to the underlying OpenGL Object.\n\tGLuint get_handle() const;\n\nprotected:\n\texplicit GlSimpleObject(const std::shared_ptr<GlContext> &,\n\t                        std::function<void(GLuint)> &&delete_fun);\n\n\t/// Context, in which this object is valid in.\n\t/// We hold this reference to keep the context active as long as this object\n\t/// is not deconstructed.\n\tstd::shared_ptr<GlContext> context;\n\n\t/// The handle to the OpenGL Object that this class represents.\n\tstd::optional<GLuint> handle;\n\n\t/// The function that deletes the underlying OpenGL Object.\n\tstd::function<void(GLuint)> delete_fun;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/texture.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"texture.h\"\n\n#include <epoxy/gl.h>\n\n#include <tuple>\n\n#include \"../../datastructure/constexpr_map.h\"\n#include \"../../error/error.h\"\n#include \"../../log/log.h\"\n\n#include \"../resources/texture_data.h\"\n#include \"lookup.h\"\n#include \"render_target.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlTexture2d::GlTexture2d(const std::shared_ptr<GlContext> &context,\n                         const resources::Texture2dData &data) :\n\tTexture2d(data.get_info()),\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteTextures(1, &handle); }) {\n\tGLuint handle;\n\tglGenTextures(1, &handle);\n\tthis->handle = handle;\n\n\tglBindTexture(GL_TEXTURE_2D, *this->handle);\n\n\t// select pixel format\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format());\n\n\t// store raw pixels to gpu\n\tauto size = this->info.get_size();\n\n\tglPixelStorei(GL_UNPACK_ALIGNMENT, this->info.get_row_alignment());\n\n\tglTexImage2D(\n\t\tGL_TEXTURE_2D,\n\t\t0,\n\t\tstd::get<0>(fmt_in_out),\n\t\tsize.first,\n\t\tsize.second,\n\t\t0,\n\t\tstd::get<1>(fmt_in_out),\n\t\tstd::get<2>(fmt_in_out),\n\t\tdata.get_data());\n\n\t// drawing settings\n\t// TODO these are outdated, use sampler settings\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n\n\tlog::log(MSG(dbg) << \"Created OpenGL texture from data (size: \"\n\t                  << size.first << \"x\" << size.second << \")\");\n}\n\nGlTexture2d::GlTexture2d(const std::shared_ptr<GlContext> &context,\n                         const resources::Texture2dInfo &info) :\n\tTexture2d(info),\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteTextures(1, &handle); }) {\n\tGLuint handle;\n\tglGenTextures(1, &handle);\n\tthis->handle = handle;\n\n\tglBindTexture(GL_TEXTURE_2D, *this->handle);\n\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format());\n\n\tauto size = this->info.get_size();\n\n\tglPixelStorei(GL_UNPACK_ALIGNMENT, this->info.get_row_alignment());\n\n\tglTexImage2D(\n\t\tGL_TEXTURE_2D,\n\t\t0,\n\t\tstd::get<0>(fmt_in_out),\n\t\tsize.first,\n\t\tsize.second,\n\t\t0,\n\t\tstd::get<1>(fmt_in_out),\n\t\tstd::get<2>(fmt_in_out),\n\t\tnullptr);\n\n\t// TODO these are outdated, use sampler settings\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n\tglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n\n\tlog::log(MSG(dbg) << \"Created OpenGL texture from info parameters (size: \"\n\t                  << size.first << \"x\" << size.second << \")\");\n}\n\nresources::Texture2dData GlTexture2d::into_data() {\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format());\n\tstd::vector<uint8_t> data(this->info.get_data_size());\n\n\tglPixelStorei(GL_PACK_ALIGNMENT, this->info.get_row_alignment());\n\tglBindTexture(GL_TEXTURE_2D, *this->handle);\n\t// TODO use a Pixel Buffer Object instead\n\tglGetTexImage(GL_TEXTURE_2D, 0, std::get<1>(fmt_in_out), std::get<2>(fmt_in_out), data.data());\n\n\treturn resources::Texture2dData(resources::Texture2dInfo(this->info), std::move(data));\n}\n\nvoid GlTexture2d::upload(resources::Texture2dData const &data) {\n\tif (this->info != data.get_info()) {\n\t\tthrow Error(MSG(err) << \"Tried to upload texture data of different format into an existing GPU texture.\");\n\t}\n\n\tglBindTexture(GL_TEXTURE_2D, *this->handle);\n\n\tauto size = this->info.get_size();\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->info.get_format());\n\n\tglTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, size.first, size.second, std::get<1>(fmt_in_out), std::get<2>(fmt_in_out), data.get_data());\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/texture.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../resources/texture_data.h\"\n#include \"../texture.h\"\n#include \"simple_object.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// A handle to an OpenGL texture.\nclass GlTexture2d final : public Texture2d\n\t, public GlSimpleObject {\npublic:\n\t/// Constructs a texture and fills it with the given data.\n\texplicit GlTexture2d(const std::shared_ptr<GlContext> &context,\n\t                     const resources::Texture2dData &);\n\n\t/// Constructs an empty texture with the given parameters.\n\tGlTexture2d(const std::shared_ptr<GlContext> &context,\n\t            resources::Texture2dInfo const &);\n\n\tresources::Texture2dData into_data() override;\n\n\tvoid upload(resources::Texture2dData const &) override;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/texture_array.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"texture_array.h\"\n\n#include <epoxy/gl.h>\n\n#include <tuple>\n\n#include \"../../datastructure/constexpr_map.h\"\n#include \"../../error/error.h\"\n#include \"../../log/log.h\"\n\n#include \"../resources/texture_data.h\"\n#include \"lookup.h\"\n#include \"render_target.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlTexture2dArray::GlTexture2dArray(const std::shared_ptr<GlContext> &context,\n                                   const std::vector<resources::Texture2dData> &data)\n\t// Call the data-less constructor first.\n\t:\n\tGlTexture2dArray(context, data.size(), data[0].get_info()) {\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->layer_info.get_format());\n\tauto size = this->layer_info.get_size();\n\n\t// Fill the array with given data\n\tsize_t i = 0;\n\tfor (auto const &tex : data) {\n\t\tglTexSubImage3D(GL_TEXTURE_2D_ARRAY,\n\t\t                0,                       // mipmap number\n\t\t                0,                       // xoffset\n\t\t                0,                       // yoffset\n\t\t                i,                       // zoffset\n\t\t                size.first,              // width\n\t\t                size.second,             // height,\n\t\t                1,                       // depth\n\t\t                std::get<1>(fmt_in_out), // format\n\t\t                std::get<2>(fmt_in_out), // type\n\t\t                tex.get_data()           // data\n\t\t);\n\n\t\ti += 1;\n\t}\n\n\tlog::log(MSG(dbg) << \"Created OpenGL texture array from data\");\n}\n\nGlTexture2dArray::GlTexture2dArray(const std::shared_ptr<GlContext> &context,\n                                   size_t n_layers,\n                                   resources::Texture2dInfo const &layer_info) :\n\tTexture2dArray(layer_info),\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteTextures(1, &handle); }),\n\tn_layers(n_layers) {\n\tGLuint handle;\n\tglGenTextures(1, &handle);\n\tthis->handle = handle;\n\n\tglBindTexture(GL_TEXTURE_2D_ARRAY, *this->handle);\n\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->layer_info.get_format());\n\tauto size = this->layer_info.get_size();\n\n\t// Create empty image\n\tglTexImage3D(GL_TEXTURE_2D_ARRAY,\n\t             0,                       // mipmap level\n\t             std::get<0>(fmt_in_out), // gpu texel format\n\t             size.first,              // width\n\t             size.second,             // height\n\t             n_layers,                // depth\n\t             0,                       // border\n\t             std::get<1>(fmt_in_out), // cpu pixel format\n\t             std::get<2>(fmt_in_out), // cpu pixel type\n\t             nullptr                  // data\n\t);\n\n\t// TODO these are outdated, use sampler settings\n\tglTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n\tglTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n\n\tlog::log(MSG(dbg) << \"Created OpenGL texture array from info parameters\");\n}\n\nvoid GlTexture2dArray::upload(size_t layer, resources::Texture2dData const &data) {\n\tif (layer >= this->n_layers) {\n\t\tthrow Error(MSG(err) << \"Cannot upload to layer \" << layer << \" in texture array with \"\n\t\t                     << this->n_layers << \" layers.\");\n\t}\n\n\tglBindTexture(GL_TEXTURE_2D_ARRAY, *this->handle);\n\n\tauto fmt_in_out = GL_PIXEL_FORMAT.get(this->layer_info.get_format());\n\tauto size = this->layer_info.get_size();\n\n\tglTexSubImage3D(GL_TEXTURE_2D_ARRAY,\n\t                0, // mipmap number\n\t                0,\n\t                0,\n\t                layer, // xoffset, yoffset, zoffset\n\t                size.first,\n\t                size.second,\n\t                1,                       // width, height, depth\n\t                std::get<1>(fmt_in_out), // format\n\t                std::get<2>(fmt_in_out), // type\n\t                data.get_data()          // data\n\t);\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/texture_array.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vector>\n\n#include \"../texture_array.h\"\n\n#include \"../resources/texture_data.h\"\n#include \"simple_object.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// An OpenGL array of 2D textures.\nclass GlTexture2dArray final : public Texture2dArray\n\t, public GlSimpleObject {\npublic:\n\t/// Constructs an array of the same number of layers as the size of the given\n\t/// vector, and fills the layers with the corresponding vector element. The\n\t/// texture formats in all vector elements must be the same as defined by\n\t/// Textur2dInfo::operator==.\n\tGlTexture2dArray(const std::shared_ptr<GlContext> &context,\n\t                 const std::vector<resources::Texture2dData> &);\n\n\t/// Constructs an array of ln_layers empty layers, with the per-layer texture\n\t/// format specified in layer_info.\n\tGlTexture2dArray(const std::shared_ptr<GlContext> &context,\n\t                 size_t n_layers,\n\t                 resources::Texture2dInfo const &layer_info);\n\n\tvoid upload(size_t layer, resources::Texture2dData const &) override;\n\nprivate:\n\t/// The number of layers (elements) in the array, AKA the depth.\n\tsize_t n_layers;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/uniform_buffer.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"uniform_buffer.h\"\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/lookup.h\"\n#include \"renderer/opengl/texture.h\"\n#include \"renderer/opengl/uniform_input.h\"\n#include \"renderer/opengl/util.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlUniformBuffer::GlUniformBuffer(const std::shared_ptr<GlContext> &context,\n                                 size_t size,\n                                 std::vector<GlInBlockUniform> uniforms,\n                                 GLuint binding_point,\n                                 GLenum usage) :\n\tGlSimpleObject(context,\n                   [&](GLuint handle) {\n\t\t\t\t\t   glDeleteBuffers(1, &handle);\n\t\t\t\t   }),\n\tuniforms{uniforms},\n\tdata_size{size},\n\tbinding_point{binding_point} {\n\tGLuint handle;\n\tglGenBuffers(1, &handle);\n\tthis->handle = handle;\n\n\tthis->bind();\n\tglBufferData(GL_UNIFORM_BUFFER, this->data_size, NULL, usage);\n\n\tuniform_id_t unif_id = 0;\n\tfor (auto &uniform : uniforms) {\n\t\tthis->uniforms_by_name.insert(std::make_pair(uniform.name, unif_id));\n\t\tunif_id += 1;\n\t}\n\n\tglBindBufferRange(GL_UNIFORM_BUFFER, this->binding_point, *this->handle, 0, this->data_size);\n\n\tlog::log(MSG(dbg) << \"Created OpenGL uniform buffer (size: \"\n\t                  << this->data_size << \", binding point: \"\n\t                  << this->binding_point << \")\");\n}\n\nGLuint GlUniformBuffer::get_binding_point() const {\n\treturn this->binding_point;\n}\n\nvoid GlUniformBuffer::set_binding_point(GLuint binding_point) {\n\tthis->binding_point = binding_point;\n\tglBindBufferBase(GL_UNIFORM_BUFFER, this->binding_point, *this->handle);\n}\n\nvoid GlUniformBuffer::update_uniforms(std::shared_ptr<UniformBufferInput> const &unif_in) {\n\tauto glunif_in = std::dynamic_pointer_cast<GlUniformBufferInput>(unif_in);\n\tENSURE(glunif_in->get_buffer().get() == this, \"Uniform input passed to different buffer than it was created with.\");\n\n\tthis->bind();\n\n\tconst auto &update_offs = glunif_in->update_offs;\n\tconst auto &used_uniforms = glunif_in->used_uniforms;\n\tconst auto &uniforms = this->uniforms;\n\tuint8_t const *data = glunif_in->update_data.data();\n\n\tsize_t unif_count = used_uniforms.size();\n\tfor (size_t i = 0; i < unif_count; ++i) {\n\t\tuniform_id_t unif_id = used_uniforms[i];\n\t\tauto offset = update_offs[unif_id];\n\n\t\tuint8_t const *ptr = data + offset.offset;\n\t\tauto &unif = uniforms[unif_id];\n\t\tauto loc = unif.offset;\n\t\tauto size = unif.size;\n\n\t\tglBufferSubData(GL_UNIFORM_BUFFER, loc, size, ptr);\n\t}\n}\n\nconst std::vector<GlInBlockUniform> &GlUniformBuffer::get_uniforms() const {\n\treturn this->uniforms;\n}\n\nbool GlUniformBuffer::has_uniform(const char *name) {\n\treturn this->uniforms_by_name.contains(name);\n}\n\nvoid GlUniformBuffer::bind() const {\n\tglBindBuffer(GL_UNIFORM_BUFFER, *this->handle);\n}\n\nstd::shared_ptr<UniformBufferInput> GlUniformBuffer::new_unif_in() {\n\tauto in = std::make_shared<GlUniformBufferInput>(this->shared_from_this());\n\n\treturn in;\n}\n\nvoid GlUniformBuffer::set_unif(UniformBufferInput &in, const char *unif, void const *val, GLenum type) {\n\tauto &unif_in = dynamic_cast<GlUniformBufferInput &>(in);\n\n\tauto unif_id = this->uniforms_by_name.find(unif)->second;\n\tENSURE(unif_id < this->uniforms.size(),\n\t       \"Tried to set uniform with invalid ID \" << unif_id);\n\n\tauto const &unif_data = this->uniforms[unif_id];\n\n\tENSURE(type == unif_data.type,\n\t       \"Tried to set uniform \" << unif << \" to a value of the wrong type.\");\n\n\tsize_t size = GL_UNIFORM_TYPE_SIZE.get(type);\n\tENSURE(size == unif_data.size,\n\t       \"Tried to set uniform \" << unif << \" to a value of the wrong size.\");\n\n\tauto &update_off = unif_in.update_offs[unif_id];\n\tauto offset = update_off.offset;\n\tmemcpy(unif_in.update_data.data() + offset, val, size);\n\tif (not update_off.used) [[unlikely]] { // only true if the uniform value was not set before\n\t\tauto lower_bound = std::lower_bound(\n\t\t\tstd::begin(unif_in.used_uniforms),\n\t\t\tstd::end(unif_in.used_uniforms),\n\t\t\tunif_id);\n\t\tunif_in.used_uniforms.insert(lower_bound, unif_id);\n\t\tupdate_off.used = true;\n\t}\n}\n\nvoid GlUniformBuffer::set_i32(UniformBufferInput &in, const char *unif, int32_t val) {\n\tthis->set_unif(in, unif, &val, GL_INT);\n}\n\nvoid GlUniformBuffer::set_u32(UniformBufferInput &in, const char *unif, uint32_t val) {\n\tthis->set_unif(in, unif, &val, GL_UNSIGNED_INT);\n}\n\nvoid GlUniformBuffer::set_f32(UniformBufferInput &in, const char *unif, float val) {\n\tthis->set_unif(in, unif, &val, GL_FLOAT);\n}\n\nvoid GlUniformBuffer::set_f64(UniformBufferInput &in, const char *unif, double val) {\n\tthis->set_unif(in, unif, &val, GL_DOUBLE);\n}\n\nvoid GlUniformBuffer::set_bool(UniformBufferInput &in, const char *unif, bool val) {\n\tthis->set_unif(in, unif, &val, GL_BOOL);\n}\n\nvoid GlUniformBuffer::set_v2f32(UniformBufferInput &in, const char *unif, Eigen::Vector2f const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_FLOAT_VEC2);\n}\n\nvoid GlUniformBuffer::set_v3f32(UniformBufferInput &in, const char *unif, Eigen::Vector3f const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_FLOAT_VEC3);\n}\n\nvoid GlUniformBuffer::set_v4f32(UniformBufferInput &in, const char *unif, Eigen::Vector4f const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_FLOAT_VEC4);\n}\n\nvoid GlUniformBuffer::set_v2i32(UniformBufferInput &in, const char *unif, Eigen::Vector2i const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_INT_VEC2);\n}\n\nvoid GlUniformBuffer::set_v3i32(UniformBufferInput &in, const char *unif, Eigen::Vector3i const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_INT_VEC3);\n}\n\nvoid GlUniformBuffer::set_v4i32(UniformBufferInput &in, const char *unif, Eigen::Vector4i const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_INT_VEC4);\n}\n\nvoid GlUniformBuffer::set_v2ui32(UniformBufferInput &in, const char *unif, Eigen::Vector2<uint32_t> const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC2);\n}\n\nvoid GlUniformBuffer::set_v3ui32(UniformBufferInput &in, const char *unif, Eigen::Vector3<uint32_t> const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC3);\n}\n\nvoid GlUniformBuffer::set_v4ui32(UniformBufferInput &in, const char *unif, Eigen::Vector4<uint32_t> const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_UNSIGNED_INT_VEC4);\n}\n\nvoid GlUniformBuffer::set_m4f32(UniformBufferInput &in, const char *unif, Eigen::Matrix4f const &val) {\n\tthis->set_unif(in, unif, val.data(), GL_FLOAT_MAT4);\n}\n\nvoid GlUniformBuffer::set_tex(UniformBufferInput &in, const char *unif, std::shared_ptr<Texture2d> const &val) {\n\tauto tex = std::dynamic_pointer_cast<GlTexture2d>(val);\n\tGLuint handle = tex->get_handle();\n\tthis->set_unif(in, unif, &handle, GL_SAMPLER_2D);\n}\n\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/uniform_buffer.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"renderer/opengl/shader_data.h\"\n#include \"renderer/opengl/simple_object.h\"\n#include \"renderer/uniform_buffer.h\"\n\nnamespace openage::renderer {\nclass UniformBuffer;\nclass UniformBufferInput;\n\nnamespace opengl {\nclass GlContext;\nclass GlUniformInput;\nclass GlUniformBufferInput;\n\nclass GlUniformBuffer final : public UniformBuffer\n\t, public GlSimpleObject {\npublic:\n\tGlUniformBuffer(const std::shared_ptr<GlContext> &context,\n\t                size_t size,\n\t                std::vector<GlInBlockUniform> uniforms,\n\t                GLuint binding_point = 0,\n\t                GLenum usage = GL_DYNAMIC_DRAW);\n\n\t/**\n\t * Get the binding point of the buffer.\n\t *\n\t * @return Binding point ID.\n\t */\n\tGLuint get_binding_point() const;\n\n\t/**\n\t * Get the uniform buffers uniforms.\n\t *\n\t * @return Uniforms in the shader program.\n\t */\n\tconst std::vector<GlInBlockUniform> &get_uniforms() const;\n\n\t/**\n\t * Set the binding point of the buffer.\n\t *\n\t * @param binding_point Binding point ID.\n\t */\n\tvoid set_binding_point(GLuint binding_point);\n\n\tvoid update_uniforms(std::shared_ptr<UniformBufferInput> const &unif_in) override;\n\n\tbool has_uniform(const char *) override;\n\n\t/**\n\t * Bind the buffer.\n\t */\n\tvoid bind() const;\n\nprotected:\n\tstd::shared_ptr<UniformBufferInput> new_unif_in() override;\n\tvoid set_i32(UniformBufferInput &in, const char *, int32_t) override;\n\tvoid set_u32(UniformBufferInput &in, const char *, uint32_t) override;\n\tvoid set_f32(UniformBufferInput &in, const char *, float) override;\n\tvoid set_f64(UniformBufferInput &in, const char *, double) override;\n\tvoid set_bool(UniformBufferInput &in, const char *, bool) override;\n\tvoid set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) override;\n\tvoid set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) override;\n\tvoid set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) override;\n\tvoid set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) override;\n\tvoid set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) override;\n\tvoid set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) override;\n\tvoid set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2<uint32_t> const &) override;\n\tvoid set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3<uint32_t> const &) override;\n\tvoid set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4<uint32_t> const &) override;\n\tvoid set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) override;\n\tvoid set_tex(UniformBufferInput &in, const char *, std::shared_ptr<Texture2d> const &) override;\n\nprivate:\n\t/**\n\t * Update a uniform value in a uniform buffer input object.\n\t *\n\t * Note that the uniform buffer itself is not updated by this. Data is only uploaded\n\t * to the buffer when \\p update_uniforms is eventually called.\n\t *\n\t * @param in Uniform buffer input object.\n\t * @param name Name of the uniform.\n\t * @param val Pointer to the value to update the uniform with.\n\t * @param type Type of the uniform.\n\t */\n\tvoid set_unif(UniformBufferInput &in,\n\t              const char *name,\n\t              void const *val,\n\t              GLenum type);\n\n\t/**\n\t * Uniform definitions inside the buffer.\n\t */\n\tstd::vector<GlInBlockUniform> uniforms;\n\n\t/**\n\t * Maps uniform names to their ID (the index in the uniform vector).\n\t */\n\tstd::unordered_map<std::string, uniform_id_t> uniforms_by_name;\n\n\t/**\n\t * Size of the buffer (in bytes).\n\t */\n\tsize_t data_size;\n\n\t/**\n\t * Binding point of the buffer.\n\t */\n\tGLuint binding_point;\n};\n\n} // namespace opengl\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/opengl/uniform_input.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"uniform_input.h\"\n\n#include \"renderer/opengl/lookup.h\"\n#include \"renderer/opengl/shader_program.h\"\n#include \"renderer/opengl/uniform_buffer.h\"\n#include \"renderer/opengl/util.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlUniformInput::GlUniformInput(const std::shared_ptr<ShaderProgram> &prog) :\n\tUniformInput{prog} {\n\tauto glprog = std::dynamic_pointer_cast<GlShaderProgram>(prog);\n\n\t// Reserve space for the used uniforms.\n\tthis->used_uniforms.reserve(glprog->get_uniforms().size());\n\n\t// Calculate the byte-wise offsets of all uniforms.\n\tsize_t offset = 0;\n\tthis->update_offs.reserve(glprog->get_uniforms().size());\n\tfor (auto &uniform : glprog->get_uniforms()) {\n\t\tthis->update_offs.push_back({offset, false});\n\t\toffset += GL_UNIFORM_TYPE_SIZE.get(uniform.type);\n\t}\n\n\t// Resize the update data buffer to the total size of all uniforms.\n\tthis->update_data.resize(offset);\n}\n\nGlUniformBufferInput::GlUniformBufferInput(const std::shared_ptr<UniformBuffer> &buffer) :\n\tUniformBufferInput{buffer} {\n\tauto glBuf = std::dynamic_pointer_cast<GlUniformBuffer>(buffer);\n\n\tauto uniforms = glBuf->get_uniforms();\n\n\t// Reserve space for the used uniforms.\n\tthis->used_uniforms.reserve(uniforms.size());\n\n\t// Calculate the byte-wise offsets of all uniforms.\n\tsize_t offset = 0;\n\tthis->update_offs.reserve(uniforms.size());\n\tfor (auto &uniform : uniforms) {\n\t\tthis->update_offs.push_back({uniform.offset, false});\n\t\toffset += GL_UNIFORM_TYPE_SIZE.get(uniform.type);\n\t}\n\n\t// Resize the update data buffer to the total size of all uniforms.\n\tthis->update_data.resize(offset);\n}\n\nbool GlUniformInput::is_complete() const {\n\tfor (const auto &uniform : this->update_offs) {\n\t\tif (not uniform.used) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/uniform_input.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <unordered_map>\n#include <vector>\n\n#include \"renderer/renderer.h\"\n#include \"renderer/types.h\"\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass ShaderProgram;\nclass UniformBuffer;\n\nnamespace opengl {\n\nclass GlShaderProgram;\nclass GlUniformBuffer;\n\n/**\n * Describes OpenGL-specific uniform valuations.\n */\nclass GlUniformInput final : public UniformInput {\nprivate:\n\tstruct GlUniformOffset {\n\t\t// Offset in the update_data buffer.\n\t\tsize_t offset;\n\t\t/// Dtermine whether the uniform value has been set.\n\t\tbool used;\n\t};\n\npublic:\n\tGlUniformInput(const std::shared_ptr<ShaderProgram> &prog);\n\n\t/**\n\t * Check if all uniforms have been set.\n\t */\n\tbool is_complete() const override;\n\n\t/**\n\t * Store the IDs of the uniforms from the shader set by this uniform input.\n\t */\n\tstd::vector<uniform_id_t> used_uniforms;\n\n\t/**\n\t * Store offsets of uniforms in the update_data buffer and\n\t * whether the uniform value has been set.\n\t *\n\t * Index in the vector corresponds to the uniform ID in the shader.\n\t */\n\tstd::vector<GlUniformOffset> update_offs;\n\n\t/**\n\t * Buffer containing untyped uniform update data.\n\t */\n\tstd::vector<uint8_t> update_data;\n};\n\n/**\n * Describes OpenGL-specific uniform buffer valuations.\n */\nclass GlUniformBufferInput final : public UniformBufferInput {\nprivate:\n\tstruct GlUniformOffset {\n\t\t// Offset in the update_data buffer.\n\t\tsize_t offset;\n\t\t/// Dtermine whether the uniform value has been set.\n\t\tbool used;\n\t};\n\n\npublic:\n\tGlUniformBufferInput(const std::shared_ptr<UniformBuffer> &buffer);\n\n\t/**\n\t * Store the IDs of the uniforms from the shader set by this uniform input.\n\t */\n\tstd::vector<uniform_id_t> used_uniforms;\n\n\t/**\n\t * Store offsets of uniforms in the update_data buffer and\n\t * whether the uniform value has been set.\n\t */\n\tstd::vector<GlUniformOffset> update_offs;\n\n\t/**\n\t * Buffer containing untyped uniform update data.\n\t */\n\tstd::vector<uint8_t> update_data;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/util.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"util.h\"\n\nnamespace openage::renderer::opengl {\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/util.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n\n#include <epoxy/gl.h>\n\n#include \"renderer/opengl/lookup.h\"\n\n\nnamespace openage::renderer::opengl {\n\n/**\n * Get the sizes of a uniform value with a given uniform type.\n *\n * Guaranteed to be a evaluated at compile time.\n *\n * @param type Uniform type.\n *\n * @return Size of uniform value (in bytes).\n */\nconsteval size_t get_uniform_type_size(GLenum type) {\n\treturn GL_UNIFORM_TYPE_SIZE.get(type);\n}\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/vertex_array.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"vertex_array.h\"\n\n#include <map>\n\n#include \"../../datastructure/constexpr_map.h\"\n#include \"../../error/error.h\"\n\n#include \"lookup.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\nGlVertexArray::GlVertexArray(const std::shared_ptr<GlContext> &context,\n                             std::vector<std::pair<GlBuffer const &,\n                                                   resources::VertexInputInfo const &>> buffers) :\n\tGlSimpleObject(context,\n                   [](GLuint handle) {\n\t\t\t\t\t   glDeleteVertexArrays(1, &handle);\n\t\t\t\t   }) {\n\tGLuint handle;\n\tglGenVertexArrays(1, &handle);\n\tthis->handle = handle;\n\n\tthis->bind();\n\n\tif (buffers.size() == 1 and buffers[0].second.get_shader_input_map()) { // Map inputs according to specified pairing\n\t\tauto const &info = buffers[0].second;\n\t\tauto const &in_map = *info.get_shader_input_map();\n\t\tauto const &in = info.get_inputs();\n\n\t\tauto &buf = buffers[0].first;\n\t\tbuf.bind(GL_ARRAY_BUFFER);\n\n\t\tstd::map<size_t, size_t> in_map_sorted(in_map.begin(), in_map.end());\n\n\t\tsize_t offset = 0;\n\t\tsize_t next_idx = 0;\n\n\t\tif (info.get_layout() == resources::vertex_layout_t::AOS) {\n\t\t\tfor (auto mapping : in_map_sorted) {\n\t\t\t\tif (mapping.first != next_idx) {\n\t\t\t\t\t// Some attributes were skipped, so add them to the offset without enabling in VAO\n\t\t\t\t\tfor (size_t i = next_idx; i < mapping.first; i++) {\n\t\t\t\t\t\toffset += resources::vertex_input_size(in[i]);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tglVertexAttribPointer(\n\t\t\t\t\tmapping.second,\n\t\t\t\t\tresources::vertex_input_count(in[mapping.first]),\n\t\t\t\t\tGL_VERT_IN_ELEM_TYPE.get(in[mapping.first]),\n\t\t\t\t\tGL_FALSE,\n\t\t\t\t\tinfo.vert_size(),\n\t\t\t\t\treinterpret_cast<void *>(offset));\n\n\t\t\t\toffset += resources::vertex_input_size(in[mapping.first]);\n\t\t\t\tnext_idx = mapping.first + 1;\n\t\t\t}\n\t\t}\n\t\telse if (info.get_layout() == resources::vertex_layout_t::SOA) {\n\t\t\tsize_t vert_count = buf.get_size() / info.vert_size();\n\n\t\t\tfor (auto mapping : in_map_sorted) {\n\t\t\t\tif (mapping.first != next_idx) {\n\t\t\t\t\t// Some attributes were skipped, so add them to the offset without enabling in VAO\n\t\t\t\t\tfor (size_t i = next_idx; i < mapping.first; i++) {\n\t\t\t\t\t\toffset += resources::vertex_input_size(in[i]) * vert_count;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tglVertexAttribPointer(\n\t\t\t\t\tmapping.second,\n\t\t\t\t\tresources::vertex_input_count(in[mapping.first]),\n\t\t\t\t\tGL_VERT_IN_ELEM_TYPE.get(in[mapping.first]),\n\t\t\t\t\tGL_FALSE,\n\t\t\t\t\t0,\n\t\t\t\t\treinterpret_cast<void *>(offset));\n\n\t\t\t\toffset += resources::vertex_input_size(in[mapping.first]) * vert_count;\n\t\t\t\tnext_idx = mapping.first + 1;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tthrow Error(MSG(err) << \"Unknown vertex format.\");\n\t\t}\n\t}\n\telse { // Map inputs in ascending order\n\t\tGLuint attrib = 0;\n\n\t\tfor (auto buf_info : buffers) {\n\t\t\tauto const &buf = buf_info.first;\n\t\t\tauto const &info = buf_info.second;\n\n\t\t\tif (info.get_shader_input_map()) [[unlikely]] {\n\t\t\t\tthrow Error(MSG(err) << \"Shader input mapping is unsupported when constructing a VAO from multiple buffers.\");\n\t\t\t}\n\n\t\t\tbuf.bind(GL_ARRAY_BUFFER);\n\n\t\t\tif (info.get_layout() == resources::vertex_layout_t::AOS) {\n\t\t\t\tsize_t offset = 0;\n\n\t\t\t\tfor (auto in : info.get_inputs()) {\n\t\t\t\t\tglEnableVertexAttribArray(attrib);\n\n\t\t\t\t\tglVertexAttribPointer(\n\t\t\t\t\t\tattrib,\n\t\t\t\t\t\tresources::vertex_input_count(in),\n\t\t\t\t\t\tGL_VERT_IN_ELEM_TYPE.get(in),\n\t\t\t\t\t\tGL_FALSE,\n\t\t\t\t\t\tinfo.vert_size(),\n\t\t\t\t\t\treinterpret_cast<void *>(offset));\n\n\t\t\t\t\toffset += resources::vertex_input_size(in);\n\t\t\t\t\tattrib += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (info.get_layout() == resources::vertex_layout_t::SOA) {\n\t\t\t\tsize_t offset = 0;\n\t\t\t\tsize_t vert_count = buf.get_size() / info.vert_size();\n\n\t\t\t\tfor (auto in : info.get_inputs()) {\n\t\t\t\t\tglEnableVertexAttribArray(attrib);\n\n\t\t\t\t\tglVertexAttribPointer(\n\t\t\t\t\t\tattrib,\n\t\t\t\t\t\tresources::vertex_input_count(in),\n\t\t\t\t\t\tGL_VERT_IN_ELEM_TYPE.get(in),\n\t\t\t\t\t\tGL_FALSE,\n\t\t\t\t\t\t0,\n\t\t\t\t\t\treinterpret_cast<void *>(offset));\n\n\t\t\t\t\toffset += resources::vertex_input_size(in) * vert_count;\n\t\t\t\t\tattrib += 1;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow Error(MSG(err) << \"Unknown vertex layout.\");\n\t\t\t}\n\t\t}\n\t}\n}\n\nGlVertexArray::GlVertexArray(const std::shared_ptr<GlContext> &context,\n                             GlBuffer const &buf,\n                             resources::VertexInputInfo const &info) :\n\tGlVertexArray(context, {{buf, info}}) {}\n\nGlVertexArray::GlVertexArray(const std::shared_ptr<GlContext> &context) :\n\tGlSimpleObject(context,\n                   [](GLuint handle) { glDeleteVertexArrays(1, &handle); }) {\n\tGLuint handle;\n\tglGenVertexArrays(1, &handle);\n\tthis->handle = handle;\n}\n\nvoid GlVertexArray::bind() const {\n\tglBindVertexArray(*this->handle);\n\n\t// Enabling the vertexAttribArrays here again is unnecessary,\n\t// because the VAO bind does it anyway.\n}\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/vertex_array.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../resources/mesh_data.h\"\n#include \"buffer.h\"\n#include \"simple_object.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace opengl {\n\n/// An OpenGL Vertex Array Object. It represents a mapping between a buffer\n/// that contains vertex data and a way for the context to interpret it.\nclass GlVertexArray final : public GlSimpleObject {\npublic:\n\t/// Initializes a vertex array from the given mesh data.\n\t/// If no shader input mapping is present, the inputs are parsed as follows - each subsequent\n\t/// (GlBuffer, VertexInputInfo) pair is added to the VAO in the same order as they appear\n\t/// in the vector. Each buffer is bound according to its input info and each subsequent vertex\n\t/// attribute is attached to an id in ascending order.\n\t/// For example, the inputs:\n\t/// in vec3 pos;\n\t/// in vec2 uv;\n\t/// in mat3 rot;\n\t/// could be bound to two buffers like so:\n\t/// GlBuffer buf1(data1, size1);\n\t/// GlBuffer buf2(data2, size2);\n\t/// GlVertexArray( {\n\t/// \tstd::make_pair(buf1, { { vertex_input_t::V3F32, vertex_input_t::V2F32 }, vertex_layout_t::SOA },\n\t/// \tstd::make_pair(buf2, { { vertex_input_t::M3F32 }, vertex_input_t::AOS /*or ::SOA, doesn't matter*/ },\n\t/// } );\n\t///\n\t/// A shader input mapping is only allowed when there is a single element in `buffers`. In such a case,\n\t/// the vertex inputs are paired with VAO attributes according to the mapping instead of in ascending order.\n\tGlVertexArray(const std::shared_ptr<GlContext> &context,\n\t              std::vector<std::pair<GlBuffer const &, resources::VertexInputInfo const &>> buffers);\n\n\t/// Executes the buffer list constructor with one element.\n\tGlVertexArray(const std::shared_ptr<GlContext> &context,\n\t              GlBuffer const &,\n\t              resources::VertexInputInfo const &);\n\n\t/// The default constructor initializes an empty VAO with no attributes.\n\t/// This is useful for bufferless drawing.\n\tGlVertexArray(const std::shared_ptr<GlContext> &context);\n\n\t/// Make this vertex array object the current one.\n\tvoid bind() const;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/opengl/window.cpp",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#include \"window.h\"\n\n#include <QGuiApplication>\n#include <QOpenGLContext>\n#include <QQuickWindow>\n#include <QString>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/opengl/renderer.h\"\n#include \"renderer/window_event_handler.h\"\n\n\nnamespace openage::renderer::opengl {\n\nGlWindow::GlWindow(const std::string &title,\n                   window_settings settings) :\n\tWindow{settings.width, settings.height} {\n\tif (QGuiApplication::instance() == nullptr) {\n\t\t// Qt windows need to attach to a QtGuiApplication\n\t\tthrow Error{MSG(err) << \"Failed to create Qt window: QGuiApplication has not been created yet.\"};\n\t}\n\n\tQQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);\n\tthis->window = std::make_shared<QWindow>();\n\n\tthis->window->setTitle(QString::fromStdString(title));\n\tthis->window->resize(settings.width, settings.height);\n\n\tthis->window->setSurfaceType(QSurface::OpenGLSurface);\n\n\tauto gl_specs = GlContext::find_spec();\n\tQSurfaceFormat format{};\n\tformat.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);\n\tformat.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer);\n\n\tif (not settings.vsync) {\n\t\tformat.setSwapInterval(0);\n\t}\n\n\tformat.setMajorVersion(gl_specs.major_version);\n\tformat.setMinorVersion(gl_specs.minor_version);\n\n\tformat.setAlphaBufferSize(8);\n\tformat.setDepthBufferSize(24);\n\tformat.setStencilBufferSize(8);\n\n\tif (settings.debug) {\n\t\tformat.setOption(QSurfaceFormat::DebugContext);\n\t}\n\n\t// TODO: Set format as default for all windows with QSurface::setDefaultFormat()\n\tthis->window->setFormat(format);\n\tthis->window->create();\n\n\t// set display mode\n\t// Reset to a known state\n\tthis->window->setWindowState(Qt::WindowNoState);\n\tswitch (settings.mode) {\n\tcase window_mode::WINDOWED:\n\t\t// nothing to do because it's the default\n\t\tbreak;\n\tcase window_mode::BORDERLESS:\n\t\tthis->window->setFlags(this->window->flags() | Qt::FramelessWindowHint);\n\t\tbreak;\n\tcase window_mode::FULLSCREEN:\n\t\tthis->window->setWindowState(Qt::WindowFullScreen);\n\t\tbreak;\n\tdefault:\n\t\tthrow Error{MSG(err) << \"Invalid window mode.\"};\n\t}\n\n\tthis->context = std::make_shared<GlContext>(this->window, settings.debug);\n\tif (not this->context->get_raw_context()->isValid()) {\n\t\tthrow Error{MSG(err) << \"Failed to create Qt OpenGL context.\"};\n\t}\n\n\tthis->window->installEventFilter(this->event_handler.get());\n\n\tthis->window->setVisible(true);\n\tlog::log(MSG(info) << \"Created Qt window with OpenGL context.\");\n\n\t// Scaling factor if highDPI\n\tthis->scale_dpr = this->window->devicePixelRatio();\n\n\tGlContext::check_error();\n}\n\nGlWindow::~GlWindow() {\n\tthis->make_context_current();\n}\n\n\nvoid GlWindow::set_size(size_t width, size_t height) {\n\tif (this->size[0] != width || this->size[1] != height) {\n\t\tthis->window->resize(width, height);\n\t\tthis->size = {width, height};\n\t}\n\n\tfor (auto &cb : this->on_resize) {\n\t\tcb(width, height, this->scale_dpr);\n\t}\n}\n\n\nvoid GlWindow::update() {\n\t// TODO: move this event handling to presenter\n\n\tauto events = this->event_handler->pop_events();\n\n\tfor (auto &&event : events) {\n\t\tswitch (event->type()) {\n\t\tcase QEvent::Resize: {\n\t\t\t// TODO: resize can be handled before the engine event processing\n\t\t\tauto ev = std::dynamic_pointer_cast<QResizeEvent>(event);\n\t\t\tsize_t width = ev->size().width();\n\t\t\tsize_t height = ev->size().height();\n\t\t\tlog::log(MSG(dbg) << \"Window resized to: \" << width << \"x\" << height);\n\n\t\t\tthis->size = {width, height};\n\t\t\tfor (auto &cb : this->on_resize) {\n\t\t\t\tcb(width, height, this->scale_dpr);\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::Close: {\n\t\t\tthis->should_be_closed = true;\n\t\t} break;\n\n\t\tcase QEvent::KeyPress:\n\t\tcase QEvent::KeyRelease: {\n\t\t\tauto const ev = std::dynamic_pointer_cast<QKeyEvent>(event);\n\t\t\tfor (auto &cb : this->on_key) {\n\t\t\t\tcb(*ev);\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::MouseButtonPress:\n\t\tcase QEvent::MouseButtonRelease:\n\t\tcase QEvent::MouseButtonDblClick: {\n\t\t\tauto const ev = std::dynamic_pointer_cast<QMouseEvent>(event);\n\t\t\tfor (auto &cb : this->on_mouse_button) {\n\t\t\t\tcb(*ev);\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::MouseMove: {\n\t\t\tauto const ev = std::dynamic_pointer_cast<QMouseEvent>(event);\n\t\t\tfor (auto &cb : this->on_mouse_move) {\n\t\t\t\tcb(*ev);\n\t\t\t}\n\t\t} break;\n\t\tcase QEvent::Wheel: {\n\t\t\tauto const ev = std::dynamic_pointer_cast<QWheelEvent>(event);\n\t\t\tfor (auto &cb : this->on_mouse_wheel) {\n\t\t\t\tcb(*ev);\n\t\t\t}\n\t\t} break;\n\t\tdefault:\n\t\t\tbreak; // unhandled event\n\t\t}\n\t}\n\n\tthis->context->get_raw_context()->swapBuffers(this->window.get());\n}\n\n\nstd::shared_ptr<Renderer> GlWindow::make_renderer() {\n\tauto renderer = std::make_shared<GlRenderer>(this->get_context(),\n\t                                             this->size * this->scale_dpr);\n\n\tthis->add_resize_callback([renderer](size_t w, size_t h, double scale) {\n\t\t// this up-scales all the default framebuffer to the \"bigger\" highdpi window.\n\t\trenderer->resize_display_target(w * scale, h * scale);\n\t});\n\n\treturn renderer;\n}\n\n\nvoid GlWindow::make_context_current() {\n\tthis->context->get_raw_context()->makeCurrent(this->window.get());\n}\n\n\nvoid GlWindow::done_context_current() {\n\tthis->context->get_raw_context()->doneCurrent();\n}\n\n\nconst std::shared_ptr<opengl::GlContext> &GlWindow::get_context() const {\n\treturn this->context;\n}\n\n\n} // namespace openage::renderer::opengl\n"
  },
  {
    "path": "libopenage/renderer/opengl/window.h",
    "content": "// Copyright 2018-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"renderer/window.h\"\n\n#include <optional>\n\n\nQT_FORWARD_DECLARE_CLASS(QWindow)\nQT_FORWARD_DECLARE_CLASS(QOpenGLContext)\n\nnamespace openage {\nnamespace renderer {\n\nclass EventHandlingQuickWindow;\n\nnamespace opengl {\n\nclass GlContext;\n\nclass GlWindow final : public Window {\npublic:\n\t/**\n\t * Create a shiny window with the given title.\n\t *\n\t * @param title The window title.\n\t * @param settings Settings for creating the window.\n\t */\n\tGlWindow(const std::string &title,\n\t         window_settings settings = {});\n\t~GlWindow();\n\n\tvoid set_size(size_t width, size_t height) override;\n\n\tvoid update() override;\n\n\tstd::shared_ptr<Renderer> make_renderer() override;\n\n\t/// Make this window's context the current rendering context of the current thread.\n\t/// Only use this and most other GL functions on a dedicated window thread.\n\tvoid make_context_current();\n\n\t/// Release this window's context from the current thread.\n\t/// Only use this and most other GL functions on a dedicated window thread.\n\tvoid done_context_current();\n\n\t/// Return a pointer to this window's GL context.\n\tconst std::shared_ptr<opengl::GlContext> &get_context() const;\n\nprivate:\n\t// OpenGL Context in Qt\n\tstd::shared_ptr<opengl::GlContext> context;\n};\n\n} // namespace opengl\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/render_factory.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_factory.h\"\n\n#include \"coord/phys.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"renderer/stages/terrain/render_stage.h\"\n#include \"renderer/stages/world/render_entity.h\"\n#include \"renderer/stages/world/render_stage.h\"\n\nnamespace openage::renderer {\nRenderFactory::RenderFactory(const std::shared_ptr<terrain::TerrainRenderStage> terrain_renderer,\n                             const std::shared_ptr<world::WorldRenderStage> world_renderer) :\n\tterrain_renderer{terrain_renderer},\n\tworld_renderer{world_renderer} {\n}\n\nstd::shared_ptr<terrain::RenderEntity> RenderFactory::add_terrain_render_entity(const util::Vector2s chunk_size,\n                                                                                const coord::tile_delta chunk_offset) {\n\tauto entity = std::make_shared<terrain::RenderEntity>();\n\tthis->terrain_renderer->add_render_entity(entity, chunk_size, chunk_offset.to_phys2().to_scene2());\n\n\treturn entity;\n}\n\nstd::shared_ptr<world::RenderEntity> RenderFactory::add_world_render_entity() {\n\tauto entity = std::make_shared<world::RenderEntity>();\n\tthis->world_renderer->add_render_entity(entity);\n\n\treturn entity;\n}\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/render_factory.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"coord/tile.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer {\nnamespace terrain {\nclass TerrainRenderStage;\nclass RenderEntity;\n} // namespace terrain\n\nnamespace world {\nclass WorldRenderStage;\nclass RenderEntity;\n} // namespace world\n\n/**\n * Creates render entities that push animation updates from the\n * engine to the renderer. Also registers the render entities with\n * the corresponding render stages.\n */\nclass RenderFactory {\npublic:\n\t/**\n\t * Create a new factory for render entities.\n\t *\n\t * @param terrain_renderer Terrain renderer.\n\t * @param world_renderer World renderer.\n\t */\n\tRenderFactory(const std::shared_ptr<terrain::TerrainRenderStage> terrain_renderer,\n\t              const std::shared_ptr<world::WorldRenderStage> world_renderer);\n\t~RenderFactory() = default;\n\n\t/**\n\t * Create a new terrain render entity and register it at the terrain renderer.\n\t *\n\t * Render entities for terrain are associated with chunks, so a new render entity\n\t * will result in the creation of a new chunk in the renderer.\n\t *\n\t * Size/offset of the chunk in the game simulation should match size/offset\n\t * in the renderer.\n\t *\n\t * @return Render entity for pushing terrain updates.\n\t */\n\tstd::shared_ptr<terrain::RenderEntity> add_terrain_render_entity(const util::Vector2s chunk_size,\n\t                                                                 const coord::tile_delta chunk_offset);\n\n\t/**\n\t * Create a new world render entity and register it at the world renderer.\n\t *\n\t * @return Render entity for pushing terrain updates.\n\t */\n\tstd::shared_ptr<world::RenderEntity> add_world_render_entity();\n\nprivate:\n\t/**\n\t * Render stage for terrain drawing.\n\t */\n\tstd::shared_ptr<terrain::TerrainRenderStage> terrain_renderer;\n\n\t/**\n\t * Render stage for game entity drawing.\n\t */\n\tstd::shared_ptr<world::WorldRenderStage> world_renderer;\n};\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/render_pass.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_pass.h\"\n\n#include \"log/log.h\"\n\n\nnamespace openage::renderer {\n\nRenderPass::RenderPass(std::vector<Renderable> &&renderables,\n                       const std::shared_ptr<RenderTarget> &target) :\n\trenderables{},\n\ttarget{target},\n\tlayers{} {\n\t// Add a default layer with the lowest priority\n\tthis->add_layer(0, LAYER_PRIORITY_MAX);\n\n\t// Add the renderables to the pass\n\tthis->add_renderables(std::move(renderables));\n\n\tlog::log(MSG(dbg) << \"Created render pass\");\n}\n\nconst std::vector<std::vector<Renderable>> &RenderPass::get_renderables() const {\n\treturn this->renderables;\n}\n\nconst std::vector<Layer> &RenderPass::get_layers() const {\n\treturn this->layers;\n}\n\nconst std::shared_ptr<RenderTarget> &RenderPass::get_target() const {\n\treturn this->target;\n}\n\nvoid RenderPass::set_target(const std::shared_ptr<RenderTarget> &target) {\n\tthis->target = target;\n}\n\nvoid RenderPass::set_renderables(std::vector<Renderable> &&renderables) {\n\tthis->clear_renderables();\n\tthis->add_renderables(std::move(renderables));\n}\n\nvoid RenderPass::add_renderables(std::vector<Renderable> &&renderables, int64_t priority) {\n\tif (priority == LAYER_PRIORITY_MAX) {\n\t\t// Add the renderables to the last (default) layer\n\t\tthis->renderables.back().insert(this->renderables.back().end(),\n\t\t                                std::make_move_iterator(renderables.begin()),\n\t\t                                std::make_move_iterator(renderables.end()));\n\t\treturn;\n\t}\n\n\t// Index of the layer where the renderables will be inserted\n\tsize_t layer_index = 0;\n\n\t// Priority of the last observed layer\n\tint64_t current_priority = LAYER_PRIORITY_MAX;\n\n\t// Find the index in renderables to insert the renderables\n\tfor (size_t i = 0; i < this->layers.size(); i++) {\n\t\tauto &layer = this->layers.at(i);\n\t\tif (layer.priority > priority) {\n\t\t\t// Priority of the next layer is lower than the desired priority\n\t\t\t// Insert the renderables directly before this layer\n\t\t\tbreak;\n\t\t}\n\t\tcurrent_priority = layer.priority;\n\t\tlayer_index = i;\n\t}\n\n\tif (current_priority != priority) {\n\t\t// Lazily add a new layer with the desired priority\n\t\tthis->add_layer(layer_index, priority);\n\t}\n\n\tthis->renderables[layer_index].insert(this->renderables[layer_index].end(),\n\t                                      std::make_move_iterator(renderables.begin()),\n\t                                      std::make_move_iterator(renderables.end()));\n}\n\nvoid RenderPass::add_renderables(Renderable &&renderable, int64_t priority) {\n\tthis->add_renderables(std::vector<Renderable>{std::move(renderable)}, priority);\n}\n\nvoid RenderPass::add_layer(int64_t priority, bool clear_depth) {\n\tsize_t layer_index = 0;\n\tfor (const auto &layer : this->layers) {\n\t\tif (layer.priority > priority) {\n\t\t\tbreak;\n\t\t}\n\t\tlayer_index++;\n\t}\n\n\tthis->add_layer(layer_index, priority, clear_depth);\n}\n\nvoid RenderPass::add_layer(size_t index, int64_t priority, bool clear_depth) {\n\tthis->layers.insert(this->layers.begin() + index, Layer{priority, clear_depth});\n\tthis->renderables.insert(this->renderables.begin() + index, std::vector<Renderable>{});\n}\n\nvoid RenderPass::clear_renderables() {\n\t// Keep layer definitions, but reset the length of each layer to 0\n\tfor (size_t i = 0; i < this->layers.size(); i++) {\n\t\tthis->renderables[i].clear();\n\t}\n}\n\nvoid RenderPass::sort(const compare_func &compare) {\n\tfor (size_t i = 0; i < this->layers.size(); i++) {\n\t\tstd::stable_sort(this->renderables[i].begin(),\n\t\t                 this->renderables[i].end(),\n\t\t                 compare);\n\t}\n}\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/render_pass.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"renderer/definitions.h\"\n#include \"renderer/renderable.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass RenderTarget;\n\n/**\n * Defines a layer in the render pass. A layer is a slice of the renderables\n * that have the same priority. Each layer can have its own settings.\n *\n * // TODO: We could also move these settings to the render pass itself and use\n * //       multiple render passes to achieve the same effect. Then we might\n * //       not need layers at all.\n */\nstruct Layer {\n\t/// Priority of the renderables.\n\tint64_t priority;\n\t/// Whether to clear the depth buffer before rendering this layer.\n\tbool clear_depth = true;\n};\n\n/**\n * A render pass is a series of draw calls represented by renderables that output\n * into the given render target.\n */\nclass RenderPass {\npublic:\n\tvirtual ~RenderPass() = default;\n\n\t/**\n\t * Get the renderables of the render pass.\n\t *\n\t * @return Renderables of the render pass.\n\t */\n\tconst std::vector<std::vector<Renderable>> &get_renderables() const;\n\n\t/**\n\t * Get the layers of the render pass.\n\t *\n\t * @return Layers of the render pass.\n\t */\n\tconst std::vector<Layer> &get_layers() const;\n\n\t/**\n\t * Set the render target to write to.\n\t *\n\t * @param target Render target.\n\t */\n\tvoid set_target(const std::shared_ptr<RenderTarget> &target);\n\n\t/**\n\t * Get the render target of the render pass.\n\t *\n\t * @return Render target.\n\t */\n\tconst std::shared_ptr<RenderTarget> &get_target() const;\n\n\t/**\n\t * Replace the current renderables with the given list of renderables.\n\t *\n\t * @param renderables New renderables.\n\t */\n\tvoid set_renderables(std::vector<Renderable> &&renderables);\n\n\t/**\n\t * Append renderables to the render pass with a given priority.\n\t *\n\t * @param renderables New renderables.\n\t * @param priority Priority of the renderables. Layers with higher priority are drawn later.\n\t */\n\tvoid add_renderables(std::vector<Renderable> &&renderables,\n\t                     int64_t priority = LAYER_PRIORITY_MAX);\n\n\t/**\n\t * Append a single renderable to the render pass with a given priority.\n\t *\n\t * @param renderable New renderable.\n\t * @param priority Priority of the renderable. Layers with higher priority are drawn later.\n\t */\n\tvoid add_renderables(Renderable &&renderable,\n\t                     int64_t priority = LAYER_PRIORITY_MAX);\n\n\t/**\n\t * Add a new layer to the render pass.\n\t *\n\t * @param priority Priority of the layer. Layers with higher priority are drawn first.\n\t * @param clear_depth If true clears the depth buffer before rendering this layer.\n\t */\n\tvoid add_layer(int64_t priority, bool clear_depth = true);\n\n\t/**\n\t * Clear the list of renderables\n\t */\n\tvoid clear_renderables();\n\n\tusing compare_func = std::function<bool(const Renderable &, const Renderable &)>;\n\n\t/**\n\t * Sort the renderables using the given comparison function.\n\t *\n\t * Layers are sorted individually, so the order of layers is not changed.\n\t *\n\t * @param compare Comparison function.\n\t */\n\tvoid sort(const compare_func &compare);\n\nprotected:\n\t/**\n\t * Create a new RenderPass. This is called from Renderer::add_render_pass,\n\t * which then creates the proper subclass of RenderPass, depending on the backend.\n\t *\n\t * @param renderables The renderables to draw.\n\t * @param target Render target to write to.\n\t */\n\tRenderPass(std::vector<Renderable> &&renderables, const std::shared_ptr<RenderTarget> &target);\n\n\t/**\n\t * The renderables to draw.\n\t *\n\t * Kept sorted by layer priorities (lowest to highest priority).\n\t */\n\tstd::vector<std::vector<Renderable>> renderables;\n\nprivate:\n\t/**\n\t * Add a new layer to the render pass at the given index.\n\t *\n\t * @param index Index in \\p layers member to insert the new layer.\n\t * @param priority Priority of the layer. Layers with higher priority are drawn first.\n\t * @param clear_depth If true clears the depth buffer before rendering this layer.\n\t */\n\tvoid add_layer(size_t index, int64_t priority, bool clear_depth = true);\n\n\t/**\n\t * Render target to write to.\n\t */\n\tstd::shared_ptr<RenderTarget> target;\n\n\t/**\n\t * Stores the layers of the render pass.\n\t *\n\t * Layers are slices of the renderables that have the same priority.\n\t * They can assign different settings to the renderables in the slice.\n\t *\n\t * Sorted from lowest to highest priority.\n\t */\n\tstd::vector<Layer> layers;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/render_target.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_target.h\"\n\n\nnamespace openage::renderer {\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/render_target.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n\nnamespace openage {\nnamespace renderer {\nclass Texture2d;\n\nnamespace resources {\nclass Texture2dData;\n} // namespace resources\n\n/// The abstract base for a render target.\nclass RenderTarget : public std::enable_shared_from_this<RenderTarget> {\nprotected:\n\tRenderTarget() = default;\n\npublic:\n\tvirtual ~RenderTarget() = default;\n\n\t/**\n\t * Get an image from the pixels in the render target's framebuffer.\n\t *\n\t * This should only be called _after_ rendering to the framebuffer has finished.\n\t *\n\t * @return RGBA texture data.\n\t */\n\tvirtual resources::Texture2dData into_data() = 0;\n\n\tvirtual std::vector<std::shared_ptr<Texture2d>> get_texture_targets() = 0;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/renderable.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"renderable.h\"\n\n\nnamespace openage::renderer {\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/renderable.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n\nnamespace openage {\nnamespace renderer {\nclass Geometry;\nclass UniformInput;\n\n/**\n * A renderable is a set of a shader UniformInput and a possible draw call.\n * Usually it is one \"step\" in a RenderPass.\n *\n * The UniformInput only stores the values the CPU at first. When the renderer\n * \"executes\" the Renderable in a pass, the UniformInput values are uploaded to\n * the shader on the GPU they were created with.\n *\n * If the geometry is nullptr, the uniform values are uploaded to the shader,\n * but no draw call is performed. This can be used to, for example, first set\n * the values of uniforms that many objects have in common, and then only\n * upload the uniforms that vary between them in each draw call. This works\n * because uniform values in any given shader are preserved across a render\n * pass.\n *\n * If geometry is set (i.e. it is not nullptr), the renderer draws the geometry\n * with the shader and other settings in the renderable. The result is written\n * into the render target, defined in the RenderPass.\n */\nstruct Renderable {\n\t/// Uniform values to be set in the appropriate shader. Contains a reference\n\t/// to the correct shader, and this is the shader that will be used for\n\t/// drawing if geometry is present.\n\tstd::shared_ptr<UniformInput> uniform;\n\t/// The geometry. It can be a simple primitive or a complex mesh.\n\t/// Can be nullptr to only set uniforms but do not perform draw call.\n\tstd::shared_ptr<Geometry> geometry;\n\t/// Whether to perform alpha-based color blending with whatever was in the\n\t/// render target before.\n\tbool alpha_blending = true;\n\t/// Whether to perform depth testing and discard occluded fragments.\n\tbool depth_test = true;\n};\n\n/**\n * Simplified form of Renderable, which is just an update for a shader.\n * When the ShaderUpdate is processed in a RenderPass,\n * the new uniform values (set on the CPU first with unif_in.update(...))\n * are uploaded to the GPU.\n */\nstruct ShaderUpdate : Renderable {\n\tShaderUpdate(std::shared_ptr<UniformInput> const &uniform) :\n\t\tRenderable{uniform, nullptr} {}\n\n\tShaderUpdate(std::shared_ptr<UniformInput> &&uniform) :\n\t\tRenderable{std::move(uniform), nullptr} {}\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/renderer.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"renderer.h\"\n\nnamespace openage::renderer {\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/renderer.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <utility>\n#include <vector>\n\n#include \"renderer/renderable.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nnamespace resources {\nclass Texture2dData;\nclass Texture2dInfo;\nclass ShaderSource;\nclass MeshData;\nclass UniformBufferInfo;\n} // namespace resources\n\nclass ShaderProgram;\nclass Geometry;\nclass RenderPass;\nclass RenderTarget;\nclass Texture2d;\nclass UniformBuffer;\nclass UniformInput;\n\n\n/// The renderer. This class is used for performing all graphics operations. It is abstract and has implementations\n/// for various low-level graphics APIs like OpenGL.\nclass Renderer {\npublic:\n\tvirtual ~Renderer() = default;\n\n\t/// Uploads the given texture data (usually loaded from disk) to graphics hardware and makes it available\n\t/// as a Texture object that can be used for various operations.\n\tvirtual std::shared_ptr<Texture2d> add_texture(resources::Texture2dData const &) = 0;\n\n\t/// Creates a new empty texture with the given parameters on the graphics hardware.\n\tvirtual std::shared_ptr<Texture2d> add_texture(resources::Texture2dInfo const &) = 0;\n\n\t/// Compiles the given shader source code into a shader program. A shader program is the main tool used\n\t/// for graphics rendering.\n\tvirtual std::shared_ptr<ShaderProgram> add_shader(std::vector<resources::ShaderSource> const &) = 0;\n\n\t/// Creates a Geometry object from the given mesh data, uploading it to the GPU by creating appropriate buffer.\n\t/// The vertex attributes will be passed to the shader as described in the mesh data.\n\tvirtual std::shared_ptr<Geometry> add_mesh_geometry(resources::MeshData const &) = 0;\n\n\t/// Adds a Geometry object that passes a simple 4-vertex drawing command with no vertex attributes to the shader.\n\t/// Useful for generating positions in the vertex shader.\n\tvirtual std::shared_ptr<Geometry> add_bufferless_quad() = 0;\n\n\t/// Creates a render pass object from the given list of renderables that output to the given render target\n\tvirtual std::shared_ptr<RenderPass> add_render_pass(std::vector<Renderable>,\n\t                                                    const std::shared_ptr<RenderTarget> &) = 0;\n\n\t/// Constructs a render target from the given textures. All subsequent drawing operations pointed at this\n\t/// target will write to these textures. Textures are attached to the target in the order in which they\n\t/// appear within the vector. Depth textures are attached as depth components. Textures of every other\n\t/// type are attached as color components.\n\tvirtual std::shared_ptr<RenderTarget> create_texture_target(std::vector<std::shared_ptr<Texture2d>> const &) = 0;\n\n\t/// Returns the built-in display target that represents the window. Passes pointed at this target will have\n\t/// their output visible on the screen.\n\tvirtual std::shared_ptr<RenderTarget> get_display_target() = 0;\n\n\t/// Creates a new uniform buffer. Uniform buffers to set uniforms across shaders invocations.\n\tvirtual std::shared_ptr<UniformBuffer> add_uniform_buffer(resources::UniformBufferInfo const &) = 0;\n\n\t/// Creates a new uniform buffer from a uniform block in a shader.\n\tvirtual std::shared_ptr<UniformBuffer> add_uniform_buffer(std::shared_ptr<ShaderProgram> const &,\n\t                                                          std::string const &) = 0;\n\n\t/// Stores the display framebuffer into a CPU-accessible data object. Essentially, this takes a screenshot.\n\tvirtual resources::Texture2dData display_into_data() = 0;\n\n\t/// Runs error checking code on the current renderer state.\n\tvirtual void check_error() = 0;\n\n\t/// Executes a render pass.\n\t/// A renderer implementation might modify the RenderPass because of optimizations.\n\tvirtual void render(const std::shared_ptr<RenderPass> &) = 0;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/CMakeLists.txt",
    "content": "add_sources(libopenage\n    buffer_info.cpp\n    frame_timing.cpp\n\tmesh_data.cpp\n\tpalette_info.cpp\n\tshader_source.cpp\n    shader_template.cpp\n\ttexture_data.cpp\n\ttexture_info.cpp\n\ttexture_subinfo.cpp\n)\n\nadd_subdirectory(animation/)\nadd_subdirectory(assets/)\nadd_subdirectory(parser/)\nadd_subdirectory(terrain/)\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tangle_info.cpp\n\tanimation_info.cpp\n\tframe_info.cpp\n\tlayer_info.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/angle_info.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"angle_info.h\"\n\nnamespace openage::renderer::resources {\n\nAngleInfo::AngleInfo(const float angle_start,\n                     std::vector<std::shared_ptr<FrameInfo>> &frames,\n                     std::shared_ptr<AngleInfo> mirror_from,\n                     flip_type mirror_type) :\n\tangle_start{angle_start},\n\tframes{frames},\n\tmirror_from{mirror_from},\n\tmirror_type{mirror_type} {}\n\nfloat AngleInfo::get_angle_start() const {\n\treturn this->angle_start;\n}\n\nbool AngleInfo::is_mirrored() const {\n\treturn this->mirror_from != nullptr;\n}\n\nconst flip_type &AngleInfo::get_mirror_type() {\n\treturn this->mirror_type;\n}\n\nconst std::shared_ptr<AngleInfo> &AngleInfo::get_mirrored_angle() const {\n\treturn this->mirror_from;\n}\n\nsize_t AngleInfo::get_frame_count() const {\n\treturn this->frames.size();\n}\n\nconst std::shared_ptr<FrameInfo> &AngleInfo::get_frame(size_t idx) const {\n\tif (!(this->mirror_from == nullptr)) {\n\t\treturn this->mirror_from->get_frame(idx);\n\t}\n\treturn this->frames.at(idx);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/angle_info.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\nnamespace openage::renderer::resources {\n\nclass FrameInfo;\n\n/**\n * Describe whether/how the frame is mirrored\n */\nenum class flip_type {\n\tNONE,   // do not flip\n\tFLIP_X, // flip across x axis\n\tFLIP_Y  // flip across y axis\n};\n\n/**\n * Contains information about a 2D animation angle. It defines the\n * angle in degrees and frame information about the frames displayed\n * at this angle.\n */\nclass AngleInfo {\npublic:\n\t/**\n\t * Create a 2D Angle Info.\n\t *\n\t * @param angle_start Rotation in degress at which the frames are displayed.\n\t * @param frames Frame information.\n\t * @param mirror_from Mirror frames from another angle instead of using uniquely\n\t *                    defined frames.\n\t */\n\tAngleInfo(const float angle_start,\n\t          std::vector<std::shared_ptr<FrameInfo>> &frames,\n\t          std::shared_ptr<AngleInfo> mirror_from = nullptr,\n\t          flip_type mirror_type = flip_type::NONE);\n\n\tAngleInfo() = default;\n\t~AngleInfo() = default;\n\n\t/**\n\t * Get the rotation in degrees at which the angle starts.\n\t *\n\t * @return A float containing the starting rotation in degrees.\n\t */\n\tfloat get_angle_start() const;\n\n\t/**\n\t * Check if the angle is mirrored from another angle.\n\t *\n\t * @return true if a mirrored angle has been defined, else false.\n\t */\n\tbool is_mirrored() const;\n\n\t/**\n\t * Get the mirror direction of the angle.\n\t *\n\t * @return const flip_type&\n\t */\n\tconst flip_type &get_mirror_type();\n\n\t/**\n\t * Get the mirrored angle information.\n\t *\n\t * @return An angle information object.\n\t */\n\tconst std::shared_ptr<AngleInfo> &get_mirrored_angle() const;\n\n\t/**\n\t * Get number of frames for the angle.\n\t *\n\t * @return Number of frames.\n\t */\n\tsize_t get_frame_count() const;\n\n\t/**\n\t * Get the frame information of the frame with the specified index.\n\t *\n\t * @param idx Index of the frame.\n\t *\n\t * @return A frame information object.\n\t */\n\tconst std::shared_ptr<FrameInfo> &get_frame(size_t idx) const;\n\nprivate:\n\t/**\n\t * Starting rotation in degress at which the frames are displayed.\n\t */\n\tfloat angle_start;\n\n\t/**\n\t * Frame information.\n\t */\n\tstd::vector<std::shared_ptr<FrameInfo>> frames;\n\n\t/**\n\t * Mirrored angle information.\n\t */\n\tstd::shared_ptr<AngleInfo> mirror_from = nullptr;\n\n\t/**\n\t * How the frame should be displayed if its mirrored.\n\t */\n\tflip_type mirror_type = flip_type::NONE;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/animation_info.cpp",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#include \"animation_info.h\"\n\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/texture_info.h\"\n\n\nnamespace openage::renderer::resources {\n\nAnimation2dInfo::Animation2dInfo(const float scalefactor,\n                                 std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n                                 std::vector<LayerInfo> &layers) :\n\tscalefactor{scalefactor},\n\ttexture_infos{textures},\n\tlayers{layers},\n\tmax_bounds{} {\n\t// Calculate max bounds\n\tfor (auto &layer : this->layers) {\n\t\tfor (size_t i = 0; i < layer.get_angle_count(); ++i) {\n\t\t\tauto &angle = layer.get_angle(i);\n\t\t\tfor (size_t j = 0; j < angle->get_frame_count(); ++j) {\n\t\t\t\tauto &frame = angle->get_frame(j);\n\t\t\t\tauto tex_idx = frame->get_texture_idx();\n\t\t\t\tauto subtex_idx = frame->get_subtexture_idx();\n\n\t\t\t\tauto &tex = this->texture_infos.at(tex_idx);\n\t\t\t\tauto &subtex = tex->get_subtex_info(subtex_idx);\n\n\t\t\t\tauto subtex_size = subtex.get_size();\n\t\t\t\tauto anchor_pos = subtex.get_anchor_pos();\n\n\t\t\t\tint left_margin = anchor_pos.x();\n\t\t\t\tint right_margin = subtex_size.x() - anchor_pos.x();\n\t\t\t\tint top_margin = anchor_pos.y();\n\t\t\t\tint bottom_margin = subtex_size.y() - anchor_pos.y();\n\n\t\t\t\tthis->max_bounds[0] = std::max(this->max_bounds[0], left_margin);\n\t\t\t\tthis->max_bounds[1] = std::max(this->max_bounds[1], right_margin);\n\t\t\t\tthis->max_bounds[2] = std::max(this->max_bounds[2], top_margin);\n\t\t\t\tthis->max_bounds[3] = std::max(this->max_bounds[3], bottom_margin);\n\t\t\t}\n\t\t}\n\t}\n}\n\nfloat Animation2dInfo::get_scalefactor() const {\n\treturn this->scalefactor;\n}\n\nsize_t Animation2dInfo::get_texture_count() const {\n\treturn this->texture_infos.size();\n}\n\nconst std::shared_ptr<Texture2dInfo> &Animation2dInfo::get_texture(size_t idx) const {\n\treturn this->texture_infos.at(idx);\n}\n\nsize_t Animation2dInfo::get_layer_count() const {\n\treturn this->layers.size();\n}\n\nconst LayerInfo &Animation2dInfo::get_layer(size_t idx) const {\n\treturn this->layers.at(idx);\n}\n\nconst util::Vector4i &Animation2dInfo::get_max_bounds() const {\n\treturn this->max_bounds;\n}\n\nconst util::Vector2s Animation2dInfo::get_max_size() const {\n\treturn {this->max_bounds[0] + this->max_bounds[1],\n\t        this->max_bounds[2] + this->max_bounds[3]};\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/animation_info.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"renderer/resources/animation/layer_info.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::resources {\nclass Texture2dInfo;\nclass Texture2dSubInfo;\n\n/**\n * Contains information about a 2D animation. The animation data can be\n * stored in several spritesheet textures, although it is usually only one.\n *\n * Animations are composed of layers which are animated independently from\n * each other. Layers each contain angle definitions which contain the frame\n * information.\n */\nclass Animation2dInfo {\npublic:\n\t/**\n\t * Create a 2D Animation Info.\n\t *\n\t * @param scalefactor Factor by which sprite images are scaled down at default zoom level.\n\t *                    Applies to all layers. Choosing a factor that is power of 2 is\n\t *                    recommended.\n\t * @param textures Information object of textures used by the animation.\n\t * @param layers Layer information.\n\t */\n\tAnimation2dInfo(const float scalefactor,\n\t                std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n\t                std::vector<LayerInfo> &layers);\n\n\tAnimation2dInfo() = default;\n\t~Animation2dInfo() = default;\n\n\t/**\n\t * Get the scaling factor of the animation.\n\t *\n\t * @return A float containing the scaling factor.\n\t */\n\tfloat get_scalefactor() const;\n\n\t/**\n\t * Get number of textures referenced by the animation.\n\t *\n\t * @return Number of textures.\n\t */\n\tsize_t get_texture_count() const;\n\n\t/**\n\t * Get the 2D texture information of the texture with the specified index.\n\t *\n\t * @param idx Index of the texture.\n\t *\n\t * @return A texture information object containing the metadata of the\n\t *         referenced texture.\n\t */\n\tconst std::shared_ptr<Texture2dInfo> &get_texture(size_t idx) const;\n\n\t/**\n\t * Get number of layers in the animation.\n\t *\n\t * @return Number of layers.\n\t */\n\tsize_t get_layer_count() const;\n\n\t/**\n\t * Get the layer information of the layer with the specified index.\n\t *\n\t * @param idx Index of the layer.\n\t *\n\t * @return A layer information object.\n\t */\n\tconst LayerInfo &get_layer(size_t idx) const;\n\n\t/**\n\t * Get the maximum boundaries for all frames in the animation.\n\t *\n\t * This represents the maximum distance from the anchor point to the\n\t * left, right, top, bottom edges of the frames (in pixels).\n\t *\n\t * @return Boundaries of the animation (in pixels): [left, right, top, bottom]\n\t */\n\tconst util::Vector4i &get_max_bounds() const;\n\n\t/**\n\t * Get the maximum size for all frames in the animation.\n\t *\n\t * This represents the maximum width and height of the frames (in pixels).\n\t *\n\t * @return Size of the animation (in pixels): [width, height]\n\t */\n\tconst util::Vector2s get_max_size() const;\n\nprivate:\n\t/**\n\t * Scaling factor of the animation across all layers at default zoom level.\n\t */\n\tfloat scalefactor;\n\n\t/**\n\t * Information about textures used by the animation.\n\t */\n\tstd::vector<std::shared_ptr<Texture2dInfo>> texture_infos;\n\n\t/**\n\t * Layer information.\n\t */\n\tstd::vector<LayerInfo> layers;\n\n\t/**\n\t * Maximum boundaries for all frames in the animation.\n\t *\n\t * This represents the maximum distance from the anchor point to the\n\t * left, right, top, bottom edges of the frames (in pixels).\n\t */\n\tutil::Vector4i max_bounds;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/frame_info.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"frame_info.h\"\n\nnamespace openage::renderer::resources {\n\nFrameInfo::FrameInfo(const size_t texture_idx,\n                     const size_t subtexture_idx) :\n\ttexture_idx{texture_idx},\n\tsubtexture_idx{subtexture_idx} {}\n\nsize_t FrameInfo::get_texture_idx() const {\n\treturn this->texture_idx;\n}\n\nsize_t FrameInfo::get_subtexture_idx() const {\n\treturn this->subtexture_idx;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/frame_info.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n\nnamespace openage::renderer::resources {\n\n/**\n * Contains information about a 2D animation frame. The frame image data\n * is stored in a 2D texture object.\n */\nclass FrameInfo {\npublic:\n\t/**\n\t * Create a 2D Frame Info.\n\t *\n\t * @param texture_idx Index of the texture containing the frame in the\n\t *                    Animation2dInfo object.\n\t * @param subtexture_idx Index of the subtexture containing the frame\n\t *                    in the Texture2dInfo object.\n\t */\n\tFrameInfo(const size_t texture_idx,\n\t          const size_t subtexture_idx);\n\n\tFrameInfo() = default;\n\t~FrameInfo() = default;\n\n\t/**\n\t * Get the texture index of the frame in the animation.\n\t *\n\t * @return Index of a texture in a Animation2dInfo object.\n\t */\n\tsize_t get_texture_idx() const;\n\n\t/**\n\t * Get the subtexture index of the frame in the texture.\n\t *\n\t * @return Index of a subtexture in a Texture2dInfo object.\n\t */\n\tsize_t get_subtexture_idx() const;\n\nprivate:\n\t/**\n\t * Index of the texture containing the frame in the Animation2dInfo object.\n\t */\n\tsize_t texture_idx;\n\n\t/**\n\t * Index of the subtexture containing the frame in the Texture2dInfo object.\n\t */\n\tsize_t subtexture_idx;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/layer_info.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"layer_info.h\"\n\n#include <algorithm>\n#include <utility>\n\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/frame_timing.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::renderer::resources {\n\nLayerInfo::LayerInfo(std::vector<std::shared_ptr<AngleInfo>> &angles,\n                     const display_mode mode,\n                     const size_t position,\n                     const float time_per_frame,\n                     const float replay_delay) :\n\tmode{mode},\n\tposition{position},\n\ttime_per_frame{time_per_frame},\n\treplay_delay{replay_delay},\n\tangles{angles},\n\tframe_timing{nullptr} {\n\tif (this->angles.size() > 0) {\n\t\t// set frame timings by calculating when they appear in the animation sequence\n\t\tauto frame_count = this->angles[0]->get_frame_count();\n\t\ttime::time_t t = 0;\n\t\tstd::vector<time::time_t> keyframes;\n\t\tfor (size_t i = 0; i < frame_count; ++i) {\n\t\t\tkeyframes.push_back(t);\n\t\t\tt += this->time_per_frame;\n\t\t}\n\t\tauto total_time = (frame_count - 1) * this->time_per_frame + this->replay_delay;\n\t\tthis->frame_timing = std::make_shared<FrameTiming>(total_time, std::move(keyframes));\n\t}\n}\n\ndisplay_mode LayerInfo::get_display_mode() const {\n\treturn this->mode;\n}\n\nsize_t LayerInfo::get_position() const {\n\treturn this->position;\n}\n\nfloat LayerInfo::get_time_per_frame() const {\n\treturn this->time_per_frame;\n}\n\nfloat LayerInfo::get_replay_delay() const {\n\treturn this->replay_delay;\n}\n\nsize_t LayerInfo::get_angle_count() const {\n\treturn this->angles.size();\n}\n\nconst std::shared_ptr<AngleInfo> &LayerInfo::get_angle(size_t idx) const {\n\treturn this->angles.at(idx);\n}\n\nconst std::shared_ptr<AngleInfo> &LayerInfo::get_direction_angle(float direction) const {\n\tif (this->angles.size() == 1) {\n\t\t// angle covers all directions\n\t\treturn this->get_angle(0);\n\t}\n\n\t// clamp to possible degrees values\n\tdirection = std::clamp(direction, 0.0f, 360.0f);\n\n\t// special case for front facing angle because values wrap\n\t// around at 360 degrees, e.g. 337,5 to 22,5\n\tif (direction > this->angles.at(0)->get_angle_start()\n\t    or direction < this->angles.at(1)->get_angle_start()) {\n\t\treturn this->get_angle(0);\n\t}\n\n\t// check if angles with index = angles.size() - 2 match\n\t// since they are ordered, we only need one comparison\n\t// (check if the NEXT angle start is larger than the supplied\n\t// direction)\n\tfor (size_t i = 1; i < this->angles.size(); ++i) {\n\t\tif (direction < this->angles.at(i)->get_angle_start()) {\n\t\t\treturn this->get_angle(i - 1);\n\t\t}\n\t}\n\n\t// last angle in the list is the only one remaining that could match\n\treturn this->get_angle(this->angles.size() - 1);\n}\n\nconst std::shared_ptr<FrameTiming> &LayerInfo::get_frame_timing() const {\n\treturn this->frame_timing;\n}\n\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/animation/layer_info.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\nnamespace openage::renderer::resources {\n\nclass AngleInfo;\nclass FrameTiming;\n\nenum class display_mode {\n\tOFF,  // Only show first frame\n\tONCE, // Play loop once\n\tLOOP  // Loop indefinitely\n};\n\n/**\n * Contains information about a 2D animation layer. The layer defines the display\n * mode, overall position and timing of frames. It also contains angle\n * definitions for choosing frames based on a game object's rotation.\n */\nclass LayerInfo {\npublic:\n\t/**\n\t * Create a 2D Layer Info.\n\t *\n\t * @param mode Determines how often the layer loops its frames.\n\t * @param position Absolute position of the layer on the screen.\n\t * @param time_per_frame Time that each frame is displayed in seconds.\n\t * @param replay_delay Additional time (in seconds) to display the last frame in a loop.\n\t * @param angles Angle information. Angles should be ordered clockwise, starting with the front-facing angle.\n\t */\n\tLayerInfo(std::vector<std::shared_ptr<AngleInfo>> &angles,\n\t          const display_mode mode = display_mode::OFF,\n\t          const size_t position = 0,\n\t          const float time_per_frame = 0.0,\n\t          const float replay_delay = 0.0);\n\n\t~LayerInfo() = default;\n\n\t/**\n\t * Get the display mode of the layer.\n\t *\n\t * @return A display mode setting.\n\t */\n\tdisplay_mode get_display_mode() const;\n\n\t/**\n\t * Get the absolute position of the layer.\n\t *\n\t * @return Position of the layer.\n\t */\n\tsize_t get_position() const;\n\n\t/**\n\t * Get the time that each frame is displayed.\n\t *\n\t * @return Time each frame is displayed in seconds.\n\t */\n\tfloat get_time_per_frame() const;\n\n\t/**\n\t * Get the additional display tme for the last frame in the loop.\n\t *\n\t * @return Additional time to display the last frame in seconds.\n\t */\n\tfloat get_replay_delay() const;\n\n\t/**\n\t * Get number of angles in the layer.\n\t *\n\t * @return Number of angles.\n\t */\n\tsize_t get_angle_count() const;\n\n\t/**\n\t * Get the angle information of the angle with the specified index.\n\t *\n\t * @param idx Index of the angle.\n\t *\n\t * @return An angle information object.\n\t */\n\tconst std::shared_ptr<AngleInfo> &get_angle(size_t idx) const;\n\n\t/**\n\t * Get the angle information of the angle matching the specified direction.\n\t *\n\t * @param direction Direction in degrees.\n\t *\n\t * @return An angle information object.\n\t */\n\tconst std::shared_ptr<AngleInfo> &get_direction_angle(float direction) const;\n\n\t/**\n\t * Get the frame timing information of this layer.\n\t *\n\t * @return Frame timing information.\n\t */\n\tconst std::shared_ptr<FrameTiming> &get_frame_timing() const;\n\nprivate:\n\t/**\n\t * Display mode of the contained frames.\n\t */\n\tdisplay_mode mode;\n\n\t/**\n\t * Absolute position of the layer.\n\t */\n\tsize_t position;\n\n\t/**\n\t * Time each frame is displayed (in seconds).\n\t */\n\tfloat time_per_frame;\n\n\t/**\n\t * Time the last frame in a loop is additionally displayed (in seconds).\n\t */\n\tfloat replay_delay;\n\n\t/**\n\t * Angle information.\n\t */\n\tstd::vector<std::shared_ptr<AngleInfo>> angles;\n\n\t/**\n\t * Frame timing in the animated sequence.\n\t */\n\tstd::shared_ptr<FrameTiming> frame_timing;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tasset_manager.cpp\n\tcache.cpp\n\ttexture_manager.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/asset_manager.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"asset_manager.h\"\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/animation/animation_info.h\"\n#include \"renderer/resources/assets/cache.h\"\n#include \"renderer/resources/assets/texture_manager.h\"\n#include \"renderer/resources/palette_info.h\"\n#include \"renderer/resources/parser/parse_blendmask.h\"\n#include \"renderer/resources/parser/parse_blendtable.h\"\n#include \"renderer/resources/parser/parse_palette.h\"\n#include \"renderer/resources/parser/parse_sprite.h\"\n#include \"renderer/resources/parser/parse_terrain.h\"\n#include \"renderer/resources/parser/parse_texture.h\"\n#include \"renderer/resources/terrain/blendpattern_info.h\"\n#include \"renderer/resources/terrain/blendtable_info.h\"\n#include \"renderer/resources/terrain/terrain_info.h\"\n#include \"renderer/resources/texture_info.h\"\n\n\nnamespace openage::renderer::resources {\n\nAssetManager::AssetManager(const std::shared_ptr<Renderer> &renderer,\n                           const util::Path &asset_base_dir) :\n\trenderer{renderer},\n\tcache{std::make_shared<AssetCache>()},\n\ttexture_manager{std::make_shared<TextureManager>(renderer)},\n\tasset_base_dir{asset_base_dir} {\n\tlog::log(INFO << \"Created asset manager\");\n}\n\nconst std::shared_ptr<Animation2dInfo> &AssetManager::request_animation(const util::Path &path) {\n\tstd::shared_ptr<Animation2dInfo> info;\n\ttry {\n\t\tif (not this->cache->check_animation_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<Animation2dInfo>(parser::parse_sprite_file(path, this->cache));\n\t\t\tthis->cache->add_animation(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_animation) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load animation file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_animation).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_animation(path);\n}\n\nconst std::shared_ptr<BlendPatternInfo> &AssetManager::request_blpattern(const util::Path &path) {\n\tstd::shared_ptr<BlendPatternInfo> info;\n\ttry {\n\t\tif (not this->cache->check_blpattern_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<BlendPatternInfo>(parser::parse_blendmask_file(path, this->cache));\n\t\t\tthis->cache->add_blpattern(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_blpattern) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load blend pattern file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_blpattern).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_blpattern(path);\n}\n\nconst std::shared_ptr<BlendTableInfo> &AssetManager::request_bltable(const util::Path &path) {\n\tstd::shared_ptr<BlendTableInfo> info;\n\ttry {\n\t\tif (not this->cache->check_bltable_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<BlendTableInfo>(parser::parse_blendtable_file(path, this->cache));\n\t\t\tthis->cache->add_bltable(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_bltable) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load blend table file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_bltable).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_bltable(path);\n}\n\nconst std::shared_ptr<PaletteInfo> &AssetManager::request_palette(const util::Path &path) {\n\tstd::shared_ptr<PaletteInfo> info;\n\ttry {\n\t\tif (not this->cache->check_palette_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<PaletteInfo>(parser::parse_palette_file(path));\n\t\t\tthis->cache->add_palette(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_palette) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load palette file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_palette).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_palette(path);\n}\n\nconst std::shared_ptr<TerrainInfo> &AssetManager::request_terrain(const util::Path &path) {\n\tstd::shared_ptr<TerrainInfo> info;\n\ttry {\n\t\tif (not this->cache->check_terrain_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<TerrainInfo>(parser::parse_terrain_file(path, this->cache));\n\t\t\tthis->cache->add_terrain(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_terrain) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load terrain file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_terrain).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_terrain(path);\n}\n\nconst std::shared_ptr<Texture2dInfo> &AssetManager::request_texture(const util::Path &path) {\n\tstd::shared_ptr<Texture2dInfo> info;\n\ttry {\n\t\tif (not this->cache->check_texture_cache(path)) {\n\t\t\t// create if not loaded\n\t\t\tinfo = std::make_shared<Texture2dInfo>(parser::parse_texture_file(path));\n\t\t\tthis->cache->add_texture(path, info);\n\t\t}\n\t}\n\tcatch (const Error &err) {\n\t\tif (this->placeholder_texture) {\n\t\t\tlog::log(MSG(warn) << \"Failed to load texture file from: \" << path\n\t\t\t                   << \" - using placeholder instead.\");\n\t\t\treturn (*this->placeholder_texture).second;\n\t\t}\n\t\telse {\n\t\t\tthrow err;\n\t\t}\n\t}\n\treturn this->cache->get_texture(path);\n}\n\nconst std::shared_ptr<Animation2dInfo> &AssetManager::request_animation(const std::string &rel_path) {\n\treturn this->request_animation(this->asset_base_dir / rel_path);\n}\n\nconst std::shared_ptr<BlendPatternInfo> &AssetManager::request_blpattern(const std::string &rel_path) {\n\treturn this->request_blpattern(this->asset_base_dir / rel_path);\n}\n\nconst std::shared_ptr<BlendTableInfo> &AssetManager::request_bltable(const std::string &rel_path) {\n\treturn this->request_bltable(this->asset_base_dir / rel_path);\n}\n\nconst std::shared_ptr<PaletteInfo> &AssetManager::request_palette(const std::string &rel_path) {\n\treturn this->request_palette(this->asset_base_dir / rel_path);\n}\n\nconst std::shared_ptr<TerrainInfo> &AssetManager::request_terrain(const std::string &rel_path) {\n\treturn this->request_terrain(this->asset_base_dir / rel_path);\n}\n\nconst std::shared_ptr<Texture2dInfo> &AssetManager::request_texture(const std::string &rel_path) {\n\treturn this->request_texture(this->asset_base_dir / rel_path);\n}\n\nvoid AssetManager::set_placeholder_animation(const util::Path &path) {\n\tthis->placeholder_animation = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<Animation2dInfo>(\n\t\t\tparser::parse_sprite_file(path, this->cache)));\n}\n\nvoid AssetManager::set_placeholder_blpattern(const util::Path &path) {\n\tthis->placeholder_blpattern = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<BlendPatternInfo>(\n\t\t\tparser::parse_blendmask_file(path, this->cache)));\n}\n\nvoid AssetManager::set_placeholder_bltable(const util::Path &path) {\n\tthis->placeholder_bltable = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<BlendTableInfo>(\n\t\t\tparser::parse_blendtable_file(path, this->cache)));\n}\n\nvoid AssetManager::set_placeholder_palette(const util::Path &path) {\n\tthis->placeholder_palette = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<PaletteInfo>(\n\t\t\tparser::parse_palette_file(path)));\n}\n\nvoid AssetManager::set_placeholder_terrain(const util::Path &path) {\n\tthis->placeholder_terrain = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<TerrainInfo>(\n\t\t\tparser::parse_terrain_file(path, this->cache)));\n}\n\nvoid AssetManager::set_placeholder_texture(const util::Path &path) {\n\tthis->placeholder_texture = std::make_pair(\n\t\tpath,\n\t\tstd::make_shared<Texture2dInfo>(\n\t\t\tparser::parse_texture_file(path)));\n}\n\nconst AssetManager::placeholder_anim_t &AssetManager::get_placeholder_animation() {\n\treturn this->placeholder_animation;\n}\n\nconst AssetManager::placeholder_blpattern_t &AssetManager::get_placeholder_blpattern() {\n\treturn this->placeholder_blpattern;\n}\n\nconst AssetManager::placeholder_bltable_t &AssetManager::get_placeholder_bltable() {\n\treturn this->placeholder_bltable;\n}\n\nconst AssetManager::placeholder_palette_t &AssetManager::get_placeholder_palette() {\n\treturn this->placeholder_palette;\n}\n\nconst AssetManager::placeholder_terrain_t &AssetManager::get_placeholder_terrain() {\n\treturn this->placeholder_terrain;\n}\n\nconst AssetManager::placeholder_texture_t &AssetManager::get_placeholder_texture() {\n\treturn this->placeholder_texture;\n}\n\nconst std::shared_ptr<TextureManager> &AssetManager::get_texture_manager() {\n\treturn this->texture_manager;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/asset_manager.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <optional>\n#include <string>\n#include <utility>\n\n#include \"util/path.h\"\n\n\nnamespace openage::renderer {\nclass Renderer;\n\nnamespace resources {\nclass AssetCache;\nclass TextureManager;\n\nclass Animation2dInfo;\nclass BlendPatternInfo;\nclass BlendTableInfo;\nclass PaletteInfo;\nclass TerrainInfo;\nclass Texture2dInfo;\n\n/**\n * Loads and stores references to shared graphics assets such as textures,\n * animations, palettes, etc.\n *\n * Using the asset manager allows quick access to already loaded assets and avoids\n * creating unnecessary duplicates.\n *\n * TODO: Hot reloading/cache invalidation with inotify.\n */\nclass AssetManager {\npublic:\n\t/**\n\t * Create a new asset manager.\n\t *\n\t * @param renderer The openage renderer instance.\n\t * @param asset_base_dir Base path for all assets.\n\t */\n\tAssetManager(const std::shared_ptr<Renderer> &renderer,\n\t             const util::Path &asset_base_dir);\n\t~AssetManager() = default;\n\n\t/**\n\t * Prevent accidental copy or assignment because it would defeat the\n\t * point of a persistent cache.\n\t */\n\tAssetManager(const AssetManager &) = delete;\n\tAssetManager &operator=(const AssetManager &) = delete;\n\n\t/**\n\t * Get the corresponding asset for the specified path.\n\t *\n\t * If the asset does not exist in the cache yet, it will be loaded\n\t * using the given path.\n\t *\n\t * @param path Path to the asset resource.\n\t *\n\t * @return Texture resource at the given path.\n\t */\n\tconst std::shared_ptr<Animation2dInfo> &request_animation(const util::Path &path);\n\tconst std::shared_ptr<BlendPatternInfo> &request_blpattern(const util::Path &path);\n\tconst std::shared_ptr<BlendTableInfo> &request_bltable(const util::Path &path);\n\tconst std::shared_ptr<PaletteInfo> &request_palette(const util::Path &path);\n\tconst std::shared_ptr<TerrainInfo> &request_terrain(const util::Path &path);\n\tconst std::shared_ptr<Texture2dInfo> &request_texture(const util::Path &path);\n\n\t/**\n\t * Get the corresponding asset for a path string relative to the\n\t * asset base directory.\n\t *\n\t * If the asset does not exist in the cache yet, it will be loaded\n\t * using the given path.\n\t *\n\t * @param path Relative path to the asset resource (from the asset base dir).\n\t *\n\t * @return Texture resource at the given path.\n\t */\n\tconst std::shared_ptr<Animation2dInfo> &request_animation(const std::string &rel_path);\n\tconst std::shared_ptr<BlendPatternInfo> &request_blpattern(const std::string &rel_path);\n\tconst std::shared_ptr<BlendTableInfo> &request_bltable(const std::string &rel_path);\n\tconst std::shared_ptr<PaletteInfo> &request_palette(const std::string &rel_path);\n\tconst std::shared_ptr<TerrainInfo> &request_terrain(const std::string &rel_path);\n\tconst std::shared_ptr<Texture2dInfo> &request_texture(const std::string &rel_path);\n\n\tusing placeholder_anim_t = std::optional<std::pair<util::Path, std::shared_ptr<Animation2dInfo>>>;\n\tusing placeholder_blpattern_t = std::optional<std::pair<util::Path, std::shared_ptr<BlendPatternInfo>>>;\n\tusing placeholder_bltable_t = std::optional<std::pair<util::Path, std::shared_ptr<BlendTableInfo>>>;\n\tusing placeholder_palette_t = std::optional<std::pair<util::Path, std::shared_ptr<PaletteInfo>>>;\n\tusing placeholder_terrain_t = std::optional<std::pair<util::Path, std::shared_ptr<TerrainInfo>>>;\n\tusing placeholder_texture_t = std::optional<std::pair<util::Path, std::shared_ptr<Texture2dInfo>>>;\n\n\t/**\n\t * Set a placeholder asset that is returned in case a regular request cannot\n\t * find the requested asset.\n\t *\n\t * @param path Path to the placeholder asset resource.\n\t */\n\tvoid set_placeholder_animation(const util::Path &path);\n\tvoid set_placeholder_blpattern(const util::Path &path);\n\tvoid set_placeholder_bltable(const util::Path &path);\n\tvoid set_placeholder_palette(const util::Path &path);\n\tvoid set_placeholder_terrain(const util::Path &path);\n\tvoid set_placeholder_texture(const util::Path &path);\n\n\t/**\n\t * Get the placeholder asset for a specific asset type.\n\t *\n\t * @return Placeholder asset resource.\n\t */\n\tconst placeholder_anim_t &get_placeholder_animation();\n\tconst placeholder_blpattern_t &get_placeholder_blpattern();\n\tconst placeholder_bltable_t &get_placeholder_bltable();\n\tconst placeholder_palette_t &get_placeholder_palette();\n\tconst placeholder_terrain_t &get_placeholder_terrain();\n\tconst placeholder_texture_t &get_placeholder_texture();\n\n\t/**\n\t * Get the texture manager for accessing cached image resources.\n\t *\n\t * @return Texture manager.\n\t */\n\tconst std::shared_ptr<TextureManager> &get_texture_manager();\n\nprivate:\n\t/**\n\t * openage renderer.\n\t */\n\tstd::shared_ptr<Renderer> renderer;\n\n\t/**\n\t * Cache of already loaded assets.\n\t */\n\tstd::shared_ptr<AssetCache> cache;\n\n\t/**\n\t * Manages individual textures/image resources used by the\n\t * high level asset formats cached by the asset manager.\n\t */\n\tstd::shared_ptr<TextureManager> texture_manager;\n\n\t/**\n\t * Base path for all assets.\n\t *\n\t * TODO: This should be the mount point of modpacks?\n\t */\n\tutil::Path asset_base_dir;\n\n\t/**\n\t * Placeholder assets that can be used if a resource is not found.\n\t */\n\tplaceholder_anim_t placeholder_animation;\n\tplaceholder_blpattern_t placeholder_blpattern;\n\tplaceholder_bltable_t placeholder_bltable;\n\tplaceholder_palette_t placeholder_palette;\n\tplaceholder_terrain_t placeholder_terrain;\n\tplaceholder_texture_t placeholder_texture;\n};\n\n} // namespace resources\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/cache.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"cache.h\"\n\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::resources {\n\nconst std::shared_ptr<Animation2dInfo> &AssetCache::get_animation(const util::Path &path) {\n\treturn this->loaded_animations.at(path);\n}\n\n\nconst std::shared_ptr<BlendPatternInfo> &AssetCache::get_blpattern(const util::Path &path) {\n\treturn this->loaded_blpatterns.at(path);\n}\n\n\nconst std::shared_ptr<BlendTableInfo> &AssetCache::get_bltable(const util::Path &path) {\n\treturn this->loaded_bltables.at(path);\n}\n\n\nconst std::shared_ptr<PaletteInfo> &AssetCache::get_palette(const util::Path &path) {\n\treturn this->loaded_palettes.at(path);\n}\n\n\nconst std::shared_ptr<TerrainInfo> &AssetCache::get_terrain(const util::Path &path) {\n\treturn this->loaded_terrains.at(path);\n}\n\n\nconst std::shared_ptr<Texture2dInfo> &AssetCache::get_texture(const util::Path &path) {\n\treturn this->loaded_textures.at(path);\n}\n\n\nvoid AssetCache::add_animation(const util::Path &path, const std::shared_ptr<Animation2dInfo> info) {\n\tthis->loaded_animations.insert({path, info});\n}\n\n\nvoid AssetCache::add_blpattern(const util::Path &path, const std::shared_ptr<BlendPatternInfo> info) {\n\tthis->loaded_blpatterns.insert({path, info});\n}\n\n\nvoid AssetCache::add_bltable(const util::Path &path, const std::shared_ptr<BlendTableInfo> info) {\n\tthis->loaded_bltables.insert({path, info});\n}\n\n\nvoid AssetCache::add_palette(const util::Path &path, const std::shared_ptr<PaletteInfo> info) {\n\tthis->loaded_palettes.insert({path, info});\n}\n\n\nvoid AssetCache::add_terrain(const util::Path &path, const std::shared_ptr<TerrainInfo> info) {\n\tthis->loaded_terrains.insert({path, info});\n}\n\nvoid AssetCache::add_texture(const util::Path &path, const std::shared_ptr<Texture2dInfo> info) {\n\tthis->loaded_textures.insert({path, info});\n}\n\nvoid AssetCache::remove_animation(const util::Path &path) {\n\tthis->loaded_animations.erase(path);\n}\n\nvoid AssetCache::remove_blpattern(const util::Path &path) {\n\tthis->loaded_blpatterns.erase(path);\n}\n\nvoid AssetCache::remove_bltable(const util::Path &path) {\n\tthis->loaded_bltables.erase(path);\n}\n\nvoid AssetCache::remove_palette(const util::Path &path) {\n\tthis->loaded_palettes.erase(path);\n}\n\nvoid AssetCache::remove_terrain(const util::Path &path) {\n\tthis->loaded_terrains.erase(path);\n}\n\nvoid AssetCache::remove_texture(const util::Path &path) {\n\tthis->loaded_textures.erase(path);\n}\n\nbool AssetCache::check_animation_cache(const util::Path &path) {\n\treturn this->loaded_animations.contains(path);\n}\n\nbool AssetCache::check_blpattern_cache(const util::Path &path) {\n\treturn this->loaded_blpatterns.contains(path);\n}\n\nbool AssetCache::check_bltable_cache(const util::Path &path) {\n\treturn this->loaded_bltables.contains(path);\n}\n\nbool AssetCache::check_palette_cache(const util::Path &path) {\n\treturn this->loaded_palettes.contains(path);\n}\n\nbool AssetCache::check_terrain_cache(const util::Path &path) {\n\treturn this->loaded_terrains.contains(path);\n}\n\nbool AssetCache::check_texture_cache(const util::Path &path) {\n\treturn this->loaded_textures.contains(path);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/cache.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n#include <unordered_map>\n\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::resources {\nclass Animation2dInfo;\nclass BlendPatternInfo;\nclass BlendTableInfo;\nclass PaletteInfo;\nclass TerrainInfo;\nclass Texture2dInfo;\n\n/**\n * Loads and stores references to shared asset descriptions such as texture info,\n * animations, palettes, etc.\n *\n * Using the asset manager allows quick access to already loaded assets and avoids\n * creating unnecessary duplicates.\n */\nclass AssetCache {\npublic:\n\t/**\n\t * Create a new asset cache.\n\t */\n\tAssetCache() = default;\n\t~AssetCache() = default;\n\n\t/**\n\t * Get the corresponding asset for the specified path.\n\t *\n\t * @param path Path to the asset resource.\n\t *\n\t * @return Asset resource at the given path.\n\t * @throw Error if the resource is not in the cache.\n\t */\n\tconst std::shared_ptr<Animation2dInfo> &get_animation(const util::Path &path);\n\tconst std::shared_ptr<BlendPatternInfo> &get_blpattern(const util::Path &path);\n\tconst std::shared_ptr<BlendTableInfo> &get_bltable(const util::Path &path);\n\tconst std::shared_ptr<PaletteInfo> &get_palette(const util::Path &path);\n\tconst std::shared_ptr<TerrainInfo> &get_terrain(const util::Path &path);\n\tconst std::shared_ptr<Texture2dInfo> &get_texture(const util::Path &path);\n\t// TODO: use std::optional<std::shared_ptr<Texture2dInfo>>\n\n\t/**\n\t * Map a specific asset info to the given path and add it to the cache.\n\t * Overwrites existing references if the path already exists in the cache.\n\t *\n\t * @param path Path to the asset resource.\n\t * @param info Existing asset object.\n\t */\n\tvoid add_animation(const util::Path &path, const std::shared_ptr<Animation2dInfo> info);\n\tvoid add_blpattern(const util::Path &path, const std::shared_ptr<BlendPatternInfo> info);\n\tvoid add_bltable(const util::Path &path, const std::shared_ptr<BlendTableInfo> info);\n\tvoid add_palette(const util::Path &path, const std::shared_ptr<PaletteInfo> info);\n\tvoid add_terrain(const util::Path &path, const std::shared_ptr<TerrainInfo> info);\n\tvoid add_texture(const util::Path &path, const std::shared_ptr<Texture2dInfo> info);\n\n\t/**\n\t * Remove an asset reference from the cache.\n\t *\n\t * @param path Path to the asset resource.\n\t */\n\tvoid remove_animation(const util::Path &path);\n\tvoid remove_blpattern(const util::Path &path);\n\tvoid remove_bltable(const util::Path &path);\n\tvoid remove_palette(const util::Path &path);\n\tvoid remove_terrain(const util::Path &path);\n\tvoid remove_texture(const util::Path &path);\n\n\t/**\n\t * Check if an asset reference is in the cache.\n\t *\n\t * @param path Path to the asset resource.\n\t *\n\t * @return true if the resource is cached, else false.\n\t */\n\tbool check_animation_cache(const util::Path &path);\n\tbool check_blpattern_cache(const util::Path &path);\n\tbool check_bltable_cache(const util::Path &path);\n\tbool check_palette_cache(const util::Path &path);\n\tbool check_terrain_cache(const util::Path &path);\n\tbool check_texture_cache(const util::Path &path);\n\nprivate:\n\tusing anim_cache_t = std::unordered_map<util::Path, std::shared_ptr<Animation2dInfo>>;\n\tusing blpattern_cache_t = std::unordered_map<util::Path, std::shared_ptr<BlendPatternInfo>>;\n\tusing bltable_cache_t = std::unordered_map<util::Path, std::shared_ptr<BlendTableInfo>>;\n\tusing palette_cache_t = std::unordered_map<util::Path, std::shared_ptr<PaletteInfo>>;\n\tusing terrain_cache_t = std::unordered_map<util::Path, std::shared_ptr<TerrainInfo>>;\n\tusing texture_cache_t = std::unordered_map<util::Path, std::shared_ptr<Texture2dInfo>>;\n\n\t/**\n\t * Cache of already loaded animations.\n\t */\n\tanim_cache_t loaded_animations;\n\n\t/**\n\t * Cache of already loaded blending patterns.\n\t */\n\tblpattern_cache_t loaded_blpatterns;\n\n\t/**\n\t * Cache of already loaded blending tables.\n\t */\n\tbltable_cache_t loaded_bltables;\n\n\t/**\n\t * Cache of already loaded colour palettes.\n\t */\n\tpalette_cache_t loaded_palettes;\n\n\t/**\n\t * Cache of already loaded terrains.\n\t */\n\tterrain_cache_t loaded_terrains;\n\n\t/**\n\t * Cache of already loaded textures.\n\t */\n\ttexture_cache_t loaded_textures;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/texture_manager.cpp",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#include \"texture_manager.h\"\n\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/texture_data.h\"\n\n\nnamespace openage::renderer::resources {\n\nTextureManager::TextureManager(const std::shared_ptr<Renderer> &renderer) :\n\trenderer{renderer},\n\tloaded{} {\n}\n\nconst std::shared_ptr<Texture2d> &TextureManager::request(const util::Path &path) {\n\tif (not this->loaded.contains(path)) {\n\t\t// create if not loaded\n\t\tauto tex_data = resources::Texture2dData(path);\n\t\tthis->loaded.insert({path, this->renderer->add_texture(tex_data)});\n\t}\n\treturn this->loaded.at(path);\n}\n\nvoid TextureManager::add(const util::Path &path) {\n\tif (not this->loaded.contains(path)) {\n\t\t// create if not loaded\n\t\tauto tex_data = resources::Texture2dData(path);\n\t\tthis->loaded.insert({path, this->renderer->add_texture(tex_data)});\n\t}\n}\n\nvoid TextureManager::add(const util::Path &path,\n                         const std::shared_ptr<Texture2d> &texture) {\n\tauto flat_path = path.resolve_native_path();\n\tthis->loaded.insert({path, texture});\n}\n\nvoid TextureManager::remove(const util::Path &path) {\n\tthis->loaded.erase(path);\n}\n\nvoid TextureManager::set_placeholder(const util::Path &path) {\n\tauto tex_data = resources::Texture2dData(path);\n\tthis->placeholder = std::make_pair(path, this->renderer->add_texture(tex_data));\n}\n\nconst TextureManager::placeholder_t &TextureManager::get_placeholder() const {\n\treturn this->placeholder;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/assets/texture_manager.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <optional>\n#include <string>\n#include <unordered_map>\n#include <utility>\n\n#include \"util/path.h\"\n\n\nnamespace openage::renderer {\nclass Renderer;\nclass Texture2d;\n\nnamespace resources {\n\n/**\n * Loads and stores references to shared texture assets.\n *\n * Using the texture manager to request texture assets allows quick access\n * to already loaded assets and avoid creating unnecessary duplicates.\n */\nclass TextureManager {\npublic:\n\t/**\n\t * Create a new texture manager.\n\t *\n\t * @param renderer The openage renderer instance.\n\t */\n\tTextureManager(const std::shared_ptr<Renderer> &renderer);\n\t~TextureManager() = default;\n\n\t/**\n\t * Prevent accidental copy or assignment because it would defeat the\n\t * point of a persistent cache.\n\t */\n\tTextureManager(const TextureManager &) = delete;\n\tTextureManager &operator=(const TextureManager &) = delete;\n\n\t/**\n\t * Get the corresponding texture for the specified path.\n\t *\n\t * If the texture does not exist in the cache yet, it will be loaded\n\t * using the given path.\n\t *\n\t * @param path Path to the texture resource.\n\t *\n\t * @return Texture resource at the given path.\n\t */\n\tconst std::shared_ptr<Texture2d> &request(const util::Path &path);\n\n\t/**\n\t * Load the texture at the given path. Does nothing if the path\n\t * already exists in the cache.\n\t *\n\t * @param path Path to the texture resource.\n\t */\n\tvoid add(const util::Path &path);\n\n\t/**\n\t * Assign a specific texture to the given path. Overwrites existing\n\t * textures references if the path already exists in the cache.\n\t *\n\t * @param path Path to the texture resource.\n\t * @param texture Existing texture object.\n\t */\n\tvoid add(const util::Path &path,\n\t         const std::shared_ptr<Texture2d> &texture);\n\n\t/**\n\t * Remove a texture reference from the cache.\n\t *\n\t * @param path Path to the texture resource.\n\t */\n\tvoid remove(const util::Path &path);\n\n\t/**\n\t * Set the placeholder texture.\n\t *\n\t * @param path Path to the texture resource.\n\t */\n\tvoid set_placeholder(const util::Path &path);\n\n\tusing placeholder_t = std::optional<std::pair<util::Path, std::shared_ptr<Texture2d>>>;\n\n\t/**\n\t * Get the placeholder texture.\n\t *\n\t * @return Placeholder texture. Can be \\p nullptr.\n\t */\n\tconst placeholder_t &get_placeholder() const;\n\nprivate:\n\t/**\n\t * openage renderer.\n\t */\n\tstd::shared_ptr<Renderer> renderer;\n\n\tusing texture_cache_t = std::unordered_map<util::Path, std::shared_ptr<Texture2d>>;\n\n\t/**\n\t * Cache of already created textures.\n\t */\n\ttexture_cache_t loaded;\n\n\t/**\n\t * Placeholder texture to use if a texture could not be loaded.\n\t */\n\tplaceholder_t placeholder;\n};\n\n} // namespace resources\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/resources/buffer_info.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"buffer_info.h\"\n\nnamespace openage::renderer::resources {\n\nUniformBufferInfo::UniformBufferInfo(ubo_layout_t layout,\n                                     const std::vector<UBOInput> &inputs) :\n\tlayout{layout},\n\tinputs{inputs} {\n}\n\nUniformBufferInfo::UniformBufferInfo(ubo_layout_t layout,\n                                     std::vector<UBOInput> &&inputs) :\n\tlayout{layout},\n\tinputs{inputs} {\n}\n\nubo_layout_t UniformBufferInfo::get_layout() const {\n\treturn this->layout;\n}\n\nconst std::vector<UBOInput> &UniformBufferInfo::get_inputs() const {\n\treturn this->inputs;\n}\n\nsize_t UniformBufferInfo::get_size() const {\n\tsize_t size = 0;\n\tfor (const auto &input : this->inputs) {\n\t\t// size of the input type\n\t\tsize_t input_size = this->get_size(input, this->layout);\n\n\t\t// inputs must additionally be aligned to a multiple of their size\n\t\tsize_t align_size = size % input_size;\n\n\t\tsize += input_size + align_size;\n\t}\n\treturn size;\n}\n\nsize_t UniformBufferInfo::get_size(const UBOInput &input, ubo_layout_t /* layout */) {\n\tif (input.count == 1) {\n\t\treturn STD140_INPUT_SIZE.get(input.type);\n\t}\n\n\t// length of single element is always padded to vec4\n\treturn STD140_INPUT_SIZE.get(ubo_input_t::V4F32) * input.count;\n}\n\nsize_t UniformBufferInfo::get_stride_size(ubo_input_t /* input */, ubo_layout_t /* layout */) {\n\treturn STD140_INPUT_SIZE.get(ubo_input_t::V4F32);\n}\n\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/buffer_info.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <string>\n#include <utility>\n#include <vector>\n\n#include \"datastructure/constexpr_map.h\"\n\n\nnamespace openage::renderer::resources {\n\n/**\n * Uniform buffer layout types.\n *\n * std140 is the only layout for which we can predict the size,\n * so it is the only one we support.\n */\nenum class ubo_layout_t {\n\tSTD140,\n};\n\n/**\n * Uniform types.\n */\nenum class ubo_input_t {\n\tI32,\n\tU32,\n\tF32,\n\tF64,\n\tBOOL,\n\tV2F32,\n\tV3F32,\n\tV4F32,\n\tV2I32,\n\tV3I32,\n\tV4I32,\n\tV2U32,\n\tV3U32,\n\tV4U32,\n\tM4F32,\n};\n\n/**\n * Uniform block input definition.\n */\nstruct UBOInput {\n\t// Uniform name.\n\tstd::string name;\n\t// Uniform type.\n\tubo_input_t type;\n\t// Length (values >1 are arrays)\n\tuint32_t count = 1;\n};\n\n\n/**\n * Size of uniform input types in std140 layout _including_ padding (in bytes).\n */\nstatic constexpr auto STD140_INPUT_SIZE = datastructure::create_const_map<ubo_input_t, size_t>(\n\tstd::pair(ubo_input_t::I32, 4),\n\tstd::pair(ubo_input_t::U32, 4),\n\tstd::pair(ubo_input_t::F32, 4),\n\tstd::pair(ubo_input_t::F64, 8),\n\tstd::pair(ubo_input_t::BOOL, 4),\n\tstd::pair(ubo_input_t::V2F32, 8),\n\tstd::pair(ubo_input_t::V3F32, 16),\n\tstd::pair(ubo_input_t::V4F32, 16),\n\tstd::pair(ubo_input_t::V2I32, 8),\n\tstd::pair(ubo_input_t::V3I32, 16),\n\tstd::pair(ubo_input_t::V4I32, 16),\n\tstd::pair(ubo_input_t::V2U32, 8),\n\tstd::pair(ubo_input_t::V3U32, 16),\n\tstd::pair(ubo_input_t::V4U32, 16),\n\tstd::pair(ubo_input_t::M4F32, 64));\n\n\nclass UniformBufferInfo {\npublic:\n\t/**\n\t * Create a new uniform buffer definition.\n\t *\n\t * @param layout Layout of the uniform block targeted by the buffer.\n\t * @param inputs Uniform definitions inside the uniform block.\n\t */\n\tUniformBufferInfo(ubo_layout_t layout, const std::vector<UBOInput> &inputs);\n\n\t/**\n\t * Create a new uniform buffer definition.\n\t *\n\t * @param layout Layout of the uniform block targeted by the buffer.\n\t * @param inputs Uniform definitions inside the uniform block.\n\t */\n\tUniformBufferInfo(ubo_layout_t layout, std::vector<UBOInput> &&inputs);\n\n\t/**\n\t * Get the layout of the uniform block targeted by the buffer.\n\t *\n\t * @return Uniform block layout.\n\t */\n\tubo_layout_t get_layout() const;\n\n\t/**\n\t * Get the uniform definitions inside the uniform block.\n\t *\n\t * @return Uniform definitions.\n\t */\n\tconst std::vector<UBOInput> &get_inputs() const;\n\n\t/**\n\t * Get the total size of the uniform block (in bytes).\n\t *\n\t * @return Size of the uniform block (in bytes).\n\t */\n\tsize_t get_size() const;\n\n\t/**\n\t * Get the size of a single uniform input (in bytes).\n\t *\n\t * @param input Uniform input type.\n\t * @param layout Layout of the uniform block targeted by the buffer.\n\t *\n\t * @return Size of the uniform input (in bytes).\n\t */\n\tstatic size_t get_size(const UBOInput &input, ubo_layout_t layout = ubo_layout_t::STD140);\n\n\t/**\n\t * Get the stride size of a uniform input in an array (in bytes).\n\t *\n\t * @param input Uniform input type.\n\t * @param layout Layout of the uniform block targeted by the buffer.\n\t *\n\t * @return Size of the uniform input (in bytes).\n\t */\n\tstatic size_t get_stride_size(ubo_input_t input, ubo_layout_t layout = ubo_layout_t::STD140);\n\nprivate:\n\t/**\n\t * Uniform block layout.\n\t */\n\tubo_layout_t layout;\n\n\t/**\n\t * Uniform definitions.\n\t */\n\tstd::vector<UBOInput> inputs;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/frame_timing.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"frame_timing.h\"\n\n#include <algorithm>\n#include <compare>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::renderer::resources {\n\nFrameTiming::FrameTiming(const time::time_t &total_length,\n                         const std::vector<keyframe_t> &&keyframes) :\n\tkeyframes{keyframes},\n\ttotal_length{total_length} {\n\tif (this->keyframes.size() < 1) [[unlikely]] {\n\t\tthrow Error(ERR << \"Frame timing sequence must have at least one keyframe.\");\n\t}\n}\n\nvoid FrameTiming::set_total_length(const time::time_t &total_length) {\n\tthis->total_length = total_length;\n}\n\nvoid FrameTiming::insert(const time::time_t &time) {\n\tfor (auto it = this->keyframes.begin(); it != this->keyframes.end(); ++it) {\n\t\tif (*it >= time) {\n\t\t\tthis->keyframes.insert(it, time);\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nsize_t FrameTiming::get_frame(const time::time_t &time) const {\n\treturn search_frame(time);\n}\n\nsize_t FrameTiming::get_frame(const time::time_t &time, const time::time_t &start) const {\n\ttime::time_t offset = time - start;\n\tif (this->total_length == 0) {\n\t\t// modulo would fail here so return early\n\t\treturn 0;\n\t}\n\n\ttime::time_t mod = offset % this->total_length;\n\treturn this->get_frame(mod);\n}\n\nsize_t FrameTiming::size() const {\n\treturn this->keyframes.size();\n}\n\nsize_t FrameTiming::search_frame(const time::time_t &time) const {\n\tsize_t left = 0;\n\tsize_t right = this->keyframes.size();\n\tsize_t mid = 0;\n\n\t// rightmost binary search\n\twhile (left < right) {\n\t\tmid = (left + right) / 2;\n\n\t\tif (time < this->keyframes.at(mid)) {\n\t\t\tright = mid;\n\t\t}\n\t\telse {\n\t\t\tleft = mid + 1;\n\t\t}\n\t}\n\n\treturn right - 1;\n}\n\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/frame_timing.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <vector>\n\n#include \"time/time.h\"\n\n\nnamespace openage::renderer::resources {\n\n/**\n * Storage for the timing of a sequence of frames.\n *\n * Optimized for fast read access.\n */\nclass FrameTiming {\npublic:\n\tusing keyframe_t = time::time_t;\n\n\t/**\n\t * Create a new frame timing sequence.\n\t *\n\t * @param total_length Total length of the sequence (in seconds).\n\t * @param keyframes Time of each frame (in seconds). Expected to be sorted.\n\t */\n\tFrameTiming(const time::time_t &total_length,\n\t            const std::vector<keyframe_t> &&keyframes);\n\n\t~FrameTiming() = default;\n\n\t/**\n\t * Set the total length of the frame timing sequence.\n\t *\n\t * @param total_length Total length of the sequence (in seconds).\n\t */\n\tvoid set_total_length(const time::time_t &total_length);\n\n\t/**\n\t * Insert a new keyframe into the sequence.\n\t *\n\t * Insertion complexity is linear (O(n)), so it should be avoided during rendering\n\t * when possible. Prefer to create several alternative sequence instead if the\n\t * timing needs to be changed.\n\t *\n\t * @param time Time of the new keyframe (in seconds).\n\t */\n\tvoid insert(const time::time_t &time);\n\n\t/**\n\t * Get the index of the frame in the sequence that should be displayed at the\n\t * given time (closest frame with t <= time).\n\t *\n\t * @param time Time in the sequence (in seconds).\n\t * @return Frame index in the sequence.\n\t */\n\tsize_t get_frame(const time::time_t &time) const;\n\n\t/**\n\t * Get the index of the frame in the sequence that should be displayed at a specified\n\t * time. This method is intended for looping frame sequences.\n\t *\n\t * Sequence time is determined from the modulus of the time difference between\n\t * \\p current (current simulation time) and \\p start (time when the sequence\n\t * was first started):\n\t *\n\t * time = (current - start) % total_length\n\t *\n\t * @param current Current simulation time (in seconds).\n\t * @param start Start time of the sequence (in seconds).\n\t * @return Frame index in the sequence.\n\t */\n\tsize_t get_frame(const time::time_t &current, const time::time_t &start) const;\n\n\t/**\n\t * Get the number of frames in the sequence.\n\t *\n\t * @return Number of frames.\n\t */\n\tsize_t size() const;\n\nprivate:\n\t/**\n\t * Search for the closest frame index with t <= time.\n\t *\n\t * Uses binary search to find the frame index. Complexity is therefore\n\t * O(log_2(this->keyframes.size())).\n\t *\n\t * @param time Time in the sequence (in seconds).\n\t * @return Frame index in the sequence.\n\t */\n\tsize_t search_frame(const time::time_t &time) const;\n\n\t/**\n\t * Time of each frame in the sequence relative to the sequence start (in seconds).\n\t */\n\tstd::vector<keyframe_t> keyframes;\n\n\t/**\n\t * Total length of the sequence (in seconds).\n\t */\n\ttime::time_t total_length;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/mesh_data.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"mesh_data.h\"\n\n#include <array>\n#include <cstring>\n#include <type_traits>\n#include <utility>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"datastructure/constexpr_map.h\"\n\n\nnamespace openage::renderer::resources {\n\nstatic constexpr auto vin_size = datastructure::create_const_map<vertex_input_t, size_t>(\n\tstd::make_pair(vertex_input_t::F32, 4),\n\tstd::make_pair(vertex_input_t::V2F32, 8),\n\tstd::make_pair(vertex_input_t::V3F32, 12),\n\tstd::make_pair(vertex_input_t::M3F32, 36));\n\nstatic constexpr auto vin_count = datastructure::create_const_map<vertex_input_t, size_t>(\n\tstd::make_pair(vertex_input_t::F32, 1),\n\tstd::make_pair(vertex_input_t::V2F32, 2),\n\tstd::make_pair(vertex_input_t::V3F32, 3),\n\tstd::make_pair(vertex_input_t::M3F32, 9));\n\nsize_t vertex_input_size(vertex_input_t in) {\n\treturn vin_size.get(in);\n}\n\nsize_t vertex_input_count(vertex_input_t in) {\n\treturn vin_count.get(in);\n}\n\nVertexInputInfo::VertexInputInfo(std::vector<vertex_input_t> inputs,\n                                 vertex_layout_t layout,\n                                 vertex_primitive_t primitive) :\n\tinputs(std::move(inputs)),\n\tlayout(layout), primitive(primitive) {}\n\nVertexInputInfo::VertexInputInfo(std::vector<vertex_input_t> inputs,\n                                 vertex_layout_t layout,\n                                 vertex_primitive_t primitive,\n                                 index_t index_type) :\n\tinputs(std::move(inputs)),\n\tlayout(layout), primitive(primitive), index_type(index_type) {}\n\nvoid VertexInputInfo::add_shader_input_map(std::unordered_map<size_t, size_t> &&in_map) {\n\tfor (auto mapping : in_map) {\n\t\tif (mapping.first >= this->inputs.size()) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"A shader input mapping is out-of-range, exceeding the available number of attributes.\");\n\t\t}\n\t}\n\n\tthis->shader_input_map = std::move(in_map);\n}\n\nsize_t VertexInputInfo::vert_size() const {\n\tsize_t size = 0;\n\tfor (auto in : this->inputs) {\n\t\tsize += vertex_input_size(in);\n\t}\n\treturn size;\n}\n\nconst std::vector<vertex_input_t> &VertexInputInfo::get_inputs() const {\n\treturn this->inputs;\n}\n\nstd::optional<std::unordered_map<size_t, size_t>> const &VertexInputInfo::get_shader_input_map() const {\n\treturn this->shader_input_map;\n}\n\nvertex_layout_t VertexInputInfo::get_layout() const {\n\treturn this->layout;\n}\n\nvertex_primitive_t VertexInputInfo::get_primitive() const {\n\treturn this->primitive;\n}\n\nstd::optional<index_t> VertexInputInfo::get_index_type() const {\n\treturn this->index_type;\n}\n\nMeshData::MeshData(const util::Path & /*path*/) {\n\t// TODO implement mesh loaders\n\tthrow \"unimplemented lol\";\n}\n\nMeshData::MeshData(std::vector<uint8_t> &&verts, const VertexInputInfo &info) :\n\tdata(std::move(verts)), info(info) {}\n\nMeshData::MeshData(std::vector<uint8_t> &&verts, std::vector<uint8_t> &&ids, const VertexInputInfo &info) :\n\tdata(std::move(verts)), ids(std::move(ids)), info(info) {}\n\nstd::vector<uint8_t> const &MeshData::get_data() const {\n\treturn this->data;\n}\n\nstd::optional<std::vector<uint8_t>> const &MeshData::get_ids() const {\n\treturn this->ids;\n}\n\nVertexInputInfo MeshData::get_info() const {\n\treturn *this->info;\n}\n\n/// Vertices of a quadrilateral filling the whole screen.\n/// Format: (pos, tex_coords) = (x, y, u, v)\nstatic constexpr const std::array<float, 16> QUAD_DATA_CENTERED = {\n\t// clang-format off\n\t// prevent clang-format from putting this matrix on single line\n\t{\n\t\t-1.0f,  1.0f, 0.0f, 1.0f,\n\t\t-1.0f, -1.0f, 0.0f, 0.0f,\n\t\t 1.0f,  1.0f, 1.0f, 1.0f,\n\t\t 1.0f, -1.0f, 1.0f, 0.0f\n\t}\n\t// clang-format on\n};\n\n/// Vertices of a quad from (0, 0) to (1, 1)\n/// Format: (pos, tex_coords) = (x, y, u, v)\nstatic constexpr const std::array<float, 16> QUAD_DATA_UNIT = {\n\t// clang-format off\n\t// prevent clang-format from putting this matrix on single line\n\t{\n\t\t0.0f, 1.0f, 0.0f, 1.0f,\n\t\t0.0f, 0.0f, 0.0f, 0.0f,\n\t\t1.0f, 1.0f, 1.0f, 1.0f,\n\t\t1.0f, 0.0f, 1.0f, 0.0f\n\t}\n\t// clang-format on\n};\n\n\nnamespace {\n\n/**\n * Generate triangle-strip meshdata for a given 4-vertex/uv-coord array.\n */\ntemplate <size_t size>\nMeshData create_float_mesh(const std::array<float, size> &src) {\n\tauto const data_size = size * sizeof(float);\n\n\tstd::vector<uint8_t> verts(data_size);\n\tstd::memcpy(verts.data(), src.data(), data_size);\n\n\tVertexInputInfo info{\n\t\t{vertex_input_t::V2F32, vertex_input_t::V2F32},\n\t\tvertex_layout_t::AOS,\n\t\tvertex_primitive_t::TRIANGLE_STRIP};\n\n\treturn MeshData(std::move(verts), info);\n}\n\n} // namespace\n\n\nMeshData MeshData::make_quad(bool centered) {\n\tif (centered) {\n\t\treturn create_float_mesh(QUAD_DATA_CENTERED);\n\t}\n\telse {\n\t\treturn create_float_mesh(QUAD_DATA_UNIT);\n\t}\n}\n\n\nMeshData MeshData::make_quad(float sidelength, bool centered) {\n\t// 8 positions and 8 uv-coords.\n\t// store pos and uv as: (x, y, uvx, uvy)\n\tstd::array<float, 16> positions;\n\n\tif (centered) {\n\t\tfloat halfsidelength = sidelength / 2;\n\t\tpositions = {\n\t\t\t// clang-format off\n\t\t\t// prevent clang-format from putting this matrix on single line\n\t\t\t{\n\t\t\t\t-halfsidelength,  halfsidelength, 0.0f, 1.0f,\n\t\t\t\t-halfsidelength, -halfsidelength, 0.0f, 0.0f,\n\t\t\t\t halfsidelength,  halfsidelength, 1.0f, 1.0f,\n\t\t\t\t halfsidelength, -halfsidelength, 1.0f, 0.0f\n\t\t\t}\n\t\t\t// clang-format on\n\t\t};\n\t}\n\telse {\n\t\tpositions = {\n\t\t\t// clang-format off\n\t\t\t// prevent clang-format from putting this matrix on single line\n\t\t\t{\n\t\t\t\t0.0f,       sidelength, 0.0f, 1.0f,\n\t\t\t\t0.0f,       0.0f,       0.0f, 0.0f,\n\t\t\t\tsidelength, sidelength, 1.0f, 1.0f,\n\t\t\t\tsidelength, 0.0f,       1.0f, 0.0f\n\t\t\t}\n\t\t\t// clang-format on\n\t\t};\n\t}\n\n\treturn create_float_mesh(positions);\n}\n\n\nMeshData MeshData::make_quad(float width, float height, bool centered) {\n\t// 8 positions and 8 uv-coords.\n\t// store pos and uv as: (x, y, uvx, uvy)\n\tstd::array<float, 16> positions;\n\n\tif (centered) {\n\t\tfloat halfwidth = width / 2;\n\t\tfloat halfheight = height / 2;\n\t\tpositions = {\n\t\t\t{\n\t\t\t\t// clang-format off\n\t\t\t\t// prevent clang-format from putting this matrix on single line\n\t\t\t\t-halfwidth,  halfheight, 0.0f, 1.0f,\n\t\t\t\t-halfwidth, -halfheight, 0.0f, 0.0f,\n\t\t\t\t halfwidth,  halfheight, 1.0f, 1.0f,\n\t\t\t\t halfwidth, -halfheight, 1.0f, 0.0f\n\t\t\t\t// clang-format on\n\t\t\t}};\n\t}\n\telse {\n\t\tpositions = {\n\t\t\t// clang-format off\n\t\t\t// prevent clang-format from putting this matrix on single line\n\t\t\t{\n\t\t\t\t0.0f,  height, 0.0f, 1.0f,\n\t\t\t\t0.0f,  0.0f,   0.0f, 0.0f,\n\t\t\t\twidth, height, 1.0f, 1.0f,\n\t\t\t\twidth, 0.0f,   1.0f, 0.0f\n\t\t\t}\n\t\t\t// clang-format on\n\t\t};\n\t}\n\n\treturn create_float_mesh(positions);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/mesh_data.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <cstdlib>\n#include <optional>\n#include <unordered_map>\n#include <vector>\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\nnamespace renderer {\nnamespace resources {\n\n/// The type of a single per-vertex input to the shader program.\nenum class vertex_input_t {\n\tF32,\n\tV2F32,\n\tV3F32,\n\tM3F32,\n};\n\n/// The primitive type that the vertices in a mesh combine into.\nenum class vertex_primitive_t {\n\tPOINTS,\n\tLINES,\n\tLINE_STRIP,\n\tLINE_LOOP,\n\tTRIANGLES,\n\tTRIANGLE_STRIP,\n\tTRIANGLE_FAN,\n};\n\n/// The type of indices used for indexed rendering.\nenum class index_t {\n\tU8,\n\tU16,\n\tU32,\n};\n\n/// Returns the size in bytes of a per-vertex input.\nsize_t vertex_input_size(vertex_input_t);\n\n/// Returns the number of elements of the underlying type in a given per-vertex input type.\n/// For example, V3F32 has 3 float elements, so the value is 3.\nsize_t vertex_input_count(vertex_input_t);\n\nenum class vertex_layout_t {\n\t/// Array of structs. XYZUV, XYZUV, XYZUV\n\tAOS,\n\t/// Struct of arrays. XYZXYZXYZ, UVUVUV\n\tSOA,\n};\n\n/// Information about vertex input data - which components a vertex contains\n/// and how vertices are laid out in memory.\nclass VertexInputInfo {\npublic:\n\t/// Initialize an input info for array rendering.\n\tVertexInputInfo(std::vector<vertex_input_t>, vertex_layout_t, vertex_primitive_t);\n\n\t/// Initialize an input info for indexed rendering.\n\tVertexInputInfo(std::vector<vertex_input_t>, vertex_layout_t, vertex_primitive_t, index_t);\n\n\t/// Adds a mesh->shader input mapping to the info. By default, attributes are mapped\n\t/// one-to-one according to their order in the input vector, e.g. (vec2 pos, vec2 uv)\n\t/// maps into (0: vec2 pos, 1: vec2 uv) in the shader. However, if a shader skips indices\n\t/// in the layout or takes its inputs in a different order, this can be specified using the\n\t/// map. The map entries must have the format (index_in_vector, index_in_shader) and will\n\t/// overwrite the default mapping. If an entry for a given index in the vector is missing,\n\t/// that attribute and its data will be skipped.\n\tvoid add_shader_input_map(std::unordered_map<size_t, size_t> &&);\n\n\t/// Returns the list of per-vertex inputs.\n\tconst std::vector<vertex_input_t> &get_inputs() const;\n\n\t/// Returns the shader input map or an empty optional if it's not present.\n\tstd::optional<std::unordered_map<size_t, size_t>> const &get_shader_input_map() const;\n\n\t/// Returns the layout of vertices in memory.\n\tvertex_layout_t get_layout() const;\n\n\t/// Returns the size of a single vertex.\n\tsize_t vert_size() const;\n\n\t/// Returns the primitive interpretation mode.\n\tvertex_primitive_t get_primitive() const;\n\n\t/// Returns the type of indices used for indexed drawing,\n\t/// which may not be present if array drawing is used.\n\tstd::optional<index_t> get_index_type() const;\n\nprivate:\n\t/// What kind of data the vertices contain and how it is laid out in memory.\n\tstd::vector<vertex_input_t> inputs;\n\n\t/// An optional attribute specifying how vertex inputs in the mesh map to vertex inputs\n\t/// in a given shader, for example to reorder inputs of the form (pos, uv) into a shader\n\t/// that takes in (uv, pos) inputs.\n\tstd::optional<std::unordered_map<size_t, size_t>> shader_input_map;\n\n\t/// How the vertices are laid out in the data buffer.\n\tvertex_layout_t layout;\n\n\t/// The primitive type.\n\tvertex_primitive_t primitive;\n\n\t/// The type of indices if they exist.\n\tstd::optional<index_t> index_type;\n};\n\nclass MeshData {\npublic:\n\t/// Tries to load the mesh data from the specified file.\n\texplicit MeshData(const util::Path &);\n\n\t/// Initializes the mesh data to a custom unindexed vertex vector described by the given info.\n\tMeshData(std::vector<uint8_t> &&verts, const VertexInputInfo &);\n\n\t/// Initializes the mesh data to a custom indexed vertex vector described by the given info.\n\tMeshData(std::vector<uint8_t> &&verts, std::vector<uint8_t> &&ids, const VertexInputInfo &);\n\n\t/// Returns the raw vertex data.\n\tstd::vector<uint8_t> const &get_data() const;\n\n\t/// Returns the indices used for indexed drawing if they exist.\n\tstd::optional<std::vector<uint8_t>> const &get_ids() const;\n\n\t/// Returns information describing the vertices in this mesh.\n\tVertexInputInfo get_info() const;\n\n\t/// Initializes the mesh data with a simple quadrilateral spanning the whole window space,\n\t/// with (vec2 pos, vec2 uv) vertex format.\n\t/// (0, 0) of this quad is at the quad center and it stretches from (-1, -1) to (1, 1)\n\t/// and uv=(0, 0) at bottom left (-> (-1, -1)) and uv=(1, 1) top right (at (1, 1)).\n\t/// When centered = false, the quad stretches from (0, 0) to (1, 1).\n\tstatic MeshData make_quad(bool centered = true);\n\n\t/// Initialize the mesh data with given sidelength. Optionally place (0, 0) bottom left.\n\tstatic MeshData make_quad(float sidelength, bool centered = true);\n\n\t/// Initialize the mesh data with given width and height. Optionally place (0, 0) bottom left.\n\tstatic MeshData make_quad(float width, float height, bool centered = true);\n\nprivate:\n\t/// The raw vertex data. The size is an integer multiple of the size of a single vertex.\n\tstd::vector<uint8_t> data;\n\n\t/// The indices of vertices to be drawn if the mesh is indexed.\n\t/// For array drawing, empty optional.\n\tstd::optional<std::vector<uint8_t>> ids;\n\n\t/// Information about how to interpret the data to make vertices.\n\t/// This is optional because initialization is deferred until the constructor\n\t/// obtains the mesh data e.g. from disk, but it should always be present after\n\t/// construction.\n\tstd::optional<VertexInputInfo> info;\n};\n\n} // namespace resources\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/palette_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"palette_info.h\"\n\nnamespace openage::renderer::resources {\n\nPaletteInfo::PaletteInfo(const std::vector<uint8_t> &colors) :\n\tcolors{} {\n\tfor (size_t i = 0; i < colors.size(); i += 4) {\n\t\tthis->colors.emplace_back(colors[i] / 256.0,\n\t\t                          colors[i + 1] / 256.0,\n\t\t                          colors[i + 2] / 256.0,\n\t\t                          colors[i + 3] / 256.0);\n\t}\n}\n\nPaletteInfo::PaletteInfo(const std::vector<Eigen::Vector4f> &colors) :\n\tcolors{colors} {}\n\nEigen::Vector4f PaletteInfo::get_color(size_t idx) {\n\treturn this->colors[idx];\n}\n\nconst std::vector<Eigen::Vector4f> PaletteInfo::get_colors() {\n\treturn this->colors;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/palette_info.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <vector>\n\n#include <eigen3/Eigen/Dense>\n\n\nnamespace openage::renderer::resources {\n\n/**\n * Contains information about a color palette.\n */\nclass PaletteInfo {\npublic:\n\t/**\n\t * Create a color palette.\n\t *\n\t * @param colors Sequence of RGBA values. Size must be a multiple of 4.\n\t */\n\tPaletteInfo(const std::vector<uint8_t> &colors);\n\n\t/**\n\t * Create a color palette.\n\t *\n\t * @param colors List of normalized RGBA colors.\n\t */\n\tPaletteInfo(const std::vector<Eigen::Vector4f> &colors);\n\n\tPaletteInfo() = default;\n\t~PaletteInfo() = default;\n\n\t/**\n\t * Get a color in the palette with a specified index.\n\t *\n\t * @return Normalized RGBA color vector.\n\t */\n\tEigen::Vector4f get_color(size_t idx);\n\n\t/**\n\t * Get the colors of the palette.\n\t *\n\t * @return List of normalized RGBA colors.\n\t */\n\tconst std::vector<Eigen::Vector4f> get_colors();\n\nprivate:\n\t/**\n\t * Colors inside the palette.\n\t */\n\tstd::vector<Eigen::Vector4f> colors;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/CMakeLists.txt",
    "content": "add_sources(libopenage\n    common.cpp\n    parse_blendmask.cpp\n    parse_blendtable.cpp\n    parse_palette.cpp\n    parse_sprite.cpp\n    parse_terrain.cpp\n    parse_texture.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/common.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"common.h\"\n\n#include <memory>\n\n\nnamespace openage::renderer::resources::parser {\n\nsize_t parse_version(const std::vector<std::string> &args) {\n\treturn std::stoul(args[1]);\n}\n\nTextureData parse_texture(const std::vector<std::string> &args) {\n\t// TODO: Splitting at the space char assumes that the path string contains no\n\t// space. While the space char is not allowed because of nyan naming requirements,\n\t// it should result in an error if wrongly used here.\n\tTextureData texture;\n\n\ttexture.texture_id = std::stoul(args[1]);\n\n\t// Call substr() to get rid of the quotes\n\ttexture.path = args[2].substr(1, args[2].size() - 2);\n\n\treturn texture;\n}\n\nfloat parse_scalefactor(const std::vector<std::string> &args) {\n\treturn std::stof(args[1]);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/common.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <string>\n#include <vector>\n\n/**\n * This file contains subparsers for arguments that work the\n * same across all formats.\n */\nnamespace openage::renderer::resources::parser {\n\n/**\n * Containers for raw data.\n */\n\n/**\n * Texture references.\n *\n * Used by:\n * .blmask: doc/media/openage/blendmask_format_spec.md.\n * .sprite: doc/media/openage/sprite_format_spec.md.\n * .terrain: doc/media/openage/terrain_format_spec.md.\n */\nstruct TextureData {\n\tsize_t texture_id;\n\tstd::string path;\n};\n\n/**\n * Subparsers for arguments.\n */\n\n/**\n * Parse the file version attribute.\n *\n * @param args Arguments from the line with a \\p version attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Version number.\n */\nsize_t parse_version(const std::vector<std::string> &args);\n\n/**\n * Parse the texture attribute.\n *\n * @param args Arguments from the line with a \\p texture attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nTextureData parse_texture(const std::vector<std::string> &args);\n\n/**\n * Parse the scalefactor attribute.\n *\n * @param args Arguments from the line with a \\p scalefactor attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Scalefactor value.\n */\nfloat parse_scalefactor(const std::vector<std::string> &args);\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_blendmask.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_blendmask.h\"\n\n#include <algorithm>\n#include <cstddef>\n#include <functional>\n#include <string>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"log/message.h\"\n\n#include \"error/error.h\"\n#include \"renderer/resources/assets/cache.h\"\n#include \"renderer/resources/parser/common.h\"\n#include \"renderer/resources/parser/parse_texture.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/**\n * Parse the mask attribute.\n *\n * @param args Arguments from the line with a \\p mask attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nblending_mask parse_mask(const std::vector<std::string> &args) {\n\tblending_mask mask;\n\n\tauto dir = 0;\n\tif (args[0].starts_with(\"0b\")) {\n\t\t// discard prefix because std::stoul doesn't understand binary prefixes\n\t\tstd::string strip = args[0].substr(2, args[2].size() - 1);\n\t\tdir = std::stoul(strip, nullptr, 2);\n\t}\n\telse {\n\t\tdir = std::stoul(args[0]);\n\t}\n\n\tif (dir > 255) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .blmask file failed. Reason: \"\n\t\t                     << \"directions value \" << dir << \" is too large\");\n\t}\n\n\tmask.directions = dir;\n\tmask.texture_id = std::stoul(args[1]);\n\tmask.subtex_id = std::stoul(args[2]);\n\n\treturn mask;\n}\n\nBlendPatternInfo parse_blendmask_file(const util::Path &path,\n                                      const std::shared_ptr<AssetCache> &cache) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .blmask file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tfloat scalefactor = 1.0;\n\tstd::vector<TextureData> textures;\n\tstd::vector<blending_mask> masks;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 2) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .blmask file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"texture\", [&](const std::vector<std::string> &args) {\n\t\t\ttextures.push_back(parse_texture(args));\n\t\t}),\n\t\tstd::make_pair(\"scalefactor\", [&](const std::vector<std::string> &args) {\n\t\t\tscalefactor = parse_scalefactor(args);\n\t\t}),\n\t\tstd::make_pair(\"mask\", [&](const std::vector<std::string> &args) {\n\t\t\tmasks.push_back(parse_mask(args));\n\t\t})};\n\n\tfor (auto line : lines) {\n\t\t// Skip empty lines and comments\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .blmask file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[args[0]](args);\n\t}\n\n\t// Order masks by directions value\n\tstd::sort(masks.begin(),\n\t          masks.end(),\n\t          [](blending_mask &m1, blending_mask &m2) {\n\t\t\t\t  return m1.directions < m2.directions;\n\t\t\t  });\n\n\t// Create ID map. Resolves IDs used in the file to array indices\n\tstd::unordered_map<size_t, size_t> texture_id_map;\n\tfor (size_t i = 0; i < textures.size(); ++i) {\n\t\ttexture_id_map.insert(std::make_pair(textures[i].texture_id, i));\n\t}\n\n\t// Parse textures\n\tstd::vector<std::shared_ptr<Texture2dInfo>> texture_infos;\n\tfor (auto texture : textures) {\n\t\tutil::Path texturepath = (path.get_parent() / texture.path);\n\n\t\tif (cache && cache->check_texture_cache(texturepath)) {\n\t\t\t// already loaded\n\t\t\ttexture_infos.push_back(cache->get_texture(texturepath));\n\t\t}\n\t\telse {\n\t\t\t// load (and cache if possible)\n\t\t\tauto info = std::make_shared<Texture2dInfo>(parse_texture_file(texturepath));\n\t\t\ttexture_infos.push_back(info);\n\t\t\tif (cache) {\n\t\t\t\tcache->add_texture(texturepath, info);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn BlendPatternInfo(scalefactor, texture_infos, masks);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_blendmask.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include \"renderer/resources/terrain/blendpattern_info.h\"\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources {\nclass AssetCache;\n\nnamespace parser {\n\n/**\n * Parse an blending table definition from a .blmask format file.\n *\n * @param file Path to the blendmask file.\n * @param cache Cache of already loaded assets (optional).\n *\n * @return The corresponding blendmask definition.\n */\nBlendPatternInfo parse_blendmask_file(const util::Path &file,\n                                      const std::shared_ptr<AssetCache> &cache = nullptr);\n\n} // namespace parser\n} // namespace renderer::resources\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_blendtable.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_blendtable.h\"\n\n#include <functional>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/assets/cache.h\"\n#include \"renderer/resources/parser/common.h\"\n#include \"renderer/resources/parser/parse_blendmask.h\"\n#include \"renderer/resources/terrain/blendpattern_info.h\"\n#include \"renderer/resources/terrain/blendtable_info.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/**\n * Parse the blendtable attribute.\n *\n * @param args Lines composing the matrix from a \\p blendtable attribute.\n *             The first line is expected to be the line containing attribute keyword.\n *             The last line is expected to be the line containing the closing bracket.\n *\n * @return Struct containing the attribute data.\n */\nstd::vector<size_t> parse_table(const std::vector<std::string> &lines) {\n\tstd::vector<size_t> entries{};\n\n\tfor (size_t i = 1; i < lines.size() - 1; ++i) {\n\t\tstd::vector<std::string> line_args{util::split(lines[i], ' ')};\n\t\tfor (auto arg : line_args) {\n\t\t\tentries.push_back(std::stoul(arg));\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Parse the pattern attribute.\n *\n * @param args Arguments from the line with a \\p pattern attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nPatternData parse_pattern(const std::vector<std::string> &args) {\n\t// TODO: Splitting at the space char assumes that the path string contains no\n\t// space. While the space char is not allowed because of nyan naming requirements,\n\t// it should result in an error if wrongly used here.\n\tPatternData pattern;\n\n\tpattern.pattern_id = std::stoul(args[1]);\n\n\t// Call substr() to get rid of the quotes\n\tpattern.path = args[2].substr(1, args[2].size() - 2);\n\n\treturn pattern;\n}\n\nBlendTableInfo parse_blendtable_file(const util::Path &path,\n                                     const std::shared_ptr<AssetCache> &cache) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .bltable file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tstd::vector<size_t> blendtable;\n\tstd::vector<PatternData> patterns;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 1) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .bltable file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"blendtable\", [&](const std::vector<std::string> &args) {\n\t\t\tblendtable = parse_table(args);\n\t\t}),\n\t\tstd::make_pair(\"pattern\", [&](const std::vector<std::string> &args) {\n\t\t\tpatterns.push_back(parse_pattern(args));\n\t\t})};\n\n\tfor (size_t i = 0; i < lines.size(); ++i) {\n\t\t// Skip empty lines and comments\n\t\tauto line = lines[i];\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .bltable file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\n\t\tif (args[0] == \"blendtable\") {\n\t\t\t// read all lines of the matrix\n\t\t\t// TODO: better parsing for matrix values\n\t\t\tstd::vector<std::string> mat_lines{};\n\t\t\tmat_lines.push_back(line);\n\n\t\t\twhile (not line.starts_with(\"]\")) {\n\t\t\t\ti += 1;\n\t\t\t\tif (i >= lines.size()) {\n\t\t\t\t\tthrow Error(MSG(err) << \"Reading .bltable file '\"\n\t\t\t\t\t                     << path.get_name()\n\t\t\t\t\t                     << \"' failed. Reason: Matrix for keyword \"\n\t\t\t\t\t                     << args[0] << \" is malformed\");\n\t\t\t\t}\n\t\t\t\tline = lines[i];\n\t\t\t\tmat_lines.push_back(line);\n\t\t\t}\n\n\t\t\tkeywordfuncs[args[0]](mat_lines);\n\t\t}\n\t\telse {\n\t\t\tkeywordfuncs[args[0]](args);\n\t\t}\n\t}\n\n\tstd::vector<std::shared_ptr<BlendPatternInfo>> pattern_infos;\n\tfor (auto pattern : patterns) {\n\t\tutil::Path maskpath = (path.get_parent() / pattern.path);\n\n\t\tif (cache && cache->check_blpattern_cache(maskpath)) {\n\t\t\t// already loaded\n\t\t\tpattern_infos.push_back(cache->get_blpattern(maskpath));\n\t\t}\n\t\telse {\n\t\t\t// load (and cache if possible)\n\t\t\tauto info = std::make_shared<BlendPatternInfo>(parse_blendmask_file(maskpath));\n\t\t\tpattern_infos.push_back(info);\n\t\t\tif (cache) {\n\t\t\t\tcache->add_blpattern(maskpath, info);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn BlendTableInfo(blendtable, pattern_infos);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_blendtable.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <string>\n\n#include \"renderer/resources/terrain/blendtable_info.h\"\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources {\nclass AssetCache;\n\nnamespace parser {\n\n/**\n * Containers for the raw data.\n *\n * Definition according to doc/media/openage/blendtable_format_spec.md.\n */\nstruct PatternData {\n\tsize_t pattern_id;\n\tstd::string path;\n};\n\n/**\n * Parse an blending table definition from a .bltable format file.\n *\n * @param file Path to the blendtable file.\n * @param cache Cache of already loaded assets (optional).\n *\n * @return The corresponding blendtable definition.\n */\nBlendTableInfo parse_blendtable_file(const util::Path &file,\n                                     const std::shared_ptr<AssetCache> &cache = nullptr);\n\n} // namespace parser\n} // namespace renderer::resources\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_palette.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_palette.h\"\n\n#include <cstddef>\n#include <cstdint>\n#include <functional>\n#include <string>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/parser/common.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/**\n * Parse the entries attribute.\n *\n * @param args Arguments from the line with a \\p entries attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Version number.\n */\nsize_t parse_entries(const std::vector<std::string> &args) {\n\treturn std::stoul(args[1]);\n}\n\n/**\n * Parse the colours attribute.\n *\n * @param args Lines composing the matrix from a \\p colours attribute.\n *             The first line is expected to be the line containing attribute keyword.\n *             The last line is expected to be the line containing the closing bracket.\n *\n * @return Struct containing the attribute data.\n */\nstd::vector<uint8_t> parse_colours(const std::vector<std::string> &lines) {\n\tstd::vector<uint8_t> entries{};\n\n\tfor (size_t i = 1; i < lines.size() - 1; ++i) {\n\t\tstd::vector<std::string> line_args{util::split(lines[i], ' ')};\n\t\tfor (auto arg : line_args) {\n\t\t\tauto channel = std::stoul(arg);\n\n\t\t\tif (channel > 255) [[unlikely]] {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .opal file failed. Reason: \"\n\t\t\t\t                     << \"color channel value \" << channel << \" is too large\");\n\t\t\t}\n\n\t\t\tentries.push_back(channel);\n\t\t}\n\t}\n\n\treturn entries;\n}\n\nPaletteInfo parse_palette_file(const util::Path &path) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .opal file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tsize_t entries = 0;\n\tstd::vector<uint8_t> colours;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 1) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .opal file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"entries\", [&](const std::vector<std::string> &args) {\n\t\t\tentries = parse_entries(args);\n\t\t}),\n\t\tstd::make_pair(\"colours\", [&](const std::vector<std::string> &args) {\n\t\t\tcolours = parse_colours(args);\n\t\t})};\n\n\tfor (size_t i = 0; i < lines.size(); ++i) {\n\t\t// Skip empty lines and comments\n\t\tauto line = lines[i];\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .opal file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\n\t\tif (args[0] == \"colours\") {\n\t\t\t// read all lines of the matrix\n\t\t\t// TODO: better parsing for matrix values\n\t\t\tstd::vector<std::string> mat_lines{};\n\t\t\tmat_lines.push_back(line);\n\n\t\t\twhile (not line.starts_with(\"]\")) {\n\t\t\t\ti += 1;\n\t\t\t\tif (i >= lines.size()) {\n\t\t\t\t\tthrow Error(MSG(err) << \"Reading .opal file '\"\n\t\t\t\t\t                     << path.get_name()\n\t\t\t\t\t                     << \"' failed. Reason: Matrix for keyword \"\n\t\t\t\t\t                     << args[0] << \" is malformed\");\n\t\t\t\t}\n\t\t\t\tline = lines[i];\n\t\t\t\tmat_lines.push_back(line);\n\t\t\t}\n\n\t\t\tkeywordfuncs[args[0]](mat_lines);\n\t\t}\n\t\telse {\n\t\t\tkeywordfuncs[args[0]](args);\n\t\t}\n\t}\n\n\treturn PaletteInfo(colours);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_palette.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"renderer/resources/palette_info.h\"\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources::parser {\n\n/**\n * Parse an palette definition from a .opal format file.\n *\n * @param file Path to the palette file.\n *\n * @return The corresponding palette definition.\n */\nPaletteInfo parse_palette_file(const util::Path &file);\n\n} // namespace renderer::resources::parser\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_sprite.cpp",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_sprite.h\"\n\n#include <algorithm>\n#include <functional>\n#include <string>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/animation/layer_info.h\"\n#include \"renderer/resources/assets/cache.h\"\n#include \"renderer/resources/parser/common.h\"\n#include \"renderer/resources/parser/parse_texture.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/**\n * Parse the layer attribute.\n *\n * @param args Arguments from the line with a \\p layer attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nLayerData parse_layer(const std::vector<std::string> &args) {\n\tLayerData layer;\n\n\tlayer.layer_id = std::stoul(args[1]);\n\n\t// Optional arguments\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(std::vector<std::string>)>>{\n\t\tstd::make_pair(\"mode\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tif (keywordargs[1] == \"off\") {\n\t\t\t\tlayer.mode = display_mode::OFF;\n\t\t\t}\n\t\t\telse if (keywordargs[1] == \"once\") {\n\t\t\t\tlayer.mode = display_mode::ONCE;\n\t\t\t}\n\t\t\telse if (keywordargs[1] == \"loop\") {\n\t\t\t\tlayer.mode = display_mode::LOOP;\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"position\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.position = std::stoul(keywordargs[1]);\n\t\t}),\n\t\tstd::make_pair(\"time_per_frame\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.time_per_frame = std::stof(keywordargs[1]);\n\t\t}),\n\t\tstd::make_pair(\"replay_delay\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.replay_delay = std::stof(keywordargs[1]);\n\t\t})};\n\n\tfor (size_t i = 2; i < args.size(); ++i) {\n\t\tstd::vector<std::string> keywordargs{util::split(args[i], '=')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(keywordargs[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Keyword argument \"\n\t\t\t                     << keywordargs[0]\n\t\t\t                     << \" of 'layer' attribute is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[keywordargs[0]](keywordargs);\n\t}\n\n\treturn layer;\n}\n\n/**\n * Parse the angle attribute.\n *\n * @param args Arguments from the line with a \\p angle attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nAngleData parse_angle(const std::vector<std::string> &args) {\n\tAngleData angle;\n\n\tangle.degree = std::stoul(args[1]);\n\n\t// Optional arguments\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(std::vector<std::string>)>>{\n\t\tstd::make_pair(\"mirror_from\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tangle.mirror_from = std::stoul(keywordargs[1]);\n\t\t})};\n\n\tfor (size_t i = 2; i < args.size(); ++i) {\n\t\tstd::vector<std::string> keywordargs{util::split(args[i], '=')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(keywordargs[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Keyword argument \"\n\t\t\t                     << keywordargs[0]\n\t\t\t                     << \" of 'angle' attribute is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[keywordargs[0]](keywordargs);\n\t}\n\n\treturn angle;\n}\n\n/**\n * Parse the frame attribute.\n *\n * @param args Arguments from the line with a \\p frame attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nFrameData parse_frame(const std::vector<std::string> &args) {\n\tFrameData frame;\n\n\tframe.index = std::stoul(args[1]);\n\tframe.angle = std::stoul(args[2]);\n\tframe.layer_id = std::stoul(args[3]);\n\tframe.texture_id = std::stoul(args[4]);\n\tframe.subtex_id = std::stoul(args[5]);\n\n\treturn frame;\n}\n\nAnimation2dInfo parse_sprite_file(const util::Path &path,\n                                  const std::shared_ptr<AssetCache> &cache) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .sprite file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tfloat scalefactor = 1.0;\n\tstd::vector<TextureData> textures;\n\tstd::vector<LayerData> layers;\n\tstd::vector<AngleData> angles;\n\n\t// largest frame index = total length of animation\n\tsize_t largest_frame_idx = 0;\n\n\t// Map frame data to angle\n\tstd::unordered_map<size_t, std::vector<FrameData>> frames;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 2) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .sprite file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"texture\", [&](const std::vector<std::string> &args) {\n\t\t\ttextures.push_back(parse_texture(args));\n\t\t}),\n\t\tstd::make_pair(\"scalefactor\", [&](const std::vector<std::string> &args) {\n\t\t\tscalefactor = parse_scalefactor(args);\n\t\t}),\n\t\tstd::make_pair(\"layer\", [&](const std::vector<std::string> &args) {\n\t\t\tlayers.push_back(parse_layer(args));\n\t\t}),\n\t\tstd::make_pair(\"angle\", [&](const std::vector<std::string> &args) {\n\t\t\tangles.push_back(parse_angle(args));\n\t\t}),\n\t\tstd::make_pair(\"frame\", [&](const std::vector<std::string> &args) {\n\t\t\tauto frame = parse_frame(args);\n\t\t\tif (frames.count(frame.angle) == 0) {\n\t\t\t\tstd::vector<FrameData> angle_frames{};\n\t\t\t\tframes.emplace(std::make_pair(frame.angle, angle_frames));\n\t\t\t}\n\t\t\tframes.at(frame.angle).push_back(frame);\n\n\t\t\t// check for the largest index, so we can use it to\n\t\t    // interpolate the total animation length\n\t\t\tif (frame.index > largest_frame_idx) {\n\t\t\t\tlargest_frame_idx = frame.index;\n\t\t\t}\n\t\t})};\n\n\tfor (auto line : lines) {\n\t\t// Skip empty lines and comments\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .sprite file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[args[0]](args);\n\t}\n\n\t// Order frames by index\n\tfor (auto angle_frames : frames) {\n\t\tstd::sort(angle_frames.second.begin(),\n\t\t          angle_frames.second.end(),\n\t\t          [](FrameData &f1, FrameData &f2) {\n\t\t\t\t\t  return f1.index < f2.index;\n\t\t\t\t  });\n\t}\n\n\t// Order angles by degree\n\tstd::sort(angles.begin(),\n\t          angles.end(),\n\t          [](AngleData &a1, AngleData &a2) {\n\t\t\t\t  return a1.degree < a2.degree;\n\t\t\t  });\n\n\t// Order layers by position\n\tstd::sort(layers.begin(),\n\t          layers.end(),\n\t          [](LayerData &l1, LayerData &l2) {\n\t\t\t\t  return l1.position < l2.position;\n\t\t\t  });\n\n\t// Create ID map. Resolves IDs used in the file to array indices\n\tstd::unordered_map<size_t, size_t> texture_id_map;\n\tfor (size_t i = 0; i < textures.size(); ++i) {\n\t\ttexture_id_map.insert(std::make_pair(textures.at(i).texture_id, i));\n\t}\n\tstd::unordered_map<size_t, size_t> angle_id_map;\n\tfor (size_t i = 0; i < angles.size(); ++i) {\n\t\tangle_id_map.insert(std::make_pair(angles.at(i).degree, i));\n\t}\n\n\tstd::vector<LayerInfo> layer_infos;\n\tfor (auto layer : layers) {\n\t\tstd::vector<std::shared_ptr<AngleInfo>> angle_infos;\n\n\t\t// degree in the file is the angle center\n\t\t// we have to calculate the angle start\n\t\t// (between the angle center and the previous angle's center)\n\t\tfloat prev_angle_degree = angles.at(angles.size() - 1).degree;\n\t\tfloat angle_start = 0;\n\n\t\tfor (auto angle : angles) {\n\t\t\tstd::vector<std::shared_ptr<FrameInfo>> frame_infos;\n\n\t\t\t// get the degree where the angle starts\n\t\t\tif (prev_angle_degree > angle.degree) {\n\t\t\t\t// when previous angle > current angle, it wraps around at 360 degrees\n\t\t\t\tangle_start = prev_angle_degree + (360 + angle.degree - prev_angle_degree) / 2;\n\t\t\t\tif (angle_start >= 360) {\n\t\t\t\t\t// ensure that result is in 360 degrees range\n\t\t\t\t\tangle_start -= 360;\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tangle_start = prev_angle_degree + (angle.degree - prev_angle_degree) / 2;\n\t\t\t}\n\t\t\tprev_angle_degree = angle.degree;\n\n\t\t\tif (angle.mirror_from >= 0) {\n\t\t\t\t// we can exit early if the angle is mirrored\n\t\t\t\t// because we don't need to store the frames\n\t\t\t\tauto angle_info_ptr = angle_infos.at(angle_id_map.at(angle.mirror_from));\n\t\t\t\tangle_infos.push_back(std::make_shared<AngleInfo>(angle_start,\n\t\t\t\t                                                  frame_infos,\n\t\t\t\t                                                  angle_info_ptr,\n\t\t\t\t                                                  flip_type::FLIP_X));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (auto frame : frames.at(angle.degree)) {\n\t\t\t\tif (frame.layer_id != layer.layer_id) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (frame.index > frame_infos.size()) {\n\t\t\t\t\t// set empty frames if an index is missing\n\t\t\t\t\tfor (size_t i = frame_infos.size() - 1; i < frame.index; ++i) {\n\t\t\t\t\t\tframe_infos.push_back(nullptr);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tframe_infos.push_back(std::make_shared<FrameInfo>(\n\t\t\t\t\ttexture_id_map.at(frame.texture_id),\n\t\t\t\t\tframe.subtex_id));\n\t\t\t}\n\t\t\tif (frame_infos.size() < largest_frame_idx) {\n\t\t\t\t// insert empty frames at the end if an indices are missing\n\t\t\t\tfor (size_t i = frame_infos.size() - 1; i < largest_frame_idx; ++i) {\n\t\t\t\t\tframe_infos.push_back(nullptr);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tangle_infos.push_back(std::make_shared<AngleInfo>(angle_start, frame_infos));\n\t\t}\n\n\t\tlayer_infos.emplace_back(angle_infos,\n\t\t                         layer.mode,\n\t\t                         layer.position,\n\t\t                         layer.time_per_frame,\n\t\t                         layer.replay_delay);\n\t}\n\n\t// Parse textures\n\tstd::vector<std::shared_ptr<Texture2dInfo>> texture_infos;\n\tfor (auto texture : textures) {\n\t\tutil::Path texturepath = (path.get_parent() / texture.path);\n\n\t\tif (cache && cache->check_texture_cache(texturepath)) {\n\t\t\t// already loaded\n\t\t\ttexture_infos.push_back(cache->get_texture(texturepath));\n\t\t}\n\t\telse {\n\t\t\t// load (and cache if possible)\n\t\t\tauto info = std::make_shared<Texture2dInfo>(parse_texture_file(texturepath));\n\t\t\ttexture_infos.push_back(info);\n\t\t\tif (cache) {\n\t\t\t\tcache->add_texture(texturepath, info);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn Animation2dInfo(scalefactor, texture_infos, layer_infos);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_sprite.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n\n#include \"renderer/resources/animation/animation_info.h\"\n#include \"renderer/resources/animation/layer_info.h\"\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources {\nclass AssetCache;\n\nnamespace parser {\n\n/**\n * Containers for the raw data.\n *\n * Definition according to doc/media/openage/sprite_format_spec.md.\n */\nstruct LayerData {\n\tsize_t layer_id;\n\tdisplay_mode mode{display_mode::OFF};\n\tsize_t position{0};\n\tfloat time_per_frame{0.0};\n\tfloat replay_delay{0.0};\n};\n\nstruct AngleData {\n\tsize_t degree;\n\tint mirror_from{-1};\n};\n\nstruct FrameData {\n\tsize_t index;\n\tsize_t angle;\n\tsize_t layer_id;\n\tsize_t texture_id;\n\tsize_t subtex_id;\n};\n\n/**\n * Parse an Animation2d definition from a .sprite format file.\n *\n * @param file Path to the sprite file.\n * @param cache Cache of already loaded assets (optional).\n *\n * @return The corresponding animation definition.\n */\nAnimation2dInfo parse_sprite_file(const util::Path &file,\n                                  const std::shared_ptr<AssetCache> &cache = nullptr);\n\n} // namespace parser\n} // namespace renderer::resources\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_terrain.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_terrain.h\"\n\n#include <algorithm>\n#include <cstddef>\n#include <functional>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/assets/cache.h\"\n#include \"renderer/resources/parser/common.h\"\n#include \"renderer/resources/parser/parse_blendtable.h\"\n#include \"renderer/resources/parser/parse_texture.h\"\n#include \"renderer/resources/terrain/blendtable_info.h\"\n#include \"renderer/resources/terrain/frame_info.h\"\n#include \"renderer/resources/terrain/layer_info.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/**\n * Parse the blendtable attribute.\n *\n * @param args Arguments from the line with a \\p blendtable attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nBlendtableData parse_blendtable(const std::vector<std::string> &args) {\n\t// TODO: Splitting at the space char assumes that the path string contains no\n\t// space. While the space char is not allowed because of nyan naming requirements,\n\t// it should result in an error if wrongly used here.\n\tBlendtableData blendtable;\n\n\tblendtable.table_id = std::stoul(args[1]);\n\n\t// Call substr() to get rid of the quotes\n\tblendtable.path = args[2].substr(1, args[2].size() - 2);\n\n\treturn blendtable;\n}\n\n/**\n * Parse the layer attribute.\n *\n * @param args Arguments from the line with a \\p layer attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nTerrainLayerData parse_terrain_layer(const std::vector<std::string> &args) {\n\tTerrainLayerData layer;\n\n\tlayer.layer_id = std::stoul(args[1]);\n\n\t// Optional arguments\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"mode\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tif (keywordargs[1] == \"off\") {\n\t\t\t\tlayer.mode = terrain_display_mode::OFF;\n\t\t\t}\n\t\t\telse if (keywordargs[1] == \"loop\") {\n\t\t\t\tlayer.mode = terrain_display_mode::LOOP;\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"position\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.position = std::stoul(keywordargs[1]);\n\t\t}),\n\t\tstd::make_pair(\"time_per_frame\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.time_per_frame = std::stof(keywordargs[1]);\n\t\t}),\n\t\tstd::make_pair(\"replay_delay\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tlayer.replay_delay = std::stof(keywordargs[1]);\n\t\t})};\n\n\tfor (size_t i = 2; i < args.size(); ++i) {\n\t\tstd::vector<std::string> keywordargs{util::split(args[i], '=')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(keywordargs[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Keyword argument \"\n\t\t\t                     << keywordargs[0]\n\t\t\t                     << \" of 'layer' attribute is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[keywordargs[0]](keywordargs);\n\t}\n\n\treturn layer;\n}\n\n/**\n * Parse the frame attribute.\n *\n * @param args Arguments from the line with a \\p frame attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nTerrainFrameData parse_terrain_frame(const std::vector<std::string> &args) {\n\tTerrainFrameData frame;\n\n\tframe.index = std::stoul(args[1]);\n\tframe.layer_id = std::stoul(args[2]);\n\tframe.texture_id = std::stoul(args[3]);\n\tframe.subtex_id = std::stoul(args[4]);\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"priority\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tframe.priority = std::stoul(keywordargs[1]);\n\t\t}),\n\t\tstd::make_pair(\"blend_mode\", [&](const std::vector<std::string> &keywordargs) {\n\t\t\tframe.blend_mode = std::stoul(keywordargs[1]);\n\t\t})};\n\n\tfor (size_t i = 4; i < args.size() - 1; ++i) {\n\t\tstd::vector<std::string> keywordargs{util::split(args[i], '=')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(keywordargs[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Keyword argument \"\n\t\t\t                     << keywordargs[0]\n\t\t\t                     << \" of 'frame' attribute is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[keywordargs[0]](keywordargs);\n\t}\n\n\treturn frame;\n}\n\nTerrainInfo parse_terrain_file(const util::Path &path,\n                               const std::shared_ptr<AssetCache> &cache) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .terrain file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tfloat scalefactor = 1.0;\n\tstd::vector<TextureData> textures;\n\tstd::optional<BlendtableData> blendtable;\n\tstd::vector<TerrainLayerData> layers;\n\n\t// largest frame index = total length of animation\n\tsize_t largest_frame_idx = 0;\n\n\t// Map frame data to layer\n\tstd::unordered_map<size_t, std::vector<TerrainFrameData>> frames;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 2) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .terrain file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"texture\", [&](const std::vector<std::string> &args) {\n\t\t\ttextures.push_back(parse_texture(args));\n\t\t}),\n\t\tstd::make_pair(\"blendtable\", [&](const std::vector<std::string> &args) {\n\t\t\tblendtable = parse_blendtable(args);\n\t\t}),\n\t\tstd::make_pair(\"scalefactor\", [&](const std::vector<std::string> &args) {\n\t\t\tscalefactor = parse_scalefactor(args);\n\t\t}),\n\t\tstd::make_pair(\"layer\", [&](const std::vector<std::string> &args) {\n\t\t\tlayers.push_back(parse_terrain_layer(args));\n\t\t}),\n\t\tstd::make_pair(\"frame\", [&](const std::vector<std::string> &args) {\n\t\t\tauto frame = parse_terrain_frame(args);\n\t\t\tif (frames.count(frame.layer_id) == 0) {\n\t\t\t\tstd::vector<TerrainFrameData> layer_frames{};\n\t\t\t\tframes.emplace(std::make_pair(frame.layer_id, layer_frames));\n\t\t\t}\n\t\t\tframes.at(frame.layer_id).push_back(frame);\n\n\t\t\t// check for the largest index, so we can use it to\n\t\t    // interpolate the total animation length\n\t\t\tif (frame.index > largest_frame_idx) {\n\t\t\t\tlargest_frame_idx = frame.index;\n\t\t\t}\n\t\t})};\n\n\tfor (auto line : lines) {\n\t\t// Skip empty lines and comments\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .terrain file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[args[0]](args);\n\t}\n\n\t// Order frames by index\n\tfor (auto angle_frames : frames) {\n\t\tstd::sort(angle_frames.second.begin(),\n\t\t          angle_frames.second.end(),\n\t\t          [](TerrainFrameData &f1, TerrainFrameData &f2) {\n\t\t\t\t\t  return f1.index < f2.index;\n\t\t\t\t  });\n\t}\n\n\t// Order layers by position\n\tstd::sort(layers.begin(),\n\t          layers.end(),\n\t          [](TerrainLayerData &l1, TerrainLayerData &l2) {\n\t\t\t\t  return l1.position < l2.position;\n\t\t\t  });\n\n\t// Create ID map. Resolves IDs used in the file to array indices\n\tstd::unordered_map<size_t, size_t> texture_id_map;\n\tfor (size_t i = 0; i < textures.size(); ++i) {\n\t\ttexture_id_map.insert(std::make_pair(textures[i].texture_id, i));\n\t}\n\n\tstd::vector<TerrainLayerInfo> layer_infos;\n\tfor (auto layer : layers) {\n\t\tstd::vector<std::shared_ptr<TerrainFrameInfo>> frame_infos;\n\t\tfor (auto frame : frames[layer.layer_id]) {\n\t\t\tif (frame.index > frame_infos.size()) {\n\t\t\t\t// set empty frames if an index is missing\n\t\t\t\tfor (size_t i = frame_infos.size() - 1; i < frame.index; ++i) {\n\t\t\t\t\tframe_infos.push_back(nullptr);\n\t\t\t\t}\n\t\t\t}\n\t\t\tframe_infos.push_back(std::make_shared<TerrainFrameInfo>(\n\t\t\t\ttexture_id_map[frame.texture_id],\n\t\t\t\tframe.subtex_id));\n\t\t}\n\t\tif (frame_infos.size() < largest_frame_idx) {\n\t\t\t// insert empty frames at the end if an indices are missing\n\t\t\tfor (size_t i = frame_infos.size() - 1; i < largest_frame_idx; ++i) {\n\t\t\t\tframe_infos.push_back(nullptr);\n\t\t\t}\n\t\t}\n\t\tlayer_infos.emplace_back(frame_infos,\n\t\t                         layer.mode,\n\t\t                         layer.position,\n\t\t                         layer.time_per_frame,\n\t\t                         layer.replay_delay);\n\t}\n\n\t// Parse textures\n\tstd::vector<std::shared_ptr<Texture2dInfo>> texture_infos;\n\tfor (auto texture : textures) {\n\t\tutil::Path texturepath = (path.get_parent() / texture.path);\n\n\t\tif (cache && cache->check_texture_cache(texturepath)) {\n\t\t\t// already loaded\n\t\t\ttexture_infos.push_back(cache->get_texture(texturepath));\n\t\t}\n\t\telse {\n\t\t\t// load (and cache if possible)\n\t\t\tauto info = std::make_shared<Texture2dInfo>(parse_texture_file(texturepath));\n\t\t\ttexture_infos.push_back(info);\n\t\t\tif (cache) {\n\t\t\t\tcache->add_texture(texturepath, info);\n\t\t\t}\n\t\t}\n\t}\n\n\tstd::shared_ptr<BlendTableInfo> blendtable_info;\n\tif (blendtable) {\n\t\tutil::Path tablepath = (path.get_parent() / blendtable.value().path);\n\n\t\tif (cache && cache->check_bltable_cache(tablepath)) {\n\t\t\t// already loaded\n\t\t\tblendtable_info = cache->get_bltable(tablepath);\n\t\t}\n\t\telse {\n\t\t\t// load (and cache if possible)\n\t\t\tblendtable_info = std::make_shared<BlendTableInfo>(parse_blendtable_file(tablepath));\n\t\t\tif (cache) {\n\t\t\t\tcache->add_bltable(tablepath, blendtable_info);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn TerrainInfo(scalefactor, texture_infos, layer_infos, blendtable_info);\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_terrain.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <optional>\n#include <string>\n\n#include \"renderer/resources/terrain/layer_info.h\"\n#include \"renderer/resources/terrain/terrain_info.h\"\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources {\nclass AssetCache;\n\nnamespace parser {\n\n/**\n * Containers for the raw data.\n *\n * Definition according to doc/media/openage/terrain_format_spec.md.\n */\nstruct BlendtableData {\n\tsize_t table_id;\n\tstd::string path;\n};\n\nstruct TerrainLayerData {\n\tsize_t layer_id;\n\tterrain_display_mode mode{terrain_display_mode::OFF};\n\tsize_t position{0};\n\tfloat time_per_frame{0.0};\n\tfloat replay_delay{0.0};\n};\n\nstruct TerrainFrameData {\n\tsize_t index;\n\tsize_t layer_id;\n\tsize_t texture_id;\n\tsize_t subtex_id;\n\tsize_t priority;\n\tstd::optional<size_t> blend_mode;\n};\n\n/**\n * Parse an Terrain definition from a .terrain format file.\n *\n * @param file Path to the terrain file.\n * @param cache Cache of already loaded assets (optional).\n *\n * @return The corresponding terrain definition.\n */\nTerrainInfo parse_terrain_file(const util::Path &file,\n                               const std::shared_ptr<AssetCache> &cache = nullptr);\n\n} // namespace parser\n} // namespace renderer::resources\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_texture.cpp",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#include \"parse_texture.h\"\n\n#include <functional>\n#include <memory>\n#include <optional>\n#include <string>\n#include <type_traits>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"renderer/resources/parser/common.h\"\n#include \"renderer/resources/texture_subinfo.h\"\n#include \"util/file.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::resources::parser {\n\n/// Tries to guess the alignment of image rows based on image parameters. Kinda\n/// black magic and might not actually work.\n/// @param width in pixels of the image\nstatic constexpr size_t guess_row_alignment(size_t width) {\n\t// Use the highest possible alignment for even-width images.\n\tif (width % 8 == 0) {\n\t\treturn 8;\n\t}\n\tif (width % 4 == 0) {\n\t\treturn 4;\n\t}\n\tif (width % 2 == 0) {\n\t\treturn 2;\n\t}\n\n\t// Bail with a sane value.\n\treturn 4;\n}\n\n/**\n * Parse the imagefile attribute.\n *\n * @param args Arguments from the line with a \\p imagefile attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Path to the image resource.\n */\nstd::string parse_imagefile(const std::vector<std::string> &args) {\n\t// TODO: Splitting at the space char assumes that the path string contains no\n\t// space. While the space char is not allowed because of nyan naming requirements,\n\t// it should result in an error if wrongly used here.\n\n\t// Call substr() to get rid of the quotes\n\treturn args[1].substr(1, args[1].size() - 2);\n}\n\n/**\n * Parse the size attribute.\n *\n * @param args Arguments from the line with a \\p size attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nSizeData parse_size(const std::vector<std::string> &args) {\n\tSizeData size;\n\n\tsize.width = std::stoul(args[1]);\n\tsize.height = std::stoul(args[2]);\n\n\treturn size;\n}\n\n/**\n * Parse the pxformat attribute.\n *\n * @param args Arguments from the line with a \\p pxformat attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nPixelFormatData parse_pxformat(const std::vector<std::string> &args) {\n\tPixelFormatData pxformat;\n\n\t// Only accepted format\n\tpxformat.format = pixel_format::rgba8;\n\n\t// Optional arguments\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(std::vector<std::string>)>>{\n\t\tstd::make_pair(\"cbits\", [&](std::vector<std::string> keywordargs) {\n\t\t\tif (keywordargs[1] == \"True\") {\n\t\t\t\tpxformat.cbits = true;\n\t\t\t}\n\t\t\telse if (keywordargs[1] == \"False\") {\n\t\t\t\tpxformat.cbits = false;\n\t\t\t}\n\t\t})};\n\n\t// Optional arguments\n\tfor (size_t i = 2; i < args.size(); ++i) {\n\t\tstd::vector<std::string> keywordargs{util::split(args[i], '=')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(keywordargs[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Keyword argument \"\n\t\t\t                     << keywordargs[0]\n\t\t\t                     << \" of 'pxformat' attribute is not defined\");\n\t\t}\n\n\t\tkeywordfuncs[keywordargs[0]](keywordargs);\n\t}\n\n\treturn pxformat;\n}\n\n/**\n * Parse the subtex attribute.\n *\n * @param args Arguments from the line with a \\p subtex attribute.\n *             The first argument is expected to be the attribute keyword.\n *\n * @return Struct containing the attribute data.\n */\nSubtextureData parse_subtex(const std::vector<std::string> &args) {\n\tSubtextureData subtex;\n\n\tsubtex.xpos = std::stoi(args[1]);\n\tsubtex.ypos = std::stoi(args[2]);\n\tsubtex.xsize = std::stoul(args[3]);\n\tsubtex.ysize = std::stoul(args[4]);\n\tsubtex.xanchor = std::stoi(args[5]);\n\tsubtex.yanchor = std::stoi(args[6]);\n\n\treturn subtex;\n}\n\nTexture2dInfo parse_texture_file(const util::Path &path) {\n\tif (not path.is_file()) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"Reading .texture file '\"\n\t\t                     << path.get_name()\n\t\t                     << \"' failed. Reason: File not found\");\n\t}\n\n\tauto file = path.open();\n\tauto lines = file.get_lines();\n\n\tstd::string imagefile;\n\tSizeData size;\n\tPixelFormatData pxformat;\n\tstd::vector<SubtextureData> subtexs;\n\n\tauto keywordfuncs = std::unordered_map<std::string, std::function<void(const std::vector<std::string> &)>>{\n\t\tstd::make_pair(\"version\", [&](const std::vector<std::string> &args) {\n\t\t\tsize_t version_no = parse_version(args);\n\n\t\t\tif (version_no != 1) {\n\t\t\t\tthrow Error(MSG(err) << \"Reading .texture file '\"\n\t\t\t                         << path.get_name()\n\t\t\t                         << \"' failed. Reason: Version \"\n\t\t\t                         << version_no << \" not supported\");\n\t\t\t}\n\t\t}),\n\t\tstd::make_pair(\"imagefile\", [&](const std::vector<std::string> &args) {\n\t\t\timagefile = parse_imagefile(args);\n\t\t}),\n\t\tstd::make_pair(\"size\", [&](const std::vector<std::string> &args) {\n\t\t\tsize = parse_size(args);\n\t\t}),\n\t\tstd::make_pair(\"pxformat\", [&](const std::vector<std::string> &args) {\n\t\t\tpxformat = parse_pxformat(args);\n\t\t}),\n\t\tstd::make_pair(\"subtex\", [&](const std::vector<std::string> &args) {\n\t\t\tsubtexs.push_back(parse_subtex(args));\n\t\t})};\n\n\tfor (auto line : lines) {\n\t\t// Skip empty lines and comments\n\t\tif (line.empty() || line.substr(0, 1) == \"#\") {\n\t\t\tcontinue;\n\t\t}\n\t\tstd::vector<std::string> args{util::split(line, ' ')};\n\n\t\t// TODO: Avoid double lookup with keywordfuncs.find(args[0])\n\t\tif (not keywordfuncs.contains(args[0])) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"Reading .texture file '\"\n\t\t\t                     << path.get_name()\n\t\t\t                     << \"' failed. Reason: Keyword \"\n\t\t\t                     << args[0] << \" is not defined\");\n\t\t}\n\t\tkeywordfuncs[args[0]](args);\n\t}\n\n\tstd::vector<Texture2dSubInfo> subinfos;\n\n\tfor (auto subtex : subtexs) {\n\t\tsubinfos.emplace_back(subtex.xpos,\n\t\t                      subtex.ypos,\n\t\t                      subtex.xsize,\n\t\t                      subtex.ysize,\n\t\t                      subtex.xanchor,\n\t\t                      subtex.yanchor,\n\t\t                      size.width,\n\t\t                      size.height);\n\t}\n\n\tauto imagepath = path.get_parent() / imagefile;\n\n\tauto align = guess_row_alignment(size.width);\n\treturn Texture2dInfo(size.width, size.height, pxformat.format, imagepath, align, std::move(subinfos));\n}\n\n} // namespace openage::renderer::resources::parser\n"
  },
  {
    "path": "libopenage/renderer/resources/parser/parse_texture.h",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n\n#include \"renderer/resources/texture_info.h\"\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\n\nnamespace renderer::resources::parser {\n\n/**\n * Containers for the raw data.\n *\n * Definition according to doc/media/openage/texture_format_spec.md.\n */\nstruct SizeData {\n\tsize_t width;\n\tsize_t height;\n};\n\nstruct PixelFormatData {\n\tpixel_format format;\n\tbool cbits;\n};\n\nstruct SubtextureData {\n\tsize_t xpos;\n\tsize_t ypos;\n\tsize_t xsize;\n\tsize_t ysize;\n\tint xanchor;\n\tint yanchor;\n};\n\n/**\n * Parse a texture definition from a .texture format file.\n *\n * @param file Path to the texture file.\n *\n * @return The corresponding texture definition.\n */\nTexture2dInfo parse_texture_file(const util::Path &file);\n\n} // namespace renderer::resources::parser\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/shader_source.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"shader_source.h\"\n\n#include <utility>\n\n#include \"util/file.h\"\n#include \"util/path.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace resources {\n\nShaderSource::ShaderSource(shader_lang_t lang, shader_stage_t stage, std::string &&code) :\n\tlang(lang), stage(stage), code(std::move(code)) {}\n\nShaderSource::ShaderSource(shader_lang_t lang, shader_stage_t stage, const util::Path &path) :\n\tlang(lang), stage(stage), code(path.open().read()) {}\n\nstd::string const &ShaderSource::get_source() const {\n\treturn this->code;\n}\n\nshader_lang_t ShaderSource::get_lang() const {\n\treturn this->lang;\n}\n\nshader_stage_t ShaderSource::get_stage() const {\n\treturn this->stage;\n}\n\n} // namespace resources\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/shader_source.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\nnamespace renderer {\nnamespace resources {\n\n/// The source language of a shader.\nenum class shader_lang_t {\n\tglsl,\n\tspirv,\n};\n\n/// Shader stages present in modern graphics pipelines.\nenum class shader_stage_t {\n\tvertex,\n\tgeometry,\n\ttesselation_control,\n\ttesselation_evaluation,\n\tfragment,\n};\n\n/// Stores source code for a part of a shader program.\nclass ShaderSource {\npublic:\n\t/// Obtain shader source code from a file.\n\tShaderSource(shader_lang_t, shader_stage_t, const util::Path &path);\n\n\t/// Obtain shader source code from a string.\n\tShaderSource(shader_lang_t, shader_stage_t, std::string &&code);\n\n\t/// Returns a view of the shader source code. This might be text\n\t/// or binary data.\n\tstd::string const &get_source() const;\n\n\t/// Returns the language of this shader source.\n\tshader_lang_t get_lang() const;\n\n\t/// Returns the stage which this shader source implements.\n\tshader_stage_t get_stage() const;\n\nprivate:\n\t/// The source language.\n\tshader_lang_t lang;\n\n\t/// The implemented stage.\n\tshader_stage_t stage;\n\n\t/// The shader source code.\n\tstd::string code;\n};\n\n} // namespace resources\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/shader_template.cpp",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#include \"shader_template.h\"\n\n#include <cstring>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\nnamespace openage::renderer::resources {\n\nShaderTemplate::ShaderTemplate(const util::Path &template_path) {\n\tauto file = template_path.open();\n\tthis->template_code = file.read();\n\tfile.close();\n\n\tstd::string marker = \"// PLACEHOLDER: \";\n\tsize_t pos = 0;\n\n\twhile ((pos = this->template_code.find(marker, pos)) != std::string::npos) {\n\t\tsize_t name_start = pos + marker.length();\n\t\tsize_t line_end = this->template_code.find('\\n', name_start);\n\t\tstd::string name = this->template_code.substr(name_start, line_end - name_start);\n\t\t// Trim trailing whitespace (space, tab, carriage return, etc.)\n\t\tname.erase(name.find_last_not_of(\" \\t\\r\\n\") + 1);\n\n\t\tthis->placeholders.push_back({name, pos, line_end - pos});\n\t\tpos = line_end;\n\t}\n}\n\nvoid ShaderTemplate::load_snippets(const util::Path &snippet_path) {\n\t// load config here\n\tutil::Path snippet_path_copy = snippet_path;\n\tfor (const auto &entry : snippet_path_copy.iterdir()) {\n\t\tif (entry.get_name().ends_with(\".snippet\")) {\n\t\t\tadd_snippet(entry);\n\t\t}\n\t}\n}\n\nvoid ShaderTemplate::add_snippet(const util::Path &snippet_path) {\n\tauto file = snippet_path.open();\n\tstd::string content = file.read();\n\tfile.close();\n\n\tstd::string name = snippet_path.get_stem();\n\n\tsnippets[name] = content;\n}\n\nrenderer::resources::ShaderSource ShaderTemplate::generate_source() const {\n\tstd::string result_src = template_code;\n\n\t// Replace placeholders in reverse order (to avoid offset issues)\n\tfor (auto it = placeholders.rbegin(); it != placeholders.rend(); ++it) {\n\t\tconst auto &ph = *it;\n\t\tauto snippet_it = snippets.find(ph.name);\n\t\tif (snippet_it != snippets.end()) {\n\t\t\tresult_src.replace(ph.position, ph.length, snippet_it->second);\n\t\t}\n\t\telse {\n\t\t\tthrow Error(MSG(err) << \"Missing snippet for placeholder: \" << ph.name);\n\t\t}\n\t}\n\n\tauto result = resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tstd::move(result_src));\n\n\treturn result;\n}\n\n} // namespace openage::renderer::world\n"
  },
  {
    "path": "libopenage/renderer/resources/shader_template.h",
    "content": "// Copyright 2024-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <map>\n#include <memory>\n#include <string>\n#include <vector>\n\n#include \"renderer/resources/shader_source.h\"\n#include \"util/path.h\"\n\nnamespace openage {\nnamespace renderer {\nnamespace resources {\n\n/**\n * Manages shader templates and their code snippets.\n * Allows loading configurable shader commands and generating\n * complete shader source code.\n */\nclass ShaderTemplate {\npublic:\n\t/**\n\t * Create a shader template from source code of shader.\n\t *\n\t * @param template_path Path to the template file.\n\t */\n\texplicit ShaderTemplate(const util::Path &template_path);\n\n\t/**\n\t * Load all snippets from a directory of .snippet files.\n\t *\n\t * @param snippet_path Path to directory containing snippet files.\n\t */\n\tvoid load_snippets(const util::Path &snippet_path);\n\n\t/**\n\t * Add a single code snippet to snippets map.\n\t *\n\t * @param name Snippet identifier.\n\t * @param snippet_path Path to the snippet file.\n\t */\n\tvoid add_snippet(const util::Path &snippet_path);\n\n\t/**\n\t * Generate final shader source code with all snippets inserted.\n\t *\n\t * @return Complete shader code.\n\t * @throws Error if any required placeholders are missing snippets.\n\t */\n\trenderer::resources::ShaderSource generate_source() const;\n\nprivate:\n\t/// Original template code with placeholders\n\tstd::string template_code;\n\t/// Mapping of placeholder IDs to their code snippets\n\tstd::map<std::string, std::string> snippets;\n\n\t/// Info about a placeholder found in the template\n\tstruct Placeholder {\n\t\t\tstd::string name;\n\t\t\tsize_t position;\n\t\t\tsize_t length;\n\t};\n\n\t/// All placeholders found in the template\n\t/// precomputed on creation\n\tstd::vector<Placeholder> placeholders;\n};\n} // namespace world\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/CMakeLists.txt",
    "content": "add_sources(libopenage\n    blendpattern_info.cpp\n    blendtable_info.cpp\n    frame_info.cpp\n    layer_info.cpp\n    terrain_info.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/blendpattern_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"blendpattern_info.h\"\n\nnamespace openage::renderer::resources {\n\nBlendPatternInfo::BlendPatternInfo(const float scalefactor,\n                                   std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n                                   std::vector<blending_mask> &masks) :\n\tscalefactor{scalefactor},\n\ttextures{textures} {\n\tfor (auto mask : masks) {\n\t\tauto mask_val = std::make_pair(mask.texture_id, mask.subtex_id);\n\t\tthis->masks.emplace(mask.directions, mask_val);\n\t}\n}\n\nfloat BlendPatternInfo::get_scalefactor() const {\n\treturn this->scalefactor;\n}\n\nsize_t BlendPatternInfo::get_texture_count() const {\n\treturn this->textures.size();\n}\n\nconst std::shared_ptr<Texture2dInfo> &BlendPatternInfo::get_texture(size_t idx) const {\n\treturn this->textures.at(idx);\n}\n\nsize_t BlendPatternInfo::get_mask_count() const {\n\treturn this->masks.size();\n}\n\nstd::vector<std::pair<size_t, size_t>> BlendPatternInfo::get_masks(char directions) const {\n\tstd::vector<std::pair<size_t, size_t>> result{};\n\n\tauto lookup = this->masks.find(directions);\n\tif (lookup != this->masks.end()) {\n\t\tresult.push_back(lookup->second);\n\t\treturn result;\n\t}\n\t// else find best matching masks\n\tfor (auto mask : this->masks) {\n\t\tif (mask.first & directions) {\n\t\t\tresult.push_back(mask.second);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/blendpattern_info.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n\nnamespace openage::renderer::resources {\nclass Texture2dInfo;\n\nstruct blending_mask {\n\tchar directions;\n\tsize_t texture_id;\n\tsize_t subtex_id;\n};\n\n/**\n * Contains information about a terrain blending pattern.\n */\nclass BlendPatternInfo {\npublic:\n\t/**\n\t * Create a blending pattern info.\n\t *\n\t * @param scalefactor Factor by which sprite images are scaled down at default zoom level.\n\t *                    Applies to all masks. Choosing a factor that is power of 2 is\n\t *                    recommended.\n\t * @param textures Information object of textures used by the pattern.\n\t * @param masks Layer information.\n\t */\n\tBlendPatternInfo(const float scalefactor,\n\t                 std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n\t                 std::vector<blending_mask> &masks);\n\n\tBlendPatternInfo() = default;\n\t~BlendPatternInfo() = default;\n\n\t/**\n\t * Get the scaling factor of the blending pattern.\n\t *\n\t * @return A float containing the scaling factor.\n\t */\n\tfloat get_scalefactor() const;\n\n\t/**\n\t * Get number of textures referenced by the blending pattern.\n\t *\n\t * @return Number of textures.\n\t */\n\tsize_t get_texture_count() const;\n\n\t/**\n\t * Get the 2D texture information of the texture with the specified index.\n\t *\n\t * @param idx Index of the texture.\n\t *\n\t * @return A texture information object containing the metadata of the\n\t *         referenced texture.\n\t */\n\tconst std::shared_ptr<Texture2dInfo> &get_texture(size_t idx) const;\n\n\t/**\n\t * Get number of masks in the blending pattern.\n\t *\n\t * @return Number of layers.\n\t */\n\tsize_t get_mask_count() const;\n\n\t/**\n\t * Get the blending masks for specified directions where two terrains overlap.\n\t *\n\t * @param directions Directions where the terrains overlap.\n\t *\n\t * @return Blending masks that match the specified directions.\n\t */\n\tstd::vector<std::pair<size_t, size_t>> get_masks(char directions) const;\n\nprivate:\n\t/**\n\t * Scaling factor of the animation across all layers at default zoom level.\n\t */\n\tfloat scalefactor;\n\n\t/**\n\t * Information about textures used by the animation.\n\t */\n\tstd::vector<std::shared_ptr<Texture2dInfo>> textures;\n\n\t/**\n\t * Layer information.\n\t */\n\tstd::unordered_map<char, std::pair<size_t, size_t>> masks;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/blendtable_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"blendtable_info.h\"\n\nnamespace openage::renderer::resources {\n\nBlendTableInfo::BlendTableInfo(const std::vector<size_t> &table,\n                               const std::vector<std::shared_ptr<BlendPatternInfo>> &patterns) :\n\ttable{table},\n\tpatterns{patterns} {}\n\nconst std::vector<size_t> &BlendTableInfo::get_table() const {\n\treturn this->table;\n}\n\nconst std::shared_ptr<BlendPatternInfo> &BlendTableInfo::get_pattern(size_t idx) const {\n\treturn this->patterns.at(idx);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/blendtable_info.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\nnamespace openage::renderer::resources {\n\nclass BlendPatternInfo;\n\n/**\n * Contains information about a blending table.\n */\nclass BlendTableInfo {\npublic:\n\t/**\n\t * Create a blending table Info.\n\t *\n\t * @param table Lookup table for blending pattern IDs.\n\t * @param patterns Blending pattern information.\n\t */\n\tBlendTableInfo(const std::vector<size_t> &table,\n\t               const std::vector<std::shared_ptr<BlendPatternInfo>> &patterns);\n\n\tBlendTableInfo() = default;\n\t~BlendTableInfo() = default;\n\n\t/**\n\t * Get the blending lookup table.\n\t *\n\t * @return Lookup table for blending patterns.\n\t */\n\tconst std::vector<size_t> &get_table() const;\n\n\t/**\n\t * Get the lending pattern information of the pattern with the specified index.\n\t *\n\t * @param idx Index of the pattern.\n\t *\n\t * @return Blending pattern information object.\n\t */\n\tconst std::shared_ptr<BlendPatternInfo> &get_pattern(size_t idx) const;\n\nprivate:\n\t/**\n\t * Lookup table for blending pattern IDs.\n\t */\n\tstd::vector<size_t> table;\n\n\t/**\n\t * Blending pattern information.\n\t */\n\tstd::vector<std::shared_ptr<BlendPatternInfo>> patterns;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/frame_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"frame_info.h\"\n\nnamespace openage::renderer::resources {\n\nTerrainFrameInfo::TerrainFrameInfo(size_t texture_idx,\n                                   size_t subtexture_idx,\n                                   size_t priority,\n                                   std::optional<size_t> blend_mode) :\n\ttexture_idx{texture_idx},\n\tsubtexture_idx{subtexture_idx},\n\tpriority{priority},\n\tblend_mode{blend_mode} {}\n\nsize_t TerrainFrameInfo::get_texture_idx() const {\n\treturn this->texture_idx;\n}\n\nsize_t TerrainFrameInfo::get_subtexture_idx() const {\n\treturn this->subtexture_idx;\n}\n\nsize_t TerrainFrameInfo::get_priority() const {\n\treturn this->priority;\n}\n\nstd::optional<size_t> TerrainFrameInfo::get_blend_mode() const {\n\treturn this->blend_mode;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/frame_info.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <optional>\n\nnamespace openage::renderer::resources {\n\n/**\n * Contains information about a 2D terrain frame. The frame image data\n * is stored in a 2D texture object.\n */\nclass TerrainFrameInfo {\npublic:\n\t/**\n\t * Create a 2D Frame Info.\n\t *\n\t * @param texture_idx Index of the texture containing the frame in the\n\t *                    TerrainInfo object.\n\t * @param subtexture_idx Index of the subtexture containing the frame\n\t *                       in the Texture2dInfo object.\n\t * @param priority Blending priority.\n\t * @param priority Blend mode index.\n\t */\n\tTerrainFrameInfo(size_t texture_idx,\n\t                 size_t subtexture_idx,\n\t                 size_t priority = 0,\n\t                 std::optional<size_t> blend_mode = std::nullopt);\n\n\tTerrainFrameInfo() = default;\n\t~TerrainFrameInfo() = default;\n\n\t/**\n\t * Get the texture index of the frame in the animation.\n\t *\n\t * @return Index of a texture in a TerrainInfo object.\n\t */\n\tsize_t get_texture_idx() const;\n\n\t/**\n\t * Get the subtexture index of the frame in the texture.\n\t *\n\t * @return Index of a subtexture in a Texture2dInfo object.\n\t */\n\tsize_t get_subtexture_idx() const;\n\n\t/**\n\t * Get the blending priority of the frame.\n\t *\n\t * @return Blending priority.\n\t */\n\tsize_t get_priority() const;\n\n\t/**\n\t * Get the blending mode of the frame.\n\t *\n\t * @return Blending mode.\n\t */\n\tstd::optional<size_t> get_blend_mode() const;\n\nprivate:\n\t/**\n\t * Index of the texture containing the frame in the TerrainInfo object.\n\t */\n\tsize_t texture_idx;\n\n\t/**\n\t * Index of the subtexture containing the frame in the Texture2dInfo object.\n\t */\n\tsize_t subtexture_idx;\n\n\t/**\n\t * Decides which blending table of the two adjacent terrain textures is selected.\n\t *\n\t * If two adjacent terrains have equal priority, the blending table of the terrain\n\t * with a lower x coordinate value is selected. If the x coordinate value is also\n\t * equal, the blending table of the terrain with the lowest y coordinate is selected.\n\t */\n\tsize_t priority;\n\n\t/**\n\t * Used for looking up the blending pattern index in a blending table.\n\t */\n\tstd::optional<size_t> blend_mode;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/layer_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"layer_info.h\"\n\n#include <algorithm>\n#include <utility>\n\n#include \"renderer/resources/frame_timing.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::renderer::resources {\n\nTerrainLayerInfo::TerrainLayerInfo(const std::vector<std::shared_ptr<TerrainFrameInfo>> &frames,\n                                   const terrain_display_mode mode,\n                                   const size_t position,\n                                   const float time_per_frame,\n                                   const float replay_delay) :\n\tmode{mode},\n\tposition{position},\n\ttime_per_frame{time_per_frame},\n\treplay_delay{replay_delay},\n\tframes{frames},\n\tframe_timing{nullptr} {\n\t// set frame timings by calculating when they appear in the animation sequence\n\tauto frame_count = this->frames.size();\n\ttime::time_t t = 0;\n\tstd::vector<time::time_t> frame_timing;\n\tfor (size_t i = 0; i < frame_count; ++i) {\n\t\tframe_timing.push_back(t);\n\t\tt += this->time_per_frame;\n\t}\n\tauto total_time = (frame_count - 1) * this->time_per_frame + this->replay_delay;\n\tthis->frame_timing = std::make_shared<FrameTiming>(total_time, std::move(frame_timing));\n}\n\nterrain_display_mode TerrainLayerInfo::get_display_mode() const {\n\treturn this->mode;\n}\n\nsize_t TerrainLayerInfo::get_position() const {\n\treturn this->position;\n}\n\nfloat TerrainLayerInfo::get_time_per_frame() const {\n\treturn this->time_per_frame;\n}\n\nfloat TerrainLayerInfo::get_replay_delay() const {\n\treturn this->replay_delay;\n}\n\nsize_t TerrainLayerInfo::get_frame_count() const {\n\treturn this->frames.size();\n}\n\nconst std::shared_ptr<TerrainFrameInfo> &TerrainLayerInfo::get_frame(size_t idx) const {\n\treturn this->frames.at(idx);\n}\n\nconst std::shared_ptr<FrameTiming> &TerrainLayerInfo::get_frame_timing() const {\n\treturn this->frame_timing;\n}\n\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/layer_info.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n\nnamespace openage::renderer::resources {\nclass TerrainFrameInfo;\nclass FrameTiming;\n\nenum class terrain_display_mode {\n\tOFF, // Only show first frame\n\tLOOP // Loop indefinitely\n};\n\n/**\n * Contains information about a 2D terrain layer. The layer defines the display\n * mode, overall position and timing of frames.\n */\nclass TerrainLayerInfo {\npublic:\n\t/**\n\t * Create a 2D Terrain Layer Info.\n\t *\n\t * @param frames Frame information.\n\t * @param mode Determines how often the layer loops its frames.\n\t * @param position Absolute position of the layer on the screen.\n\t * @param time_per_frame Time that each frame is displayed in seconds.\n\t * @param replay_delay Additional time (in seconds) to display the last frame in a loop.\n\t */\n\tTerrainLayerInfo(const std::vector<std::shared_ptr<TerrainFrameInfo>> &frames,\n\t                 const terrain_display_mode mode = terrain_display_mode::OFF,\n\t                 const size_t position = 0,\n\t                 const float time_per_frame = 0.0,\n\t                 const float replay_delay = 0.0);\n\n\t~TerrainLayerInfo() = default;\n\n\t/**\n\t * Get the display mode of the layer.\n\t *\n\t * @return Display mode setting.\n\t */\n\tterrain_display_mode get_display_mode() const;\n\n\t/**\n\t * Get the absolute position of the layer.\n\t *\n\t * @return Position of the layer.\n\t */\n\tsize_t get_position() const;\n\n\t/**\n\t * Get the time that each frame is displayed.\n\t *\n\t * @return Time each frame is displayed in seconds.\n\t */\n\tfloat get_time_per_frame() const;\n\n\t/**\n\t * Get the additional display tme for the last frame in the loop.\n\t *\n\t * @return Additional time to display the last frame in seconds.\n\t */\n\tfloat get_replay_delay() const;\n\n\t/**\n\t * Get number of frames for the layer.\n\t *\n\t * @return Number of frames.\n\t */\n\tsize_t get_frame_count() const;\n\n\t/**\n\t * Get the frame information of the frame with the specified index.\n\t *\n\t * @param idx Index of the frame.\n\t *\n\t * @return A frame information object.\n\t */\n\tconst std::shared_ptr<TerrainFrameInfo> &get_frame(size_t idx) const;\n\n\t/**\n\t * Get the frame timing information of this layer.\n\t *\n\t * @return Curve associating time with frame indices.\n\t */\n\tconst std::shared_ptr<FrameTiming> &get_frame_timing() const;\n\n\nprivate:\n\t/**\n\t * Display mode of the contained frames.\n\t */\n\tterrain_display_mode mode;\n\n\t/**\n\t * Absolute position of the layer.\n\t */\n\tsize_t position;\n\n\t/**\n\t * Time each frame is displayed (in seconds).\n\t */\n\tfloat time_per_frame;\n\n\t/**\n\t * Time the last frame in a loop is additionally displayed (in seconds).\n\t */\n\tfloat replay_delay;\n\n\t/**\n\t * Frame information.\n\t */\n\tstd::vector<std::shared_ptr<TerrainFrameInfo>> frames;\n\n\t/**\n\t * Frame timing in the animated sequence.\n\t */\n\tstd::shared_ptr<FrameTiming> frame_timing;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/terrain_info.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"terrain_info.h\"\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"renderer/resources/terrain/layer_info.h\"\n\n\nnamespace openage::renderer::resources {\n\nTerrainInfo::TerrainInfo(const float scalefactor,\n                         std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n                         std::vector<TerrainLayerInfo> &layers,\n                         const std::shared_ptr<BlendTableInfo> &blendtable) :\n\tscalefactor{scalefactor},\n\ttextures{textures},\n\tlayers{layers},\n\tblendtable{blendtable} {}\n\nfloat TerrainInfo::get_scalefactor() const {\n\treturn this->scalefactor;\n}\n\nsize_t TerrainInfo::get_texture_count() const {\n\treturn this->textures.size();\n}\n\nconst std::shared_ptr<Texture2dInfo> &TerrainInfo::get_texture(size_t idx) const {\n\treturn this->textures[idx];\n}\n\nconst std::shared_ptr<BlendTableInfo> &TerrainInfo::get_blendtable() const {\n\treturn this->blendtable;\n}\n\nsize_t TerrainInfo::get_layer_count() const {\n\treturn this->layers.size();\n}\n\nconst TerrainLayerInfo &TerrainInfo::get_layer(size_t idx) const {\n\treturn this->layers.at(idx);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/terrain/terrain_info.h",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"renderer/resources/terrain/layer_info.h\"\n\n\nnamespace openage::renderer::resources {\nclass BlendTableInfo;\nclass Texture2dInfo;\n\n\n/**\n * Contains information about a 2D terrain texture.\n */\nclass TerrainInfo {\npublic:\n\t/**\n\t * Create a Terrain Info.\n\t *\n\t * @param scalefactor Factor by which sprite images are scaled down at default zoom level.\n\t *                    Applies to all layers. Choosing a factor that is power of 2 is\n\t *                    recommended.\n\t * @param textures Information object of textures used by the terrain.\n\t * @param layers Layer information.\n\t * @param blendtable Blending table for setting edge blending patterns.\n\t */\n\tTerrainInfo(const float scalefactor,\n\t            std::vector<std::shared_ptr<Texture2dInfo>> &textures,\n\t            std::vector<TerrainLayerInfo> &layers,\n\t            const std::shared_ptr<BlendTableInfo> &blendtable = nullptr);\n\n\tTerrainInfo() = default;\n\t~TerrainInfo() = default;\n\n\t/**\n\t * Get the scaling factor of the terrain.\n\t *\n\t * @return A float containing the scaling factor.\n\t */\n\tfloat get_scalefactor() const;\n\n\t/**\n\t * Get number of textures referenced by the terrain.\n\t *\n\t * @return Number of textures.\n\t */\n\tsize_t get_texture_count() const;\n\n\t/**\n\t * Get the 2D texture information of the texture with the specified index.\n\t *\n\t * @param idx Index of the texture.\n\t *\n\t * @return A texture information object containing the metadata of the\n\t *         referenced texture.\n\t */\n\tconst std::shared_ptr<Texture2dInfo> &get_texture(size_t idx) const;\n\n\t/**\n\t * Get the blend table information of the layer.\n\t *\n\t * @return Blend table information object or \\p nullptr if no blendtable is used.\n\t */\n\tconst std::shared_ptr<BlendTableInfo> &get_blendtable() const;\n\n\t/**\n\t * Get number of layers in the terrain.\n\t *\n\t * @return Number of layers.\n\t */\n\tsize_t get_layer_count() const;\n\n\t/**\n\t * Get the layer information of the layer with the specified index.\n\t *\n\t * @param idx Index of the layer.\n\t *\n\t * @return A layer information object.\n\t */\n\tconst TerrainLayerInfo &get_layer(size_t idx) const;\n\nprivate:\n\t/**\n\t * Scaling factor of the terrain across all layers at default zoom level.\n\t */\n\tfloat scalefactor;\n\n\t/**\n\t * Information about textures used by the terrain.\n\t */\n\tstd::vector<std::shared_ptr<Texture2dInfo>> textures;\n\n\t/**\n\t * Layer information.\n\t */\n\tstd::vector<TerrainLayerInfo> layers;\n\n\t/**\n\t * Blending patterns at terrain edges.\n\t */\n\tstd::shared_ptr<BlendTableInfo> blendtable;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_data.cpp",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#include \"texture_data.h\"\n\n#include <algorithm>\n#include <cstring>\n#include <optional>\n#include <string>\n\n#include <QImage>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/resources/texture_subinfo.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::resources {\n\n/// Tries to guess the alignment of image rows based on image parameters. Kinda\n/// black magic and might not actually work.\n/// @param width in pixels of the image\n/// @param fmt of pixels in the image\n/// @param row_size the actual size in bytes of an image row, including padding\nstatic constexpr size_t guess_row_alignment(size_t width, pixel_format fmt, size_t row_size) {\n\t// Use the highest possible alignment for even-width images.\n\tif (width % 8 == 0) {\n\t\treturn 8;\n\t}\n\tif (width % 4 == 0) {\n\t\treturn 4;\n\t}\n\tif (width % 2 == 0) {\n\t\treturn 2;\n\t}\n\n\t// The size of meaningful data in each row.\n\tsize_t pix_bytes = width * pixel_size(fmt);\n\t// The size of padding.\n\tsize_t padding = row_size - pix_bytes;\n\n\tif (padding == 0) {\n\t\treturn 1;\n\t}\n\tif (padding <= 1) {\n\t\treturn 2;\n\t}\n\tif (padding <= 3) {\n\t\treturn 4;\n\t}\n\tif (padding <= 7) {\n\t\treturn 8;\n\t}\n\n\t// Bail with a sane value.\n\treturn 4;\n}\n\nTexture2dData::Texture2dData(const util::Path &path) {\n\tstd::string native_path = path.resolve_native_path();\n\n\t// TODO: use QImageIOHandler to directly create the correct surface format.\n\tQImage image{native_path.c_str()};\n\timage.convertTo(QImage::Format_RGBA8888);\n\n\tlog::log(MSG(dbg) << \"Texture has been loaded from \" << native_path);\n\n\tauto surf_fmt = image.format();\n\n\tpixel_format pix_fmt;\n\tswitch (surf_fmt) {\n\tcase QImage::Format_RGB32:\n\t\tthrow Error{MSG(err) << \"Qt ARGB format not supported, needs conversion first.\"};\n\tcase QImage::Format_RGB888:\n\t\tpix_fmt = pixel_format::rgb8;\n\t\tbreak;\n\tcase QImage::Format_BGR888:\n\t\tpix_fmt = pixel_format::bgr8;\n\t\tbreak;\n\tcase QImage::Format_RGBA8888:\n\t\tpix_fmt = pixel_format::rgba8;\n\t\tbreak;\n\tdefault:\n\t\tthrow Error(MSG(err) << \"Texture \" << native_path << \" uses an unsupported format: \" << static_cast<int>(surf_fmt));\n\t}\n\n\tauto w = uint32_t(image.width());\n\tauto h = uint32_t(image.height());\n\n\tsize_t data_size = image.sizeInBytes();\n\n\t// copy pixel data from surface\n\tthis->data = std::vector<uint8_t>(data_size);\n\tstd::memcpy(this->data.data(), image.bits(), data_size);\n\n\tstd::vector<Texture2dSubInfo> subtextures;\n\t// we don't have a texture description file.\n\t// use the whole image as one texture then.\n\tTexture2dSubInfo s(0, 0, w, h, w / 2, h / 2, w, h);\n\n\tsubtextures.push_back(s);\n\n\tsize_t align = guess_row_alignment(w, pix_fmt, image.bytesPerLine());\n\tthis->info = Texture2dInfo(w, h, pix_fmt, path, align, std::move(subtextures));\n}\n\nTexture2dData::Texture2dData(Texture2dInfo const &info) :\n\tinfo{info} {\n\tstd::string native_path = info.get_image_path().value().resolve_native_path();\n\n\t// TODO: use QImageIOHandler to directly create the correct surface format.\n\tQImage image{native_path.c_str()};\n\timage.convertTo(QImage::Format_RGBA8888);\n\n\tlog::log(MSG(dbg) << \"Texture has been loaded from \" << native_path);\n\n\tsize_t data_size = image.sizeInBytes();\n\n\t// copy pixel data from surface\n\tthis->data = std::vector<uint8_t>(data_size);\n\tstd::memcpy(this->data.data(), image.bits(), data_size);\n}\n\nTexture2dData::Texture2dData(Texture2dInfo const &info, std::vector<uint8_t> &&data) :\n\tinfo(info), data(std::move(data)) {}\n\nTexture2dData Texture2dData::flip_y() {\n\tsize_t row_size = this->info.get_row_size();\n\tsize_t height = this->info.get_size().second;\n\n\tstd::vector<uint8_t> new_data(this->data.size());\n\n\tfor (size_t y = 0; y < height; ++y) {\n\t\tstd::copy(this->data.data() + row_size * y, this->data.data() + row_size * (y + 1), new_data.end() - row_size * (y + 1));\n\t}\n\n\tthis->data = new_data;\n\n\tTexture2dInfo new_info(this->info);\n\n\treturn Texture2dData(std::move(new_info), std::move(new_data));\n}\n\nconst Texture2dInfo &Texture2dData::get_info() const {\n\treturn this->info;\n}\n\nconst uint8_t *Texture2dData::get_data() const {\n\treturn this->data.data();\n}\n\nvoid Texture2dData::store(const util::Path &file) const {\n\tlog::log(MSG(info) << \"Saving texture data to \" << file);\n\n\tif (this->info.get_format() != pixel_format::rgba8) {\n\t\tthrow Error(MSG(err) << \"Storing 2D textures into files is unimplemented. PRs welcome :D\");\n\t}\n\n\tauto size = this->info.get_size();\n\n\tQImage::Format pix_fmt;\n\tpixel_format fmt = this->info.get_format();\n\tswitch (fmt) {\n\tcase pixel_format::rgb8:\n\t\tpix_fmt = QImage::Format_RGB888;\n\t\tbreak;\n\tcase pixel_format::bgr8:\n\t\tpix_fmt = QImage::Format_BGR888;\n\t\tbreak;\n\tcase pixel_format::rgba8:\n\t\tpix_fmt = QImage::Format_RGBA8888;\n\t\tbreak;\n\tdefault:\n\t\tthrow Error(MSG(err) << \"Texture uses an unsupported format.\");\n\t}\n\n\tQImage image{this->data.data(), size.first, size.second, pix_fmt};\n\n\t// Call QImage for saving the screenshot to PNG\n\tstd::string path = file.resolve_native_path_w();\n\timage.save(path.c_str());\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_data.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <utility>\n#include <vector>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n#include \"texture_info.h\"\n\n\nnamespace openage {\nnamespace util {\nclass Path;\n}\nnamespace renderer::resources {\n\n/// Stores 2D texture data in a CPU-accessible byte buffer. Provides methods for loading from\n/// and storing onto disk, as well as sending to and receiving from graphics hardware.\nclass Texture2dData {\npublic:\n\t/// Create a texture from an image file.\n\t/// @param path Path to the image file.\n\t///\n\t/// Uses QImage internally.\n\tTexture2dData(const util::Path &path);\n\n\t/// Create a texture from info.\n\t///\n\t/// Uses QImage internally. For supported image file types,\n\t/// see the QImage initialization in the engine.\n\tTexture2dData(Texture2dInfo const &info);\n\n\t/// Construct by moving the information and raw texture data from somewhere else.\n\tTexture2dData(Texture2dInfo const &info, std::vector<uint8_t> &&data);\n\n\t/// Flips the texture along the Y-axis and returns the flipped data with the same info.\n\t/// Sometimes necessary when converting between storage modes.\n\tTexture2dData flip_y();\n\n\t/// Returns the information describing this texture data.\n\tconst Texture2dInfo &get_info() const;\n\n\t/// Returns a pointer to the raw texture data, in row-major order.\n\tconst uint8_t *get_data() const;\n\n\t/// Reads the pixel at the given position and casts it to the given type.\n\t/// The texture is _not_ read as if it consisted of pixels of the given type,\n\t/// but rather according to its original pixel format, so the coordinates\n\t/// have to be specified according to that.\n\ttemplate <typename T>\n\tT read_pixel(size_t x, size_t y) const {\n\t\tconst uint8_t *data = this->data.data();\n\t\tauto dims = this->info.get_size();\n\t\tsize_t off = (dims.second - y - 1) * this->info.get_row_size();\n\t\toff += x * pixel_size(this->info.get_format());\n\n\t\tif ((off + sizeof(T)) > this->info.get_data_size()) {\n\t\t\tthrow Error(MSG(err) << \"Pixel position (\" << x << \", \" << y << \") is outside texture.\");\n\t\t}\n\n\t\treturn *reinterpret_cast<const T *>(data + off);\n\t}\n\n\t/// Stores this texture data in the given file in the PNG format.\n\tvoid store(const util::Path &file) const;\n\nprivate:\n\t/// Information about this texture data.\n\tTexture2dInfo info;\n\n\t/// The raw texture data.\n\tstd::vector<uint8_t> data;\n};\n\n} // namespace renderer::resources\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_info.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"texture_info.h\"\n\n#include <eigen3/Eigen/Dense>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n\n\nnamespace openage::renderer::resources {\n\nTexture2dInfo::Texture2dInfo(size_t width,\n                             size_t height,\n                             pixel_format fmt,\n                             std::optional<util::Path> imagepath,\n                             size_t row_alignment,\n                             std::vector<Texture2dSubInfo> &&subs) :\n\tw(width),\n\th(height),\n\tformat{fmt},\n\trow_alignment{row_alignment},\n\timagepath{imagepath},\n\tsubtextures{std::move(subs)} {}\n\nbool Texture2dInfo::operator==(Texture2dInfo const &other) {\n\treturn other.w == this->w\n\t       and other.h == this->h\n\t       and other.format == this->format\n\t       and other.row_alignment == this->row_alignment;\n}\n\nbool Texture2dInfo::operator!=(Texture2dInfo const &other) {\n\treturn not(*this == other);\n}\n\nstd::pair<int32_t, int32_t> Texture2dInfo::get_size() const {\n\treturn std::make_pair(this->w, this->h);\n}\n\npixel_format Texture2dInfo::get_format() const {\n\treturn this->format;\n}\n\nsize_t Texture2dInfo::get_row_alignment() const {\n\treturn this->row_alignment;\n}\n\nsize_t Texture2dInfo::get_row_size() const {\n\tsize_t px_size = pixel_size(this->format);\n\tsize_t row_size = this->w * px_size;\n\n\tif (row_size % this->row_alignment != 0) {\n\t\t// Unaligned rows, have to add padding.\n\t\tsize_t padding = this->row_alignment - (row_size % this->row_alignment);\n\t\trow_size += padding;\n\t}\n\n\treturn row_size;\n}\n\nsize_t Texture2dInfo::get_data_size() const {\n\treturn this->get_row_size() * this->h;\n}\n\nconst std::optional<util::Path> &Texture2dInfo::get_image_path() const {\n\treturn this->imagepath;\n}\n\nsize_t Texture2dInfo::get_subtex_count() const {\n\treturn this->subtextures.size();\n}\n\nconst Texture2dSubInfo &Texture2dInfo::get_subtex_info(size_t subidx) const {\n\tif (subidx < this->subtextures.size()) [[likely]] {\n\t\treturn this->subtextures[subidx];\n\t}\n\n\tthrow Error(MSG(err) << \"Unknown subtexture requested: \" << subidx);\n}\n\nstd::tuple<float, float, float, float> Texture2dInfo::get_subtexture_coordinates(size_t subidx) const {\n\tauto tx = this->get_subtex_info(subidx);\n\treturn std::make_tuple(\n\t\t(static_cast<float>(tx.get_pos()[0])) / this->w,\n\t\t(static_cast<float>(tx.get_pos()[0] + tx.get_size()[0])) / this->w,\n\t\t(static_cast<float>(tx.get_pos()[1])) / this->h,\n\t\t(static_cast<float>(tx.get_pos()[1] + tx.get_size()[1])) / this->h);\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_info.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <optional>\n#include <tuple>\n#include <utility>\n#include <vector>\n\n#include \"datastructure/constexpr_map.h\"\n#include \"renderer/resources/texture_subinfo.h\"\n#include \"util/path.h\"\n\n\nnamespace openage::renderer::resources {\n\n/**\n * How the pixels are represented in a texture.\n */\nenum class pixel_format {\n\t/// 16 bits per pixel, unsigned integer, single channel\n\tr16ui,\n\t/// 32 bits per pixel, unsigned integer, single channel\n\tr32ui,\n\t/// 24 bits per pixel, float, RGB order\n\trgb8,\n\t/// 24 bits per pixel, float, BGR order\n\tbgr8,\n\t/// 24 bits per pixel, depth texture\n\tdepth24,\n\t/// 32 bits per pixel, float, alpha channel, RGBA order\n\trgba8,\n\t/// 32 bits per pixel, unsigned integer, alpha channel, RGBA order\n\trgba8ui,\n};\n\n/**\n * Get the size in bytes of a single pixel of the specified format.\n *\n * @param fmt Pixel format enum value.\n *\n * @return Size of single pixel (in bytes).\n */\nconstexpr size_t pixel_size(pixel_format fmt) {\n\tconstexpr auto pix_size = datastructure::create_const_map<pixel_format, size_t>(\n\t\tstd::make_pair(pixel_format::r16ui, 2),\n\t\tstd::make_pair(pixel_format::r32ui, 4),\n\t\tstd::make_pair(pixel_format::rgb8, 3),\n\t\tstd::make_pair(pixel_format::bgr8, 3),\n\t\tstd::make_pair(pixel_format::rgba8, 4),\n\t\tstd::make_pair(pixel_format::rgba8ui, 4),\n\t\tstd::make_pair(pixel_format::depth24, 3));\n\n\treturn pix_size.get(fmt);\n}\n\n/**\n * Information about a 2D texture surface, without actual texture data.\n * The class supports subtextures, so that one big texture (\"texture atlas\")\n * can contain several smaller images, which can be extracted for rendering\n * one-by-one.\n */\nclass Texture2dInfo {\npublic:\n\t/**\n\t * Create a new 2D texture information.\n\t *\n\t * @param width Width of texture.\n\t * @param height Height of texture.\n\t * @param fmt Pixel format used by the texture.\n\t * @param imagepath Path to the texture image (optional).\n\t * @param row_alignment Byte alignment of pixels (optional).\n\t * @param subs List of subtexture information (optional).\n\t */\n\tTexture2dInfo(size_t width,\n\t              size_t height,\n\t              pixel_format fmt,\n\t              std::optional<util::Path> imagepath = std::nullopt,\n\t              size_t row_alignment = 1,\n\t              std::vector<Texture2dSubInfo> &&subs = std::vector<Texture2dSubInfo>{});\n\n\tTexture2dInfo() = default;\n\tTexture2dInfo(Texture2dInfo const &) = default;\n\t~Texture2dInfo() = default;\n\n\t/**\n\t * Compares the texture parameters _excluding_ the subtexture information.\n\t *\n\t * @return true if the parameters are equal, else false.\n\t */\n\tbool operator==(Texture2dInfo const &);\n\n\t/**\n\t * Compares the texture parameters _excluding_ the subtexture information.\n\t * Inverse of \\p operator==(..) .\n\t *\n\t * @return true if the parameters are not equal, else false.\n\t */\n\tbool operator!=(Texture2dInfo const &);\n\n\t/**\n\t * Get the dimensions of the whole texture bitmap.\n\t *\n\t * @return Size of texture as (width, height) tuple.\n\t */\n\tstd::pair<int32_t, int32_t> get_size() const;\n\n\t/**\n\t * Get the format of pixels in this texture.\n\t *\n\t * @return Pixel format.\n\t */\n\tpixel_format get_format() const;\n\n\t/**\n\t * Get the byte alignment of texture rows to byte boundaries.\n\t *\n\t * @return Size of byte alignment in bytes.\n\t */\n\tsize_t get_row_alignment() const;\n\n\t/**\n\t * Get the size of a single row in bytes, including possible\n\t * padding at its end.\n\t *\n\t * @return Row size (in bytes).\n\t */\n\tsize_t get_row_size() const;\n\n\t/**\n\t * Get the size in bytes of the raw pixel data. It is equal to\n\t * \\p get_row_size() * \\p get_size().second .\n\t *\n\t * @return Size of the raw pixel data (in bytes).\n\t */\n\tsize_t get_data_size() const;\n\n\t/**\n\t * Get the path to the image resource of this texture.\n\t *\n\t * @return Image path to texture.\n\t */\n\tconst std::optional<util::Path> &get_image_path() const;\n\n\t/**\n\t * Get the number of subtextures in this texture.\n\t *\n\t * @return Number of subtextures.\n\t */\n\tsize_t get_subtex_count() const;\n\n\t/**\n\t * Get the subtexture information for a specific subtexture.\n\t *\n\t * @param subidx Index of the subtexture.\n\t * @return Subtexture information object.\n\t */\n\tconst Texture2dSubInfo &get_subtex_info(size_t subidx) const;\n\n\t/**\n\t * Get the coordinates of a specific subtexture inside the main texture.\n\t * Coordinates are returned as normalized values (floats in range 0.0 to 1.0).\n\t *\n\t * @deprecated Use \\p get_subtex_tile_params() instead.\n\t *\n\t * @param subidx Index of the subtexture.\n\t *\n\t * @return 4-tuple with normalized coordinates: (left, right, top, bottom)\n\t */\n\tstd::tuple<float, float, float, float> get_subtexture_coordinates(size_t subidx) const;\n\nprivate:\n\t/**\n\t * Width and height of this texture.\n\t */\n\tint32_t w, h;\n\n\t/**\n\t * Storage format of the pixels inside the texture.\n\t */\n\tpixel_format format;\n\n\t/**\n\t * The alignment of texture rows to byte boundaries. Can be 1, 2, 4 or 8.\n\t * There is padding at the end of each row to match the alignment if the\n\t * row size is not a multiple of the alignment.\n\t */\n\tsize_t row_alignment;\n\n\t/**\n\t * Path to image resource.\n\t */\n\tstd::optional<util::Path> imagepath;\n\n\t/**\n\t * Positions of subtextures inside the texture.\n\t *\n\t * Some textures are merged together into texture atlases, large images which contain\n\t * more than one individual texture.\n\t */\n\tstd::vector<Texture2dSubInfo> subtextures;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_subinfo.cpp",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#include \"texture_subinfo.h\"\n\n\nnamespace openage::renderer::resources {\n\nTexture2dSubInfo::Texture2dSubInfo(int32_t x,\n                                   int32_t y,\n                                   uint32_t w,\n                                   uint32_t h,\n                                   int32_t cx,\n                                   int32_t cy,\n                                   uint32_t atlas_width,\n                                   uint32_t atlas_height) :\n\tpos{x, y},\n\tsize{w, h},\n\tanchor_pos{cx, cy},\n\ttile_params{\n\t\tstatic_cast<float>(x) / atlas_width,\n\t\tstatic_cast<float>(y) / atlas_height,\n\t\tstatic_cast<float>(w) / atlas_width,\n\t\tstatic_cast<float>(h) / atlas_height,\n\t},\n\tanchor_params{\n\t\tw - 2 * cx,\n\t\t-h + 2 * cy} {}\n\nconst Eigen::Vector2i &Texture2dSubInfo::get_pos() const {\n\treturn this->pos;\n}\n\nconst Eigen::Vector2<uint32_t> &Texture2dSubInfo::get_size() const {\n\treturn this->size;\n}\n\nconst Eigen::Vector2i &Texture2dSubInfo::get_anchor_pos() const {\n\treturn this->anchor_pos;\n}\n\nconst Eigen::Vector4f &Texture2dSubInfo::get_subtex_coords() const {\n\treturn this->tile_params;\n}\n\nconst Eigen::Vector2i &Texture2dSubInfo::get_anchor_params() const {\n\treturn this->anchor_params;\n}\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/resources/texture_subinfo.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n#include <eigen3/Eigen/Dense>\n\nnamespace openage::renderer::resources {\n\n/**\n * Describes a position of a (sub)texture within a larger texture.\n * Used for texture atlases and animations where all frames\n * are within one texture file.\n */\nclass Texture2dSubInfo {\npublic:\n\t/**\n\t * Create a new 2D subtexture information.\n\t *\n\t * @param x Vertical position inside texture atlas.\n\t * @param y Horizontal position inside texture atlas.\n\t * @param w Width of subtexture.\n\t * @param h Height of subtexture.\n\t * @param cx Vertical anchor of subtexture.\n\t * @param cy Horizontal anchor of subtexture.\n\t * @param atlas_width Width of the texture atlas containing the subtexture.\n\t * @param atlas_height Height of the texture atlas containing the subtexture.\n\t */\n\tTexture2dSubInfo(int32_t x,\n\t                 int32_t y,\n\t                 uint32_t w,\n\t                 uint32_t h,\n\t                 int32_t cx,\n\t                 int32_t cy,\n\t                 uint32_t atlas_width,\n\t                 uint32_t atlas_height);\n\n\tTexture2dSubInfo() = default;\n\n\t/**\n\t * Get the position of the subtexture within the atlas.\n\t *\n\t * @return Pixel coordinates as 2-dimensional Eigen vector: (x, y)\n\t */\n\tconst Eigen::Vector2i &get_pos() const;\n\n\t/**\n\t * Get the size of the subtexture.\n\t *\n\t * @return Pixel coordinates as 2-dimensional Eigen vector: (width, height)\n\t */\n\tconst Eigen::Vector2<uint32_t> &get_size() const;\n\n\t/**\n\t * Get the position of the subtexture anchor (origin == top left).\n\t *\n\t * @return Anchor coordinates as 2-dimensional Eigen vector: (x, y)\n\t */\n\tconst Eigen::Vector2i &get_anchor_pos() const;\n\n\t/**\n\t * Get the normalized shader parameters of the subtexture. Use in the shader\n\t * to sample the subtexture from the atlas.\n\t *\n\t * Values are in range (0.0, 1.0) and can be passed directly to a shader uniform.\n\t * These parameters pre-computed and should be used whenever possible.\n\t *\n\t * @return Tile parameters as 4-dimensional Eigen vector: (x, y, width, height)\n\t */\n\tconst Eigen::Vector4f &get_subtex_coords() const;\n\n\t/**\n\t * Get the anchor parameters of the subtexture center. Used in the shader\n\t * to calculate the offset position for displaying the subtexture inside\n\t * the OpenGL viewport.\n\t *\n\t * The parameters represent the pixel distance of the anchor point to the subtexture\n\t * center, multiplied by 2 to account for the normalized viewport size (which is 2.0\n\t * because it spans from -1.0 to 1.0).\n\t *\n\t * To get the normalized offset distance, the parameters have to be divided by the\n\t * viewport size and then multiplied by additional scaling factors (e.g. from the\n\t * animation).\n\t *\n\t * @return Parameters as 2-dimensional Eigen vector: (x, y)\n\t */\n\tconst Eigen::Vector2i &get_anchor_params() const;\n\nprivate:\n\t/**\n\t * Position within the atlas (top left corner: x, y).\n\t */\n\tEigen::Vector2i pos;\n\n\t/**\n\t * Size in pixels (width, height).\n\t */\n\tEigen::Vector2<uint32_t> size;\n\n\t/**\n\t * Subtexture anchor relative to the subtexture's top left (x, y).\n\t */\n\tEigen::Vector2i anchor_pos;\n\n\t/**\n\t * Pre-computed normalized coordinates of the subtexture.\n\t */\n\tEigen::Vector4f tile_params;\n\n\t/**\n\t * Pre-computed normalized coordinates of the subtexture anchor.\n\t */\n\tEigen::Vector2i anchor_params;\n};\n\n} // namespace openage::renderer::resources\n"
  },
  {
    "path": "libopenage/renderer/shader_program.cpp",
    "content": "// Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n#include \"shader_program.h\"\n"
  },
  {
    "path": "libopenage/renderer/shader_program.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <cstdint>\n#include <map>\n#include <memory>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/types.h\"\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage {\nnamespace renderer {\nclass Texture2d;\nclass UniformBuffer;\n\n\nclass ShaderProgram : public std::enable_shared_from_this<ShaderProgram> {\n\tfriend UniformInput;\n\npublic:\n\tvirtual ~ShaderProgram() = default;\n\n\t/**\n\t * Get the ID of a uniform variable in the shader program.\n\t *\n\t * @param unif Name of the uniform.\n\t *\n\t * @return ID of the uniform in the shader.\n\t */\n\tvirtual uniform_id_t get_uniform_id(const char *name) = 0;\n\n\t/**\n\t * Check whether the shader program contains a uniform variable with the given ID.\n\t *\n\t * @param unif Name of the uniform.\n\t *\n\t * @return true if the shader program contains the uniform, false otherwise.\n\t */\n\tvirtual bool has_uniform(const char *name) = 0;\n\n\t/**\n\t * Binds a uniform block in the shader program to the same binding point as\n\t * the given uniform buffer.\n\t *\n\t * @param buffer Uniform buffer to bind.\n\t * @param block_name Name of the uniform block in the shader program.\n\t */\n\tvirtual void bind_uniform_buffer(const char *block_name,\n\t                                 std::shared_ptr<UniformBuffer> const &) = 0;\n\n\t/**\n\t * Creates a new uniform input (a binding of uniform names to values) for this shader\n\t * and optionally sets some uniform values. To do that, just pass two arguments -\n\t * - a string literal and the value for that uniform for any uniform you want to set.\n\t *\n\t * For example new_uniform_input(\"color\", { 0.5, 0.5, 0.5, 1.0 }, \"num\", 5) will set\n\t * \"color\" to { 0.5, 0.5, 0.5, 0.5 } and \"num\" to 5. Types are important here and a type\n\t * mismatch between the uniform variable and the input might result in an error.\n\t */\n\ttemplate <typename... Ts>\n\tstd::shared_ptr<UniformInput> new_uniform_input(Ts &&...vals) {\n\t\tauto input = this->new_unif_in();\n\t\tinput->update(vals...);\n\t\treturn input;\n\t}\n\n\t/**\n\t * Alias for the \\p new_uniform_input() function to create empty shader program inputs. Makes more sense\n\t * name-wise when the shader doesn't have any uniforms.\n\t */\n\tstd::shared_ptr<UniformInput> create_empty_input() {\n\t\treturn this->new_uniform_input();\n\t}\n\n\t/**\n\t * Get a list of _active_ vertex attributes in the shader program. Active attributes\n\t * are those which have an effect on the shader output, meaning they are included in the\n\t * output calculation and are not unused. Inactive attributes may or may not be present\n\t * in the list - in particular, in the OpenGL implementation they will most likely be missing.\n\t *\n\t * @return Map from the attribute location to its type. Locations do not need to be consecutive.\n\t */\n\tvirtual std::map<size_t, resources::vertex_input_t> vertex_attributes() const = 0;\n\nprotected:\n\tvirtual std::shared_ptr<UniformInput> new_unif_in() = 0;\n\n\t/**\n\t * Set a uniform input variable in the actual shader program.\n\t */\n\tvirtual void set_i32(UniformInput &in, const char *, int32_t) = 0;\n\tvirtual void set_u32(UniformInput &in, const char *, uint32_t) = 0;\n\tvirtual void set_f32(UniformInput &in, const char *, float) = 0;\n\tvirtual void set_f64(UniformInput &in, const char *, double) = 0;\n\tvirtual void set_bool(UniformInput &in, const char *, bool) = 0;\n\tvirtual void set_v2f32(UniformInput &in, const char *, Eigen::Vector2f const &) = 0;\n\tvirtual void set_v3f32(UniformInput &in, const char *, Eigen::Vector3f const &) = 0;\n\tvirtual void set_v4f32(UniformInput &in, const char *, Eigen::Vector4f const &) = 0;\n\tvirtual void set_v2i32(UniformInput &in, const char *, Eigen::Vector2i const &) = 0;\n\tvirtual void set_v3i32(UniformInput &in, const char *, Eigen::Vector3i const &) = 0;\n\tvirtual void set_v4i32(UniformInput &in, const char *, Eigen::Vector4i const &) = 0;\n\tvirtual void set_v2ui32(UniformInput &in, const char *, Eigen::Vector2<uint32_t> const &) = 0;\n\tvirtual void set_v3ui32(UniformInput &in, const char *, Eigen::Vector3<uint32_t> const &) = 0;\n\tvirtual void set_v4ui32(UniformInput &in, const char *, Eigen::Vector4<uint32_t> const &) = 0;\n\tvirtual void set_m4f32(UniformInput &in, const char *, Eigen::Matrix4f const &) = 0;\n\tvirtual void set_tex(UniformInput &in, const char *, std::shared_ptr<Texture2d> const &) = 0;\n\n\tvirtual void set_i32(UniformInput &in, uniform_id_t id, int32_t) = 0;\n\tvirtual void set_u32(UniformInput &in, uniform_id_t id, uint32_t) = 0;\n\tvirtual void set_f32(UniformInput &in, uniform_id_t id, float) = 0;\n\tvirtual void set_f64(UniformInput &in, uniform_id_t id, double) = 0;\n\tvirtual void set_bool(UniformInput &in, uniform_id_t id, bool) = 0;\n\tvirtual void set_v2f32(UniformInput &in, uniform_id_t id, Eigen::Vector2f const &) = 0;\n\tvirtual void set_v3f32(UniformInput &in, uniform_id_t id, Eigen::Vector3f const &) = 0;\n\tvirtual void set_v4f32(UniformInput &in, uniform_id_t id, Eigen::Vector4f const &) = 0;\n\tvirtual void set_v2i32(UniformInput &in, uniform_id_t id, Eigen::Vector2i const &) = 0;\n\tvirtual void set_v3i32(UniformInput &in, uniform_id_t id, Eigen::Vector3i const &) = 0;\n\tvirtual void set_v4i32(UniformInput &in, uniform_id_t id, Eigen::Vector4i const &) = 0;\n\tvirtual void set_v2ui32(UniformInput &in, uniform_id_t id, Eigen::Vector2<uint32_t> const &) = 0;\n\tvirtual void set_v3ui32(UniformInput &in, uniform_id_t id, Eigen::Vector3<uint32_t> const &) = 0;\n\tvirtual void set_v4ui32(UniformInput &in, uniform_id_t id, Eigen::Vector4<uint32_t> const &) = 0;\n\tvirtual void set_m4f32(UniformInput &in, uniform_id_t id, Eigen::Matrix4f const &) = 0;\n\tvirtual void set_tex(UniformInput &in, uniform_id_t id, std::shared_ptr<Texture2d> const &) = 0;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/stages/CMakeLists.txt",
    "content": "add_sources(libopenage\n\trender_entity.cpp\n)\n\nadd_subdirectory(camera/)\nadd_subdirectory(hud/)\nadd_subdirectory(screen/)\nadd_subdirectory(skybox/)\nadd_subdirectory(terrain/)\nadd_subdirectory(world/)\n"
  },
  {
    "path": "libopenage/renderer/stages/camera/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tmanager.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/camera/manager.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"manager.h\"\n\n#include <eigen3/Eigen/Dense>\n#include <numbers>\n\n#include \"renderer/uniform_buffer.h\"\n#include \"renderer/uniform_input.h\"\n\nnamespace openage::renderer::camera {\n\nCameraManager::CameraManager(const std::shared_ptr<renderer::camera::Camera> &camera,\n                             const CameraBoundaries &camera_boundaries) :\n\tcamera{camera},\n\tmove_motion_directions{static_cast<int>(MoveDirection::NONE)},\n\tzoom_motion_direction{static_cast<int>(ZoomDirection::NONE)},\n\tmove_motion_speed{0.2f},\n\tzoom_motion_speed{0.05f},\n\tcamera_boundaries{camera_boundaries} {\n\tthis->uniforms = this->camera->get_uniform_buffer()->new_uniform_input(\n\t\t\"view\",\n\t\tcamera->get_view_matrix(),\n\t\t\"proj\",\n\t\tcamera->get_projection_matrix());\n}\n\nvoid CameraManager::update() {\n\tthis->update_motion();\n\tthis->update_uniforms();\n}\n\nvoid CameraManager::move_frame(MoveDirection direction, float speed) {\n\tswitch (direction) {\n\tcase MoveDirection::LEFT:\n\t\t// half the speed because the relationship between forward/back and\n\t\t// left/right is 1:2 in our ortho projection.\n\t\tthis->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, 1.0f), speed / 2, this->camera_boundaries);\n\t\tbreak;\n\tcase MoveDirection::RIGHT:\n\t\t// half the speed because the relationship between forward/back and\n\t\t// left/right is 1:2 in our ortho projection.\n\t\tthis->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, -1.0f), speed / 2, this->camera_boundaries);\n\t\tbreak;\n\tcase MoveDirection::FORWARD:\n\t\tthis->camera->move_rel(Eigen::Vector3f(-1.0f, 0.0f, -1.0f), speed, this->camera_boundaries);\n\t\tbreak;\n\tcase MoveDirection::BACKWARD:\n\t\tthis->camera->move_rel(Eigen::Vector3f(1.0f, 0.0f, 1.0f), speed, this->camera_boundaries);\n\t\tbreak;\n\n\tdefault:\n\t\tbreak;\n\t}\n}\n\nvoid CameraManager::zoom_frame(ZoomDirection direction, float speed) {\n\tswitch (direction) {\n\tcase ZoomDirection::IN:\n\t\tthis->camera->zoom_in(speed);\n\t\tbreak;\n\tcase ZoomDirection::OUT:\n\t\tthis->camera->zoom_out(speed);\n\t\tbreak;\n\n\tdefault:\n\t\tbreak;\n\t}\n}\n\nvoid CameraManager::set_camera_boundaries(const CameraBoundaries &camera_boundaries) {\n\tthis->camera_boundaries = camera_boundaries;\n}\n\nvoid CameraManager::update_motion() {\n\tif (this->move_motion_directions != static_cast<int>(MoveDirection::NONE)) {\n\t\tEigen::Vector3f move_dir{0.0f, 0.0f, 0.0f};\n\n\t\tif (this->move_motion_directions & static_cast<int>(MoveDirection::LEFT)) {\n\t\t\tmove_dir += Eigen::Vector3f(-1.0f, 0.0f, 1.0f);\n\t\t}\n\t\tif (this->move_motion_directions & static_cast<int>(MoveDirection::RIGHT)) {\n\t\t\tmove_dir += Eigen::Vector3f(1.0f, 0.0f, -1.0f);\n\t\t}\n\t\tif (this->move_motion_directions & static_cast<int>(MoveDirection::FORWARD)) {\n\t\t\tmove_dir += Eigen::Vector3f(-1.0f, 0.0f, -1.0f);\n\t\t}\n\t\tif (this->move_motion_directions & static_cast<int>(MoveDirection::BACKWARD)) {\n\t\t\tmove_dir += Eigen::Vector3f(1.0f, 0.0f, 1.0f);\n\t\t}\n\n\t\tthis->camera->move_rel(move_dir, this->move_motion_speed, this->camera_boundaries);\n\t}\n\n\tif (this->zoom_motion_direction != static_cast<int>(ZoomDirection::NONE)) {\n\t\tif (this->zoom_motion_direction & static_cast<int>(ZoomDirection::IN)) {\n\t\t\tthis->camera->zoom_in(this->zoom_motion_speed);\n\t\t}\n\t\telse if (this->zoom_motion_direction & static_cast<int>(ZoomDirection::OUT)) {\n\t\t\tthis->camera->zoom_out(this->zoom_motion_speed);\n\t\t}\n\t}\n}\n\nvoid CameraManager::update_uniforms() {\n\t// transformation matrices\n\tthis->uniforms->update(\n\t\t\"view\",\n\t\tthis->camera->get_view_matrix(),\n\t\t\"proj\",\n\t\tthis->camera->get_projection_matrix());\n\n\t// zoom scaling\n\tthis->uniforms->update(\n\t\t\"inv_zoom\",\n\t\t1.0f / this->camera->get_zoom());\n\n\tauto viewport_size = this->camera->get_viewport_size();\n\tEigen::Vector2f viewport_size_vec{\n\t\t1.0f / static_cast<float>(viewport_size[0]),\n\t\t1.0f / static_cast<float>(viewport_size[1])};\n\tthis->uniforms->update(\"inv_viewport_size\", viewport_size_vec);\n\n\t// update the uniform buffer\n\tthis->camera->get_uniform_buffer()->update_uniforms(this->uniforms);\n}\n\nvoid CameraManager::set_move_motion_dirs(int directions) {\n\tthis->move_motion_directions = directions;\n}\n\nvoid CameraManager::set_zoom_motion_dir(int direction) {\n\tthis->zoom_motion_direction = direction;\n}\n\nvoid CameraManager::set_move_motion_speed(float speed) {\n\tthis->move_motion_speed = speed;\n}\n\nvoid CameraManager::set_zoom_motion_speed(float speed) {\n\tthis->zoom_motion_speed = speed;\n}\n\n} // namespace openage::renderer::camera\n"
  },
  {
    "path": "libopenage/renderer/stages/camera/manager.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <utility>\n\n#include \"renderer/camera/camera.h\"\n\nnamespace openage::renderer {\nclass UniformBufferInput;\n\nnamespace camera {\n\nclass Camera;\n\nenum class MoveDirection {\n\tNONE = 0x0000,\n\tLEFT = 0x0001,\n\tRIGHT = 0x0002,\n\tFORWARD = 0x0004,\n\tBACKWARD = 0x0008,\n};\n\nenum class ZoomDirection {\n\tNONE = 0x0000,\n\tIN = 0x0001,\n\tOUT = 0x0002,\n};\n\n/**\n * Manages per-frame changes to camera parameters.\n *\n * The camera manager operates with directions instead of\n * scene coordinate, which should make it easier to use.\n *\n * There are two update modes:\n *     - Frame updates: Applies only to the current frame (e.g. from a key press).\n *     - Motion updates: Applies to the current frame and all subsequent frames\n *                       until the motion is stopped.\n *\n * TODO: Multiple cameras?\n * TODO: Priority queues (let updates with higher priority override lower ones)?\n *\n */\nclass CameraManager {\npublic:\n\t/**\n\t * Create a new camera manager.\n\t *\n\t * @param camera Camera to manage.\n\t * @param camera_boundaries Boundaries for the camera movement in the scene.\n\t */\n\tCameraManager(const std::shared_ptr<renderer::camera::Camera> &camera,\n\t              const CameraBoundaries &camera_boundaries = DEFAULT_CAM_BOUNDARIES);\n\t~CameraManager() = default;\n\n\t/**\n\t * Update the camera position and zoom level.\n\t *\n\t * Additionally updates the camera uniform buffer with the view and projection\n\t * matrices.\n\t */\n\tvoid update();\n\n\t/**\n\t * Move into the given direction for the current frame.\n\t *\n\t * @param direction Move direction.\n\t * @param delta Move speed, i.e. a distance multiplier.\n\t */\n\tvoid move_frame(MoveDirection direction, float speed = 1.0f);\n\n\t/**\n\t * Zoom into the given direction for the current frame.\n\t *\n\t * @param direction Zoom direction.\n\t * @param delta Zoom speed, i.e. a distance multiplier.\n\t */\n\tvoid zoom_frame(ZoomDirection direction, float speed = 1.0f);\n\n\t/**\n\t * Set the move directions of the camera.\n\t *\n\t * @param directions Bitfield of the move directions.\n\t */\n\tvoid set_move_motion_dirs(int directions);\n\n\t/**\n\t * Set the zoom direction of the camera.\n\t *\n\t * @param direction Zoom direction.\n\t */\n\tvoid set_zoom_motion_dir(int direction);\n\n\t/**\n\t * Set the move speed of the camera.\n\t *\n\t * @param speed Speed of the camera.\n\t */\n\tvoid set_move_motion_speed(float speed);\n\n\t/**\n\t * Set the zoom speed of the camera.\n\t *\n\t * @param speed Speed of the camera.\n\t */\n\tvoid set_zoom_motion_speed(float speed);\n\n\t/**\n\t * Set boundaries for camera movement in the scene.\n\t *\n\t * @param camera_boundaries XYZ boundaries for the camera movement.\n\t */\n\tvoid set_camera_boundaries(const CameraBoundaries &camera_boundaries);\n\nprivate:\n\t/**\n\t * Update the camera parameters.\n\t */\n\tvoid update_motion();\n\n\t/**\n\t * Update the camera uniform buffer.\n\t */\n\tvoid update_uniforms();\n\n\t/**\n\t * Camera.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Bitfield of the current move motion directions.\n\t */\n\tint move_motion_directions;\n\n\t/**\n\t * Bitfield of the current zoom motion direction.\n\t */\n\tint zoom_motion_direction;\n\n\t/**\n\t * Move motion speed of the camera.\n\t */\n\tfloat move_motion_speed;\n\n\t/**\n\t * Zoom motion speed of the camera.\n\t */\n\tfloat zoom_motion_speed;\n\n\t/**\n\t * Uniform buffer input for the camera.\n\t */\n\tstd::shared_ptr<renderer::UniformBufferInput> uniforms;\n\n\t/**\n\t * Camera boundaries for X and Z movement. Contains minimum and maximum values for each axes.\n\t */\n\tCameraBoundaries camera_boundaries;\n};\n\n} // namespace camera\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tobject.cpp\n\trender_entity.cpp\n\trender_stage.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/object.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"object.h\"\n\n#include \"renderer/geometry.h\"\n#include \"renderer/stages/hud/render_entity.h\"\n\n\nnamespace openage::renderer::hud {\n\nHudDragObject::HudDragObject(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager) :\n\trequire_renderable{true},\n\tchanged{false},\n\tcamera{nullptr},\n\tasset_manager{asset_manager},\n\trender_entity{nullptr},\n\tdrag_pos{nullptr, 0, \"\", nullptr, {0, 0}},\n\tdrag_start{0, 0},\n\tuniforms{nullptr},\n\tgeometry{nullptr},\n\tlast_update{0.0} {\n}\n\nvoid HudDragObject::set_render_entity(const std::shared_ptr<DragRenderEntity> &entity) {\n\tthis->render_entity = entity;\n\tthis->fetch_updates();\n}\n\nvoid HudDragObject::set_camera(const std::shared_ptr<renderer::camera::Camera> &camera) {\n\tthis->camera = camera;\n}\n\nvoid HudDragObject::fetch_updates(const time::time_t &time) {\n\tif (not this->render_entity->is_changed()) {\n\t\t// exit early because there is nothing to do\n\t\treturn;\n\t}\n\n\t// Get data from render entity\n\tthis->drag_start = this->render_entity->get_drag_start();\n\n\t// Thread-safe access to curves needs a lock on the render entity's mutex\n\tauto read_lock = this->render_entity->get_read_lock();\n\n\tthis->drag_pos.sync(this->render_entity->get_drag_pos() /* , this->last_update */);\n\n\t// Unlock the render entity mutex\n\tread_lock.unlock();\n\n\t// Set self to changed so that world renderer can update the renderable\n\tthis->changed = true;\n\tthis->render_entity->clear_changed_flag();\n\tthis->last_update = time;\n}\n\nvoid HudDragObject::update_uniforms(const time::time_t & /* time */) {\n\t// TODO: Only update uniforms that changed since last update\n\tif (this->uniforms == nullptr) [[unlikely]] {\n\t\treturn;\n\t}\n\n\t// TODO: Do something with the uniforms\n}\n\nvoid HudDragObject::update_geometry(const time::time_t &time) {\n\t// TODO: Only update geometry that changed since last update\n\tif (this->geometry == nullptr) [[unlikely]] {\n\t\treturn;\n\t}\n\n\tauto drag_start_ndc = this->drag_start.to_viewport(this->camera).to_ndc_space(this->camera);\n\tauto drag_pos_ndc = this->drag_pos.get(time).to_viewport(this->camera).to_ndc_space(this->camera);\n\n\tfloat top = std::max(drag_start_ndc.y(), drag_pos_ndc.y());\n\tfloat bottom = std::min(drag_start_ndc.y(), drag_pos_ndc.y());\n\tfloat left = std::min(drag_start_ndc.x(), drag_pos_ndc.x());\n\tfloat right = std::max(drag_start_ndc.x(), drag_pos_ndc.x());\n\n\tlog::log(SPAM << \"top: \" << top\n\t              << \", bottom: \" << bottom\n\t              << \", left: \" << left\n\t              << \", right: \" << right);\n\n\tstd::array<float, 16> quad_vertices{\n\t\tleft, top, 0.0f, 1.0f, // top left corner\n\t\tleft,\n\t\tbottom,\n\t\t0.0f,\n\t\t0.0f, // bottom left corner\n\t\tright,\n\t\ttop,\n\t\t1.0f,\n\t\t1.0f, // top right corner\n\t\tright,\n\t\tbottom,\n\t\t1.0f,\n\t\t0.0f // bottom right corner\n\t};\n\n\tstd::vector<uint8_t> vertex_data(quad_vertices.size() * sizeof(float));\n\tstd::memcpy(vertex_data.data(), quad_vertices.data(), vertex_data.size());\n\n\tthis->geometry->update_verts(vertex_data);\n}\n\nbool HudDragObject::requires_renderable() {\n\treturn this->require_renderable;\n}\n\nvoid HudDragObject::clear_requires_renderable() {\n\tthis->require_renderable = false;\n}\n\nbool HudDragObject::is_changed() {\n\treturn this->changed;\n}\n\nvoid HudDragObject::clear_changed_flag() {\n\tthis->changed = false;\n}\n\nvoid HudDragObject::set_uniforms(const std::shared_ptr<renderer::UniformInput> &uniforms) {\n\tthis->uniforms = uniforms;\n}\n\nvoid HudDragObject::set_geometry(const std::shared_ptr<renderer::Geometry> &geometry) {\n\tthis->geometry = geometry;\n}\n\n} // namespace openage::renderer::hud\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/object.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <list>\n#include <memory>\n#include <string>\n\n#include \"coord/pixel.h\"\n#include \"curve/continuous.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::renderer {\nclass Geometry;\nclass UniformInput;\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace resources {\nclass AssetManager;\nclass Animation2dInfo;\n} // namespace resources\n\nnamespace hud {\nclass DragRenderEntity;\n\n/**\n * Stores the state of a renderable object in the HUD render stage.\n */\nclass HudDragObject {\npublic:\n\t/**\n\t * Create a new object for the HUD render stage.\n\t *\n\t * @param asset_manager Asset manager for loading resources.\n\t */\n\tHudDragObject(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager);\n\t~HudDragObject() = default;\n\n\t/**\n\t * Set the world render entity.\n\t *\n\t * @param entity New world render entity.\n\t */\n\tvoid set_render_entity(const std::shared_ptr<DragRenderEntity> &entity);\n\n\t/**\n\t * Set the current camera of the scene.\n\t *\n\t * @param camera Camera object viewing the scene.\n\t */\n\tvoid set_camera(const std::shared_ptr<renderer::camera::Camera> &camera);\n\n\t/**\n\t * Fetch updates from the render entity.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid fetch_updates(const time::time_t &time = 0.0);\n\n\t/**\n\t * Update the uniforms of the renderable associated with this object.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_uniforms(const time::time_t &time = 0.0);\n\n\t/**\n\t * Update the geometry of the renderable associated with this object.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_geometry(const time::time_t &time = 0.0);\n\n\t/**\n\t * Check whether a new renderable needs to be created for this mesh.\n\t *\n\t * If true, the old renderable should be removed from the render pass.\n\t * The updated uniforms and geometry should be passed to this mesh.\n\t * Afterwards, clear the requirement flag with \\p clear_requires_renderable().\n\t *\n\t * @return true if a new renderable is required, else false.\n\t */\n\tbool requires_renderable();\n\n\t/**\n\t * Indicate to this mesh that a new renderable has been created.\n\t */\n\tvoid clear_requires_renderable();\n\n\t/**\n\t * Check whether the object was changed by \\p update().\n\t *\n\t * @return true if changes were made, else false.\n\t */\n\tbool is_changed();\n\n\t/**\n\t * Clear the update flag by setting it to false.\n\t */\n\tvoid clear_changed_flag();\n\n\t/**\n\t * Set the reference to the uniform inputs of the renderable\n\t * associated with this object. Relevant uniforms are updated\n\t * when calling \\p update().\n\t *\n\t * @param uniforms Uniform inputs of this object's renderable.\n\t */\n\tvoid set_uniforms(const std::shared_ptr<renderer::UniformInput> &uniforms);\n\n\t/**\n\t * Set the geometry of the renderable associated with this object.\n\t *\n\t * The geometry is updated when calling \\p update().\n\t *\n\t * @param geometry Geometry of this object's renderable.\n\t */\n\tvoid set_geometry(const std::shared_ptr<renderer::Geometry> &geometry);\n\nprivate:\n\t/**\n\t * Stores whether a new renderable for this object needs to be created\n\t * for the render pass.\n\t */\n\tbool require_renderable;\n\n\t/**\n\t * Stores whether the \\p update() call changed the object.\n\t */\n\tbool changed;\n\n\t/**\n\t * Camera for model uniforms.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Asset manager for central accessing and loading asset resources.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Source for positional and texture data.\n\t */\n\tstd::shared_ptr<DragRenderEntity> render_entity;\n\n\t/**\n\t * Position of the dragged corner.\n\t */\n\tcurve::Continuous<coord::input> drag_pos;\n\n\t/**\n\t * Position of the start corner.\n\t */\n\tcoord::input drag_start;\n\n\t/**\n\t * Shader uniforms for the renderable in the HUD render pass.\n\t */\n\tstd::shared_ptr<renderer::UniformInput> uniforms;\n\n\t/**\n\t * Geometry of the renderable in the HUD render pass.\n\t */\n\tstd::shared_ptr<renderer::Geometry> geometry;\n\n\t/**\n\t * Time of the last update call.\n\t */\n\ttime::time_t last_update;\n};\n} // namespace hud\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/render_entity.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_entity.h\"\n\n#include <mutex>\n\n\nnamespace openage::renderer::hud {\n\nDragRenderEntity::DragRenderEntity(const coord::input drag_start) :\n\trenderer::RenderEntity{},\n\tdrag_pos{nullptr, 0, \"\", nullptr, drag_start},\n\tdrag_start{drag_start} {\n}\n\nvoid DragRenderEntity::update(const coord::input drag_pos,\n                              const time::time_t time) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->drag_pos.set_insert(time, drag_pos);\n\n\tthis->last_update = time;\n\tthis->changed = true;\n}\n\nconst curve::Continuous<coord::input> &DragRenderEntity::get_drag_pos() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->drag_pos;\n}\n\nconst coord::input DragRenderEntity::get_drag_start() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->drag_start;\n}\n\n} // namespace openage::renderer::hud\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/render_entity.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n#include \"coord/pixel.h\"\n#include \"curve/continuous.h\"\n#include \"renderer/stages/render_entity.h\"\n\n\nnamespace openage::renderer::hud {\n\n/**\n * Render entity for pushing drag selection updates to the HUD renderer.\n */\nclass DragRenderEntity final : public renderer::RenderEntity {\npublic:\n\t/**\n\t * Create a new render entity for drag selection in the HUD.\n\t *\n\t * @param drag_start Position of the start corner.\n\t */\n\tDragRenderEntity(const coord::input drag_start);\n\t~DragRenderEntity() = default;\n\n\t/**\n\t * Update the render entity with information from the gamestate\n\t * or input system.\n\t *\n\t * Updating the render entity with this method is thread-safe.\n\t *\n\t * @param drag_pos Position of the dragged corner.\n\t * @param time Current simulation time.\n\t */\n\tvoid update(const coord::input drag_pos,\n\t            const time::time_t time = 0.0);\n\n\t/**\n\t * Get the position of the dragged corner.\n\t *\n\t * Accessing the drag position curve REQUIRES a read lock on the render entity\n\t * (using \\p get_read_lock()) to ensure thread safety.\n\t *\n\t * @return Coordinates of the dragged corner.\n\t */\n\tconst curve::Continuous<coord::input> &get_drag_pos();\n\n\t/**\n\t * Get the position of the start corner.\n\t *\n\t * Accessing the drag start is thread-safe.\n\t *\n\t * @return Coordinates of the start corner.\n\t */\n\tconst coord::input get_drag_start();\n\nprivate:\n\t/**\n\t * Position of the dragged corner.\n\t */\n\tcurve::Continuous<coord::input> drag_pos;\n\n\t/**\n\t * Position of the start corner.\n\t */\n\tcoord::input drag_start;\n};\n} // namespace openage::renderer::hud\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/render_stage.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_stage.h\"\n\n#include \"renderer/camera/camera.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/stages/hud/object.h\"\n#include \"renderer/texture.h\"\n#include \"renderer/window.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::renderer::hud {\n\nHudRenderStage::HudRenderStage(const std::shared_ptr<Window> &window,\n                               const std::shared_ptr<renderer::Renderer> &renderer,\n                               const std::shared_ptr<renderer::camera::Camera> &camera,\n                               const util::Path &shaderdir,\n                               const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n                               const std::shared_ptr<time::Clock> clock) :\n\trenderer{renderer},\n\tcamera{camera},\n\tasset_manager{asset_manager},\n\tdrag_object{nullptr},\n\tclock{clock} {\n\trenderer::opengl::GlContext::check_error();\n\n\tauto size = window->get_size();\n\tthis->initialize_render_pass(size[0], size[1], shaderdir);\n\n\twindow->add_resize_callback([this](size_t width, size_t height, double /*scale*/) {\n\t\tthis->resize(width, height);\n\t});\n\n\tlog::log(INFO << \"Created render stage 'HUD'\");\n}\n\nstd::shared_ptr<renderer::RenderPass> HudRenderStage::get_render_pass() {\n\treturn this->render_pass;\n}\n\nvoid HudRenderStage::add_drag_entity(const std::shared_ptr<DragRenderEntity> entity) {\n\tstd::unique_lock lock{this->mutex};\n\n\tauto hud_object = std::make_shared<HudDragObject>(this->asset_manager);\n\thud_object->set_render_entity(entity);\n\thud_object->set_camera(this->camera);\n\tthis->drag_object = hud_object;\n}\n\nvoid HudRenderStage::remove_drag_entity() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->drag_object = nullptr;\n\tthis->render_pass->clear_renderables();\n}\n\nvoid HudRenderStage::update() {\n\tstd::unique_lock lock{this->mutex};\n\tauto current_time = this->clock->get_real_time();\n\n\tif (this->drag_object) {\n\t\tthis->drag_object->fetch_updates(current_time);\n\t\tif (this->drag_object->requires_renderable()) {\n\t\t\tauto geometry = this->renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\t\t\tauto transform_unifs = this->drag_select_shader->new_uniform_input(\n\t\t\t\t\"in_col\",\n\t\t\t\tEigen::Vector4f{0.0f, 0.0f, 0.0f, 0.2f});\n\n\t\t\tRenderable display_obj{\n\t\t\t\ttransform_unifs,\n\t\t\t\tgeometry,\n\t\t\t\ttrue,\n\t\t\t\ttrue,\n\t\t\t};\n\n\t\t\tthis->render_pass->add_renderables(std::move(display_obj));\n\t\t\tthis->drag_object->clear_requires_renderable();\n\n\t\t\tthis->drag_object->set_uniforms(transform_unifs);\n\t\t\tthis->drag_object->set_geometry(geometry);\n\t\t}\n\t\tthis->drag_object->update_uniforms(current_time);\n\t\tthis->drag_object->update_geometry(current_time);\n\t}\n}\n\nvoid HudRenderStage::resize(size_t width, size_t height) {\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture});\n\tthis->render_pass->set_target(fbo);\n}\n\nvoid HudRenderStage::initialize_render_pass(size_t width,\n                                            size_t height,\n                                            const util::Path &shaderdir) {\n\t// Drag select shader\n\tauto vert_shader_file = (shaderdir / \"hud_drag_select.vert.glsl\").open();\n\tauto vert_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_file = (shaderdir / \"hud_drag_select.frag.glsl\").open();\n\tauto frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tfrag_shader_file.read());\n\tfrag_shader_file.close();\n\n\tthis->drag_select_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src});\n\n\t// Texture targets\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture});\n\tthis->render_pass = this->renderer->add_render_pass({}, fbo);\n}\n\n} // namespace openage::renderer::hud\n"
  },
  {
    "path": "libopenage/renderer/stages/hud/render_stage.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n#include <vector>\n\n#include \"util/path.h\"\n\nnamespace openage {\n\nnamespace time {\nclass Clock;\n}\n\nnamespace renderer {\nclass Renderer;\nclass RenderPass;\nclass ShaderProgram;\nclass Texture2d;\nclass Window;\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace resources {\nclass AssetManager;\n}\n\nnamespace hud {\nclass HudDragObject;\nclass DragRenderEntity;\n\n/**\n * Renderer for the \"Heads-Up Display\" (HUD).\n * Draws UI elements that are not part of the GUI, e.g. health bars, selection boxes, minimap, etc.\n *\n * TODO: Currently only supports drag selection.\n */\nclass HudRenderStage {\npublic:\n\t/**\n\t * Create a new render stage for the HUD.\n\t *\n\t * @param window openage window targeted for rendering.\n\t * @param renderer openage low-level renderer.\n\t * @param camera Camera used for the rendered scene.\n\t * @param shaderdir Directory containing the shader source files.\n\t * @param asset_manager Asset manager for loading resources.\n\t * @param clock Simulation clock for timing animations.\n\t */\n\tHudRenderStage(const std::shared_ptr<Window> &window,\n\t               const std::shared_ptr<renderer::Renderer> &renderer,\n\t               const std::shared_ptr<renderer::camera::Camera> &camera,\n\t               const util::Path &shaderdir,\n\t               const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n\t               const std::shared_ptr<time::Clock> clock);\n\t~HudRenderStage() = default;\n\n\t/**\n\t * Get the render pass of the HUD renderer.\n\t *\n\t * @return Render pass for HUD drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass();\n\n\t/**\n\t * Add a new render entity for drag selection.\n\t *\n\t * @param render_entity New render entity.\n\t */\n\tvoid add_drag_entity(const std::shared_ptr<DragRenderEntity> entity);\n\n\t/**\n\t * Remove the render object for drag selection.\n\t *\n\t * @param render_entity Render entity to remove.\n\t */\n\tvoid remove_drag_entity();\n\n\t/**\n\t * Update the render entities and render positions.\n\t */\n\tvoid update();\n\n\t/**\n\t * Resize the FBO for the HUD rendering. This basically updates the output\n\t * texture size.\n\t *\n\t * @param width New width of the FBO.\n\t * @param height New height of the FBO.\n\t */\n\tvoid resize(size_t width, size_t height);\n\nprivate:\n\t/**\n\t * Create the render pass for HUD drawing.\n\t *\n\t * Called during initialization of the HUD renderer.\n\t *\n\t * @param width Width of the FBO.\n\t * @param height Height of the FBO.\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(size_t width,\n\t                            size_t height,\n\t                            const util::Path &shaderdir);\n\n\t/**\n\t * Reference to the openage renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Camera for model uniforms.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Texture manager for loading assets.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Render pass for the HUD drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\t/**\n\t * Render object for the drag select rectangle.\n\t */\n\tstd::shared_ptr<HudDragObject> drag_object;\n\n\t/**\n\t * Shader for rendering the drag select rectangle.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> drag_select_shader;\n\n\t/**\n\t * Simulation clock for timing animations.\n\t */\n\tstd::shared_ptr<time::Clock> clock;\n\n\t/**\n\t * Output texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> output_texture;\n\n\t/**\n\t * Depth texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> depth_texture;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n} // namespace hud\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/stages/render_entity.cpp",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_entity.h\"\n\n#include <mutex>\n\n\nnamespace openage::renderer {\n\nRenderEntity::RenderEntity() :\n\tchanged{false},\n\tlast_update{time::time_t::zero()} {\n}\n\ntime::time_t RenderEntity::get_update_time() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->last_update;\n}\n\nbool RenderEntity::is_changed() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->changed;\n}\n\nvoid RenderEntity::clear_changed_flag() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->changed = false;\n}\n\nstd::shared_lock<std::shared_mutex> RenderEntity::get_read_lock() {\n\treturn std::shared_lock{this->mutex};\n}\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/render_entity.h",
    "content": "// Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <mutex>\n#include <shared_mutex>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"time/time.h\"\n\n\nnamespace openage::renderer {\n\n/**\n * Interface for render entities that allow pushing updates from game simulation\n * to renderer.\n */\nclass RenderEntity {\npublic:\n\t~RenderEntity() = default;\n\n\t/**\n\t * Get the time of the last update.\n\t *\n\t * Accessing the update time is thread-safe.\n\t *\n\t * @return Time of last update.\n\t */\n\ttime::time_t get_update_time();\n\n\t/**\n\t * Check whether the render entity has received new updates from the\n\t * gamestate.\n\t *\n\t * @return true if updates have been received, else false.\n\t */\n\tbool is_changed();\n\n\t/**\n\t * Clear the update flag by setting it to false.\n\t */\n\tvoid clear_changed_flag();\n\n\t/**\n\t * Get a shared lock for thread-safe reading from the render entity.\n\t *\n\t * The caller is responsible for unlocking the mutex after reading.\n\t *\n\t * @return Lock for the render entity.\n\t */\n\tstd::shared_lock<std::shared_mutex> get_read_lock();\n\nprotected:\n\t/**\n\t * Create a new render entity.\n\t *\n\t * Members are initialized to these values by default:\n\t * - \\p changed: false\n\t * - \\p last_update: time::time_t::zero()\n\t *\n\t * Declared as protected to prevent direct instantiation of this class.\n\t */\n\tRenderEntity();\n\n\t/**\n\t * Flag for determining if the render entity has been updated by the\n\t * corresponding gamestate entity. Set to true every time \\p update()\n\t * is called.\n\t */\n\tbool changed;\n\n\t/**\n\t * Time of the last update call.\n\t */\n\ttime::time_t last_update;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/screen/CMakeLists.txt",
    "content": "add_sources(libopenage\n\trender_stage.cpp\n\tscreenshot.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/screen/render_stage.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_stage.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/texture.h\"\n#include \"renderer/uniform_input.h\"\n#include \"renderer/window.h\"\n#include \"util/path.h\"\n\nnamespace openage::renderer::screen {\n\nScreenRenderStage::ScreenRenderStage(const std::shared_ptr<Window> & /* window */,\n                                     const std::shared_ptr<renderer::Renderer> &renderer,\n                                     const util::Path &shaderdir) :\n\trenderer{renderer},\n\trender_targets{},\n\tpass_outputs{} {\n\trenderer::opengl::GlContext::check_error();\n\n\tthis->initialize_render_pass(shaderdir);\n\n\tlog::log(INFO << \"Created render stage 'Screen'\");\n}\n\nstd::shared_ptr<renderer::RenderPass> ScreenRenderStage::get_render_pass() {\n\treturn this->render_pass;\n}\n\nvoid ScreenRenderStage::set_render_targets(const std::vector<std::shared_ptr<renderer::RenderTarget>> &targets) {\n\tthis->render_targets = targets;\n\tthis->update_render_pass();\n}\n\nvoid ScreenRenderStage::initialize_render_pass(const util::Path &shaderdir) {\n\tauto vert_shader_file = (shaderdir / \"final.vert.glsl\").open();\n\tauto vert_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_file = (shaderdir / \"final.frag.glsl\").open();\n\tauto frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tfrag_shader_file.read());\n\tfrag_shader_file.close();\n\n\tauto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\tthis->display_shader = renderer->add_shader({vert_shader_src, frag_shader_src});\n\n\t// nothing is drawn until render targets are added\n\tthis->render_pass = renderer->add_render_pass({}, renderer->get_display_target());\n}\n\nvoid ScreenRenderStage::update_render_pass() {\n\tauto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad());\n\n\tstd::vector<renderer::Renderable> output_layers{};\n\toutput_layers.reserve(this->render_targets.size());\n\tfor (auto &target : this->render_targets) {\n\t\tauto textures = target->get_texture_targets();\n\t\tauto texture_unif = this->display_shader->create_empty_input();\n\n\t\t// TODO: Dirty hack that only selects color textures\n\t\t// use this->pass_outputs in the future to assign output\n\t\t// textures we want to render\n\t\tfor (const auto &tex : textures) {\n\t\t\tauto format = tex->get_info().get_format();\n\t\t\tif (format == renderer::resources::pixel_format::rgba8) {\n\t\t\t\ttexture_unif->update(\"tex\", tex);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\trenderer::Renderable display_obj{\n\t\t\ttexture_unif,\n\t\t\tquad,\n\t\t\ttrue,\n\t\t\tfalse,\n\t\t};\n\t\toutput_layers.push_back(display_obj);\n\t}\n\tthis->render_pass->clear_renderables();\n\tthis->render_pass->add_renderables(std::move(output_layers));\n}\n\n} // namespace openage::renderer::screen\n"
  },
  {
    "path": "libopenage/renderer/stages/screen/render_stage.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include \"util/path.h\"\n\nnamespace openage::renderer {\nclass Renderer;\nclass RenderPass;\nclass RenderTarget;\nclass ShaderProgram;\nclass Texture2d;\nclass Window;\n\nnamespace screen {\n\n/**\n * Draws the results of previous render passes to the window screen\n * (i.e. the renderers display target). This should always be the\n * last render stage.\n */\nclass ScreenRenderStage {\npublic:\n\t/**\n\t * Create a new render stage for drawing to the screen.\n\t *\n\t * @param window openage window targeted for rendering.\n\t * @param renderer openage low-level renderer.\n\t * @param shaderdir Directory containing the shader source files.\n\t */\n\tScreenRenderStage(const std::shared_ptr<Window> &window,\n\t                  const std::shared_ptr<renderer::Renderer> &renderer,\n\t                  const util::Path &shaderdir);\n\t~ScreenRenderStage() = default;\n\n\t/**\n\t * Get the render pass of the screen renderer.\n\t *\n\t * @return Render pass for screen drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass();\n\n\t/**\n\t * Set the render targets whose outputs should be drawn into the\n\t * display target.\n\t *\n\t * Targets are drawn in the order they appear in the vector.\n\t * Alpha blending is enabled.\n\t *\n\t * TODO: Replace this with a \\p set_textures() method that\n\t * only receives the output textures.\n\t *\n\t * @param targets Render targets that should be drawn.\n\t */\n\tvoid set_render_targets(const std::vector<std::shared_ptr<renderer::RenderTarget>> &targets);\n\nprivate:\n\t/**\n\t * Create the render pass for the screen output.\n\t *\n\t * Called during initialization of the screen renderer.\n\t *\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(const util::Path &shaderdir);\n\n\t/**\n\t * Recreates the renderables and the render pass of the screen renderer\n\t * when \\p set_render_targets() is called. This is necessary to take\n\t * the textures of the new targets into account.\n\t */\n\tvoid update_render_pass();\n\n\t/**\n\t * Reference to the openage renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Render pass for the screen drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\t/**\n\t * Output texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> output_texture;\n\n\t/**\n\t * FBO render targets that are drawn to the screen.\n\t *\n\t * TODO: Use pass_outputs instead\n\t */\n\tstd::vector<std::shared_ptr<renderer::RenderTarget>> render_targets;\n\n\t/**\n\t * Texture targets of the individual FBOs.\n\t */\n\tstd::vector<std::shared_ptr<renderer::Texture2d>> pass_outputs;\n\n\t/**\n\t * Shader for rendering to the window.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n};\n\n} // namespace screen\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/screen/screenshot.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include \"screenshot.h\"\n\n#include <cmath>\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n#include <ctime>\n#include <epoxy/gl.h>\n#include <memory>\n#include <png.h>\n\n#include \"job/job_manager.h\"\n#include \"log/log.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/stages/screen/render_stage.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::renderer::screen {\n\n\nScreenshotManager::ScreenshotManager(std::shared_ptr<ScreenRenderStage> &renderer,\n                                     util::Path &outdir,\n                                     std::shared_ptr<job::JobManager> &job_mgr) :\n\toutdir{outdir},\n\tcount{0},\n\tlast_time{0},\n\trenderer{renderer},\n\tjob_manager{job_mgr} {\n}\n\nstd::string ScreenshotManager::gen_next_filename() {\n\tstd::time_t t = std::time(NULL);\n\n\tif (t == this->last_time) {\n\t\tthis->count++;\n\t}\n\telse {\n\t\tthis->count = 0;\n\t\tthis->last_time = t;\n\t}\n\n\t// these two values (32) *must* be the same for safety reasons\n\tchar timestamp[32];\n\tstd::strftime(timestamp, 32, \"%Y-%m-%d_%H-%M-%S\", std::localtime(&t));\n\n\treturn util::sformat(\"openage_%s_%02d.png\", timestamp, this->count);\n}\n\n\nvoid ScreenshotManager::save_screenshot() {\n\t// get screenshot image from scren renderer\n\tauto pass = this->renderer->get_render_pass();\n\tauto target = pass->get_target();\n\tauto image = target->into_data();\n\n\tauto store_function = [this, image]() {\n\t\timage.store(this->outdir / this->gen_next_filename());\n\t\treturn true;\n\t};\n\tthis->job_manager->enqueue<bool>(store_function);\n}\n\n} // namespace openage::renderer::screen\n"
  },
  {
    "path": "libopenage/renderer/stages/screen/screenshot.h",
    "content": "// Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <ctime>\n#include <memory>\n#include <string>\n\n#include \"util/path.h\"\n\n\nnamespace openage {\n\nnamespace job {\nclass JobManager;\n}\n\nnamespace renderer::screen {\nclass ScreenRenderStage;\n\n/**\n * Takes screenshots, duh.\n */\nclass ScreenshotManager {\npublic:\n\t/**\n\t * Create a new screenshot manager.\n\t *\n\t * @param renderer Screen render stage to take the screenshot from.\n\t * @param outdir Directory where the screenshots are saved.\n\t * @param job_mgr Job manager to use for writing the screenshot to disk.\n\t */\n\tScreenshotManager(std::shared_ptr<ScreenRenderStage> &renderer,\n\t                  util::Path &outdir,\n\t                  std::shared_ptr<job::JobManager> &job_mgr);\n\n\t~ScreenshotManager() = default;\n\n\t/**\n\t * Generate and save a screenshot of the last frame.\n\t */\n\tvoid save_screenshot();\n\nprivate:\n\t/**\n\t * Generates a filename for the screenshot.\n\t *\n\t * @return Filename for the screenshot.\n\t */\n\tstd::string gen_next_filename();\n\n\t/**\n\t * Directory where the screenshots are saved.\n\t */\n\tutil::Path outdir;\n\n\t/**\n\t * Counter for the screenshot filename. Used if multiple screenshots\n\t * are taken in the same second.\n\t */\n\tunsigned count;\n\n\t/**\n\t * Last time when a screenshot was taken.\n\t */\n\tstd::time_t last_time;\n\n\t/**\n\t * Screen render stage to take the screenshot from.\n\t */\n\tstd::shared_ptr<ScreenRenderStage> renderer;\n\n\t/**\n\t * Job manager to use for writing the screenshot to disk.\n\t */\n\tstd::shared_ptr<job::JobManager> job_manager;\n};\n\n} // namespace renderer::screen\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/stages/skybox/CMakeLists.txt",
    "content": "add_sources(libopenage\n\trender_stage.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/skybox/render_stage.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_stage.h\"\n\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/uniform_input.h\"\n#include \"renderer/window.h\"\n#include \"util/path.h\"\n\nnamespace openage::renderer::skybox {\n\nSkyboxRenderStage::SkyboxRenderStage(const std::shared_ptr<Window> &window,\n                                     const std::shared_ptr<renderer::Renderer> &renderer,\n                                     const util::Path &shaderdir) :\n\trenderer{renderer},\n\tbg_color{0.0, 0.0, 0.0, 1.0} // black\n{\n\trenderer::opengl::GlContext::check_error();\n\n\tauto size = window->get_size();\n\tthis->initialize_render_pass(size[0], size[1], shaderdir);\n\n\twindow->add_resize_callback([this](size_t width, size_t height, double /*scale*/) {\n\t\tthis->resize(width, height);\n\t});\n\n\tlog::log(INFO << \"Created render stage 'Skybox'\");\n}\n\nstd::shared_ptr<renderer::RenderPass> SkyboxRenderStage::get_render_pass() {\n\treturn this->render_pass;\n}\n\nvoid SkyboxRenderStage::set_color(const Eigen::Vector4i col) {\n\tthis->bg_color = Eigen::Vector4f(\n\t\tcol[0] / 255,\n\t\tcol[1] / 255,\n\t\tcol[2] / 255,\n\t\tcol[3] / 255);\n\tthis->color_unif->update(\"in_col\", this->bg_color);\n}\n\nvoid SkyboxRenderStage::set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {\n\tthis->bg_color = Eigen::Vector4f(\n\t\tr / 255,\n\t\tg / 255,\n\t\tb / 255,\n\t\ta / 255);\n\tthis->color_unif->update(\"in_col\", this->bg_color);\n}\n\nvoid SkyboxRenderStage::set_color(const Eigen::Vector4f col) {\n\tthis->bg_color = col;\n\tthis->color_unif->update(\"in_col\", this->bg_color);\n}\n\nvoid SkyboxRenderStage::set_color(float r, float g, float b, float a) {\n\tthis->bg_color = Eigen::Vector4f(r, g, b, a);\n\tthis->color_unif->update(\"in_col\", this->bg_color);\n}\n\nvoid SkyboxRenderStage::resize(size_t width, size_t height) {\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture});\n\tthis->render_pass->set_target(fbo);\n}\n\nvoid SkyboxRenderStage::initialize_render_pass(size_t width,\n                                               size_t height,\n                                               const util::Path &shaderdir) {\n\tauto vert_shader_file = (shaderdir / \"skybox.vert.glsl\").open();\n\tauto vert_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_file = (shaderdir / \"skybox.frag.glsl\").open();\n\tauto frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tfrag_shader_file.read());\n\tfrag_shader_file.close();\n\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\n\tauto geometry = this->renderer->add_mesh_geometry(resources::MeshData::make_quad());\n\tauto shader = this->renderer->add_shader({vert_shader_src, frag_shader_src});\n\n\tthis->color_unif = shader->new_uniform_input(\"in_col\", this->bg_color);\n\tRenderable display_obj{\n\t\tcolor_unif,\n\t\tgeometry,\n\t\tfalse,\n\t\tfalse,\n\t};\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture});\n\tthis->render_pass = this->renderer->add_render_pass({display_obj}, fbo);\n}\n\n} // namespace openage::renderer::skybox\n"
  },
  {
    "path": "libopenage/renderer/stages/skybox/render_stage.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"util/path.h\"\n\nnamespace openage::renderer {\nclass Renderer;\nclass RenderPass;\nclass Texture2d;\nclass UniformInput;\nclass Window;\n\nnamespace skybox {\n\n/**\n * Draws the dark background thingy behind the terrain.\n *\n * (Technically not a skybox because we look down from above into\n * a black devilish void of nothingness. Maybe \"hellbox\" is more\n * appropriate.)\n */\nclass SkyboxRenderStage {\npublic:\n\t/**\n\t * Create a new render stage for the skybox.\n\t *\n\t * @param window openage window targeted for rendering.\n\t * @param renderer openage low-level renderer.\n\t * @param shaderdir Directory containing the shader source files.\n\t */\n\tSkyboxRenderStage(const std::shared_ptr<Window> &window,\n\t                  const std::shared_ptr<renderer::Renderer> &renderer,\n\t                  const util::Path &shaderdir);\n\t~SkyboxRenderStage() = default;\n\n\t/**\n\t * Get the render pass of the skybox renderer.\n\t *\n\t * @return Render pass for skybox drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass();\n\n\t/**\n\t * Set the color used for the skybox using integer values\n\t * for each channel (0 to 255).\n\t *\n\t * @param col RGBA color value.\n\t */\n\tvoid set_color(const Eigen::Vector4i col);\n\tvoid set_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a);\n\n\t/**\n\t * Set the color used for the skybox using float values\n\t * for each channel (0.0 to 1.0).\n\t *\n\t * @param col RGBA color value.\n\t */\n\tvoid set_color(const Eigen::Vector4f col);\n\tvoid set_color(float r, float g, float b, float a);\n\n\t/**\n\t * Resize the FBO for the skybox rendering. This basically updates the output\n\t * texture size.\n\t *\n\t * @param width New width of the FBO.\n\t * @param height New height of the FBO.\n\t */\n\tvoid resize(size_t width, size_t height);\n\nprivate:\n\t/**\n\t * Create the render pass for the skybox.\n\t *\n\t * Called during initialization of the skybox renderer.\n\t *\n\t * @param width Width of the FBO.\n\t * @param height Height of the FBO.\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(size_t width,\n\t                            size_t height,\n\t                            const util::Path &shaderdir);\n\n\n\t/**\n\t * Reference to the openage renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Render pass for the skybox drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\t/**\n\t * Output texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> output_texture;\n\n\t/**\n\t * Background color.\n\t */\n\tEigen::Vector4f bg_color;\n\n\t/**\n\t * Stores background color uniform for the shader.\n\t */\n\tstd::shared_ptr<renderer::UniformInput> color_unif;\n};\n\n} // namespace skybox\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/CMakeLists.txt",
    "content": "add_sources(libopenage\n    chunk.cpp\n\tmesh.cpp\n\tmodel.cpp\n\trender_entity.cpp\n\trender_stage.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/chunk.cpp",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#include \"chunk.h\"\n\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/stages/terrain/mesh.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n\n\nnamespace openage::renderer::terrain {\n\nTerrainChunk::TerrainChunk(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n                           const util::Vector2s size,\n                           const coord::scene2_delta offset) :\n\tsize{size},\n\toffset{offset},\n\tasset_manager{asset_manager} {}\n\nvoid TerrainChunk::set_render_entity(const std::shared_ptr<RenderEntity> &entity) {\n\tthis->render_entity = entity;\n}\n\nvoid TerrainChunk::fetch_updates(const time::time_t & /* time */) {\n\t// TODO: Don't create model if render entity is not set\n\tif (not this->render_entity) {\n\t\treturn;\n\t}\n\n\t// Check render entity for updates\n\tif (not this->render_entity->is_changed()) {\n\t\treturn;\n\t}\n\n\t// Get the terrain data from the render entity\n\tauto terrain_size = this->render_entity->get_size();\n\tauto terrain_paths = this->render_entity->get_terrain_paths();\n\tauto tiles = this->render_entity->get_tiles();\n\tauto heightmap_verts = this->render_entity->get_vertices();\n\n\t// Recreate the mesh data\n\t// TODO: Change mesh instead of recreating it\n\t// TODO: Multiple meshes\n\tthis->meshes.clear();\n\tfor (const auto &terrain_path : terrain_paths) {\n\t\tauto new_mesh = this->create_mesh(terrain_size, tiles, heightmap_verts, terrain_path);\n\t\tnew_mesh->create_model_matrix(this->offset);\n\t\tthis->meshes.push_back(new_mesh);\n\t}\n\n\t// auto new_mesh = this->create_mesh();\n\t// new_mesh->create_model_matrix(this->offset);\n\t// this->meshes.clear();\n\t// this->meshes.push_back(new_mesh);\n\n\t// Indicate to the render entity that its updates have been processed.\n\tthis->render_entity->clear_changed_flag();\n}\n\nvoid TerrainChunk::update_uniforms(const time::time_t &time) {\n\tfor (auto &mesh : this->meshes) {\n\t\tmesh->update_uniforms(time);\n\t}\n}\n\nconst std::vector<std::shared_ptr<TerrainRenderMesh>> &TerrainChunk::get_meshes() const {\n\treturn this->meshes;\n}\n\nstd::shared_ptr<TerrainRenderMesh> TerrainChunk::create_mesh(const util::Vector2s vert_size,\n                                                             const RenderEntity::tiles_t &tiles,\n                                                             const std::vector<coord::scene3> &heightmap_verts,\n                                                             const std::string &texture_path) {\n\tauto v_width = vert_size[0];\n\tauto v_height = vert_size[1];\n\n\t// vertex data for the mesh\n\tstd::vector<float> mesh_verts{};\n\n\t// vertex indices for the mesh\n\tstd::vector<uint16_t> idxs{};\n\n\t// maps indices of verts in the heightmap to indices in the vertex data vector\n\tstd::unordered_map<size_t, size_t> index_map;\n\n\tfor (size_t i = 0; i < v_width - 1; ++i) {\n\t\tfor (size_t j = 0; j < v_height - 1; ++j) {\n\t\t\tauto tile = tiles.at(j + i * (v_height - 1));\n\t\t\tif (tile.second != texture_path) {\n\t\t\t\t// Skip tiles with different textures\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// indices of the vertices of the current tile\n\t\t\t// in the hightmap\n\t\t\tstd::array<size_t, 4> tile_verts{\n\t\t\t\tj + i * v_height,           // top left\n\t\t\t\tj + (i + 1) * v_height,     // bottom left\n\t\t\t\tj + 1 + (i + 1) * v_height, // bottom right\n\t\t\t\tj + 1 + i * v_height,       // top right\n\t\t\t};\n\n\t\t\t// add the vertices of the current tile to the vertex data vector\n\t\t\tfor (size_t v_idx : tile_verts) {\n\t\t\t\t// skip if the vertex is already in the vertex data vector\n\t\t\t\tif (not index_map.contains(v_idx)) {\n\t\t\t\t\tauto v = heightmap_verts[v_idx];\n\t\t\t\t\tauto v_vec = v.to_world_space();\n\t\t\t\t\tmesh_verts.push_back(v_vec[0]);\n\t\t\t\t\tmesh_verts.push_back(v_vec[1]);\n\t\t\t\t\tmesh_verts.push_back(v_vec[2]);\n\t\t\t\t\tmesh_verts.push_back((v.ne / 10).to_float());\n\t\t\t\t\tmesh_verts.push_back((v.se / 10).to_float());\n\n\t\t\t\t\t// update the index map\n\t\t\t\t\t// since new verts are added to the end of the vertex data vector\n\t\t\t\t\t// the mapped index is the current size of the index map\n\t\t\t\t\tindex_map[v_idx] = index_map.size();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// first triangle\n\t\t\tidxs.push_back(index_map[tile_verts[0]]); // top left\n\t\t\tidxs.push_back(index_map[tile_verts[1]]); // bottom left\n\t\t\tidxs.push_back(index_map[tile_verts[2]]); // bottom right\n\n\t\t\t// second triangle\n\t\t\tidxs.push_back(index_map[tile_verts[0]]); // top left\n\t\t\tidxs.push_back(index_map[tile_verts[2]]); // bottom right\n\t\t\tidxs.push_back(index_map[tile_verts[3]]); // top right\n\t\t}\n\t}\n\n\tresources::VertexInputInfo info{\n\t\t{resources::vertex_input_t::V3F32, resources::vertex_input_t::V2F32},\n\t\tresources::vertex_layout_t::AOS,\n\t\tresources::vertex_primitive_t::TRIANGLES,\n\t\tresources::index_t::U16};\n\n\tauto const vert_data_size_new = mesh_verts.size() * sizeof(float);\n\tstd::vector<uint8_t> vert_data_new(vert_data_size_new);\n\tstd::memcpy(vert_data_new.data(), mesh_verts.data(), vert_data_size_new);\n\n\tauto const idx_data_size = idxs.size() * sizeof(uint16_t);\n\tstd::vector<uint8_t> idx_data(idx_data_size);\n\tstd::memcpy(idx_data.data(), idxs.data(), idx_data_size);\n\n\tresources::MeshData meshdata{std::move(vert_data_new), std::move(idx_data), info};\n\n\t// Create the terrain mesh\n\tauto terrain_info = this->asset_manager->request_terrain(texture_path);\n\tauto terrain_mesh = std::make_shared<TerrainRenderMesh>(\n\t\tthis->asset_manager,\n\t\tterrain_info,\n\t\tstd::move(meshdata));\n\n\treturn terrain_mesh;\n}\n\nutil::Vector2s &TerrainChunk::get_size() {\n\treturn this->size;\n}\n\ncoord::scene2_delta &TerrainChunk::get_offset() {\n\treturn this->offset;\n}\n\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/chunk.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n#include <memory>\n#include <vector>\n\n#include \"coord/scene.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"time/time.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer {\n\nnamespace resources {\nclass AssetManager;\n}\n\nnamespace terrain {\nclass TerrainRenderMesh;\n\n/**\n * Stores the state of a terrain chunk in the terrain render stage.\n */\nclass TerrainChunk {\npublic:\n\t/**\n\t * Create a new terrain chunk.\n\t *\n\t * @param asset_manager Asset manager for loading textures.\n\t */\n\tTerrainChunk(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n\t             const util::Vector2s size,\n\t             const coord::scene2_delta offset);\n\n\t~TerrainChunk() = default;\n\n\t/**\n\t * Set the terrain render entity for vertex updates of this mesh.\n\t *\n\t * @param entity New terrain render entity.\n\t * @param size Size of the chunk in tiles.\n\t * @param offset Offset of the chunk from origin in tiles.\n\t */\n\tvoid set_render_entity(const std::shared_ptr<RenderEntity> &entity);\n\n\t/**\n\t * Fetch updates from the render entity.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid fetch_updates(const time::time_t &time = 0.0);\n\n\t/**\n\t * Update the uniforms of the meshes.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_uniforms(const time::time_t &time = 0.0);\n\n\t/**\n\t * Get the meshes composing the terrain.\n\t *\n\t * @return Vector of terrain meshes.\n\t */\n\tconst std::vector<std::shared_ptr<TerrainRenderMesh>> &get_meshes() const;\n\n\t/**\n\t * Get the size of the chunk in tiles.\n\t *\n\t * @return Size of the chunk (in tiles).\n\t */\n\tutil::Vector2s &get_size();\n\n\t/**\n\t * Get the offset of the chunk from origin in tiles.\n\t *\n\t * @return Offset of the chunk (in tiles).\n\t */\n\tcoord::scene2_delta &get_offset();\n\nprivate:\n\t/**\n\t * Create a terrain mesh from the data provided by the render entity.\n\t *\n\t * @param vert_size Size of the terrain in vertices.\n\t * @param tiles Data for each tile (elevation, terrain path).\n\t * @param heightmap_verts Position of each vertex in the chunk.\n\t * @param texture_path Path to the texture for the terrain.\n\t *\n\t * @return New terrain mesh.\n\t */\n\tstd::shared_ptr<TerrainRenderMesh> create_mesh(const util::Vector2s vert_size,\n\t                                               const RenderEntity::tiles_t &tiles,\n\t                                               const std::vector<coord::scene3> &heightmap_verts,\n\t                                               const std::string &texture_path);\n\n\t/**\n\t * Size of the chunk in tiles (width x height).\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Offset of the chunk from origin in tiles (x, y).\n\t */\n\tcoord::scene2_delta offset;\n\n\t/**\n\t * Meshes composing the terrain. Each mesh represents a drawable vertex surface\n\t * and a texture.\n\t */\n\tstd::vector<std::shared_ptr<TerrainRenderMesh>> meshes;\n\n\t/**\n\t * Asset manager for central accessing and loading textures.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Source for ingame terrain coordinates. These coordinates are translated into\n\t * our render vertex mesh when \\p update() is called.\n\t */\n\tstd::shared_ptr<RenderEntity> render_entity;\n};\n\n\n} // namespace terrain\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/mesh.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"mesh.h\"\n\n#include <functional>\n#include <optional>\n#include <utility>\n\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/assets/texture_manager.h\"\n#include \"renderer/resources/terrain/terrain_info.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage::renderer::terrain {\n\n\nTerrainRenderMesh::TerrainRenderMesh(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager) :\n\trequire_renderable{false},\n\tchanged{false},\n\tasset_manager{asset_manager},\n\tterrain_info{nullptr},\n\tuniforms{nullptr},\n\tmesh{renderer::resources::MeshData::make_quad()} {\n}\n\nTerrainRenderMesh::TerrainRenderMesh(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n                                     const std::shared_ptr<renderer::resources::TerrainInfo> &info,\n                                     renderer::resources::MeshData &&mesh) :\n\trequire_renderable{true},\n\tchanged{true},\n\tasset_manager{asset_manager},\n\tterrain_info{nullptr},\n\tuniforms{nullptr},\n\tmesh{std::move(mesh)} {\n\tthis->set_terrain_info(info);\n}\n\nvoid TerrainRenderMesh::set_mesh(renderer::resources::MeshData &&mesh) {\n\tthis->mesh = mesh;\n\tthis->require_renderable = true;\n\tthis->changed = true;\n}\n\nconst renderer::resources::MeshData &TerrainRenderMesh::get_mesh() {\n\treturn this->mesh;\n}\n\nvoid TerrainRenderMesh::set_terrain_info(const std::shared_ptr<renderer::resources::TerrainInfo> &info) {\n\tthis->changed = true;\n\tthis->terrain_info = info;\n}\n\nvoid TerrainRenderMesh::update_uniforms(const time::time_t & /* time */) {\n\t// TODO: Only update uniforms that changed since last update\n\tif (this->uniforms == nullptr) [[unlikely]] {\n\t\treturn;\n\t}\n\n\tif (not this->is_changed()) [[likely]] {\n\t\treturn;\n\t}\n\n\t// local space -> world space\n\tthis->uniforms->update(\"model\", this->model_matrix);\n\n\tauto tex_info = this->terrain_info->get_texture(0);\n\tauto tex_manager = this->asset_manager->get_texture_manager();\n\tauto texture = tex_manager->request(tex_info->get_image_path().value());\n\n\tthis->uniforms->update(\"tex\", texture);\n\n\tthis->changed = false;\n}\n\nbool TerrainRenderMesh::requires_renderable() {\n\treturn this->require_renderable;\n}\n\nvoid TerrainRenderMesh::clear_requires_renderable() {\n\tthis->require_renderable = false;\n}\n\nvoid TerrainRenderMesh::set_uniforms(const std::shared_ptr<renderer::UniformInput> &uniforms) {\n\tthis->uniforms = uniforms;\n}\n\nconst std::shared_ptr<renderer::UniformInput> &TerrainRenderMesh::get_uniforms() {\n\treturn this->uniforms;\n}\n\nvoid TerrainRenderMesh::create_model_matrix(const coord::scene2_delta &offset) {\n\t// TODO: Needs input from engine\n\tauto model = Eigen::Affine3f::Identity();\n\tmodel.translate(offset.to_world_space());\n\tthis->model_matrix = model.matrix();\n}\n\nbool TerrainRenderMesh::is_changed() {\n\treturn this->changed;\n}\n\nvoid TerrainRenderMesh::clear_changed_flag() {\n\tthis->changed = false;\n}\n\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/mesh.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <string>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/scene.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"time/time.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer {\nclass UniformInput;\n\nnamespace resources {\nclass AssetManager;\nclass TerrainInfo;\n} // namespace resources\n\nnamespace terrain {\n\n/**\n * Drawable chunk of terrain with a single texture.\n */\nclass TerrainRenderMesh {\npublic:\n\t/**\n\t * Create a new terrain render mesh with empty values.\n\t *\n\t * Mesh and texture need to be set before the terrain mesh becomes renderable.\n\t *\n\t * @param asset_manager Asset manager for central accessing and loading textures.\n\t */\n\tTerrainRenderMesh(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager);\n\n\t/**\n\t * Create a new terrain render mesh.\n\t *\n\t * @param asset_manager Asset manager for central accessing and loading textures.\n\t * @param info Terrain info for the renderable.\n\t * @param mesh Vertex data of the mesh.\n\t */\n\tTerrainRenderMesh(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n\t                  const std::shared_ptr<renderer::resources::TerrainInfo> &info,\n\t                  renderer::resources::MeshData &&mesh);\n\n\t~TerrainRenderMesh() = default;\n\n\t/**\n\t * Set the reference to the uniform inputs of the renderable\n\t * associated with this mesh. Relevant uniforms are updated\n\t * when calling \\p update().\n\t *\n\t * @param uniforms Uniform inputs of this mesh's renderable.\n\t */\n\tvoid set_uniforms(const std::shared_ptr<renderer::UniformInput> &uniforms);\n\n\t/**\n\t * Get the reference to the uniform inputs of the mesh's renderable.\n\t *\n\t * @return Uniform inputs of the renderable.\n\t */\n\tconst std::shared_ptr<renderer::UniformInput> &get_uniforms();\n\n\t/**\n\t * Set the vertex mesh for the terrain.\n\t *\n\t * @param mesh Mesh for creating a renderer geometry object.\n\t */\n\tvoid set_mesh(renderer::resources::MeshData &&mesh);\n\n\t/**\n\t * Get the vertex mesh for the terrain.\n\t *\n\t * @return Mesh for creating a renderer geometry object.\n\t */\n\tconst renderer::resources::MeshData &get_mesh();\n\n\t/**\n\t * Set the terrain info that is drawn onto the mesh.\n\t *\n\t * @param texture Terrain info.\n\t */\n\tvoid set_terrain_info(const std::shared_ptr<renderer::resources::TerrainInfo> &info);\n\n\t/**\n\t * Update the uniforms of the renderable associated with this object.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_uniforms(const time::time_t &time = 0.0);\n\n\t/**\n\t * Check whether a new renderable needs to be created for this mesh.\n\t *\n\t * If true, the old renderable should be removed from the render pass.\n\t * The updated uniforms and geometry should be passed to this mesh.\n\t * Afterwards, clear the requirement flag with \\p clear_requires_renderable().\n\t *\n\t * @return true if a new renderable is required, else false.\n\t */\n\tbool requires_renderable();\n\n\t/**\n\t * Indicate to this mesh that a new renderable has been created.\n\t */\n\tvoid clear_requires_renderable();\n\n\t/**\n\t * Create the model transformation matrix for rendering.\n\t *\n\t * @param offset Offset of the terrain mesh to the scene origin.\n\t */\n\tvoid create_model_matrix(const coord::scene2_delta &offset);\n\n\t/**\n\t * Check whether the mesh or texture were changed.\n\t *\n\t * @return true if changes were made, else false.\n\t */\n\tbool is_changed();\n\n\t/**\n\t * Clear the update flag by setting it to false.\n\t */\n\tvoid clear_changed_flag();\n\nprivate:\n\t/**\n\t * Stores whether a new renderable for this mesh needs to be created\n\t * for the render pass.\n\t */\n\tbool require_renderable;\n\n\t/**\n\t * Stores whether the the mesh or the texture are updated.\n\t */\n\tbool changed;\n\n\t/**\n\t * Asset manager for central accessing and loading textures.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Terrain information for the renderables.\n\t */\n\tstd::shared_ptr<renderer::resources::TerrainInfo> terrain_info;\n\n\t/**\n\t * Shader uniforms for the renderable in the terrain render pass.\n\t */\n\tstd::shared_ptr<renderer::UniformInput> uniforms;\n\n\t/**\n\t * Pre-transformation vertices for the terrain model.\n\t */\n\trenderer::resources::MeshData mesh;\n\n\t/**\n\t * Transformation matrix for the terrain model.\n\t */\n\tEigen::Matrix4f model_matrix;\n};\n} // namespace terrain\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/model.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"model.h\"\n\n#include <array>\n#include <cstdint>\n#include <cstring>\n#include <utility>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"coord/scene.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/stages/terrain/chunk.h\"\n#include \"renderer/stages/terrain/render_entity.h\"\n#include \"util/fixed_point.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::terrain {\n\nTerrainRenderModel::TerrainRenderModel(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager) :\n\tchunks{},\n\tcamera{nullptr},\n\tasset_manager{asset_manager} {\n}\n\nvoid TerrainRenderModel::add_chunk(const std::shared_ptr<RenderEntity> &entity,\n                                   const util::Vector2s size,\n                                   const coord::scene2_delta offset) {\n\tauto chunk = std::make_shared<TerrainChunk>(this->asset_manager, size, offset);\n\tchunk->set_render_entity(entity);\n\tchunk->fetch_updates();\n\n\tthis->chunks.push_back(chunk);\n}\n\nvoid TerrainRenderModel::set_camera(const std::shared_ptr<renderer::camera::Camera> &camera) {\n\tthis->camera = camera;\n}\n\nvoid TerrainRenderModel::fetch_updates(const time::time_t &time) {\n\tfor (auto &chunk : this->chunks) {\n\t\tchunk->fetch_updates(time);\n\t}\n}\n\nvoid TerrainRenderModel::update_uniforms(const time::time_t &time) {\n\tfor (auto &chunk : this->chunks) {\n\t\tchunk->update_uniforms(time);\n\t}\n}\n\nconst std::vector<std::shared_ptr<TerrainChunk>> &TerrainRenderModel::get_chunks() const {\n\treturn this->chunks;\n}\n\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/model.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <vector>\n\n#include \"coord/scene.h\"\n#include \"time/time.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer {\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace resources {\nclass AssetManager;\n}\n\nnamespace terrain {\nclass RenderEntity;\nclass TerrainRenderMesh;\nclass TerrainChunk;\n\n/**\n * 3D model of the whole terrain. Combines the individual meshes\n * into one single structure.\n */\nclass TerrainRenderModel {\npublic:\n\t/**\n\t * Create a new model for the terrain.\n\t *\n\t * @param asset_manager Asset manager for loading resources.\n\t */\n\tTerrainRenderModel(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager);\n\t~TerrainRenderModel() = default;\n\n\t/**\n\t * Add a new chunk to the terrain model.\n\t *\n\t * @param entity Render entity of the chunk.\n\t * @param chunk_size Size of the chunk in tiles.\n\t * @param chunk_offset Offset of the chunk from origin in tiles.\n\t */\n\tvoid add_chunk(const std::shared_ptr<RenderEntity> &entity,\n\t               const util::Vector2s chunk_size,\n\t               const coord::scene2_delta chunk_offset);\n\n\t/**\n\t * Set the current camera of the scene.\n\t *\n\t * @param camera Camera object viewing the scene.\n\t */\n\tvoid set_camera(const std::shared_ptr<renderer::camera::Camera> &camera);\n\n\t/**\n\t * Fetch updates from the render entity.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid fetch_updates(const time::time_t &time = 0.0);\n\n\t/**\n\t * Update the uniforms of the renderable associated with this object.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_uniforms(const time::time_t &time = 0.0);\n\n\t/**\n\t * Get the chunks composing the terrain.\n\t *\n\t * @return Chunks of the terrain.\n\t */\n\tconst std::vector<std::shared_ptr<TerrainChunk>> &get_chunks() const;\n\nprivate:\n\t/**\n\t * Chunks composing the terrain.\n\t */\n\tstd::vector<std::shared_ptr<TerrainChunk>> chunks;\n\n\t/**\n\t * Camera for view and projection uniforms.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Asset manager for central accessing and loading textures.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n};\n\n} // namespace terrain\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/render_entity.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_entity.h\"\n\n#include <algorithm>\n#include <array>\n#include <mutex>\n\n\nnamespace openage::renderer::terrain {\n\nRenderEntity::RenderEntity() :\n\trenderer::RenderEntity{},\n\tsize{0, 0},\n\ttiles{},\n\tterrain_paths{},\n\tvertices{} {\n}\n\nvoid RenderEntity::update_tile(const util::Vector2s size,\n                               const coord::tile &pos,\n                               const terrain_elevation_t elevation,\n                               const std::string terrain_path,\n                               const time::time_t time) {\n\tstd::unique_lock lock{this->mutex};\n\n\tif (this->vertices.empty()) {\n\t\tthrow Error(MSG(err) << \"Cannot update tile: Vertices have not been initialized yet.\");\n\t}\n\n\t// find the postion of the tile in the vertex array\n\tauto left_corner = pos.ne * size[0] + pos.se;\n\n\t// update the 4 vertices of the tile\n\tthis->vertices[left_corner].up = elevation.to_float();                 // left corner\n\tthis->vertices[left_corner + 1].up = elevation.to_float();             // bottom corner\n\tthis->vertices[left_corner + (size[0] + 1)].up = elevation.to_float(); // top corner\n\tthis->vertices[left_corner + (size[0] + 2)].up = elevation.to_float(); // right corner\n\n\t// update tile\n\tthis->tiles[left_corner] = {elevation, terrain_path};\n\n\t// update the last update time\n\tthis->last_update = time;\n\n\t// update the terrain paths\n\tthis->terrain_paths.insert(terrain_path);\n\n\tthis->changed = true;\n}\n\nvoid RenderEntity::update(const util::Vector2s size,\n                          const tiles_t tiles,\n                          const time::time_t time) {\n\tstd::unique_lock lock{this->mutex};\n\n\t// increase by 1 in every dimension because tiles\n\t// size is number of tiles, but we want number of vertices\n\tutil::Vector2i tile_size{size[0], size[1]};\n\tthis->size = util::Vector2s{size[0] + 1, size[1] + 1};\n\n\t// transfer mesh\n\tauto vert_count = this->size[0] * this->size[1];\n\tthis->vertices.clear();\n\tthis->vertices.reserve(vert_count);\n\tfor (int j = 0; j < (int)this->size[1]; ++j) {\n\t\tfor (int i = 0; i < (int)this->size[0]; ++i) {\n\t\t\t// for each vertex, compare the surrounding tiles\n\t\t\tstd::vector<float> surround{};\n\t\t\tif (j - 1 >= 0 and i - 1 >= 0) {\n\t\t\t\tsurround.push_back(tiles[(i - 1) * size[1] + j - 1].first.to_float());\n\t\t\t}\n\t\t\tif (j < tile_size[1] and i - 1 >= 0) {\n\t\t\t\tsurround.push_back(tiles[(i - 1) * size[1] + j].first.to_float());\n\t\t\t}\n\t\t\tif (j < tile_size[1] and i < tile_size[0]) {\n\t\t\t\tsurround.push_back(tiles[i * size[1] + j].first.to_float());\n\t\t\t}\n\t\t\tif (j - 1 >= 0 and i < tile_size[0]) {\n\t\t\t\tsurround.push_back(tiles[i * size[1] + j - 1].first.to_float());\n\t\t\t}\n\t\t\t// select the height of the highest surrounding tile\n\t\t\tauto max_height = *std::max_element(surround.begin(), surround.end());\n\t\t\tcoord::scene3 v{\n\t\t\t\tstatic_cast<float>(i),\n\t\t\t\tstatic_cast<float>(j),\n\t\t\t\tmax_height,\n\t\t\t};\n\t\t\tthis->vertices.push_back(v);\n\t\t}\n\t}\n\n\t// update tiles\n\tthis->tiles = tiles;\n\n\t// update the last update time\n\tthis->last_update = time;\n\n\t// update the terrain paths\n\tthis->terrain_paths.clear();\n\tfor (const auto &tile : this->tiles) {\n\t\tthis->terrain_paths.insert(tile.second);\n\t}\n\n\tthis->changed = true;\n}\n\nconst std::vector<coord::scene3> RenderEntity::get_vertices() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->vertices;\n}\n\nconst RenderEntity::tiles_t RenderEntity::get_tiles() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->tiles;\n}\n\nconst std::unordered_set<std::string> RenderEntity::get_terrain_paths() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->terrain_paths;\n}\n\nconst util::Vector2s RenderEntity::get_size() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->size;\n}\n\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/render_entity.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <unordered_set>\n#include <vector>\n\n#include \"coord/scene.h\"\n#include \"coord/tile.h\"\n#include \"curve/discrete.h\"\n#include \"renderer/stages/render_entity.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::terrain {\n\n/**\n * Render entity for pushing updates to the Terrain renderer.\n */\nclass RenderEntity final : public renderer::RenderEntity {\npublic:\n\tRenderEntity();\n\t~RenderEntity() = default;\n\n\tusing terrain_elevation_t = util::FixedPoint<uint64_t, 16>;\n\tusing tiles_t = std::vector<std::pair<terrain_elevation_t, std::string>>;\n\n\t/**\n\t * Update a single tile of the displayed terrain (chunk) with information from the\n\t * gamestate.\n\t *\n\t * Updating the render entity with this method is thread-safe.\n\t *\n\t * @param size Size of the terrain in tiles (width x length)\n\t * @param pos Position of the tile in the chunk.\n\t * @param elevation Height of terrain tile.\n\t * @param terrain_path Path to the terrain definition.\n\t * @param time Simulation time of the update.\n\t */\n\tvoid update_tile(const util::Vector2s size,\n\t                 const coord::tile &pos,\n\t                 const terrain_elevation_t elevation,\n\t                 const std::string terrain_path,\n\t                 const time::time_t time = 0.0);\n\n\t/**\n\t * Update the full grid of the displayed terrain (chunk) with information from the\n\t * gamestate.\n\t *\n\t * Updating the render entity with this method is thread-safe.\n\t *\n\t * @param size Size of the terrain in tiles (width x length)\n\t * @param tiles Animation data for each tile (elevation, terrain path).\n\t * @param time Simulation time of the update.\n\t */\n\tvoid update(const util::Vector2s size,\n\t            const tiles_t tiles,\n\t            const time::time_t time = 0.0);\n\n\t/**\n\t * Get the vertices of the terrain.\n\t *\n\t * Accessing the terrain vertices is thread-safe.\n\t *\n\t * @return Vector of vertex coordinates.\n\t */\n\tconst std::vector<coord::scene3> get_vertices();\n\n\t/**\n\t * Get the tiles of the terrain.\n\t *\n\t * Accessing the terrain tiles is thread-safe.\n\t *\n\t * @return Terrain tiles.\n\t */\n\tconst tiles_t get_tiles();\n\n\t/**\n\t * Get the terrain paths used in the terrain.\n\t *\n\t * Accessing the terrain paths is thread-safe.\n\t *\n\t * @return Terrain paths.\n\t */\n\tconst std::unordered_set<std::string> get_terrain_paths();\n\n\t/**\n\t * Get the number of vertices on each side of the terrain.\n\t *\n\t * Accessing the vertices size is thread-safe.\n\t *\n\t * @return Vector with width as first element and height as second element.\n\t */\n\tconst util::Vector2s get_size();\n\nprivate:\n\t/**\n\t * Chunk dimensions (width x height).\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Terrain tile information (elevation, terrain path).\n\t */\n\ttiles_t tiles;\n\n\t/**\n\t * Terrain texture paths used in \\p tiles .\n\t */\n\tstd::unordered_set<std::string> terrain_paths;\n\n\t/**\n\t * Terrain vertices (ingame coordinates).\n\t */\n\tstd::vector<coord::scene3> vertices;\n};\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/render_stage.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_stage.h\"\n\n#include \"renderer/camera/camera.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/renderer.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/stages/terrain/chunk.h\"\n#include \"renderer/stages/terrain/mesh.h\"\n#include \"renderer/stages/terrain/model.h\"\n#include \"renderer/window.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::renderer::terrain {\n\nTerrainRenderStage::TerrainRenderStage(const std::shared_ptr<Window> &window,\n                                       const std::shared_ptr<renderer::Renderer> &renderer,\n                                       const std::shared_ptr<renderer::camera::Camera> &camera,\n                                       const util::Path &shaderdir,\n                                       const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n                                       const std::shared_ptr<time::Clock> &clock) :\n\trenderer{renderer},\n\tcamera{camera},\n\trender_entity{nullptr},\n\tmodel{std::make_shared<TerrainRenderModel>(asset_manager)},\n\tclock{clock} {\n\trenderer::opengl::GlContext::check_error();\n\n\tauto size = window->get_size();\n\tthis->initialize_render_pass(size[0], size[1], shaderdir);\n\n\twindow->add_resize_callback([this](size_t width, size_t height, double /*scale*/) {\n\t\tthis->resize(width, height);\n\t});\n\n\tthis->model->set_camera(this->camera);\n\n\tlog::log(INFO << \"Created render stage 'Terrain'\");\n}\n\nstd::shared_ptr<renderer::RenderPass> TerrainRenderStage::get_render_pass() {\n\treturn this->render_pass;\n}\n\nvoid TerrainRenderStage::add_render_entity(const std::shared_ptr<RenderEntity> entity,\n                                           const util::Vector2s chunk_size,\n                                           const coord::scene2_delta chunk_offset) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->render_entity = entity;\n\tthis->model->add_chunk(this->render_entity, chunk_size, chunk_offset);\n\tthis->update();\n}\n\nvoid TerrainRenderStage::update() {\n\tthis->model->fetch_updates();\n\tauto current_time = this->clock->get_real_time();\n\tfor (auto &chunk : this->model->get_chunks()) {\n\t\tfor (auto &mesh : chunk->get_meshes()) {\n\t\t\tif (mesh->requires_renderable()) [[unlikely]] { /*probably doesn't happen that often?*/\n\t\t\t\t// TODO: Update uniforms and geometry individually, depending on what changed\n\t\t\t\t// TODO: Update existing renderable instead of recreating it\n\t\t\t\tauto geometry = this->renderer->add_mesh_geometry(mesh->get_mesh());\n\t\t\t\tauto transform_unifs = this->display_shader->create_empty_input();\n\n\t\t\t\tRenderable display_obj{\n\t\t\t\t\ttransform_unifs,\n\t\t\t\t\tgeometry,\n\t\t\t\t\ttrue,\n\t\t\t\t\ttrue, // it's a 3D object, so we need depth testing\n\t\t\t\t};\n\n\t\t\t\t// TODO: Remove old renderable instead of clearing everything\n\t\t\t\tthis->render_pass->add_renderables(std::move(display_obj));\n\t\t\t\tmesh->clear_requires_renderable();\n\n\t\t\t\tmesh->set_uniforms(transform_unifs);\n\t\t\t}\n\t\t}\n\t}\n\tthis->model->update_uniforms(current_time);\n}\n\nvoid TerrainRenderStage::resize(size_t width, size_t height) {\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture});\n\tthis->render_pass->set_target(fbo);\n}\n\nvoid TerrainRenderStage::initialize_render_pass(size_t width,\n                                                size_t height,\n                                                const util::Path &shaderdir) {\n\tauto vert_shader_file = (shaderdir / \"terrain.vert.glsl\").open();\n\tauto vert_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_file = (shaderdir / \"terrain.frag.glsl\").open();\n\tauto frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tfrag_shader_file.read());\n\tfrag_shader_file.close();\n\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\n\tthis->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src});\n\tthis->display_shader->bind_uniform_buffer(\"camera\", this->camera->get_uniform_buffer());\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture});\n\tthis->render_pass = this->renderer->add_render_pass({}, fbo);\n}\n\n} // namespace openage::renderer::terrain\n"
  },
  {
    "path": "libopenage/renderer/stages/terrain/render_stage.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n\n#include \"coord/scene.h\"\n#include \"util/path.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage {\n\nnamespace time {\nclass Clock;\n}\n\nnamespace renderer {\nclass Renderer;\nclass RenderPass;\nclass ShaderProgram;\nclass Texture2d;\nclass Window;\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace resources {\nclass AssetManager;\n}\n\nnamespace terrain {\nclass RenderEntity;\nclass TerrainRenderMesh;\nclass TerrainRenderModel;\n\n/**\n * Manage and render terrain geometry and graphics.\n */\nclass TerrainRenderStage {\npublic:\n\t/**\n\t * Create a new render stage for the terrain.\n\t *\n\t * @param window openage window targeted for rendering.\n\t * @param renderer openage low-level renderer.\n\t * @param camera Camera used for the rendered scene.\n\t * @param shaderdir Directory containing the shader source files.\n\t * @param asset_manager Asset manager for loading resources.\n\t * @param clock Simulation clock for timing animations.\n\t */\n\tTerrainRenderStage(const std::shared_ptr<Window> &window,\n\t                   const std::shared_ptr<renderer::Renderer> &renderer,\n\t                   const std::shared_ptr<renderer::camera::Camera> &camera,\n\t                   const util::Path &shaderdir,\n\t                   const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n\t                   const std::shared_ptr<time::Clock> &clock);\n\t~TerrainRenderStage() = default;\n\n\t/**\n\t * Get the render pass of the terrain renderer.\n\t *\n\t * @return Render pass for terrain drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass();\n\n\t/**\n\t * Add a new render entity to the terrain renderer.\n\t *\n\t * This creates a new terrain chunk and add it to the model.\n\t *\n\t * @param render_entity New render entity.\n\t */\n\tvoid add_render_entity(const std::shared_ptr<RenderEntity> entity,\n\t                       const util::Vector2s chunk_size,\n\t                       const coord::scene2_delta chunk_offset);\n\n\t/**\n\t * Update the terrain mesh and texture information.\n\t */\n\tvoid update();\n\n\t/**\n\t * Resize the FBO for the terrain rendering. This basically updates the output\n\t * texture size.\n\t *\n\t * @param width New width of the FBO.\n\t * @param height New height of the FBO.\n\t */\n\tvoid resize(size_t width, size_t height);\n\nprivate:\n\t/**\n\t * Create the render pass for terrains.\n\t *\n\t * Called during initialization of the terrain renderer.\n\t *\n\t * @param width Width of the FBO.\n\t * @param height Height of the FBO.\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(size_t width,\n\t                            size_t height,\n\t                            const util::Path &shaderdir);\n\n\n\t/**\n\t * Reference to the openage renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Camera for view and projection matrices.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Engine interface for updating terrain draw information.\n\t */\n\tstd::shared_ptr<RenderEntity> render_entity;\n\n\t/**\n\t * 3D model of the terrain.\n\t */\n\tstd::shared_ptr<TerrainRenderModel> model;\n\n\t/**\n\t * Render pass for the terrain drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\t/**\n\t * Shader for rendering the world objects.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n\n\t/**\n\t * Simulation clock for timing animations.\n\t */\n\tstd::shared_ptr<time::Clock> clock;\n\n\t/**\n\t * Output texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> output_texture;\n\n\t/**\n\t * Depth texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> depth_texture;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n\n} // namespace terrain\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/stages/world/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tobject.cpp\n\trender_entity.cpp\n\trender_stage.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/stages/world/object.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"object.h\"\n\n#include <array>\n#include <cstddef>\n#include <functional>\n#include <optional>\n#include <utility>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"renderer/camera/frustum_2d.h\"\n#include \"renderer/definitions.h\"\n#include \"renderer/resources/animation/angle_info.h\"\n#include \"renderer/resources/animation/animation_info.h\"\n#include \"renderer/resources/animation/frame_info.h\"\n#include \"renderer/resources/animation/layer_info.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/assets/texture_manager.h\"\n#include \"renderer/resources/frame_timing.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/resources/texture_subinfo.h\"\n#include \"renderer/stages/world/render_entity.h\"\n#include \"renderer/uniform_input.h\"\n#include \"util/fixed_point.h\"\n#include \"util/vector.h\"\n\n\nnamespace openage::renderer::world {\n\nWorldObject::WorldObject(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager) :\n\trequire_renderable{true},\n\tchanged{false},\n\tasset_manager{asset_manager},\n\trender_entity{nullptr},\n\tref_id{0},\n\tposition{nullptr, 0, \"\", nullptr, SCENE_ORIGIN},\n\tangle{nullptr, 0, \"\", nullptr, 0},\n\tanimation_info{nullptr, 0},\n\tlayer_uniforms{},\n\tlast_update{0.0} {\n}\n\nvoid WorldObject::set_render_entity(const std::shared_ptr<RenderEntity> &entity) {\n\tthis->render_entity = entity;\n\tthis->fetch_updates();\n}\n\nvoid WorldObject::fetch_updates(const time::time_t &time) {\n\t// TODO: Calling this once per frame is very expensive\n\tauto layer_count = this->get_required_layer_count(time);\n\tif (this->layer_uniforms.size() != layer_count) {\n\t\t// The number of layers changed, so we need to update the renderables\n\t\tthis->require_renderable = true;\n\t}\n\n\tif (not this->render_entity->is_changed()) {\n\t\t// exit early because there is nothing to update\n\t\treturn;\n\t}\n\n\t// Get data from render entity\n\tthis->ref_id = this->render_entity->get_id();\n\n\t// Thread-safe access to curves needs a lock on the render entity's mutex\n\tauto read_lock = this->render_entity->get_read_lock();\n\tthis->position.sync(this->render_entity->get_position());\n\tthis->animation_info.sync(this->render_entity->get_animation_path(),\n\t                          std::function<std::shared_ptr<renderer::resources::Animation2dInfo>(const std::string &)>(\n\t\t\t\t\t\t\t\t  [&](const std::string &path) {\n\t\t\t\t\t\t\t\t\t  if (path.empty()) {\n\t\t\t\t\t\t\t\t\t\t  auto placeholder = this->asset_manager->get_placeholder_animation();\n\t\t\t\t\t\t\t\t\t\t  if (placeholder) {\n\t\t\t\t\t\t\t\t\t\t\t  return (*placeholder).second;\n\t\t\t\t\t\t\t\t\t\t  }\n\t\t\t\t\t\t\t\t\t\t  return std::shared_ptr<renderer::resources::Animation2dInfo>{nullptr};\n\t\t\t\t\t\t\t\t\t  }\n\t\t\t\t\t\t\t\t\t  return this->asset_manager->request_animation(path);\n\t\t\t\t\t\t\t\t  }),\n\t                          this->last_update);\n\tthis->angle.sync(this->render_entity->get_angle(), this->last_update);\n\n\t// Unlock mutex of the render entity\n\tread_lock.unlock();\n\n\t// Set self to changed so that world renderer can update the renderable\n\tthis->changed = true;\n\tthis->render_entity->clear_changed_flag();\n\tthis->last_update = time;\n}\n\nvoid WorldObject::update_uniforms(const time::time_t &time) {\n\t// TODO: Only update uniforms that changed since last update\n\tif (this->layer_uniforms.empty()) [[unlikely]] {\n\t\treturn;\n\t}\n\n\t// Object world position\n\tauto current_pos = this->position.get(time);\n\n\t// Direction angle the object is facing towards currently\n\tauto angle_degrees = this->angle.get(time).to_float();\n\n\t// Animation information\n\tauto [last_update, animation_info] = this->animation_info.frame(time);\n\n\tfor (size_t layer_idx = 0; layer_idx < this->layer_uniforms.size(); ++layer_idx) {\n\t\tauto &layer_unifs = this->layer_uniforms.at(layer_idx);\n\t\tlayer_unifs->update(this->obj_world_position, current_pos.to_world_space());\n\n\t\t// Frame subtexture\n\t\tauto &layer = animation_info->get_layer(layer_idx);\n\t\tauto &angle = layer.get_direction_angle(angle_degrees);\n\n\t\t// Flip subtexture horizontally if angle is mirrored\n\t\tif (angle->is_mirrored()) {\n\t\t\tlayer_unifs->update(this->flip_x, true);\n\t\t}\n\t\telse {\n\t\t\tlayer_unifs->update(this->flip_x, false);\n\t\t}\n\n\t\t// Current frame index considering current time\n\t\tsize_t frame_idx;\n\t\tswitch (layer.get_display_mode()) {\n\t\tcase renderer::resources::display_mode::ONCE:\n\t\tcase renderer::resources::display_mode::LOOP: {\n\t\t\t// ONCE and LOOP are animated based on time\n\t\t\tauto &timing = layer.get_frame_timing();\n\t\t\tframe_idx = timing->get_frame(time, last_update);\n\t\t} break;\n\t\tcase renderer::resources::display_mode::OFF:\n\t\tdefault:\n\t\t\t// OFF only shows the first frame\n\t\t\tframe_idx = 0;\n\t\t\tbreak;\n\t\t}\n\n\t\t// Index of texture and subtexture where the frame's pixels are located\n\t\tauto &frame_info = angle->get_frame(frame_idx);\n\t\tauto tex_idx = frame_info->get_texture_idx();\n\t\tauto subtex_idx = frame_info->get_subtexture_idx();\n\n\t\tauto &tex_info = animation_info->get_texture(tex_idx);\n\t\tauto &tex_manager = this->asset_manager->get_texture_manager();\n\t\tauto &texture = tex_manager->request(tex_info->get_image_path().value());\n\t\tlayer_unifs->update(this->tex, texture);\n\n\t\t// Subtexture coordinates.inside texture\n\t\tauto coords = tex_info->get_subtex_info(subtex_idx).get_subtex_coords();\n\t\tlayer_unifs->update(this->tile_params, coords);\n\n\t\t// Animation scale factor\n\t\t// Scales the subtex up or down in the shader\n\t\tauto scale = animation_info->get_scalefactor();\n\t\tlayer_unifs->update(this->scale, scale);\n\n\t\t// Subtexture size in pixels\n\t\tauto subtex_size = tex_info->get_subtex_info(subtex_idx).get_size();\n\t\tEigen::Vector2f subtex_size_vec{\n\t\t\tstatic_cast<float>(subtex_size[0]),\n\t\t\tstatic_cast<float>(subtex_size[1])};\n\t\tlayer_unifs->update(this->subtex_size, subtex_size_vec);\n\n\t\t// Anchor point offset (in pixels)\n\t\t// moves the subtex in the shader so that the anchor point is at the object's position\n\t\tauto anchor = tex_info->get_subtex_info(subtex_idx).get_anchor_params();\n\t\tEigen::Vector2f anchor_offset{\n\t\t\tstatic_cast<float>(anchor[0]),\n\t\t\tstatic_cast<float>(anchor[1])};\n\t\tlayer_unifs->update(this->anchor_offset, anchor_offset);\n\t}\n}\n\nuint32_t WorldObject::get_id() {\n\treturn this->ref_id;\n}\n\nconst renderer::resources::MeshData WorldObject::get_mesh() {\n\treturn resources::MeshData::make_quad();\n}\n\nconst Eigen::Matrix4f WorldObject::get_model_matrix() {\n\treturn Eigen::Matrix4f::Identity();\n}\n\nbool WorldObject::requires_renderable() const {\n\treturn this->require_renderable;\n}\n\nvoid WorldObject::clear_requires_renderable() {\n\tthis->require_renderable = false;\n}\n\nsize_t WorldObject::get_required_layer_count(const time::time_t &time) const {\n\tauto animation_info = this->animation_info.get(time);\n\tif (not animation_info) {\n\t\treturn 0;\n\t}\n\n\treturn animation_info->get_layer_count();\n}\n\nstd::vector<size_t> WorldObject::get_layer_positions(const time::time_t &time) const {\n\tauto animation_info = this->animation_info.get(time);\n\tif (not animation_info) {\n\t\treturn {};\n\t}\n\n\tstd::vector<size_t> positions;\n\tfor (size_t i = 0; i < animation_info->get_layer_count(); ++i) {\n\t\tauto layer = animation_info->get_layer(i);\n\t\tpositions.push_back(layer.get_position());\n\t}\n\n\treturn positions;\n}\n\nbool WorldObject::is_changed() {\n\treturn this->changed;\n}\n\nvoid WorldObject::clear_changed_flag() {\n\tthis->changed = false;\n}\n\nvoid WorldObject::set_uniforms(std::vector<std::shared_ptr<renderer::UniformInput>> &&uniforms) {\n\tthis->layer_uniforms = std::move(uniforms);\n}\n\nbool WorldObject::is_visible(const camera::Frustum2d &frustum,\n                             const time::time_t &time) {\n\tstatic const Eigen::Matrix4f model_matrix = this->get_model_matrix();\n\tEigen::Vector3f current_pos = this->position.get(time).to_world_space();\n\tauto animation_info = this->animation_info.get(time);\n\treturn frustum.in_frustum(current_pos,\n\t                          model_matrix,\n\t                          animation_info->get_scalefactor(),\n\t                          animation_info->get_max_bounds());\n}\n\n} // namespace openage::renderer::world\n"
  },
  {
    "path": "libopenage/renderer/stages/world/object.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <list>\n#include <memory>\n#include <string>\n\n#include \"coord/scene.h\"\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"curve/segmented.h\"\n#include \"renderer/resources/mesh_data.h\"\n#include \"renderer/types.h\"\n#include \"time/time.h\"\n\n\nnamespace openage::renderer {\nclass UniformInput;\n\nnamespace camera {\nclass Frustum2d;\n}\n\nnamespace resources {\nclass AssetManager;\nclass Animation2dInfo;\n} // namespace resources\n\nnamespace world {\nclass RenderEntity;\n\n/**\n * Stores the state of a renderable object in the World render stage.\n */\nclass WorldObject {\npublic:\n\t/**\n\t * Create a new object for the World render stage.\n\t *\n\t * @param asset_manager Asset manager for loading resources.\n\t */\n\tWorldObject(const std::shared_ptr<renderer::resources::AssetManager> &asset_manager);\n\t~WorldObject() = default;\n\n\t/**\n\t * Set the world render entity.\n\t *\n\t * @param entity New world render entity.\n\t */\n\tvoid set_render_entity(const std::shared_ptr<RenderEntity> &entity);\n\n\t/**\n\t * Fetch updates from the render entity.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid fetch_updates(const time::time_t &time = 0.0);\n\n\t/**\n\t * Update the uniforms of the renderable associated with this object.\n\t *\n\t * @param time Current simulation time.\n\t */\n\tvoid update_uniforms(const time::time_t &time = 0.0);\n\n\t/**\n\t * Get the ID of the corresponding game entity.\n\t *\n\t * @return Game entity ID.\n\t */\n\tuint32_t get_id();\n\n\t/**\n\t * Get the quad for creating the geometry.\n\t *\n\t * Since the object is a bunch of sprite layers, the mesh is always a quad.\n\t *\n\t * @return Mesh for creating a renderer geometry object.\n\t */\n\tstatic const renderer::resources::MeshData get_mesh();\n\n\t/**\n\t * Get the model matrix for the uniform input of a layer.\n\t *\n\t * @return Model matrix.\n\t */\n\tstatic const Eigen::Matrix4f get_model_matrix();\n\n\t/**\n\t * Check whether a new renderable needs to be created for this mesh.\n\t *\n\t * If true, the old renderable should be removed from the render pass.\n\t * The updated uniforms and geometry should be passed to this mesh.\n\t * Afterwards, clear the requirement flag with \\p clear_requires_renderable().\n\t *\n\t * @return true if a new renderable is required, else false.\n\t */\n\tbool requires_renderable() const;\n\n\t/**\n\t * Indicate to this mesh that a new renderable has been created.\n\t */\n\tvoid clear_requires_renderable();\n\n\t/**\n\t * Get the number of layers required by this object.\n\t *\n\t * @return Number of layers.\n\t */\n\tsize_t get_required_layer_count(const time::time_t &time) const;\n\n\tstd::vector<size_t> get_layer_positions(const time::time_t &time) const;\n\n\t/**\n\t * Check whether the object was changed by \\p update().\n\t *\n\t * @return true if changes were made, else false.\n\t */\n\tbool is_changed();\n\n\t/**\n\t * Clear the update flag by setting it to false.\n\t */\n\tvoid clear_changed_flag();\n\n\t/**\n\t * Set the uniform inputs for the layers of this object.\n\t * Layer uniforms are updated on every update call.\n\t *\n\t * @param uniforms Uniform inputs of this object's layers.\n\t */\n\tvoid set_uniforms(std::vector<std::shared_ptr<renderer::UniformInput>> &&uniforms);\n\n\t/**\n\t * Check whether the object is visible in the camera view.\n\t *\n\t * @param frustum Camera frustum for culling.\n\t * @param time Current simulation time.\n\t *\n\t * @return true if the object is visible, else false.\n\t */\n\tbool is_visible(const camera::Frustum2d &frustum,\n\t                const time::time_t &time);\n\n\t/**\n\t * Shader uniform IDs for setting uniform values.\n\t */\n\tinline static uniform_id_t obj_world_position;\n\tinline static uniform_id_t flip_x;\n\tinline static uniform_id_t flip_y;\n\tinline static uniform_id_t tex;\n\tinline static uniform_id_t tile_params;\n\tinline static uniform_id_t scale;\n\tinline static uniform_id_t subtex_size;\n\tinline static uniform_id_t anchor_offset;\n\nprivate:\n\t/**\n\t * Stores whether a new renderable for this object needs to be created\n\t * for the render pass.\n\t */\n\tbool require_renderable;\n\n\t/**\n\t * Stores whether the \\p update() call changed the object.\n\t */\n\tbool changed;\n\n\t/**\n\t * Asset manager for central accessing and loading asset resources.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Entity that gets updates from the gamestate, e.g. the position and\n\t * requested animation data.\n\t */\n\tstd::shared_ptr<RenderEntity> render_entity;\n\n\t/**\n\t * Reference ID for passing interaction with the graphic (e.g. mouse clicks) back to\n\t * the engine.\n\t */\n\tuint32_t ref_id;\n\n\t/**\n\t * Position of the object.\n\t */\n\tcurve::Continuous<coord::scene3> position;\n\n\t/**\n\t * Angle of the object.\n\t */\n\tcurve::Segmented<coord::phys_angle_t> angle;\n\n\t/**\n\t * Animation information for the layers.\n\t */\n\tcurve::Discrete<std::shared_ptr<renderer::resources::Animation2dInfo>> animation_info;\n\n\t/**\n\t * Shader uniforms for the layers of the object. Each layer corresponds to a\n\t * renderable in the render pass.\n\t */\n\tstd::vector<std::shared_ptr<renderer::UniformInput>> layer_uniforms;\n\n\t/**\n\t * Time of the last update call.\n\t */\n\ttime::time_t last_update;\n};\n} // namespace world\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/stages/world/render_entity.cpp",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#include \"render_entity.h\"\n\n#include <functional>\n#include <mutex>\n\n#include \"renderer/definitions.h\"\n\n\nnamespace openage::renderer::world {\n\nRenderEntity::RenderEntity() :\n\trenderer::RenderEntity{},\n\tref_id{0},\n\tposition{nullptr, 0, \"\", nullptr, SCENE_ORIGIN},\n\tangle{nullptr, 0, \"\", nullptr, 0},\n\tanimation_path{nullptr, 0} {\n}\n\nvoid RenderEntity::update(const uint32_t ref_id,\n                          const curve::Continuous<coord::phys3> &position,\n                          const curve::Segmented<coord::phys_angle_t> &angle,\n                          const std::string animation_path,\n                          const time::time_t time) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->ref_id = ref_id;\n\tstd::function<coord::scene3(const coord::phys3 &)> to_scene3 = [](const coord::phys3 &pos) {\n\t\treturn pos.to_scene3();\n\t};\n\tthis->position.sync(position,\n\t                    std::function<coord::scene3(const coord::phys3 &)>([](const coord::phys3 &pos) {\n\t\t\t\t\t\t\treturn pos.to_scene3();\n\t\t\t\t\t\t}),\n\t                    this->last_update);\n\tthis->angle.sync(angle, this->last_update);\n\tthis->animation_path.set_last(time, animation_path);\n\tthis->changed = true;\n\tthis->last_update = time;\n}\n\nvoid RenderEntity::update(const uint32_t ref_id,\n                          const coord::phys3 position,\n                          const std::string animation_path,\n                          const time::time_t time) {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->ref_id = ref_id;\n\tthis->position.set_last(time, position.to_scene3());\n\tthis->animation_path.set_last(time, animation_path);\n\tthis->changed = true;\n\tthis->last_update = time;\n}\n\nuint32_t RenderEntity::get_id() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->ref_id;\n}\n\nconst curve::Continuous<coord::scene3> &RenderEntity::get_position() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->position;\n}\n\nconst curve::Segmented<coord::phys_angle_t> &RenderEntity::get_angle() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->angle;\n}\n\nconst curve::Discrete<std::string> &RenderEntity::get_animation_path() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->animation_path;\n}\n\n} // namespace openage::renderer::world\n"
  },
  {
    "path": "libopenage/renderer/stages/world/render_entity.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <string>\n\n#include \"coord/phys.h\"\n#include \"coord/scene.h\"\n#include \"curve/continuous.h\"\n#include \"curve/discrete.h\"\n#include \"curve/segmented.h\"\n#include \"renderer/stages/render_entity.h\"\n\n\nnamespace openage::renderer::world {\n\n/**\n * Render entity for pushing updates to the World renderer.\n */\nclass RenderEntity final : public renderer::RenderEntity {\npublic:\n\tRenderEntity();\n\t~RenderEntity() = default;\n\n\t/**\n\t * Update the render entity with information from the gamestate.\n\t *\n\t * Updating the render entity with this method is thread-safe.\n\t *\n\t * @param ref_id Game entity ID.\n\t * @param position Position of the game entity inside the game world.\n\t * @param angle Angle of the game entity inside the game world.\n\t * @param animation_path Path to the animation definition.\n\t * @param time Simulation time of the update.\n\t */\n\tvoid update(const uint32_t ref_id,\n\t            const curve::Continuous<coord::phys3> &position,\n\t            const curve::Segmented<coord::phys_angle_t> &angle,\n\t            const std::string animation_path,\n\t            const time::time_t time = 0.0);\n\n\t/**\n\t * This function is for DEBUGGING and should not be used.\n\t *\n\t * Update the render entity with information from the gamestate.\n\t *\n\t * Updating the render entity with this method is thread-safe.\n\t *\n\t * @param ref_id Game entity ID.\n\t * @param position Position of the game entity inside the game world.\n\t * @param animation_path Path to the animation definition.\n\t * @param time Simulation time of the update.\n\t */\n\tvoid update(const uint32_t ref_id,\n\t            const coord::phys3 position,\n\t            const std::string animation_path,\n\t            const time::time_t time = 0.0);\n\n\t/**\n\t * Get the ID of the corresponding game entity.\n\t *\n\t * Accessing the game entity ID is thread-safe.\n\t *\n\t * @return Game entity ID.\n\t */\n\tuint32_t get_id();\n\n\t/**\n\t * Get the position of the entity inside the game world.\n\t *\n\t * Accessing the position curve REQUIRES a read lock on the render entity\n\t * (using \\p get_read_lock()) to ensure thread safety.\n\t *\n\t * @return Position curve of the entity.\n\t */\n\tconst curve::Continuous<coord::scene3> &get_position();\n\n\t/**\n\t * Get the angle of the entity inside the game world.\n\t *\n\t * Accessing the angle curve REQUIRES a read lock on the render entity\n\t * (using \\p get_read_lock()) to ensure thread safety.\n\t *\n\t * @return Angle curve of the entity.\n\t */\n\tconst curve::Segmented<coord::phys_angle_t> &get_angle();\n\n\t/**\n\t * Get the animation definition path.\n\t *\n\t * Accessing the animation path curve requires a read lock on the render entity\n\t * (using \\p get_read_lock()) to ensure thread safety.\n\t *\n\t * @return Path to the animation definition file.\n\t */\n\tconst curve::Discrete<std::string> &get_animation_path();\n\nprivate:\n\t/**\n\t * ID of the game entity in the gamestate.\n\t */\n\tuint32_t ref_id;\n\n\t/**\n\t * Position inside the game world.\n\t */\n\tcurve::Continuous<coord::scene3> position;\n\n\t/**\n\t * Angle of the entity inside the game world.\n\t */\n\tcurve::Segmented<coord::phys_angle_t> angle;\n\n\t/**\n\t * Path to the animation definition file.\n\t */\n\tcurve::Discrete<std::string> animation_path;\n};\n} // namespace openage::renderer::world\n"
  },
  {
    "path": "libopenage/renderer/stages/world/render_stage.cpp",
    "content": "// Copyright 2022-2025 the openage authors. See copying.md for legal info.\n\n#include \"render_stage.h\"\n\n#include \"renderer/camera/camera.h\"\n#include \"renderer/camera/frustum_3d.h\"\n#include \"renderer/opengl/context.h\"\n#include \"renderer/render_pass.h\"\n#include \"renderer/render_target.h\"\n#include \"renderer/resources/assets/asset_manager.h\"\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/resources/texture_info.h\"\n#include \"renderer/shader_program.h\"\n#include \"renderer/stages/world/object.h\"\n#include \"renderer/texture.h\"\n#include \"renderer/window.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::renderer::world {\n\nbool WorldRenderStage::ENABLE_FRUSTUM_CULLING = false;\n\nWorldRenderStage::WorldRenderStage(const std::shared_ptr<Window> &window,\n                                   const std::shared_ptr<renderer::Renderer> &renderer,\n                                   const std::shared_ptr<renderer::camera::Camera> &camera,\n                                   const util::Path &shaderdir,\n                                   const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n                                   const std::shared_ptr<time::Clock> clock) :\n\trenderer{renderer},\n\tcamera{camera},\n\tasset_manager{asset_manager},\n\trender_objects{},\n\tclock{clock},\n\tdefault_geometry{this->renderer->add_mesh_geometry(WorldObject::get_mesh())} {\n\trenderer::opengl::GlContext::check_error();\n\n\tauto size = window->get_size();\n\tthis->initialize_render_pass(size[0], size[1], shaderdir);\n\tthis->init_uniform_ids();\n\n\twindow->add_resize_callback([this](size_t width, size_t height, double /*scale*/) {\n\t\tthis->resize(width, height);\n\t});\n\n\tlog::log(INFO << \"Created render stage 'World'\");\n}\n\nstd::shared_ptr<renderer::RenderPass> WorldRenderStage::get_render_pass() {\n\treturn this->render_pass;\n}\n\nvoid WorldRenderStage::add_render_entity(const std::shared_ptr<RenderEntity> entity) {\n\tstd::unique_lock lock{this->mutex};\n\n\tauto world_object = std::make_shared<WorldObject>(this->asset_manager);\n\tworld_object->set_render_entity(entity);\n\tthis->render_objects.push_back(world_object);\n}\n\nvoid WorldRenderStage::update() {\n\tstd::unique_lock lock{this->mutex};\n\tauto current_time = this->clock->get_real_time();\n\tauto &camera_frustum = this->camera->get_frustum_2d();\n\tfor (auto &obj : this->render_objects) {\n\t\tobj->fetch_updates(current_time);\n\n\t\tif (WorldRenderStage::ENABLE_FRUSTUM_CULLING\n\t\t    and not obj->is_visible(camera_frustum, current_time)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (obj->is_changed()) {\n\t\t\tif (obj->requires_renderable()) {\n\t\t\t\tauto layer_positions = obj->get_layer_positions(current_time);\n\t\t\t\tstatic const Eigen::Matrix4f model_matrix = obj->get_model_matrix();\n\n\t\t\t\tstd::vector<std::shared_ptr<renderer::UniformInput>> transform_unifs;\n\t\t\t\tfor (auto layer_pos : layer_positions) {\n\t\t\t\t\t// Set uniforms that don't change or are not changed often\n\t\t\t\t\tauto layer_unifs = this->display_shader->new_uniform_input(\n\t\t\t\t\t\t\"model\",\n\t\t\t\t\t\tmodel_matrix,\n\t\t\t\t\t\t\"flip_x\",\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\"flip_y\",\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t\"u_id\",\n\t\t\t\t\t\tobj->get_id());\n\n\t\t\t\t\tRenderable display_obj{\n\t\t\t\t\t\tlayer_unifs,\n\t\t\t\t\t\tthis->default_geometry,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t};\n\t\t\t\t\tthis->render_pass->add_renderables(std::move(display_obj), layer_pos);\n\t\t\t\t\ttransform_unifs.push_back(layer_unifs);\n\t\t\t\t}\n\n\t\t\t\tobj->clear_requires_renderable();\n\n\t\t\t\t// update remaining uniforms for the object\n\t\t\t\tobj->set_uniforms(std::move(transform_unifs));\n\t\t\t}\n\t\t}\n\t\tobj->update_uniforms(current_time);\n\t}\n}\n\nvoid WorldRenderStage::resize(size_t width, size_t height) {\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\tthis->id_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::r32ui));\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture, this->id_texture});\n\tthis->render_pass->set_target(fbo);\n}\n\nvoid WorldRenderStage::initialize_render_pass(size_t width,\n                                              size_t height,\n                                              const util::Path &shaderdir) {\n\tauto vert_shader_file = (shaderdir / \"world2d.vert.glsl\").open();\n\tauto vert_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::vertex,\n\t\tvert_shader_file.read());\n\tvert_shader_file.close();\n\n\tauto frag_shader_file = (shaderdir / \"world2d.frag.glsl\").open();\n\tauto frag_shader_src = renderer::resources::ShaderSource(\n\t\tresources::shader_lang_t::glsl,\n\t\tresources::shader_stage_t::fragment,\n\t\tfrag_shader_file.read());\n\tfrag_shader_file.close();\n\n\tthis->output_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::rgba8));\n\tthis->depth_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::depth24));\n\tthis->id_texture = renderer->add_texture(resources::Texture2dInfo(width, height, resources::pixel_format::r32ui));\n\n\tthis->display_shader = this->renderer->add_shader({vert_shader_src, frag_shader_src});\n\tthis->display_shader->bind_uniform_buffer(\"camera\", this->camera->get_uniform_buffer());\n\n\tauto fbo = this->renderer->create_texture_target({this->output_texture, this->depth_texture, this->id_texture});\n\tthis->render_pass = this->renderer->add_render_pass({}, fbo);\n}\n\nvoid WorldRenderStage::init_uniform_ids() {\n\tWorldObject::obj_world_position = this->display_shader->get_uniform_id(\"obj_world_position\");\n\tWorldObject::flip_x = this->display_shader->get_uniform_id(\"flip_x\");\n\tWorldObject::flip_y = this->display_shader->get_uniform_id(\"flip_y\");\n\tWorldObject::tex = this->display_shader->get_uniform_id(\"tex\");\n\tWorldObject::tile_params = this->display_shader->get_uniform_id(\"tile_params\");\n\tWorldObject::scale = this->display_shader->get_uniform_id(\"scale\");\n\tWorldObject::subtex_size = this->display_shader->get_uniform_id(\"subtex_size\");\n\tWorldObject::anchor_offset = this->display_shader->get_uniform_id(\"anchor_offset\");\n}\n\n} // namespace openage::renderer::world\n"
  },
  {
    "path": "libopenage/renderer/stages/world/render_stage.h",
    "content": "// Copyright 2022-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n#include <vector>\n\n#include \"util/path.h\"\n\nnamespace openage {\n\nnamespace time {\nclass Clock;\n}\n\nnamespace renderer {\nclass Geometry;\nclass Renderer;\nclass RenderPass;\nclass ShaderProgram;\nclass Texture2d;\nclass Window;\n\nnamespace camera {\nclass Camera;\n}\n\nnamespace resources {\nclass AssetManager;\n}\n\nnamespace world {\nclass RenderEntity;\nclass WorldObject;\n\n/**\n * Renderer for drawing and displaying entities in the game world (units, buildings, etc.)\n */\nclass WorldRenderStage {\npublic:\n\t/**\n\t * Enable or disable frustum culling (default = false).\n\t */\n\tstatic bool ENABLE_FRUSTUM_CULLING;\n\n\t/**\n\t * Create a new render stage for the game world.\n\t *\n\t * @param window openage window targeted for rendering.\n\t * @param renderer openage low-level renderer.\n\t * @param camera Camera used for the rendered scene.\n\t * @param shaderdir Directory containing the shader source files.\n\t * @param asset_manager Asset manager for loading resources.\n\t * @param clock Simulation clock for timing animations.\n\t */\n\tWorldRenderStage(const std::shared_ptr<Window> &window,\n\t                 const std::shared_ptr<renderer::Renderer> &renderer,\n\t                 const std::shared_ptr<renderer::camera::Camera> &camera,\n\t                 const util::Path &shaderdir,\n\t                 const std::shared_ptr<renderer::resources::AssetManager> &asset_manager,\n\t                 const std::shared_ptr<time::Clock> clock);\n\t~WorldRenderStage() = default;\n\n\t/**\n\t * Get the render pass of the world renderer.\n\t *\n\t * @return Render pass for world drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> get_render_pass();\n\n\t/**\n\t * Add a new render entity of the world renderer.\n\t *\n\t * @param render_entity New render entity.\n\t */\n\tvoid add_render_entity(const std::shared_ptr<RenderEntity> entity);\n\n\t/**\n\t * Update the render entities and render positions.\n\t */\n\tvoid update();\n\n\t/**\n\t * Resize the FBO for the world rendering. This basically updates the output\n\t * texture size.\n\t *\n\t * @param width New width of the FBO.\n\t * @param height New height of the FBO.\n\t */\n\tvoid resize(size_t width, size_t height);\n\nprivate:\n\t/**\n\t * Create the render pass for world drawing.\n\t *\n\t * Called during initialization of the world renderer.\n\t *\n\t * @param width Width of the FBO.\n\t * @param height Height of the FBO.\n\t * @param shaderdir Directory containg the shader source files.\n\t */\n\tvoid initialize_render_pass(size_t width, size_t height, const util::Path &shaderdir);\n\n\t/**\n\t * Fetch the uniform IDs for the uniforms of the world shader from OpenGL\n\t * and assign them to the WorldObject class.\n\t *\n\t * This method must be called after the shader program has been created but\n\t * before any uniforms are set.\n\t */\n\tvoid init_uniform_ids();\n\n\t/**\n\t * Reference to the openage renderer.\n\t */\n\tstd::shared_ptr<renderer::Renderer> renderer;\n\n\t/**\n\t * Camera for model uniforms.\n\t */\n\tstd::shared_ptr<renderer::camera::Camera> camera;\n\n\t/**\n\t * Texture manager for loading assets.\n\t */\n\tstd::shared_ptr<renderer::resources::AssetManager> asset_manager;\n\n\t/**\n\t * Render pass for the world drawing.\n\t */\n\tstd::shared_ptr<renderer::RenderPass> render_pass;\n\n\t/**\n\t * Render entities requested by the game world.\n\t */\n\tstd::vector<std::shared_ptr<WorldObject>> render_objects;\n\n\t/**\n\t * Shader for rendering the world objects.\n\t */\n\tstd::shared_ptr<renderer::ShaderProgram> display_shader;\n\n\t/**\n\t * Simulation clock for timing animations.\n\t */\n\tstd::shared_ptr<time::Clock> clock;\n\n\t/**\n\t * Default geometry for every world object.\n\t *\n\t * Since all world objects are sprites, their mesh is always quad\n\t * with the same vertex info. Reusing the geometry allows us to\n\t * use the same vetrex buffer for every object.\n\t */\n\tconst std::shared_ptr<renderer::Geometry> default_geometry;\n\n\t/**\n\t * Output texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> output_texture;\n\n\t/**\n\t * Depth texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> depth_texture;\n\n\t/**\n\t * ID texture.\n\t */\n\tstd::shared_ptr<renderer::Texture2d> id_texture;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n} // namespace world\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/texture.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include <cstring>\n\n#include \"texture.h\"\n#include \"../error/error.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nTexture2d::Texture2d(const resources::Texture2dInfo& info)\n\t: info(info) {}\n\nTexture2d::~Texture2d() = default;\n\nconst resources::Texture2dInfo& Texture2d::get_info() const {\n\treturn this->info;\n}\n\n}} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/texture.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"resources/texture_data.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\n/// An abstract base for a handle to a texture buffer allocated in graphics hardware.\n/// Can be obtained by passing texture data to the renderer.\nclass Texture2d {\npublic:\n\tvirtual ~Texture2d();\n\n\t/// Returns the texture information.\n\tconst resources::Texture2dInfo &get_info() const;\n\n\t/// Copies this texture's data from graphics hardware into a CPU-accessible\n\t/// Texture2dData buffer.\n\tvirtual resources::Texture2dData into_data() = 0;\n\n\t/// Uploads the provided data into the GPU texture storage. The format has\n\t/// to match the format this Texture was originally created with.\n\tvirtual void upload(resources::Texture2dData const &) = 0;\n\nprotected:\n\t/// Constructs the base with the given information.\n\tTexture2d(const resources::Texture2dInfo &);\n\n\t/// Information about the size, format, etc. of this texture.\n\tresources::Texture2dInfo info;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/texture_array.cpp",
    "content": "// Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n#include \"texture_array.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\nTexture2dArray::Texture2dArray(const resources::Texture2dInfo& info)\n\t: layer_info(info) {}\n\nTexture2dArray::~Texture2dArray() = default;\n\nresources::Texture2dInfo const& Texture2dArray::get_info() const {\n\treturn this->layer_info;\n}\n\n}} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/texture_array.h",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstddef>\n\n#include \"renderer/resources/texture_data.h\"\n#include \"renderer/resources/texture_info.h\"\n\n\nnamespace openage {\nnamespace renderer {\n\n/// A texture array, where individual elements are 2D textures. The array elements\n/// are referred to as \"layers\", and every layer must have the same format\n/// (size, pixel format, etc).\nclass Texture2dArray {\npublic:\n\tvirtual ~Texture2dArray();\n\n\t/// Returns information about the layer format.\n\tresources::Texture2dInfo const &get_info() const;\n\n\t/// Uploads the given texture data into the specified layer. `layer` must\n\t/// be strictly less than the size of the array and the data format must\n\t/// match the format this array was originally created with.\n\tvirtual void upload(size_t layer, resources::Texture2dData const &) = 0;\n\nprotected:\n\t/// Constructs the base class.\n\tTexture2dArray(const resources::Texture2dInfo &);\n\n\t/// Information about the size, format, etc. of every layer in this array.\n\tresources::Texture2dInfo layer_info;\n};\n\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/types.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"types.h\"\n\n\nnamespace openage::renderer {\n\n// this file is intentionally empty\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/types.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n\nnamespace openage::renderer {\n\n/**\n * IDs for shader uniforms.\n */\nusing uniform_id_t = uint32_t;\n\n/**\n * Graphics API types.\n */\nenum class graphics_api_t {\n\tDEFAULT,\n\tOPENGL,\n\tVULKAN,\n};\n\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/uniform_buffer.cpp",
    "content": "// Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n#include \"uniform_buffer.h\"\n"
  },
  {
    "path": "libopenage/renderer/uniform_buffer.h",
    "content": "// Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <memory>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"renderer/uniform_input.h\"\n\n\nnamespace openage::renderer {\nclass Texture2d;\nclass UniformBufferInput;\n\nclass UniformBuffer : public std::enable_shared_from_this<UniformBuffer> {\n\tfriend UniformBufferInput;\n\npublic:\n\tvirtual ~UniformBuffer() = default;\n\n\t/**\n\t * Update the uniforms in the buffer.\n\t *\n\t * @param unif_in Uniform input to update the buffer with.\n\t */\n\tvirtual void update_uniforms(std::shared_ptr<UniformBufferInput> const &unif_in) = 0;\n\n\t/**\n\t * Check whether the buffer contains a uniform variable with the given ID.\n\t *\n\t * @param unif ID of the uniform.\n\t *\n\t * @return true if the buffer contains the uniform, false otherwise.\n\t */\n\tvirtual bool has_uniform(const char *unif) = 0;\n\n\ttemplate <typename... Ts>\n\tstd::shared_ptr<UniformBufferInput> new_uniform_input(Ts &&...vals) {\n\t\tauto input = this->new_unif_in();\n\t\tinput->update(vals...);\n\t\treturn input;\n\t}\n\n\tstd::shared_ptr<UniformBufferInput> create_empty_input() {\n\t\treturn this->new_uniform_input();\n\t}\n\nprotected:\n\tvirtual std::shared_ptr<UniformBufferInput> new_unif_in() = 0;\n\n\tvirtual void set_i32(UniformBufferInput &in, const char *, int32_t) = 0;\n\tvirtual void set_u32(UniformBufferInput &in, const char *, uint32_t) = 0;\n\tvirtual void set_f32(UniformBufferInput &in, const char *, float) = 0;\n\tvirtual void set_f64(UniformBufferInput &in, const char *, double) = 0;\n\tvirtual void set_bool(UniformBufferInput &in, const char *, bool) = 0;\n\tvirtual void set_v2f32(UniformBufferInput &in, const char *, Eigen::Vector2f const &) = 0;\n\tvirtual void set_v3f32(UniformBufferInput &in, const char *, Eigen::Vector3f const &) = 0;\n\tvirtual void set_v4f32(UniformBufferInput &in, const char *, Eigen::Vector4f const &) = 0;\n\tvirtual void set_v2i32(UniformBufferInput &in, const char *, Eigen::Vector2i const &) = 0;\n\tvirtual void set_v3i32(UniformBufferInput &in, const char *, Eigen::Vector3i const &) = 0;\n\tvirtual void set_v4i32(UniformBufferInput &in, const char *, Eigen::Vector4i const &) = 0;\n\tvirtual void set_v2ui32(UniformBufferInput &in, const char *, Eigen::Vector2<uint32_t> const &) = 0;\n\tvirtual void set_v3ui32(UniformBufferInput &in, const char *, Eigen::Vector3<uint32_t> const &) = 0;\n\tvirtual void set_v4ui32(UniformBufferInput &in, const char *, Eigen::Vector4<uint32_t> const &) = 0;\n\tvirtual void set_m4f32(UniformBufferInput &in, const char *, Eigen::Matrix4f const &) = 0;\n\tvirtual void set_tex(UniformBufferInput &in, const char *, std::shared_ptr<Texture2d> const &) = 0;\n};\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/uniform_input.cpp",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#include \"uniform_input.h\"\n\n#include \"renderer/shader_program.h\"\n#include \"renderer/uniform_buffer.h\"\n\n\nnamespace openage::renderer {\n\nUniformInput::UniformInput(std::shared_ptr<ShaderProgram> const &prog) :\n\tprogram{prog} {}\n\nvoid UniformInput::update() {}\n\nvoid UniformInput::update(const char *unif, int32_t val) {\n\tthis->program->set_i32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, uint32_t val) {\n\tthis->program->set_u32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, float val) {\n\tthis->program->set_f32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, double val) {\n\tthis->program->set_f64(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, bool val) {\n\tthis->program->set_bool(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector2f const &val) {\n\tthis->program->set_v2f32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector3f const &val) {\n\tthis->program->set_v3f32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector4f const &val) {\n\tthis->program->set_v4f32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector2i const &val) {\n\tthis->program->set_v2i32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector3i const &val) {\n\tthis->program->set_v3i32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector4i const &val) {\n\tthis->program->set_v4i32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector2<uint32_t> const &val) {\n\tthis->program->set_v2ui32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector3<uint32_t> const &val) {\n\tthis->program->set_v3ui32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Vector4<uint32_t> const &val) {\n\tthis->program->set_v4ui32(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, std::shared_ptr<Texture2d> const &val) {\n\tthis->program->set_tex(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, std::shared_ptr<Texture2d> &val) {\n\tthis->program->set_tex(*this, unif, val);\n}\n\nvoid UniformInput::update(const char *unif, Eigen::Matrix4f const &val) {\n\tthis->program->set_m4f32(*this, unif, val);\n}\n\n\nvoid UniformInput::update(uniform_id_t id, int32_t val) {\n\tthis->program->set_i32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, uint32_t val) {\n\tthis->program->set_u32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, float val) {\n\tthis->program->set_f32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, double val) {\n\tthis->program->set_f64(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, bool val) {\n\tthis->program->set_bool(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector2f const &val) {\n\tthis->program->set_v2f32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector3f const &val) {\n\tthis->program->set_v3f32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector4f const &val) {\n\tthis->program->set_v4f32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector2i const &val) {\n\tthis->program->set_v2i32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector3i const &val) {\n\tthis->program->set_v3i32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector4i const &val) {\n\tthis->program->set_v4i32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector2<uint32_t> const &val) {\n\tthis->program->set_v2ui32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector3<uint32_t> const &val) {\n\tthis->program->set_v3ui32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Vector4<uint32_t> const &val) {\n\tthis->program->set_v4ui32(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, std::shared_ptr<Texture2d> const &val) {\n\tthis->program->set_tex(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, std::shared_ptr<Texture2d> &val) {\n\tthis->program->set_tex(*this, id, val);\n}\n\nvoid UniformInput::update(uniform_id_t id, Eigen::Matrix4f const &val) {\n\tthis->program->set_m4f32(*this, id, val);\n}\n\n\nUniformBufferInput::UniformBufferInput(std::shared_ptr<UniformBuffer> const &buffer) :\n\tbuffer{buffer} {}\n\nvoid UniformBufferInput::update() {}\n\nvoid UniformBufferInput::update(const char *unif, int32_t val) {\n\tthis->buffer->set_i32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, uint32_t val) {\n\tthis->buffer->set_u32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, float val) {\n\tthis->buffer->set_f32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, double val) {\n\tthis->buffer->set_f64(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, bool val) {\n\tthis->buffer->set_bool(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector2f const &val) {\n\tthis->buffer->set_v2f32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector3f const &val) {\n\tthis->buffer->set_v3f32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector4f const &val) {\n\tthis->buffer->set_v4f32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector2i const &val) {\n\tthis->buffer->set_v2i32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector3i const &val) {\n\tthis->buffer->set_v3i32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector4i const &val) {\n\tthis->buffer->set_v4i32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector2<uint32_t> const &val) {\n\tthis->buffer->set_v2ui32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector3<uint32_t> const &val) {\n\tthis->buffer->set_v3ui32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Vector4<uint32_t> const &val) {\n\tthis->buffer->set_v4ui32(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, std::shared_ptr<Texture2d> const &val) {\n\tthis->buffer->set_tex(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, std::shared_ptr<Texture2d> &val) {\n\tthis->buffer->set_tex(*this, unif, val);\n}\n\nvoid UniformBufferInput::update(const char *unif, Eigen::Matrix4f const &val) {\n\tthis->buffer->set_m4f32(*this, unif, val);\n}\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/uniform_input.h",
    "content": "// Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <memory>\n\n#include <eigen3/Eigen/Dense>\n\n#include \"error/error.h\"\n#include \"log/message.h\"\n#include \"renderer/types.h\"\n#include \"util/compiler.h\"\n\n\nnamespace openage::renderer {\nclass ShaderProgram;\nclass Texture2d;\nclass UniformBuffer;\n\nclass DataInput {\npublic:\n\tvirtual ~DataInput() = default;\n\n\t/**\n\t * Template dispatches for uniform variable setting. Each method is used\n\t * for a specific input type.\n\t *\n\t * @param unif ID of the uniform.\n\t * @param val New uniform value.\n\t */\n\tvirtual void update() = 0;\n\tvirtual void update(const char *unif, int32_t val) = 0;\n\tvirtual void update(const char *unif, uint32_t val) = 0;\n\tvirtual void update(const char *unif, float val) = 0;\n\tvirtual void update(const char *unif, double val) = 0;\n\tvirtual void update(const char *unif, bool val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector2f const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector3f const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector4f const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector2i const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector3i const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector4i const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector2<uint32_t> const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector3<uint32_t> const &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Vector4<uint32_t> const &val) = 0;\n\tvirtual void update(const char *unif, std::shared_ptr<Texture2d> const &val) = 0;\n\tvirtual void update(const char *unif, std::shared_ptr<Texture2d> &val) = 0;\n\tvirtual void update(const char *unif, Eigen::Matrix4f const &val) = 0;\n};\n\n\n/**\n * Abstract base for uniform input. Besides the uniform values, it stores information about\n * which shader program the input was created for.\n */\nclass UniformInput : public DataInput {\nprotected:\n\t/**\n\t * Create a new uniform input for a given shader program.\n\t *\n\t * @param prog Shader program the uniform input belongs to.\n\t */\n\tUniformInput(std::shared_ptr<ShaderProgram> const &prog);\n\npublic:\n\tvirtual ~UniformInput() = default;\n\n\tvirtual bool is_complete() const = 0;\n\n\tvoid update() override;\n\tvoid update(const char *unif, int32_t val) override;\n\tvoid update(const char *unif, uint32_t val) override;\n\tvoid update(const char *unif, float val) override;\n\tvoid update(const char *unif, double val) override;\n\tvoid update(const char *unif, bool val) override;\n\tvoid update(const char *unif, Eigen::Vector2f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector2i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector2<uint32_t> const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3<uint32_t> const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4<uint32_t> const &val) override;\n\tvoid update(const char *unif, std::shared_ptr<Texture2d> const &val) override;\n\tvoid update(const char *unif, std::shared_ptr<Texture2d> &val) override;\n\tvoid update(const char *unif, Eigen::Matrix4f const &val) override;\n\n\tvoid update(uniform_id_t id, int32_t val);\n\tvoid update(uniform_id_t id, uint32_t val);\n\tvoid update(uniform_id_t id, float val);\n\tvoid update(uniform_id_t id, double val);\n\tvoid update(uniform_id_t id, bool val);\n\tvoid update(uniform_id_t id, Eigen::Vector2f const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector3f const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector4f const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector2i const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector3i const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector4i const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector2<uint32_t> const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector3<uint32_t> const &val);\n\tvoid update(uniform_id_t id, Eigen::Vector4<uint32_t> const &val);\n\tvoid update(uniform_id_t id, std::shared_ptr<Texture2d> const &val);\n\tvoid update(uniform_id_t id, std::shared_ptr<Texture2d> &val);\n\tvoid update(uniform_id_t id, Eigen::Matrix4f const &val);\n\n\t/**\n\t * Catch-all template in order to handle unsupported types and avoid infinite recursion.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T>\n\tvoid update(const char *unif, T) {\n\t\t// TODO: maybe craft an static_assert that contains the `unif` content\n\t\tthrow Error(MSG(err) << \"Tried to set uniform '\" << unif\n\t\t                     << \"' using unsupported type '\" << util::typestring<T>() << \"'\");\n\t}\n\n\t/**\n\t * Catch-all template in order to handle unsupported types and avoid infinite recursion.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T>\n\tvoid update(const uniform_id_t &id, T) {\n\t\t// TODO: maybe craft an static_assert that contains the `unif` content\n\t\tthrow Error(MSG(err) << \"Tried to set uniform with ID \" << id\n\t\t                     << \" using unsupported type '\" << util::typestring<T>() << \"'\");\n\t}\n\n\t/**\n\t * Updates the given uniform input with new uniform values similarly to new_uniform_input.\n\t * For example, update_uniform_input(in, \"awesome\", true) will set the \"awesome\" uniform\n\t * in addition to whatever values were in the uniform input before.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T, typename... Ts>\n\tvoid update(const char *unif, T val, Ts... vals) {\n\t\tthis->update(unif, val);\n\t\tthis->update(vals...);\n\t}\n\n\t/**\n\t * Updates the given uniform input with new uniform values similarly to new_uniform_input.\n\t * For example, update_uniform_input(in, \"awesome\", true) will set the \"awesome\" uniform\n\t * in addition to whatever values were in the uniform input before.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T, typename... Ts>\n\tvoid update(const uniform_id_t &id, T val, Ts... vals) {\n\t\tthis->update(id, val);\n\t\tthis->update(vals...);\n\t}\n\n\t/**\n\t * Get the shader program the uniform input is used for.\n\t *\n\t * @return Shader program.\n\t */\n\tstd::shared_ptr<ShaderProgram> const &get_program() const {\n\t\treturn this->program;\n\t}\n\nprotected:\n\t/**\n\t * The program that this uniform input handle was created for.\n\t */\n\tstd::shared_ptr<ShaderProgram> program;\n};\n\n\nclass UniformBufferInput : public DataInput\n\t, public std::enable_shared_from_this<UniformBufferInput> {\nprotected:\n\tUniformBufferInput(std::shared_ptr<UniformBuffer> const &buffer);\n\npublic:\n\tvirtual ~UniformBufferInput() = default;\n\n\tvoid update() override;\n\tvoid update(const char *unif, int32_t val) override;\n\tvoid update(const char *unif, uint32_t val) override;\n\tvoid update(const char *unif, float val) override;\n\tvoid update(const char *unif, double val) override;\n\tvoid update(const char *unif, bool val) override;\n\tvoid update(const char *unif, Eigen::Vector2f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4f const &val) override;\n\tvoid update(const char *unif, Eigen::Vector2i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4i const &val) override;\n\tvoid update(const char *unif, Eigen::Vector2<uint32_t> const &val) override;\n\tvoid update(const char *unif, Eigen::Vector3<uint32_t> const &val) override;\n\tvoid update(const char *unif, Eigen::Vector4<uint32_t> const &val) override;\n\tvoid update(const char *unif, std::shared_ptr<Texture2d> const &val) override;\n\tvoid update(const char *unif, std::shared_ptr<Texture2d> &val) override;\n\tvoid update(const char *unif, Eigen::Matrix4f const &val) override;\n\n\t/**\n\t * Catch-all template in order to handle unsupported types and avoid infinite recursion.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T>\n\tvoid update(const char *unif, T) {\n\t\t// TODO: maybe craft an static_assert that contains the `unif` content\n\t\tthrow Error(MSG(err) << \"Tried to set uniform '\" << unif\n\t\t                     << \"' using unsupported type '\" << util::typestring<T>() << \"'\");\n\t}\n\n\t/**\n\t * Updates the given uniform input with new uniform values similarly to new_uniform_input.\n\t * For example, update_uniform_input(in, \"awesome\", true) will set the \"awesome\" uniform\n\t * in addition to whatever values were in the uniform input before.\n\t *\n\t * @param unif ID of the uniform.\n\t */\n\ttemplate <typename T, typename... Ts>\n\tvoid update(const char *unif, T val, Ts... vals) {\n\t\tthis->update(unif, val);\n\t\tthis->update(vals...);\n\t}\n\n\t/**\n\t * Get the buffer the uniform input is used for.\n\t *\n\t * @return Uniform buffer.\n\t */\n\tstd::shared_ptr<UniformBuffer> const &get_buffer() const {\n\t\treturn this->buffer;\n\t}\n\nprotected:\n\t/**\n\t * The buffer that this uniform input handle was created for.\n\t */\n\tstd::shared_ptr<UniformBuffer> buffer;\n};\n\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/util.cpp",
    "content": "// Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\n#include \"util.h\"\n\n#include <utility>\n\n\nnamespace openage::renderer::util {\n\nEigen::Matrix4d ortho_matrix_d(double left, double right, double bottom, double top, double near, double far) {\n\treturn ortho_matrix<Eigen::Matrix4d, double>(left, right, bottom, top, near, far);\n}\n\n\nEigen::Matrix4f ortho_matrix_f(float left, float right, float bottom, float top, float near, float far) {\n\treturn ortho_matrix<Eigen::Matrix4f, float>(left, right, bottom, top, near, far);\n}\n\n} // namespace openage::renderer::util\n"
  },
  {
    "path": "libopenage/renderer/util.h",
    "content": "// Copyright 2018-2022 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n#include <eigen3/Eigen/Dense>\n\n\nnamespace openage::renderer::util {\n\n/**\n * Creates a orthographic projection matrix.\n *\n * @param left Left vertical clipping plane coordinates.\n * @param right Right vertical clipping plane coordinates.\n * @param bottom: Bottom horizontal clipping plane coordinates.\n * @param top: Top horizontal clipping plane coordinates.\n * @param near: Distances to nearer depth clipping planes. Specify as negative if the plane is behind the viewpoint.\n * @param far: Distances to farther depth clipping planes. Specify as negative if the plane is behind the viewpoint.\n */\ntemplate <typename matrix_t, typename number_t>\nmatrix_t ortho_matrix(number_t left, number_t right, number_t bottom, number_t top, number_t near, number_t far) {\n\tmatrix_t ortho;\n\tortho << 2 / (right - left), 0, 0, -(right + left) / (right - left),\n\t\t0, 2 / (top - bottom), 0, -(top + bottom) / (top - bottom),\n\t\t0, 0, -2 / (far - near), -(far + near) / (far - near),\n\t\t0, 0, 0, 1;\n\n\treturn ortho;\n}\n\n\n/**\n * Create a orthographic projection matrix. Double variant.\n *\n * @see ortho_matrix\n */\nEigen::Matrix4d ortho_matrix_d(double left, double right, double bottom, double top, double near, double far);\n\n\n/**\n * Create a orthographic projection matrix. Float variant.\n *\n * @see ortho_matrix\n */\nEigen::Matrix4f ortho_matrix_f(float left, float right, float bottom, float top, float near, float far);\n\n} // namespace openage::renderer::util\n"
  },
  {
    "path": "libopenage/renderer/vulkan/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tgraphics_device.cpp\n\tloader.cpp\n\trenderer.cpp\n\twindowvk.cpp\n)\n"
  },
  {
    "path": "libopenage/renderer/vulkan/README.md",
    "content": "# Vulkan renderer\n\nAn experimental, mostly unimplemented Vulkan renderer. Can draw one triangle at best.\n"
  },
  {
    "path": "libopenage/renderer/vulkan/graphics_device.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"graphics_device.h\"\n\n#include <algorithm>\n#include <cstring>\n\n#include \"../../error/error.h\"\n#include \"../../log/log.h\"\n\n#include \"util.h\"\n\n\nnamespace openage::renderer::vulkan {\n\nstd::optional<SurfaceSupportDetails> VlkGraphicsDevice::find_device_surface_support(VkPhysicalDevice dev, VkSurfaceKHR surf) {\n\t// Search for queue families in the device\n\tauto q_fams = vk_do_ritual(vkGetPhysicalDeviceQueueFamilyProperties, dev);\n\n\tstd::optional<uint32_t> maybe_graphics_fam = {};\n\tstd::optional<uint32_t> maybe_present_fam = {};\n\n\t// Figure out if any of the families supports graphics\n\tfor (size_t i = 0; i < q_fams.size(); i++) {\n\t\tauto const &q_fam = q_fams[i];\n\n\t\tif (q_fam.queueCount > 0) {\n\t\t\tif ((q_fam.queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0u) {\n\t\t\t\tmaybe_graphics_fam = i;\n\n\t\t\t\t// See if it also supports present\n\t\t\t\tVkBool32 support = VK_FALSE;\n\t\t\t\tvkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surf, &support);\n\t\t\t\tif (support != VK_FALSE) {\n\t\t\t\t\t// This family support both, we're done\n\t\t\t\t\tmaybe_present_fam = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!maybe_graphics_fam) {\n\t\t// This device has no graphics queue family that works with the surface\n\t\treturn {};\n\t}\n\n\tSurfaceSupportDetails details = {};\n\tdetails.phys_device = dev;\n\tdetails.surface = surf;\n\n\t// If we have found a family that support both graphics and present\n\tif (maybe_present_fam) {\n\t\tdetails.graphics_fam = *maybe_graphics_fam;\n\t\tdetails.maybe_present_fam = {};\n\t}\n\telse {\n\t\t// Otherwise look for a present-only queue\n\t\tfor (size_t i = 0; i < q_fams.size(); i++) {\n\t\t\tauto const &q_fam = q_fams[i];\n\t\t\tif (q_fam.queueCount > 0) {\n\t\t\t\tVkBool32 support = VK_FALSE;\n\t\t\t\tvkGetPhysicalDeviceSurfaceSupportKHR(dev, i, surf, &support);\n\t\t\t\tif (support != VK_FALSE) {\n\t\t\t\t\tmaybe_present_fam = i;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!maybe_present_fam) {\n\t\t\t// This device has no present queue family that works with the surface\n\t\t\treturn {};\n\t\t}\n\n\t\tdetails.graphics_fam = *maybe_graphics_fam;\n\t\tdetails.maybe_present_fam = maybe_present_fam;\n\t}\n\n\t// Obtain other information\n\tdetails.surface_formats = vk_do_ritual(vkGetPhysicalDeviceSurfaceFormatsKHR, dev, surf);\n\tdetails.present_modes = vk_do_ritual(vkGetPhysicalDeviceSurfacePresentModesKHR, dev, surf);\n\tvkGetPhysicalDeviceSurfaceCapabilitiesKHR(dev, surf, &details.surface_caps);\n\n\t// Finally, check that we have at least one format and present mode\n\tif (details.surface_formats.empty() || details.present_modes.empty()) {\n\t\treturn {};\n\t}\n\n\treturn details;\n}\n\nVlkGraphicsDevice::VlkGraphicsDevice(VkPhysicalDevice dev, std::vector<uint32_t> const &q_fams) :\n\tphys_device(dev) {\n\t// Prepare queue creation info for each family requested\n\tstd::vector<VkDeviceQueueCreateInfo> q_infos(q_fams.size());\n\tconst float p = 1.0f;\n\n\tfor (size_t i = 0; i < q_fams.size(); i++) {\n\t\tq_infos[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;\n\t\tq_infos[i].queueFamilyIndex = q_fams[i];\n\t\tq_infos[i].queueCount = 1;\n\t\tq_infos[i].pQueuePriorities = &p;\n\t}\n\n\t// Request these extensions\n\tstd::vector<const char *> ext_names = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};\n\n\t// Check if extensions are available\n\tauto exts = vk_do_ritual(vkEnumerateDeviceExtensionProperties, dev, nullptr);\n\tfor (auto ext : ext_names) {\n\t\tif (std::count_if(exts.begin(), exts.end(), [=](VkExtensionProperties const &p) {\n\t\t\t\treturn std::strcmp(p.extensionName, ext) == 0;\n\t\t\t})\n\t\t    == 0) {\n\t\t\tthrow Error(MSG(err) << \"Tried to instantiate device, but it's missing this extension: \" << ext);\n\t\t}\n\t}\n\n#ifndef NDEBUG\n\t{\n\t\tVkPhysicalDeviceProperties dev_props;\n\t\tvkGetPhysicalDeviceProperties(this->phys_device, &dev_props);\n\t\tlog::log(MSG(dbg) << \"Chosen Vulkan graphics device: \" << dev_props.deviceName);\n\t\tlog::log(MSG(dbg) << \"Device extensions:\");\n\t\tfor (auto const &ext : exts) {\n\t\t\tlog::log(MSG(dbg) << \"\\t\" << ext.extensionName);\n\t\t}\n\t}\n#endif\n\n\t// Prepare device creation\n\tVkDeviceCreateInfo create_dev{};\n\tcreate_dev.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;\n\tcreate_dev.queueCreateInfoCount = q_infos.size();\n\tcreate_dev.pQueueCreateInfos = q_infos.data();\n\tcreate_dev.enabledExtensionCount = ext_names.size();\n\tcreate_dev.ppEnabledExtensionNames = ext_names.data();\n\n\tVkPhysicalDeviceFeatures features{};\n\t// TODO request features\n\tcreate_dev.pEnabledFeatures = &features;\n\n\tVK_CALL_CHECKED(vkCreateDevice, this->phys_device, &create_dev, nullptr, &this->device);\n\n\t// Obtain handles for the created queues\n\tthis->queues.resize(q_fams.size());\n\tfor (size_t i = 0; i < q_fams.size(); i++) {\n\t\tvkGetDeviceQueue(this->device, q_fams[i], 0, &this->queues[i]);\n\t}\n}\n\nVkPhysicalDevice VlkGraphicsDevice::get_physical_device() const {\n\treturn this->phys_device;\n}\n\nVkDevice VlkGraphicsDevice::get_device() const {\n\treturn this->device;\n}\n\nVkQueue VlkGraphicsDevice::get_queue(size_t idx) const {\n\treturn this->queues[idx];\n}\n\nVlkGraphicsDevice::~VlkGraphicsDevice() {\n\tvkDestroyDevice(this->device, nullptr);\n}\n\n} // namespace openage::renderer::vulkan\n"
  },
  {
    "path": "libopenage/renderer/vulkan/graphics_device.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <optional>\n#include <vector>\n\n#include <vulkan/vulkan.h>\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\n/// Contains information about the support of a given physical device for a given surface,\n/// for example the formats using which it can present onto the surface.\nstruct SurfaceSupportDetails {\n\t/// The physical device.\n\tVkPhysicalDevice phys_device;\n\n\t/// The surface to which it can draw.\n\tVkSurfaceKHR surface;\n\n\t/// Available present modes.\n\tstd::vector<VkPresentModeKHR> present_modes;\n\n\t/// Available surface formats.\n\tstd::vector<VkSurfaceFormatKHR> surface_formats;\n\n\t/// Various capabilities of presentation to the surface.\n\tVkSurfaceCapabilitiesKHR surface_caps;\n\n\t/// Index of the device queue family with graphics support.\n\tuint32_t graphics_fam;\n\n\t/// Index of the queue family with presentation support. This might be the same as the graphics\n\t/// family, in which case this optional is empty.\n\tstd::optional<uint32_t> maybe_present_fam;\n};\n\n/// Owns a device capable of graphics operations and surface presentation using WSI.\nclass VlkGraphicsDevice {\n\t/// The underlying physical device.\n\tVkPhysicalDevice phys_device;\n\n\t/// Logical device, owned by this object.\n\tVkDevice device;\n\n\t/// The queues instantiated for this device.\n\tstd::vector<VkQueue> queues;\n\npublic:\n\t/// Given a physical device and a surface, checks whether the device is capable of presenting to the surface.\n\t/// If it is, returns information about its presentation capabilities, otherwise returns an empty optional.\n\tstatic std::optional<SurfaceSupportDetails> find_device_surface_support(VkPhysicalDevice, VkSurfaceKHR);\n\n\t/// Given a physical device and a list of queue family indices in that device, instantiates\n\t/// a logical device with a queue per each of the families. The device has to support the\n\t/// swapchain extension.\n\tVlkGraphicsDevice(VkPhysicalDevice dev, std::vector<uint32_t> const &q_fams);\n\n\tVkPhysicalDevice get_physical_device() const;\n\tVkDevice get_device() const;\n\tVkQueue get_queue(size_t idx) const;\n\n\t// TODO structure isn't ideal, maybe store SurfaceSupportDetails in here?\n\n\t~VlkGraphicsDevice();\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/loader.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"loader.h\"\n\n#include \"../../error/error.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\nVlkLoader::VlkLoader() :\n\tinited(false) {}\n\nvoid VlkLoader::init(VkInstance instance) {\n#ifndef NDEBUG\n\tthis->pCreateDebugReportCallbackEXT = PFN_vkCreateDebugReportCallbackEXT(vkGetInstanceProcAddr(instance, \"vkCreateDebugReportCallbackEXT\"));\n\tthis->pDestroyDebugReportCallbackEXT = PFN_vkDestroyDebugReportCallbackEXT(vkGetInstanceProcAddr(instance, \"vkDestroyDebugReportCallbackEXT\"));\n#endif\n\n\tthis->inited = true;\n}\n\n#ifndef NDEBUG\nVkResult VlkLoader::vkCreateDebugReportCallbackEXT(\n\tVkInstance instance,\n\tconst VkDebugReportCallbackCreateInfoEXT *pCreateInfo,\n\tconst VkAllocationCallbacks *pAllocator,\n\tVkDebugReportCallbackEXT *pCallback) {\n\tif (!this->inited) {\n\t\tthrow Error(MSG(err) << \"Tried to request function from Vulkan extension loader before initializing it.\");\n\t}\n\n\tif (this->pCreateDebugReportCallbackEXT != nullptr) {\n\t\treturn this->pCreateDebugReportCallbackEXT(instance, pCreateInfo, pAllocator, pCallback);\n\t}\n\n\treturn VK_ERROR_EXTENSION_NOT_PRESENT;\n}\n\nvoid VlkLoader::vkDestroyDebugReportCallbackEXT(\n\tVkInstance instance,\n\tVkDebugReportCallbackEXT callback,\n\tconst VkAllocationCallbacks *pAllocator) {\n\tif (!this->inited) {\n\t\tthrow Error(MSG(err) << \"Tried to request function from Vulkan extension loader before initializing it.\");\n\t}\n\n\tif (this->pDestroyDebugReportCallbackEXT != nullptr) {\n\t\tthis->pDestroyDebugReportCallbackEXT(instance, callback, pAllocator);\n\t}\n}\n#endif\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/loader.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vulkan/vulkan.h>\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\n/// A class for dynamically loading Vulkan extension functions.\nclass VlkLoader {\n#ifndef NDEBUG\n\tPFN_vkCreateDebugReportCallbackEXT pCreateDebugReportCallbackEXT;\n\tPFN_vkDestroyDebugReportCallbackEXT pDestroyDebugReportCallbackEXT;\n#endif\n\n\tbool inited;\n\npublic:\n\tVlkLoader();\n\n\t/// Initialize this loader for the given Vulkan instance.\n\tvoid init(VkInstance);\n\n#ifndef NDEBUG\n\t/// Part of VK_EXT_debug_report, allows setting a callback for debug events.\n\tVkResult vkCreateDebugReportCallbackEXT(\n\t\tVkInstance instance,\n\t\tconst VkDebugReportCallbackCreateInfoEXT *pCreateInfo,\n\t\tconst VkAllocationCallbacks *pAllocator,\n\t\tVkDebugReportCallbackEXT *pCallback);\n\n\n\t/// Part of VK_EXT_debug_report, destroys the debug callback object.\n\tvoid vkDestroyDebugReportCallbackEXT(\n\t\tVkInstance instance,\n\t\tVkDebugReportCallbackEXT callback,\n\t\tconst VkAllocationCallbacks *pAllocator);\n#endif\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/render_target.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n#include <limits>\n\n#include <vulkan/vulkan.h>\n\n#include \"log/log.h\"\n\n#include \"renderer/render_target.h\"\n#include \"renderer/vulkan/graphics_device.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\n/// A Vulkan representation of a display target that can be drawn onto directly,\n/// that is _not_ by copying data from another object.\nclass VlkDrawableDisplay {\npublic:\n\t// use shared_ptr?\n\tVkDevice device;\n\tVkSwapchainKHR swapchain;\n\tVkFormat format;\n\tVkExtent2D extent;\n\tstd::vector<VkImage> images;\n\tstd::vector<VkImageView> image_views;\n\tstd::vector<VkFramebuffer> framebuffers;\n\n\tVkSurfaceFormatKHR choose_best_surface_format(std::vector<VkSurfaceFormatKHR> const &formats) {\n\t\t// If the implementation doesn't have preferred formats, choose our own\n\t\tif (formats.size() == 1\n\t\t    && formats[0].format == VK_FORMAT_UNDEFINED) {\n\t\t\treturn {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};\n\t\t}\n\n\t\t// Otherwise if one of the preferred formats is good, choose that\n\t\tfor (const auto &fmt : formats) {\n\t\t\tif (fmt.format == VK_FORMAT_B8G8R8_UNORM\n\t\t\t    && fmt.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {\n\t\t\t\treturn fmt;\n\t\t\t}\n\t\t}\n\n\t\t// Otherwise select any format\n\t\treturn formats[0];\n\t}\n\n\tVkPresentModeKHR choose_best_present_mode(std::vector<VkPresentModeKHR> const &modes) {\n\t\tVkPresentModeKHR ret = VK_PRESENT_MODE_MAILBOX_KHR;\n\n\t\tfor (const auto &mode : modes) {\n\t\t\tif (mode == VK_PRESENT_MODE_MAILBOX_KHR) {\n\t\t\t\treturn mode;\n\t\t\t}\n\t\t\telse if (mode == VK_PRESENT_MODE_FIFO_KHR) {\n\t\t\t\tret = mode;\n\t\t\t}\n\t\t\telse if (mode == VK_PRESENT_MODE_FIFO_RELAXED_KHR\n\t\t\t         && ret != VK_PRESENT_MODE_FIFO_KHR) {\n\t\t\t\tret = mode;\n\t\t\t}\n\t\t\telse if (mode == VK_PRESENT_MODE_IMMEDIATE_KHR\n\t\t\t         && ret != VK_PRESENT_MODE_FIFO_KHR\n\t\t\t         && ret != VK_PRESENT_MODE_FIFO_RELAXED_KHR) {\n\t\t\t\tret = mode;\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t}\n\n\tVlkDrawableDisplay(VkDevice device, SurfaceSupportDetails details) :\n\t\tdevice(device) {\n\t\tVkSurfaceFormatKHR format = choose_best_surface_format(details.surface_formats);\n\t\tthis->format = format.format;\n\n\t\tVkPresentModeKHR mode = choose_best_present_mode(details.present_modes);\n\n\t\tif (details.surface_caps.currentExtent.width != std::numeric_limits<uint32_t>::max()) {\n\t\t\tthis->extent = details.surface_caps.currentExtent;\n\t\t}\n\t\telse {\n\t\t\t// TODO choose a size from this->size in this case\n\t\t\tthrow Error(MSG(err) << \"Window manager does not provide a window size.\");\n\t\t}\n\n\t\tuint32_t img_count = details.surface_caps.minImageCount + 1;\n\t\tif (details.surface_caps.maxImageCount != 0) {\n\t\t\timg_count = std::max(img_count, details.surface_caps.maxImageCount);\n\t\t}\n\n\t\tVkSwapchainCreateInfoKHR cr_swap = {};\n\t\tcr_swap.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;\n\t\tcr_swap.surface = details.surface;\n\t\tcr_swap.minImageCount = img_count;\n\t\tcr_swap.imageFormat = this->format;\n\t\tcr_swap.imageColorSpace = format.colorSpace;\n\t\tcr_swap.presentMode = mode;\n\t\t// TODO why doesn't validation work?\n\t\tcr_swap.imageExtent = this->extent;\n\t\tcr_swap.imageArrayLayers = 1;\n\t\t// or VK_IMAGE_USAGE_TRANSFER_DST_BIT if not drawing directly (postprocess)\n\t\tcr_swap.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;\n\n\t\tif (details.maybe_present_fam) {\n\t\t\t// We have to share the swapchain between different queues in this case\n\t\t\tcr_swap.imageSharingMode = VK_SHARING_MODE_CONCURRENT;\n\t\t\tcr_swap.queueFamilyIndexCount = 2;\n\t\t\tstd::array<uint32_t, 2> q_fams = {{details.graphics_fam, *details.maybe_present_fam}};\n\t\t\tcr_swap.pQueueFamilyIndices = q_fams.data();\n\t\t}\n\t\telse {\n\t\t\t// Otherwise only one queue can access it\n\t\t\tcr_swap.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;\n\t\t}\n\n\t\tcr_swap.preTransform = details.surface_caps.currentTransform;\n\t\tcr_swap.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;\n\t\t// Discard pixels which are not visible\n\t\tcr_swap.clipped = VK_TRUE;\n\t\tcr_swap.oldSwapchain = VK_NULL_HANDLE;\n\n\t\tVK_CALL_CHECKED(vkCreateSwapchainKHR, this->device, &cr_swap, nullptr, &this->swapchain);\n\n\t\tthis->images = vk_do_ritual(vkGetSwapchainImagesKHR, this->device, this->swapchain);\n\n\t\t// TODO move out?\n\t\tfor (auto img : this->images) {\n\t\t\tVkImageViewCreateInfo cr_view = {};\n\t\t\tcr_view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;\n\t\t\tcr_view.image = img;\n\t\t\tcr_view.viewType = VK_IMAGE_VIEW_TYPE_2D;\n\t\t\tcr_view.format = this->format;\n\t\t\tcr_view.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;\n\t\t\tcr_view.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;\n\t\t\tcr_view.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;\n\t\t\tcr_view.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;\n\t\t\tcr_view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;\n\t\t\tcr_view.subresourceRange.baseMipLevel = 0;\n\t\t\tcr_view.subresourceRange.levelCount = 1;\n\t\t\tcr_view.subresourceRange.baseArrayLayer = 0;\n\t\t\tcr_view.subresourceRange.layerCount = 1;\n\n\t\t\tVkImageView view;\n\t\t\tVK_CALL_CHECKED(vkCreateImageView, this->device, &cr_view, nullptr, &view);\n\n\t\t\tthis->image_views.push_back(view);\n\t\t}\n\t}\n\n\t~VlkDrawableDisplay() {\n\t\tvkDestroySwapchainKHR(this->device, this->swapchain, nullptr);\n\t}\n};\n\nclass VlkFramebuffer final : public RenderTarget {\n\t// std::vector<VkImageView> attachments;\n\t// VkFramebuffer framebuffer;\n\t// VkViewport viewport;\n\npublic:\n\tVlkFramebuffer(VkRenderPass /*pass*/, std::vector<VkImageView> const & /*attachments*/) {\n\t\t// TODO.\n\t}\n\n\tstd::vector<std::shared_ptr<Texture2d>> get_texture_targets() override {\n\t\t// TODO\n\t\tstd::vector<std::shared_ptr<Texture2d>> textures{};\n\t\treturn textures;\n\t}\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/renderer.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"renderer.h\"\n\n#include \"../../error/error.h\"\n#include \"../../util/fslike/directory.h\"\n\n#include \"../resources/shader_source.h\"\n\n#include \"util.h\"\n#include \"graphics_device.h\"\n#include \"render_target.h\"\n#include \"shader_program.h\"\n\n\nnamespace openage::renderer::vulkan {\n\nvoid VlkRenderer::do_the_thing(util::Path& dir) {\n\t// Please keep in mind that this function is only for testing and so might be messy.\n\n\t// Enumerate available physical devices\n\tauto devices = vk_do_ritual(vkEnumeratePhysicalDevices, this->instance);\n\tif (devices.empty()) {\n\t\tthrow Error(MSG(err) << \"No Vulkan devices available.\");\n\t}\n\n\tstd::vector<std::pair<VkPhysicalDevice, SurfaceSupportDetails>> support_per_dev;\n\tfor (auto dev : devices) {\n\t\tauto support = VlkGraphicsDevice::find_device_surface_support(dev, surface);\n\t\tif (support) {\n\t\t\tsupport_per_dev.emplace_back(dev, *support);\n\t\t}\n\t}\n\n\tif (support_per_dev.empty()) {\n\t\tthrow Error(MSG(err) << \"No valid Vulkan device available.\");\n\t}\n\n\t// TODO rate devices based on capabilities\n\t// Given an instance and surface, selects the best (fastest, most capable, etc.) graphics device\n\t// which supports rendering onto that particular surface and constructs the object.\n\n\tauto const& info = support_per_dev[0];\n\n\t// Create a logical device with a single queue for both graphics and present\n\tif (info.second.maybe_present_fam) {\n\t\tthrow 0;\n\t}\n\n\tVlkGraphicsDevice dev(info.first, { info.second.graphics_fam } );\n\n\tVlkDrawableDisplay display(dev.get_device(), info.second);\n\n\t// TODO reinit swapchain on window resize\n\n\tauto vert = resources::ShaderSource(\n\t\tresources::shader_lang_t::spirv,\n\t\tresources::shader_stage_t::vertex,\n\t\tutil::Path(dir) / \"assets/shaders/vert.spv\"\n\t);\n\n\tauto frag = resources::ShaderSource(\n\t\tresources::shader_lang_t::spirv,\n\t\tresources::shader_stage_t::fragment,\n\t\tutil::Path(dir) / \"assets/shaders/frag.spv\"\n\t);\n\n\tVlkShaderProgram prog(dev.get_device(), { vert, frag } );\n\n\tVkPipelineVertexInputStateCreateInfo cr_vert_in = {};\n\tcr_vert_in.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;\n\t// all init'd to 0\n\n\tVkPipelineInputAssemblyStateCreateInfo cr_in_asm = {};\n\tcr_in_asm.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;\n\tcr_in_asm.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;\n\tcr_in_asm.primitiveRestartEnable = VK_FALSE;\n\n\tVkViewport viewport = {};\n\tviewport.x = 0.0f;\n\tviewport.y = 0.0f;\n\tviewport.width = display.extent.width;\n\tviewport.height = display.extent.height;\n\tviewport.minDepth = 0.0f;\n\tviewport.maxDepth = 1.0f;\n\n\tVkRect2D scissor = {};\n\tscissor.offset = { 0, 0 };\n\tscissor.extent = display.extent;\n\n\tVkPipelineViewportStateCreateInfo cr_view = {};\n\tcr_view.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;\n\tcr_view.viewportCount = 1;\n\tcr_view.pViewports = &viewport;\n\tcr_view.scissorCount = 1;\n\tcr_view.pScissors = &scissor;\n\n\tVkPipelineRasterizationStateCreateInfo cr_raster = {};\n\tcr_raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;\n\tcr_raster.depthClampEnable = VK_FALSE;\n\tcr_raster.rasterizerDiscardEnable = VK_FALSE;\n\tcr_raster.polygonMode = VK_POLYGON_MODE_FILL;\n\tcr_raster.lineWidth = 1.0f;\n\tcr_raster.cullMode = VK_CULL_MODE_BACK_BIT;\n\tcr_raster.frontFace = VK_FRONT_FACE_CLOCKWISE;\n\tcr_raster.depthBiasEnable = VK_FALSE;\n\n\tVkPipelineMultisampleStateCreateInfo cr_msaa = {};\n\tcr_msaa.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;\n\tcr_msaa.sampleShadingEnable = VK_FALSE;\n\tcr_msaa.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;\n\n\tVkPipelineColorBlendAttachmentState blend_state = {};\n\tblend_state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT\n\t                             | VK_COLOR_COMPONENT_G_BIT\n\t                             | VK_COLOR_COMPONENT_B_BIT\n\t                             | VK_COLOR_COMPONENT_A_BIT;\n\tblend_state.blendEnable = VK_TRUE;\n\tblend_state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;\n\tblend_state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;\n\tblend_state.colorBlendOp = VK_BLEND_OP_ADD;\n\tblend_state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;\n\tblend_state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;\n\tblend_state.alphaBlendOp = VK_BLEND_OP_ADD;\n\n\tVkPipelineColorBlendStateCreateInfo cr_blend = {};\n\tcr_blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;\n\tcr_blend.logicOpEnable = VK_FALSE;\n\tcr_blend.attachmentCount = 1;\n\tcr_blend.pAttachments = &blend_state;\n\n\tVkPipelineLayoutCreateInfo cr_layout = {};\n\tcr_layout.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;\n\t// empty object\n\n\tVkPipelineLayout layout;\n\tVK_CALL_CHECKED(vkCreatePipelineLayout, dev.get_device(), &cr_layout, nullptr, &layout);\n\n\t/// RENDERPASS\n\tVkAttachmentDescription color_attachment = {};\n\tcolor_attachment.format = display.format;\n\tcolor_attachment.samples = VK_SAMPLE_COUNT_1_BIT;\n\tcolor_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;\n\tcolor_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;\n\tcolor_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;\n\tcolor_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;\n\tcolor_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;\n\tcolor_attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;\n\n\tVkAttachmentReference color_attachment_ref = {};\n\tcolor_attachment_ref.attachment = 0;\n\tcolor_attachment_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;\n\n\tVkSubpassDescription subpass = {};\n\tsubpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;\n\tsubpass.colorAttachmentCount = 1;\n\tsubpass.pColorAttachments = &color_attachment_ref;\n\n\tVkSubpassDependency dep = {};\n\tdep.srcSubpass = VK_SUBPASS_EXTERNAL;\n\tdep.dstSubpass = 0;\n\tdep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;\n\tdep.srcAccessMask = 0;\n\tdep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;\n\tdep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;\n\n\tVkRenderPassCreateInfo cr_render_pass = {};\n\tcr_render_pass.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;\n\tcr_render_pass.attachmentCount = 1;\n\tcr_render_pass.pAttachments = &color_attachment;\n\tcr_render_pass.subpassCount = 1;\n\tcr_render_pass.pSubpasses = &subpass;\n\tcr_render_pass.dependencyCount = 1;\n\tcr_render_pass.pDependencies = &dep;\n\n\tVkRenderPass render_pass;\n\tVK_CALL_CHECKED(vkCreateRenderPass, dev.get_device(), &cr_render_pass, nullptr, &render_pass);\n\t/// RENDERPASS\n\n\tVkGraphicsPipelineCreateInfo cr_pipeline = {};\n\tcr_pipeline.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;\n\tcr_pipeline.stageCount = 2;\n\tcr_pipeline.pStages = prog.pipeline_stage_infos.data();\n\tcr_pipeline.pVertexInputState = &cr_vert_in;\n\tcr_pipeline.pInputAssemblyState = &cr_in_asm;\n\tcr_pipeline.pViewportState = &cr_view;\n\tcr_pipeline.pRasterizationState = &cr_raster;\n\tcr_pipeline.pMultisampleState = &cr_msaa;\n\tcr_pipeline.pDepthStencilState = nullptr;\n\tcr_pipeline.pColorBlendState = &cr_blend;\n\tcr_pipeline.pDynamicState = nullptr;\n\tcr_pipeline.layout = layout;\n\tcr_pipeline.renderPass = render_pass;\n\tcr_pipeline.subpass = 0;\n\tcr_pipeline.basePipelineHandle = VK_NULL_HANDLE;\n\tcr_pipeline.basePipelineIndex = -1;\n\n\tVkPipeline pipeline;\n\tVK_CALL_CHECKED(vkCreateGraphicsPipelines, dev.get_device(), VK_NULL_HANDLE, 1, &cr_pipeline, nullptr, &pipeline);\n\n\tstd::vector<VkFramebuffer> fbufs;\n\tfor (auto view : display.image_views) {\n\t\tVkFramebufferCreateInfo cr_fbuf = {};\n\t\tcr_fbuf.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;\n\t\tcr_fbuf.renderPass = render_pass;\n\t\tcr_fbuf.attachmentCount = 1;\n\t\tcr_fbuf.pAttachments = &view;\n\t\tcr_fbuf.width = display.extent.width;\n\t\tcr_fbuf.height = display.extent.height;\n\t\tcr_fbuf.layers = 1;\n\n\t\tVkFramebuffer fbuf;\n\t\tVK_CALL_CHECKED(vkCreateFramebuffer, dev.get_device(), &cr_fbuf, nullptr, &fbuf);\n\n\t\tfbufs.push_back(fbuf);\n\t}\n\n\tVkCommandPoolCreateInfo cr_pool = {};\n\tcr_pool.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;\n\tcr_pool.queueFamilyIndex = info.second.graphics_fam;\n\tcr_pool.flags = 0;\n\n\tVkCommandPool pool;\n\tVK_CALL_CHECKED(vkCreateCommandPool, dev.get_device(), &cr_pool, nullptr, &pool);\n\n\tstd::vector<VkCommandBuffer> cmd_bufs(fbufs.size());\n\tVkCommandBufferAllocateInfo cr_cmd_bufs = {};\n\tcr_cmd_bufs.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;\n\tcr_cmd_bufs.commandPool = pool;\n\tcr_cmd_bufs.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;\n\tcr_cmd_bufs.commandBufferCount = static_cast<uint32_t>(cmd_bufs.size());\n\n\tVK_CALL_CHECKED(vkAllocateCommandBuffers, dev.get_device(), &cr_cmd_bufs, cmd_bufs.data());\n\n\tfor (size_t i = 0; i < cmd_bufs.size(); i++) {\n\t\tauto cmd_buf = cmd_bufs[i];\n\t\tauto fbuf = fbufs[i];\n\n\t\tVkCommandBufferBeginInfo begin_info = {};\n\t\tbegin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;\n\t\tbegin_info.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;\n\t\tbegin_info.pInheritanceInfo = nullptr;\n\n\t\tvkBeginCommandBuffer(cmd_buf, &begin_info);\n\n\t\tVkRenderPassBeginInfo cmd_render = {};\n\t\tcmd_render.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;\n\t\tcmd_render.renderPass = render_pass;\n\t\tcmd_render.framebuffer = fbuf;\n\t\tcmd_render.renderArea.offset = { 0, 0 };\n\t\tcmd_render.renderArea.extent = display.extent;\n\n\t\tVkClearValue clear_color = {{{ 0.0f, 0.0f, 0.0f, 1.0f }}};\n\t\tcmd_render.clearValueCount = 1;\n\t\tcmd_render.pClearValues = &clear_color;\n\n\t\tvkCmdBeginRenderPass(cmd_buf, &cmd_render, VK_SUBPASS_CONTENTS_INLINE);\n\n\t\tvkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);\n\n\t\tvkCmdDraw(cmd_buf, 3, 1, 0, 0);\n\n\t\tvkCmdEndRenderPass(cmd_buf);\n\n\t\tVK_CALL_CHECKED(vkEndCommandBuffer, cmd_buf);\n\n\t\tVkSemaphoreCreateInfo cr_sem = {};\n\t\tcr_sem.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;\n\n\t\tVkSemaphore sem_image_ready;\n\t\tVkSemaphore sem_render_done;\n\t\tVK_CALL_CHECKED(vkCreateSemaphore, dev.get_device(), &cr_sem, nullptr, &sem_image_ready);\n\t\tVK_CALL_CHECKED(vkCreateSemaphore, dev.get_device(), &cr_sem, nullptr, &sem_render_done);\n\n\t\tuint32_t img_idx = 0;\n\t\tVK_CALL_CHECKED(vkAcquireNextImageKHR, dev.get_device(), display.swapchain, std::numeric_limits<uint64_t>::max(), sem_image_ready, VK_NULL_HANDLE, &img_idx);\n\n\t\tVkSubmitInfo submit = {};\n\t\tsubmit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;\n\t\tsubmit.waitSemaphoreCount = 1;\n\t\tsubmit.pWaitSemaphores = &sem_image_ready;\n\t\tVkPipelineStageFlags mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;\n\t\tsubmit.pWaitDstStageMask = &mask;\n\n\t\tsubmit.commandBufferCount = 1;\n\t\tsubmit.pCommandBuffers = &cmd_bufs[img_idx];\n\n\t\tsubmit.signalSemaphoreCount = 1;\n\t\tsubmit.pSignalSemaphores = &sem_render_done;\n\n\t\tVK_CALL_CHECKED(vkQueueSubmit, dev.get_queue(0), 1, &submit, VK_NULL_HANDLE);\n\n\t\tVkPresentInfoKHR present_info = {};\n\t\tpresent_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;\n\t\tpresent_info.waitSemaphoreCount = 1;\n\t\tpresent_info.pWaitSemaphores = &sem_render_done;\n\n\t\tpresent_info.swapchainCount = 1;\n\t\tpresent_info.pSwapchains = &display.swapchain;\n\t\tpresent_info.pImageIndices = &img_idx;\n\t\tpresent_info.pResults = nullptr;\n\n\t\tvkQueuePresentKHR(dev.get_queue(0), &present_info);\n\n\t\tvkQueueWaitIdle(dev.get_queue(0));\n\n\t\tvkDeviceWaitIdle(dev.get_device());\n\t}\n}\n\n} // openage::renderer::vulkan\n"
  },
  {
    "path": "libopenage/renderer/vulkan/renderer.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vulkan/vulkan.h>\n\n#include \"../../util/path.h\"\n#include \"../renderer.h\"\n\n#include \"graphics_device.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\n/// A renderer using the Vulkan API.\nclass VlkRenderer {\n\tVkInstance instance;\n\n\tVkSurfaceKHR surface;\n\npublic:\n\tVlkRenderer(VkInstance instance, VkSurfaceKHR surface) :\n\t\tinstance(instance), surface(surface) {}\n\n\t/// Testing function that draws a triangle. Not part of the final renderer implementation.\n\tvoid do_the_thing(util::Path &dir);\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/shader_program.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include <vulkan/vulkan.h>\n\n#include \"renderer/resources/shader_source.h\"\n#include \"renderer/shader_program.h\"\n\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\nstatic VkShaderStageFlagBits vk_shader_stage(resources::shader_stage_t stage) {\n\tswitch (stage) {\n\tcase resources::shader_stage_t::vertex:\n\t\treturn VK_SHADER_STAGE_VERTEX_BIT;\n\tcase resources::shader_stage_t::geometry:\n\t\treturn VK_SHADER_STAGE_GEOMETRY_BIT;\n\tcase resources::shader_stage_t::tesselation_control:\n\t\treturn VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;\n\tcase resources::shader_stage_t::tesselation_evaluation:\n\t\treturn VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;\n\tcase resources::shader_stage_t::fragment:\n\t\treturn VK_SHADER_STAGE_FRAGMENT_BIT;\n\tdefault:\n\t\tthrow Error(MSG(err) << \"Unknown shader stage.\");\n\t}\n}\n\nclass VlkShaderProgram /* final : public ShaderProgram */ {\npublic:\n\tstd::vector<VkShaderModule> modules;\n\tstd::vector<VkPipelineShaderStageCreateInfo> pipeline_stage_infos;\n\n\texplicit VlkShaderProgram(VkDevice dev, std::vector<resources::ShaderSource> const &srcs) {\n\t\t// TODO reflect with spirv-cross\n\t\t// TODO if glsl, compile to spirv with libshaderc\n\n\t\tfor (auto const &src : srcs) {\n\t\t\tif (src.get_lang() != resources::shader_lang_t::spirv) {\n\t\t\t\tthrow Error(MSG(err) << \"Unsupported shader language in Vulkan shader.\");\n\t\t\t}\n\n\t\t\tVkShaderModuleCreateInfo cr_shdr = {};\n\t\t\tcr_shdr.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;\n\t\t\tcr_shdr.codeSize = src.get_source().size();\n\t\t\tcr_shdr.pCode = reinterpret_cast<uint32_t const *>(src.get_source().data());\n\n\t\t\tVkShaderModule mod;\n\t\t\tVK_CALL_CHECKED(vkCreateShaderModule, dev, &cr_shdr, nullptr, &mod);\n\n\t\t\tthis->modules.push_back(mod);\n\n\t\t\tVkPipelineShaderStageCreateInfo cr_stage = {};\n\t\t\tcr_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;\n\t\t\tcr_stage.stage = vk_shader_stage(src.get_stage());\n\t\t\tcr_stage.module = mod;\n\t\t\tcr_stage.pName = \"main\";\n\n\t\t\tthis->pipeline_stage_infos.push_back(cr_stage);\n\t\t}\n\n\t\tlog::log(MSG(dbg) << \"Created modules for Vulkan shader\");\n\t}\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/vulkan/util.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <vulkan/vulkan.h>\n\n#include \"../../error/error.h\"\n\n\ntemplate <typename R, typename R2>\nstd::vector<R> vk_do_ritual(R2 (*func)(uint32_t *, R *)) {\n\tuint32_t count = 0;\n\tfunc(&count, nullptr);\n\tstd::vector<R> ret(count);\n\tfunc(&count, ret.data());\n\n\treturn ret;\n}\n\ntemplate <typename T, typename T2, typename R, typename R2>\nstd::vector<R> vk_do_ritual(R2 (*func)(T, uint32_t *, R *), T2 &&a) {\n\tuint32_t count = 0;\n\tfunc(std::forward<T2>(a), &count, nullptr);\n\tstd::vector<R> ret(count);\n\tfunc(std::forward<T2>(a), &count, ret.data());\n\n\treturn ret;\n}\n\ntemplate <typename T, typename T2, typename U, typename U2, typename R, typename R2>\nstd::vector<R> vk_do_ritual(R2 (*func)(T, U, uint32_t *, R *), T2 &&a, U2 &&b) {\n\tuint32_t count = 0;\n\tfunc(std::forward<T2>(a), std::forward<U2>(b), &count, nullptr);\n\tstd::vector<R> ret(count);\n\tfunc(std::forward<T2>(a), std::forward<U2>(b), &count, ret.data());\n\n\treturn ret;\n}\n\ntemplate <typename T, typename T2, typename U, typename U2, typename V, typename V2, typename R, typename R2>\nstd::vector<R> vk_do_ritual(R2 (*func)(T, U, V, uint32_t *, R *), T2 &&a, U2 &&b, V2 &&c) {\n\tuint32_t count = 0;\n\tfunc(std::forward<T2>(a), std::forward<U2>(b), std::forward<V2>(c), &count, nullptr);\n\tstd::vector<R> ret(count);\n\tfunc(std::forward<T2>(a), std::forward<U2>(b), std::forward<V2>(c), &count, ret.data());\n\n\treturn ret;\n}\n\n#define VK_CALL_CHECKED(fun, ...) \\\n\t{ \\\n\t\tVkResult res = fun(__VA_ARGS__); \\\n\t\tif (res != VK_SUCCESS) { \\\n\t\t\tthrow Error(MSG(err) << \"Call to Vulkan function \" << #fun << \" failed with \" << res << \".\"); \\\n\t\t} \\\n\t}\n"
  },
  {
    "path": "libopenage/renderer/vulkan/windowvk.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"windowvk.h\"\n\n#include <set>\n\n#include <QGuiApplication>\n#include <QQuickWindow>\n#include <QVulkanInstance>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n#include \"renderer/window_event_handler.h\"\n\n#include \"renderer/vulkan/graphics_device.h\"\n#include \"renderer/vulkan/util.h\"\n\n\nnamespace openage::renderer::vulkan {\n\n#ifndef NDEBUG\nstatic VKAPI_ATTR VkBool32 VKAPI_CALL vlk_debug_cb(\n\tVkDebugReportFlagsEXT /*flags*/,\n\tVkDebugReportObjectTypeEXT /*objType*/,\n\tuint64_t /*obj*/,\n\tsize_t /*location*/,\n\tint32_t /*code*/,\n\tconst char *layerPrefix,\n\tconst char *msg,\n\tvoid * /*userData*/) {\n\tlog::log(MSG(dbg) << layerPrefix << \" \" << msg);\n\n\treturn VK_FALSE;\n}\n#endif\n\n/// Queries the Vulkan implementation for available extensions and layers.\nstatic vlk_spec find_spec() {\n\tvlk_spec specs;\n\n\t// Find which layers are available.\n\tauto layers = vk_do_ritual(vkEnumerateInstanceLayerProperties);\n\n\tlog::log(MSG(dbg) << \"Available Vulkan layers:\");\n\tfor (auto const &lr : layers) {\n\t\tspecs.layers.insert(lr.layerName);\n\t\tlog::log(MSG(dbg) << \"\\t\" << lr.layerName);\n\t}\n\n\t// Find which extensions are available.\n\t// This is annoying, since an enumeration call without a filter-by-layer parameter\n\t// won't return all extensions. We thus have to enumerate extensions for each layer\n\t// and then remove duplicates.\n\n\t// First retrieve non-layer extensions.\n\tauto props = vk_do_ritual(vkEnumerateInstanceExtensionProperties, nullptr);\n\n\tfor (auto const &p : props) {\n\t\tspecs.extensions.emplace(p.extensionName);\n\t}\n\n\t// Then retrieve extensions from layers.\n\tfor (auto const &lr : layers) {\n\t\tauto lr_props = vk_do_ritual(vkEnumerateInstanceExtensionProperties, lr.layerName);\n\n\t\tfor (auto const &p : lr_props) {\n\t\t\tspecs.extensions.emplace(p.extensionName);\n\t\t}\n\t}\n\n\tlog::log(MSG(dbg) << \"Available Vulkan extensions:\");\n\tfor (const auto &ext : specs.extensions) {\n\t\tlog::log(MSG(dbg) << \"\\t\" << ext);\n\t}\n\n\treturn specs;\n}\n\nVlkWindow::VlkWindow(const char *title, size_t width, size_t height) :\n\tWindow(width, height), capabilities(find_spec()) {\n\tstd::vector<const char *> extension_names;\n\n#ifndef NDEBUG\n\textension_names.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);\n#endif\n\textension_names.push_back(VK_KHR_SURFACE_EXTENSION_NAME);\n\n\t// Check if all extensions are available.\n\tfor (auto ext : extension_names) {\n\t\tif (this->capabilities.extensions.count(ext) == 0) {\n\t\t\tthrow Error(MSG(err) << \"Vulkan driver is missing required extension: \" << ext);\n\t\t}\n\t}\n\n\t// The names of Vulkan layers which we need.\n\tstd::vector<const char *> layer_names;\n#ifndef NDEBUG\n\tlayer_names.push_back(\"VK_LAYER_LUNARG_standard_validation\");\n#endif\n\n\t// Check if all layers are available\n\tfor (auto lr : layer_names) {\n\t\tif (this->capabilities.layers.count(lr) == 0) {\n\t\t\tthrow Error(MSG(err) << \"Vulkan driver is missing required layer: \" << lr);\n\t\t}\n\t}\n\n\t// Setup application description.\n\tVkApplicationInfo app_info = {};\n\tapp_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;\n\tapp_info.pApplicationName = title;\n\tapp_info.apiVersion = VK_MAKE_VERSION(1, 0, 57);\n\n\tVkInstanceCreateInfo inst_info = {};\n\tinst_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;\n\tinst_info.pApplicationInfo = &app_info;\n\tinst_info.enabledExtensionCount = extension_names.size();\n\tinst_info.ppEnabledExtensionNames = extension_names.data();\n\tinst_info.enabledLayerCount = layer_names.size();\n\tinst_info.ppEnabledLayerNames = layer_names.data();\n\n\t// A Vulkan instance is a proxy for all usage of Vulkan from our application,\n\t// kind of like a GL context. Initialize it.\n\tVK_CALL_CHECKED(vkCreateInstance, &inst_info, nullptr, &this->instance);\n\n\tthis->loader.init(this->instance);\n\n#ifndef NDEBUG\n\tVkDebugReportCallbackCreateInfoEXT cb_info = {};\n\tcb_info.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT;\n\tcb_info.flags =\n\t\tVK_DEBUG_REPORT_ERROR_BIT_EXT\n\t\t| VK_DEBUG_REPORT_WARNING_BIT_EXT\n\t\t| VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT\n\t\t| VK_DEBUG_REPORT_INFORMATION_BIT_EXT\n\t\t| VK_DEBUG_REPORT_DEBUG_BIT_EXT;\n\tcb_info.pfnCallback = vlk_debug_cb;\n\n\tVK_CALL_CHECKED(this->loader.vkCreateDebugReportCallbackEXT, this->instance, &cb_info, nullptr, &this->debug_callback);\n#endif\n\n\tif (QGuiApplication::instance() == nullptr) {\n\t\t// Qt windows need to attach to a QtGuiApplication\n\t\tthrow Error{MSG(err) << \"Failed to create Qt window: QGuiApplication has not been created yet.\"};\n\t}\n\n\tQQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);\n\tthis->window = std::make_shared<QWindow>();\n\tthis->window->setTitle(QString::fromStdString(title));\n\tthis->window->resize(width, height);\n\tthis->window->setSurfaceType(QSurface::VulkanSurface);\n\n\t// Attach vulkan instance\n\tQVulkanInstance qinstance;\n\tqinstance.setVkInstance(this->instance);\n\tqinstance.create();\n\tQVulkanInstance::surfaceForWindow(this->window.get());\n\tthis->window->setVulkanInstance(&qinstance);\n\n\tQSurfaceFormat format;\n\tformat.setDepthBufferSize(16);\n\tformat.setStencilBufferSize(8);\n\tthis->window->setFormat(format);\n\n\tthis->window->installEventFilter(this->event_handler.get());\n\n\tthis->window->show();\n\tlog::log(MSG(info) << \"Created Qt window with Vulkan instance.\");\n}\n\nVlkWindow::~VlkWindow() {\n#ifndef NDEBUG\n\tthis->loader.vkDestroyDebugReportCallbackEXT(this->instance, this->debug_callback, nullptr);\n#endif\n\tvkDestroySurfaceKHR(this->instance, this->surface, nullptr);\n\tvkDestroyInstance(this->instance, nullptr);\n}\n\n\nVkInstance VlkWindow::get_instance() const {\n\treturn this->instance;\n}\n\n\nVkSurfaceKHR VlkWindow::get_surface() const {\n\treturn this->surface;\n}\n\n} // namespace openage::renderer::vulkan\n"
  },
  {
    "path": "libopenage/renderer/vulkan/windowvk.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstring>\n#include <optional>\n#include <set>\n\n#include <vulkan/vulkan.h>\n\n#include \"../window.h\"\n\n#include \"loader.h\"\n\nQT_FORWARD_DECLARE_CLASS(QWindow)\n\nnamespace openage {\nnamespace renderer {\nnamespace vulkan {\n\nstruct vlk_spec {\n\t/// Names of available layers.\n\tstd::set<std::string> layers;\n\t/// Names of available extensions.\n\t// TODO are these only available when the corresponding layer is active?\n\tstd::set<std::string> extensions;\n};\n\n// TODO dirty hack to graft vk functionality onto window.\n// needs better structure (not inheritance! (?)) for proper support\nclass VlkWindow : public openage::renderer::Window {\npublic:\n\tVlkWindow(const char *title, size_t width, size_t height);\n\t~VlkWindow();\n\n\tVkInstance get_instance() const;\n\tVkSurfaceKHR get_surface() const;\n\nprivate:\n\tvlk_spec capabilities;\n\n\tVkInstance instance;\n\tVkSurfaceKHR surface;\n#ifndef NDEBUG\n\tVkDebugReportCallbackEXT debug_callback;\n#endif\n\tVlkLoader loader;\n};\n\n} // namespace vulkan\n} // namespace renderer\n} // namespace openage\n"
  },
  {
    "path": "libopenage/renderer/window.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"window.h\"\n\n#include <QWindow>\n\n#include \"opengl/window.h\"\n#include \"window_event_handler.h\"\n\n\nnamespace openage::renderer {\n\nstd::shared_ptr<Window> Window::create(const std::string &title,\n                                       window_settings settings) {\n\t// currently we only have a functional GL window\n\t// TODO: support other renderer windows\n\t//       and add some selection mechanism.\n\treturn std::make_shared<opengl::GlWindow>(title, settings);\n}\n\n\nWindow::Window(size_t width, size_t height) :\n\tsize{width, height}, event_handler{std::make_shared<WindowEventHandler>()} {}\n\n\nconst util::Vector2s &Window::get_size() const {\n\treturn this->size;\n}\n\ndouble Window::get_scale() const {\n\treturn this->scale_dpr;\n}\n\nbool Window::should_close() const {\n\treturn this->should_be_closed;\n}\n\nvoid Window::add_mouse_button_callback(const mouse_button_cb_t &cb) {\n\tthis->on_mouse_button.push_back(cb);\n}\n\nvoid Window::add_mouse_move_callback(const mouse_move_cb_t &cb) {\n\tthis->on_mouse_move.push_back(cb);\n}\n\nvoid Window::add_mouse_wheel_callback(const mouse_wheel_cb_t &cb) {\n\tthis->on_mouse_wheel.push_back(cb);\n}\n\nvoid Window::add_key_callback(const key_cb_t &cb) {\n\tthis->on_key.push_back(cb);\n}\n\nvoid Window::add_resize_callback(const resize_cb_t &cb) {\n\tthis->on_resize.push_back(cb);\n}\n\nconst std::shared_ptr<QWindow> &Window::get_qt_window() const {\n\treturn this->window;\n}\n\nvoid Window::close() {\n\tthis->window->close();\n}\n\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/window.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <vector>\n\n#include <QObject>\n\n#include \"renderer/renderer.h\"\n#include \"renderer/types.h\"\n#include \"util/vector.h\"\n\nQT_FORWARD_DECLARE_CLASS(QWindow)\nQT_FORWARD_DECLARE_CLASS(QKeyEvent)\nQT_FORWARD_DECLARE_CLASS(QMouseEvent)\nQT_FORWARD_DECLARE_CLASS(QWheelEvent)\n\nnamespace openage::renderer {\n\nclass WindowEventHandler;\n\n/**\n * Modes for window display.\n */\nenum class window_mode {\n\tFULLSCREEN,\n\tBORDERLESS,\n\tWINDOWED\n};\n\n/**\n * Settings for creating a window.\n */\nstruct window_settings {\n\t// Width of the window in pixels.\n\tsize_t width = 1024;\n\t// Height of the window in pixels.\n\tsize_t height = 768;\n\t// Graphics API to use in the window's renderer.\n\tgraphics_api_t backend = graphics_api_t::DEFAULT;\n\t// If true, enable vsync.\n\tbool vsync = true;\n\t// If true, enable debug logging for the selected backend.\n\tbool debug = false;\n\t// Display mode for the window.\n\twindow_mode mode = window_mode::WINDOWED;\n};\n\n\n/**\n * Represents a window that can be used to display graphics.\n */\nclass Window {\npublic:\n\t/**\n\t * Create a new Window instance for displaying stuff.\n\t *\n\t * @param title Window title shown in the Desktop Environment.\n\t * @param settings Settings for creating the window.\n\t *\n\t * @return The created Window instance.\n\t */\n\tstatic std::shared_ptr<Window> create(const std::string &title,\n\t                                      window_settings settings = {});\n\n\tvirtual ~Window() = default;\n\n\t/**\n\t * Get the dimensions of this window.\n\t *\n\t * @return (width, height) as a size-2 vector.\n\t */\n\tconst util::Vector2s &get_size() const;\n\n\t/**\n\t * Get the scaling factor of the window.\n\t *\n\t * @return Scaling factor.\n\t */\n\tdouble get_scale() const;\n\n\t/**\n\t * Returns \\p true if this window should be closed.\n\t *\n\t * @return true if the window should close, else false.\n\t */\n\tbool should_close() const;\n\n\tusing key_cb_t = std::function<void(const QKeyEvent &)>;\n\tusing mouse_button_cb_t = std::function<void(const QMouseEvent &)>;\n\tusing mouse_move_cb_t = std::function<void(const QMouseEvent &)>;\n\tusing mouse_wheel_cb_t = std::function<void(const QWheelEvent &)>;\n\tusing resize_cb_t = std::function<void(size_t, size_t, double)>;\n\n\t/**\n\t * Register a function that executes when a key is pressed.\n\t *\n\t * @param cb Callback function.\n\t */\n\tvoid add_key_callback(const key_cb_t &cb);\n\n\t/**\n\t * Register a function that executes when a mouse button is pressed.\n\t *\n\t * @param cb Callback function.\n\t */\n\tvoid add_mouse_button_callback(const mouse_button_cb_t &cb);\n\n\t/**\n\t * Register a function that executes when the mouse is moved.\n\t *\n\t * @param cb Callback function.\n\t */\n\tvoid add_mouse_move_callback(const mouse_move_cb_t &cb);\n\n\t/**\n\t * Register a function that executes when a mouse wheel action is used.\n\t *\n\t * @param cb Callback function.\n\t */\n\tvoid add_mouse_wheel_callback(const mouse_wheel_cb_t &cb);\n\n\t/**\n\t * Register a function that executes when the window is resized.\n\t *\n\t * @param cb Callback function.\n\t */\n\tvoid add_resize_callback(const resize_cb_t &cb);\n\n\t/**\n\t * Get the underlying QWindow that is used for drawing.\n\t *\n\t * @return Pointer to the QWindow.\n\t */\n\tconst std::shared_ptr<QWindow> &get_qt_window() const;\n\n\t/**\n\t * Force this window to the given size. It's generally not a good idea to use this,\n\t * as it makes the window jump around wierdly.\n\t *\n\t * @param width Width in pixels.\n\t * @param height Height in pixels.\n\t */\n\tvirtual void set_size(size_t width, size_t height) = 0;\n\n\t/**\n\t * Polls for window events, calls callbacks for these events, swaps front and back framebuffers\n\t * to present graphics onto screen. This has to be called at the end of every graphics frame.\n\t */\n\tvirtual void update() = 0;\n\n\t/**\n\t * Creates a renderer which uses the window's graphics API and targets the window.\n\t *\n\t * @return The created Renderer instance.\n\t */\n\tvirtual std::shared_ptr<Renderer> make_renderer() = 0;\n\n\t/**\n\t * Instruct the Window widget to disappear.\n\t */\n\tvoid close();\n\nprotected:\n\tWindow(size_t width, size_t height);\n\n\t/**\n\t * Determines if the window should be closed.\n\t */\n\tbool should_be_closed = false;\n\n\t/**\n\t * Current size of the window (in pixels).\n\t */\n\tutil::Vector2s size;\n\n\t/**\n\t * Scaling factor for the window size (also known as \"device pixel ratio\"\n\t * in Qt). Used if OS-level high DPI/fractional scaling is applied.\n\t */\n\tdouble scale_dpr = 1.0;\n\n\t/**\n\t * Callbacks for key presses.\n\t */\n\tstd::vector<key_cb_t> on_key;\n\n\t/**\n\t * Callbacks for mouse button presses.\n\t */\n\tstd::vector<mouse_button_cb_t> on_mouse_button;\n\n\t/**\n\t * Callbacks for mouse move actions.\n\t */\n\tstd::vector<mouse_move_cb_t> on_mouse_move;\n\n\t/**\n\t * Callbacks for mouse wheel actions.\n\t */\n\tstd::vector<mouse_wheel_cb_t> on_mouse_wheel;\n\n\t/**\n\t * Callbacks for resize actions.\n\t */\n\tstd::vector<resize_cb_t> on_resize;\n\n\t/**\n\t * Main Qt window handle.\n\t */\n\tstd::shared_ptr<QWindow> window;\n\n\t/**\n\t * Observes and filters events from the Qt window.\n\t * Gets attached to window in the window subclasses.\n\t */\n\tstd::shared_ptr<WindowEventHandler> event_handler;\n};\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/window_event_handler.cpp",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#include \"window_event_handler.h\"\n\n#include <QEvent>\n\n#include \"log/log.h\"\n\n\nnamespace openage::renderer {\n\n\nbool WindowEventHandler::eventFilter(QObject * /* obj */, QEvent *event) {\n\tauto add_to_queue = [this, event]() {\n\t\tthis->event_queue.push_back(std::shared_ptr<QEvent>(event->clone()));\n\t};\n\n\tswitch (event->type()) {\n\tcase QEvent::KeyPress:\n\tcase QEvent::KeyRelease:\n\tcase QEvent::MouseButtonDblClick:\n\tcase QEvent::MouseButtonPress:\n\tcase QEvent::MouseButtonRelease:\n\tcase QEvent::MouseMove:\n\tcase QEvent::Resize:\n\tcase QEvent::Wheel:\n\t\tadd_to_queue();\n\t\t// pass on the event\n\t\treturn false;\n\tcase QEvent::Close:\n\t\tadd_to_queue();\n\t\t// filter out the event\n\t\treturn true;\n\n\tdefault:\n\t\tbreak;\n\t}\n\n\t// true: event is filtered = ignored\n\t// false: event is forwarded to its registered qt handler\n\treturn false;\n}\n\n\nstd::vector<std::shared_ptr<QEvent>> WindowEventHandler::pop_events() {\n\tauto ret = this->event_queue;\n\tthis->event_queue.clear();\n\treturn ret;\n}\n\n\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/renderer/window_event_handler.h",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <QObject>\n\n\nnamespace openage::renderer {\n\n/**\n * Filter events of a Qt window so they can be processed by the\n * openage renderer.\n */\nclass WindowEventHandler : public QObject {\n\tQ_OBJECT\n\npublic:\n\t/**\n\t * Create a new event handler for filtering window events. After creation,\n\t * it has to be attached to a QWindow with the installEventFilter() method.\n\t *\n\t */\n\tWindowEventHandler() = default;\n\t~WindowEventHandler() = default;\n\n\t/**\n\t * Filter events of the window the filter is attached to and store them in the\n\t * object's event queue.\n\t *\n\t * We filter these types of events:\n\t *     - Key presses\n\t *     - Mouse button presses\n\t *     - Mouse moves\n\t *     - Window resize\n\t *     - Window close events\n\t *\n\t * @param object Window that received the event.\n\t * @param event Received event.\n\t *\n\t * @return false if the event is forwarded to the window, true otherwise.\n\t */\n\tbool eventFilter(QObject *object, QEvent *event) override;\n\n\t/**\n\t * Fetch all collected events and clear the queue.\n\t *\n\t * @return List of filtered events.\n\t */\n\tstd::vector<std::shared_ptr<QEvent>> pop_events();\n\nprotected:\n\t/**\n\t * Event queue for window events.\n\t */\n\tstd::vector<std::shared_ptr<QEvent>> event_queue;\n};\n} // namespace openage::renderer\n"
  },
  {
    "path": "libopenage/rng/CMakeLists.txt",
    "content": "add_sources(libopenage\n\trng.cpp\n\tglobal_rng.cpp\n\trng_tests.cpp\n)\n"
  },
  {
    "path": "libopenage/rng/global_rng.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n#include \"rng.h\"\n#include \"global_rng.h\"\n\n#include <mutex>\n\nnamespace openage {\nnamespace rng {\n\nstatic std::mutex rand_mutex;\nstatic RNG global_rand_gen(global_seed());\n\nuint64_t global_seed() {\n\tstatic const uint64_t seed = random_seed();\n\treturn seed;\n}\n\nuint64_t random() {\n\tstd::lock_guard<std::mutex> rand_lock(rand_mutex);\n\treturn global_rand_gen.random();\n}\n\nuint64_t random_range(uint64_t lower, uint64_t upper) {\n\tstd::lock_guard<std::mutex> rand_lock(rand_mutex);\n\treturn global_rand_gen.random_range(lower, upper);\n}\n\ndouble real() {\n\tstd::lock_guard<std::mutex> rand_lock(rand_mutex);\n\treturn global_rand_gen.real();\n}\n\ndouble real_range(double lower, double upper) {\n\tstd::lock_guard<std::mutex> rand_lock(rand_mutex);\n\treturn global_rand_gen.real_range(lower, upper);\n}\n\n}} // rng::openage\n"
  },
  {
    "path": "libopenage/rng/global_rng.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n\n/** @file\n * This file contains functions for the global random number generator.\n *\n * Please avoid using it, instead, create rng objects for your current\n * class as members.\n */\n\n\nnamespace openage {\nnamespace rng {\n\n/**  Returns the value of the global seed. */\nuint64_t global_seed();\n\n/** Returns a random number from the global rng. */\nuint64_t random();\n\n/** Returns a random number n [lower, upper) from the global rng. */\nuint64_t random_range(uint64_t lower, uint64_t upper);\n\n/** Returns a double in [0, 1) from the global rng. */\ndouble real();\n\n/** Returns a double in [lower, upper) from the global rng. */\ndouble real_range(double lower, double upper);\n\n} // namespace rng\n} // namespace openage\n"
  },
  {
    "path": "libopenage/rng/rng.cpp",
    "content": "// Copyright 2015-2020 the openage authors. See copying.md for legal info.\n\n#include \"rng.h\"\n\n#include <random>\n#include <sstream>\n#include <array>\n\n#include \"../util/timing.h\"\n#include \"../util/hash.h\"\n#include \"../error/error.h\"\n\n/**\n * Contains an easy to use and fast random number generator.\n *\n * This was never designed or intended as a cryptographic-quality,\n * RNG, so don't use it as one. If you do, I'll tell my mom.\n */\n\nnamespace openage::rng {\n\n\n// Key for seed hashing, just some hardcoded key\nstatic const std::array<uint8_t, 16> seed_key {{\n\t0xba, 0xda, 0x55, 0x00, 0x5e, 0xed, 0x00, 0x00,\n\t0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90\n}};\n\n\nRNG::RNG(uint64_t v1) {\n\tthis->seed(v1);\n}\n\nRNG::RNG(const void *data, size_t len) {\n\tthis->seed(data, len);\n}\n\n\nRNG::RNG(const std::string &instr) {\n\tthis->from_string(instr);\n}\n\n\nRNG::RNG(std::istream &instream) {\n\tthis->from_stream(instream);\n}\n\n\nvoid RNG::seed(uint64_t val) {\n\tthis->seed(&val, sizeof(val));\n}\n\n\nvoid RNG::seed(const void *dat, size_t count) {\n\tif (count == 0) {\n\t\tthrow Error(MSG(err) << \"0 bytes supplied as seed data\");\n\t}\n\n\tconst auto *data = static_cast<const uint8_t *>(dat);\n\n\topenage::util::Siphash siphash(seed_key);\n\n\tstate[0] = siphash.digest(data, count);\n\t// Previously, for state[1] we hashed (data ^ state[0])\n\t// As `data` now has variable length I can't do that, I think that just\n\t// hashing `state[0]` is enough\n\tstate[1] = siphash.digest(state[0]);\n\n\t// This helps prevent low-zero states caused by a bad seed\n\tthis->discard(discard_on_seed);\n}\n\n\n/*\n * actually does the number crunching and state updates\n * http://en.wikipedia.org/wiki/Xorshift\n */\nstatic inline uint64_t do_rng(uint64_t &s0, uint64_t &s1) {\n\ts1 ^= s1 << 23;\n\ts1 = (s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26));\n\treturn s1 + s0;\n}\n\n\nuint64_t RNG::random() {\n\tstd::swap(this->state[0], this->state[1]);\n\treturn do_rng(this->state[0], this->state[1]);\n}\n\n\n/*\n * Provides serious performance optimizations over the standard\n * generator by eliminating memory writes and data dependencies.\n */\ntemplate<class T, class Lambda>\nstatic inline void act_fill(T *dat, uint64_t *state,\n                            size_t len, const Lambda &op) {\n\n\tuint64_t s0 = state[0];\n\tuint64_t s1 = state[1];\n\tsize_t ev_len = len - (len & 1);\n\n\tfor (size_t i = 0; i < ev_len; i += 2) {\n\t\top(do_rng(s1, s0), dat, i);\n\t\top(do_rng(s0, s1), dat, i + 1);\n\t}\n\tif (len & 1) {\n\t\top(do_rng(s1, s0), dat, len-1);\n\t\tstd::swap(s1, s0);\n\t}\n\tstate[0] = s0;\n\tstate[1] = s1;\n}\n\n\nvoid RNG::fill(uint64_t *dat, size_t len) {\n\tact_fill<>(\n\t\tdat, this->state, len,\n\t\t[](uint64_t v, uint64_t *d, size_t i) {\n\t\t\td[i] = v;\n\t\t}\n\t);\n}\n\n\nvoid RNG::discard(size_t len) {\n\tact_fill<uint64_t>(\n\t\tnullptr, this->state, len,\n\t\t[](uint64_t, uint64_t *, size_t) {}\n\t);\n}\n\n\nvoid RNG::fill_real(double *dat, size_t len) {\n\tact_fill(\n\t\tdat, this->state, len,\n\t\t[](double v, double *d, size_t i) {\n\t\t\td[i] = v / (double) RNG::max();\n\t\t}\n\t);\n}\n\n\nstd::ostream &RNG::to_stream(std::ostream &out) const {\n\tif ((out << this->state[0] << \" \").good() &&\n\t    !(out << this->state[1]).fail()) {\n\t\treturn out;\n\t}\n\tthrow Error(MSG(err) << \"Unable to write rng state serialization to output stream\");\n}\n\n\nstd::istream &RNG::from_stream(std::istream &in) {\n\tif ((in >> this->state[0]).good() &&\n\t    !(in >> this->state[1]).fail()) {\n\t\treturn in;\n\t}\n\tthrow Error(MSG(err) << \"Unable to read rng state serialization from input stream\");\n}\n\n\nstd::string RNG::to_string() const {\n\tutil::FString fs;\n\tfs << *this;\n\treturn fs;\n}\n\n\nvoid RNG::from_string(const std::string &instr) {\n\tstd::stringstream strstr(instr);\n\tstrstr >> *this;\n}\n\n\nstd::ostream &operator <<(std::ostream &ostream, const RNG &inrng) {\n\tinrng.to_stream(ostream);\n\treturn ostream;\n}\n\n\nstd::istream &operator >>(std::istream &instream, RNG &inrng) {\n\tinrng.from_stream(instream);\n\treturn instream;\n}\n\n\nstatic uint64_t try_random_device() {\n\tuint64_t rand1 = 0;\n\t// std::random_device constructor may throw if it isn't available\n\ttry {\n\t\tstd::random_device rnd;\n\t\tif (rnd.entropy() > 0) {\n\t\t\trand1 = rnd() + (uint64_t(rnd()) << 32);\n\t\t}\n\t}\n\tcatch (std::exception &e) {\n\t\tthrow Error(MSG(err) << \"failed using std::random_device: \" << e.what());\n\t}\n\treturn rand1;\n}\n\n\nstatic uint64_t time_seed(bool randomize) {\n\tuint64_t nanos = timing::get_real_time();\n\n\tif (randomize) {\n\t\tnanos ^= try_random_device();\n\t}\n\treturn nanos;\n}\n\n\nuint64_t time_seed() {\n\treturn time_seed(false);\n}\n\n\nuint64_t random_seed() {\n\treturn time_seed(true);\n}\n\n\n} // namespace openage::rng\n"
  },
  {
    "path": "libopenage/rng/rng.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n#include <iostream>\n#include <string>\n\nnamespace openage::rng {\n\n\n/** \\class RNG\n * This class holds an xorshift128+ rng, as well as\n * utility generator functions\n */\nclass RNG {\npublic:\n\t/**\n\t * Creates an rng and seeds it with the 64 bit seed\n\t * @param seed The inital seed for the following number generation.\n\t */\n\texplicit RNG(uint64_t seed);\n\n\t/**\n\t * Initializes the rng using data from the buffer pointed to by data\n\t * @param data The buffer that contains data for seeding the rng\n\t * @param count The number of bytes in the buffer\n\t * @throws Error if 0 bytes are passed\n\t */\n\t[[maybe_unused]] RNG(const void *data, size_t count);\n\n\n\t/**\n\t * Reads the rng in from the passed string. If you want to\n\t * use the data in the string as a seed, then pass the c_str() to\n\t * rng(const void*, size_t).\n\t * @param instr The string from which the rng is serialized\n\t * @throws Error if the rng cannot be read from the string\n\t */\n\texplicit RNG(const std::string &instr);\n\n\n\t/**\n\t * Reads the rng in from the passed std::istream. This serializes\n\t * the rng, and does not use the data as a random seed.\n\t * @param instream The input stream for serializing the rng\n\t * @throws Error if stream initialization fails\n\t */\n\texplicit RNG(std::istream &instream);\n\n\n\t/**\n\t * Seeds with the 64 bit seed\n\t */\n\tvoid seed(uint64_t val);\n\n\n\t/**\n\t * Initializes the rng using bits from the buffer pointed to by data\n\t * @param data The buffer that contains data for seeding the rng\n\t * @param count The number of bytes in the buffer\n\t * @throws Error if 0 bytes are passed\n\t */\n\tvoid seed(const void *data, size_t count);\n\n\n\t/**\n\t * Retrieves a random value from the generator\n\t */\n\tuint64_t random();\n\n\n\t/**\n\t * Retrieves a random value from the generator\n\t */\n\tinline uint64_t operator()() {\n\t\treturn this->random();\n\t}\n\n\n\t/**\n\t * Returns a random integer in the range [lower, upper]\n\t */\n\tinline uint64_t random_range(uint64_t lower, uint64_t upper) {\n\t\treturn (this->random() % (upper - lower)) + lower;\n\t}\n\n\n\t/**\n\t * Returns true with the passed probability\n\t */\n\tinline bool probability(double prob) {\n\t\treturn this->random() < (RNG::max() * prob);\n\t}\n\n\n\t/**\n\t * Retrieves a double in [0, 1)\n\t */\n\tinline double real() {\n\t\treturn static_cast<double>(this->random()) / RNG::max();\n\t}\n\n\n\t/**\n\t * Returns a double in the range [min, max)\n\t */\n\tinline double real_range(double lower, double upper) {\n\t\treturn this->real() * (upper - lower) + lower;\n\t}\n\n\n\t/**\n\t * Effeciently fills a chunk of 64 bit integers.\n\t * Gives identical result to calling random() len times\n\t * @param data The buffer to be filled with random numbers\n\t * @param len the length of the data\n\t */\n\tvoid fill(uint64_t *data, size_t len);\n\n\n\t/**\n\t * Fills an array of doubles with values in [0, 1)\n\t * Gives identical results as real() but more effeciently\n\t * @param data A pointer t0 the buffer of doubles\n\t * @param len The length of the buffer\n\t */\n\tvoid fill_real(double *data, size_t len);\n\n\n\t/**\n\t * Discards num_discard numbers from the generator.\n\t */\n\tvoid discard(size_t num_discard);\n\n\n\t/**\n\t * Outputs the rng state to a stream\n\t * @throws Error if writing data fails\n\t */\n\tstd::ostream &to_stream(std::ostream &out) const;\n\n\n\t/**\n\t * Reads the rng state from a stream\n\t * @throws Error if reading the stream fails\n\t */\n\tstd::istream &from_stream(std::istream &in);\n\n\n\t/**\n\t * Writes the rng state to a string\n\t */\n\t[[nodiscard]] std::string to_string() const;\n\n\n\t/**\n\t * Reads the rng in from a string\n\t * @throws Error if reading from the string fails\n\t */\n\tvoid from_string(const std::string &instr);\n\n\n\tstatic constexpr uint64_t max() {\n\t\treturn UINT64_MAX;\n\t}\n\n\n\tstatic constexpr uint64_t min() {\n\t\treturn 0;\n\t}\n\n\nprivate:\n\t/**\n\t * The internal state array\n\t */\n\tuint64_t state[2]{};\n\n\n\t/**\n\t * elements to discard on seed to escape from high-zero seeds\n\t */\n\tconstexpr static size_t discard_on_seed = 50;\n};\n\n\n/**\n * Reads the rng from a stream\n * @throws Error if reading from the stream fails\n */\nstd::istream &operator>>(std::istream &instream, RNG &inrng);\n\n/**\n * Writes the rng state to a stream\n * @throws Error if writing data fails\n */\nstd::ostream &operator<<(std::ostream &ostream, const RNG &inrng);\n\n\n/**\n * Returns a seed based on time since epoch\n */\nuint64_t time_seed();\n\n\n/**\n * Returns a seed based on time since epoch\n * as well as data from std::random_device if available\n */\nuint64_t random_seed();\n\n\n} // namespace openage::rng\n"
  },
  {
    "path": "libopenage/rng/rng_tests.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"rng.h\"\n\n#include \"../log/log.h\"\n#include \"../error/error.h\"\n#include \"../testing/testing.h\"\n\n#include <limits>\n#include <vector>\n#include <cmath>\n\nnamespace openage::rng::tests {\n\n/**\n * This is the number of generation repetitions\n */\nconstexpr size_t num_rand = (1 << 18);\n\n/**\n * Allow the result to be in this range of the expected\n * perfect distribution.\n */\nconstexpr float difference_factor = 0.1;\n\n\n/**\n * Tests the distribution of the bit generator.\n *\n * We generate many many random numbers, and select a bucket by it.\n * After n random numbers, each bucket should have been selected\n * n/bucket_count times.\n */\nvoid freq_dist() {\n\t// one random sample from the generator.\n\tuint64_t sample;\n\n\t// number of possible chars, likely 256.\n\t// this is bucket_count.\n\tconstexpr size_t dsize = std::numeric_limits<unsigned char>::max() + 1;\n\n\t// each bucket should be selected n/bucket_count times.\n\t// each sample contains multiple chars.\n\tconstexpr float mean = (sizeof(sample) * num_rand) / dsize;\n\n\t// maximum difference from the expected mean.\n\tconstexpr float max_diff = mean * difference_factor;\n\n\t// holds one entry for each char bit.\n\tsize_t buckets[dsize];\n\n\tfor (auto &dat : buckets) {\n\t\tdat = 0;\n\t}\n\n\tRNG myrng{time_seed()};\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\tsample = myrng();  // get a random value\n\t\tauto *valptr = reinterpret_cast<unsigned char*>(&sample);\n\n\t\t// for each bit of the generated value:\n\t\t// increment the \"weight\" of the bit.\n\t\tfor (size_t j = 0; j < sizeof(sample); j++) {\n\t\t\t// randomly increment one of the buckets\n\t\t\tunsigned char rbyte = valptr[j];\n\t\t\tbuckets[rbyte] += 1;\n\t\t}\n\t}\n\n\t// test if each bucket was selected the amount we expected.\n\tfor (auto count : buckets) {\n\t\tstd::abs(mean - count) > max_diff and TESTFAIL;\n\t}\n}\n\n/**\n * Tests the distribution of the boolean generation.\n *\n * For n draws it should result in n/2 trues and n/2 falses.\n */\nvoid bool_dist() {\n\tconstexpr float expected = num_rand / 2;\n\tconstexpr float max_diff = expected * difference_factor;\n\n\tRNG mybool{random_seed()};\n\tsize_t num_true = 0;\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\tif (mybool() & 1) {\n\t\t\tnum_true += 1;\n\t\t}\n\t}\n\n\tstd::abs(num_true - expected) > max_diff and TESTFAIL;\n}\n\n/**\n * Tests the distribution of the floating point numbers.\n *\n * Perform the test by placing the [0, 1] range into buckets.\n * each bucket should be drawn n/bucket_count times.\n *\n * Also, the sum of all numbers drawn should be n/2.\n */\nvoid real_dist() {\n\tconstexpr size_t bckt_cnt    = num_rand >> 11;\n\tconstexpr float  mean        = num_rand / bckt_cnt;\n\tconstexpr float  max_diff    = mean * difference_factor;\n\tconstexpr float  max_sumdiff = (num_rand/2) * difference_factor;\n\tssize_t          buckets[bckt_cnt+1];\n\n\tfor (auto &dat : buckets) {\n\t\tdat = 0;\n\t}\n\n\tdouble sum = 0;\n\n\tRNG myrng{time_seed()};\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\tdouble val = myrng.real();\n\t\tsum += val;\n\t\tbuckets[static_cast<size_t>(val * bckt_cnt)] += 1;\n\t}\n\n\tstd::abs(sum - (num_rand / 2)) > max_sumdiff and TESTFAIL;\n\n\tfor (size_t i = 0; i < bckt_cnt; i++) {\n\t\tstd::abs(mean - buckets[i]) > max_diff and TESTFAIL;\n\t}\n\n\tbuckets[bckt_cnt] and TESTFAIL;\n}\n\n\n\n/**\n * Tests reproducibility.\n */\nvoid reproduce() {\n\tauto val = time_seed();\n\tRNG test0{val};\n\tRNG test1{val};\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n\n\t// seed both rngs\n\tchar initstr[] = \"abcdefghijk\";\n\ttest0.seed(initstr, sizeof(initstr));\n\ttest1.seed(initstr, sizeof(initstr));\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n\n\t// same result expected, as we only use the first 3 chars as seed.\n\tchar initstr2[] = \"abcqq\";\n\ttest0.seed(initstr, 3);\n\ttest1.seed(initstr2, 3);\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n\n\t// make sure that seeding with nullptr fails.\n\tTESTTHROWS(test0.seed(nullptr, 0));\n}\n\n/**\n * Tests the serializing functionality.\n */\nvoid serialize() {\n\t// create seeded rng\n\t// and duplicate state to another one.\n\tRNG test0{random_seed()};\n\tRNG test1{test0.to_string()};\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n\n\tTESTTHROWS(test0.from_string(\"100 aa\"));\n\tTESTTHROWS(test0.from_string(\"\"));\n\tTESTTHROWS(test0.from_string(\"100\"));\n\tTESTTHROWS(test0.from_string(\"100 \"));\n}\n\n\n/**\n * Tests filling integers.\n */\nvoid fill() {\n\tconstexpr size_t n = 1 << 7;\n\tuint64_t data[n];\n\n\tRNG test0{random_seed()};\n\tRNG test1{test0};\n\n\ttest0.fill(data, n);\n\n\tfor (size_t i = 0; i < n; i++) {\n\t\ttest1() == data[i] or TESTFAIL;\n\t}\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n}\n\n\n/**\n * Tests filling doubles.\n */\nvoid fill_real() {\n\tconstexpr size_t n = 1 << 7;\n\tRNG test0{time_seed()};\n\tRNG test1{test0};\n\tdouble data[n];\n\n\ttest1.fill_real(data, n);\n\tfor (size_t i = 0; i < n; i++) {\n\t\ttest0.real() == data[i] or TESTFAIL;\n\t}\n\n\tfor (size_t i = 0; i < num_rand; i++) {\n\t\ttest0() == test1() or TESTFAIL;\n\t}\n}\n\n\nvoid run() {\n\tfreq_dist();\n\tbool_dist();\n\treal_dist();\n\treproduce();\n\tfill();\n\tfill_real();\n}\n\n\n} // namespace openage::rng::tests\n"
  },
  {
    "path": "libopenage/testing/CMakeLists.txt",
    "content": "add_sources(libopenage\n\ttesting.cpp\n\tbenchmark_test.cpp\n)\n\npxdgen(\n\ttestlist.h\n)\n"
  },
  {
    "path": "libopenage/testing/benchmark_test.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include <chrono>\n#include <thread>\n\nnamespace openage {\nnamespace test {\n\nvoid benchmark() {\n\tusing namespace std::chrono_literals;\n\tstd::this_thread::sleep_for(1ms);\n}\n\n}} // openage::test\n"
  },
  {
    "path": "libopenage/testing/testing.cpp",
    "content": "// Copyright 2014-2017 the openage authors. See copying.md for legal info.\n\n#include \"testing.h\"\n\nnamespace openage {\nnamespace testing {\n\n\nbool fail(const log::message &msg) {\n\tthrow TestError{msg};\n}\n\n\n}} // opeange::testing\n"
  },
  {
    "path": "libopenage/testing/testing.h",
    "content": "// Copyright 2014-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"../error/error.h\"\n\nnamespace openage {\nnamespace testing {\n\n\n/**\n * To be used by all tests to indicate an error condition.\n *\n * An optional error message may be passed as part of the message,\n * but the file name and line number are of primary concern.\n */\nclass TestError : public Error {\n\tusing Error::Error;\n};\n\n\n/**\n * Raises TestError with the message msg.\n * Mostly designed for usage with the TESTFAIL macro.\n */\nbool fail(const log::message &msg);\n\n\n/**\n * Convenience macro, for usage in test functions:\n *\n * test_stuff() or TESTFAIL;\n */\n#define TESTFAIL ::openage::testing::fail(MSG(err))\n\n\n/**\n * As above, but allows printing a message:\n *\n * test_stuff() or TESTFAIL(\"lolnope\");\n */\n#define TESTFAILMSG(...) ::openage::testing::fail(MSG(err) << __VA_ARGS__)\n\n\n/**\n * Asserts that the left expression equals the right expression,\n * and that no exception is thrown.\n */\n#define TESTEQUALS(left, right) \\\n\tdo { \\\n\t\ttry { \\\n\t\t\tauto &&test_result_left = (left); \\\n\t\t\tif (test_result_left != (right)) { \\\n\t\t\t\tTESTFAILMSG(__FILE__ << \":\" << __LINE__ << \": Expected \" \\\n\t\t\t\t                     << test_result_left << \" and \" \\\n\t\t\t\t                     << (right) << \" to be equal\"); \\\n\t\t\t} \\\n\t\t} \\\n\t\tcatch (::openage::testing::TestError & e) { \\\n\t\t\tthrow; \\\n\t\t} \\\n\t\tcatch (::openage::error::Error & e) { \\\n\t\t\tTESTFAILMSG(\"unexpected exception: \" << e); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n/**\n * Asserts that the left expression does not equal the right expression,\n * and that no exception is thrown.\n */\n#define TESTNOTEQUALS(left, right) \\\n\tdo { \\\n\t\ttry { \\\n\t\t\tauto &&test_result_left = (left); \\\n\t\t\tif (test_result_left == (right)) { \\\n\t\t\t\tTESTFAILMSG(__FILE__ << \":\" << __LINE__ << \": Expected \" \\\n\t\t\t\t                     << test_result_left << \" and \" \\\n\t\t\t\t                     << (right) << \" to be NOT equal\"); \\\n\t\t\t} \\\n\t\t} \\\n\t\tcatch (::openage::testing::TestError & e) { \\\n\t\t\tthrow; \\\n\t\t} \\\n\t\tcatch (::openage::error::Error & e) { \\\n\t\t\tTESTFAILMSG(\"unexpected exception: \" << e); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n/**\n * Asserts that the left expression equals the right expression,\n * within a margin of error epsilon, and that no exception is thrown.\n */\n#define TESTEQUALS_FLOAT(left, right, epsilon) \\\n\tdo { \\\n\t\ttry { \\\n\t\t\tauto &&test_result_left = (left); \\\n\t\t\tauto &&test_result_right = (right); \\\n\t\t\tif ((test_result_left < (test_result_right - epsilon)) or (test_result_left > (test_result_right + epsilon))) { \\\n\t\t\t\tTESTFAILMSG(__FILE__ << \":\" << __LINE__ << \": Expected \" << (test_result_left) << \" and \" << (test_result_right) << \" to be equal\"); \\\n\t\t\t} \\\n\t\t} \\\n\t\tcatch (::openage::testing::TestError & e) { \\\n\t\t\tthrow; \\\n\t\t} \\\n\t\tcatch (::openage::error::Error & e) { \\\n\t\t\tTESTFAILMSG(\"unexpected exception: \" << e); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n\n/**\n * Asserts that the expression throws an exception.\n */\n#define TESTTHROWS(expression) \\\n\tdo { \\\n\t\tbool expr_has_thrown = false; \\\n\t\ttry { \\\n\t\t\texpression; \\\n\t\t} \\\n\t\tcatch (::openage::error::Error & e) { \\\n\t\t\texpr_has_thrown = true; \\\n\t\t} \\\n\t\tif (not expr_has_thrown) { \\\n\t\t\tTESTFAILMSG(__FILE__ << \":\" << __LINE__ << \": no exception\"); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n\n/**\n * Assets that the expression throws no exception (mostly for void expressions).\n */\n#define TESTNOEXCEPT(expression) \\\n\tdo { \\\n\t\ttry { \\\n\t\t\texpression; \\\n\t\t} \\\n\t\tcatch (::openage::error::Error & e) { \\\n\t\t\tTESTFAILMSG(__FILE__ << \":\" << __LINE__ << \": unexpected exception\"); \\\n\t\t} \\\n\t} \\\n\twhile (0)\n\n\n} // namespace testing\n} // namespace openage\n"
  },
  {
    "path": "libopenage/testing/testlist.cpp.template",
    "content": "// Copyright 2014-2018 the openage authors. See copying.md for legal info.\n\n#include \"testing/testlist.h\"\n\n#include <map>\n\n// function prototypes in their proper namespaces\nFUNCTION_PROTOTYPES\n\nnamespace openage {\nnamespace testing {\n\n\nstd::map<std::string, void (*)()> method_list = {\n\tMETHOD_MAPPINGS\n};\n\n\nvoid run_method(const std::string &name) {\n\tauto method = method_list.find(name);\n\n\tif (method == method_list.end()) {\n\t\tthrow Error(MSG(err) << \"No such method: \" << name);\n\t}\n\n\tmethod->second();\n}\n\n\n}} // namespace openage::testing\n"
  },
  {
    "path": "libopenage/testing/testlist.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"testing.h\"\n\n\nnamespace openage {\nnamespace testing {\n\n\n/**\n * Invokes the test or demo with the given name.\n *\n * pxd: void run_method(string name) except +\n */\nOAAPI void run_method(const std::string &name);\n\n\n} // namespace testing\n} // namespace openage\n"
  },
  {
    "path": "libopenage/time/CMakeLists.txt",
    "content": "add_sources(libopenage\n    clock.cpp\n    time.cpp\n    time_loop.cpp\n)\n"
  },
  {
    "path": "libopenage/time/clock.cpp",
    "content": "// Copyright 2022-2023 the openage authors. See copying.md for legal info.\n\n#include \"clock.h\"\n\n#include <thread>\n\n#include \"log/log.h\"\n\n\nnamespace openage::time {\n\nClock::Clock() :\n\tstate{ClockState::INIT},\n\tmax_tick_time{50},\n\tspeed{1.0f},\n\tlast_check{simclock_t::now()},\n\tstart_time{simclock_t::now()},\n\tsim_time{0},\n\tsim_real_time{0} {\n}\n\nClockState Clock::get_state() {\n\tstd::shared_lock lock{this->mutex};\n\treturn this->state;\n}\n\nvoid Clock::update_time() {\n\tif (this->state == ClockState::RUNNING) {\n\t\tstd::unique_lock lock{this->mutex};\n\n\t\tauto now = simclock_t::now();\n\t\tauto passed = std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_check);\n\t\tif (passed.count() == 0) {\n\t\t\t// prevent the clock from stalling the thread forever\n\t\t\tstd::this_thread::sleep_for(std::chrono::milliseconds(1));\n\t\t}\n\t\telse if (passed.count() > this->max_tick_time) {\n\t\t\t// if too much real time passes between two time updates, we only advance time by a small amount\n\t\t\t// this prevents the simulation from getting out of control during unplanned stops,\n\t\t\t// e.g. when debugging or if you close your laptop lid\n\t\t\tthis->sim_time += this->speed * this->max_tick_time;\n\t\t\tthis->sim_real_time += this->max_tick_time;\n\t\t}\n\t\telse {\n\t\t\tthis->sim_time += this->speed * passed.count();\n\t\t\tthis->sim_real_time += passed.count();\n\t\t}\n\t\tthis->last_check = now;\n\t\t// TODO: Stop clock if it reaches 0.0s with negative speed?\n\t}\n}\n\ntime::time_t Clock::get_time() {\n\tstd::shared_lock lock{this->mutex};\n\n\t// convert time unit from milliseconds to seconds\n\treturn this->sim_time / 1000;\n}\n\ntime::time_t Clock::get_real_time() {\n\tstd::shared_lock lock{this->mutex};\n\n\t// convert time unit from milliseconds to seconds\n\treturn this->sim_real_time / 1000;\n}\n\nspeed_t Clock::get_speed() {\n\tstd::shared_lock lock{this->mutex};\n\treturn this->speed;\n}\n\nvoid Clock::set_speed(speed_t speed) {\n\tthis->update_time();\n\n\tstd::unique_lock lock{this->mutex};\n\tthis->speed = speed;\n\n\tlog::log(MSG(info) << \"Clock speed set to \" << this->speed);\n}\n\nvoid Clock::start() {\n\tstd::unique_lock lock{this->mutex};\n\n\tauto now = simclock_t::now();\n\tthis->start_time = now;\n\tthis->last_check = now;\n\tthis->state = ClockState::RUNNING;\n}\n\nvoid Clock::stop() {\n\tif (this->state == ClockState::RUNNING) [[likely]] {\n\t\tthis->update_time();\n\t}\n\n\tstd::unique_lock lock{this->mutex};\n\tthis->state = ClockState::STOPPED;\n\n\tlog::log(MSG(info) << \"Clock stopped at \"\n\t                   << this->sim_time << \"ms (simulated) / \"\n\t                   << this->sim_real_time << \"ms (real)\");\n}\n\nvoid Clock::pause() {\n\tif (this->state == ClockState::RUNNING) [[likely]] {\n\t\tthis->update_time();\n\t}\n\n\tstd::unique_lock lock{this->mutex};\n\tthis->state = ClockState::PAUSED;\n\n\tlog::log(MSG(info) << \"Clock paused at \"\n\t                   << this->sim_time << \"ms (simulated) / \"\n\t                   << this->sim_real_time << \"ms (real)\");\n}\n\nvoid Clock::resume() {\n\tstd::unique_lock lock{this->mutex};\n\n\tif (this->state == ClockState::PAUSED) [[likely]] {\n\t\tthis->last_check = simclock_t::now();\n\t\tthis->state = ClockState::RUNNING;\n\t}\n\n\tlog::log(MSG(info) << \"Clock resumed at \"\n\t                   << this->sim_time << \"ms (simulated) / \"\n\t                   << this->sim_real_time << \"ms (real)\");\n}\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/time/clock.h",
    "content": "// Copyright 2022-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <chrono>\n#include <shared_mutex>\n\n#include \"time/time.h\"\n\n\nnamespace openage::time {\n\nusing simclock_t = std::chrono::steady_clock;\nusing timepoint_t = std::chrono::time_point<simclock_t>;\n\nusing speed_t = util::FixedPoint<int64_t, 16>;\n\nusing dt_s_t = std::chrono::duration<long, std::ratio<1>>;\nusing dt_ms_t = std::chrono::duration<long, std::milli>;\n\n\nenum class ClockState {\n\tINIT,\n\tSTOPPED,\n\tPAUSED,\n\tRUNNING\n};\n\n/**\n * Clock for timing simulation events.\n *\n * Time values have a precision of milliseconds which should\n * be accurate enough for all applications.\n */\nclass Clock {\npublic:\n\tClock();\n\t~Clock() = default;\n\n\t/**\n\t * Get the current state of the clock.\n\t */\n\tClockState get_state();\n\n\t/**\n\t * Update the simulation time.\n\t */\n\tvoid update_time();\n\n\t/**\n\t * Get the current simulation time (in seconds).\n\t *\n\t * The returned value has a precision of milliseconds, so it is\n\t * accurate to three decimal places.\n\t *\n\t * @return Time passed (in seconds).\n\t */\n\ttime::time_t get_time();\n\n\t/**\n\t * Get the current simulation time without speed adjustments.\n\t *\n\t * The returned value has a precision of milliseconds, so it is\n\t * accurate to three decimal places.\n\t *\n\t * @return Time passed (in seconds).\n\t */\n\ttime::time_t get_real_time();\n\n\t/**\n\t * Get the current speed of the clock.\n\t *\n\t * @return Speed of the clock.\n\t */\n\tspeed_t get_speed();\n\n\t/**\n\t * Set the speed of the clock.\n\t *\n\t * Simulation time is updated before changing speed.\n\t *\n\t * @param speed New speed of the clock.\n\t */\n\tvoid set_speed(speed_t speed);\n\n\t/**\n\t * Start the simulation timer.\n\t */\n\tvoid start();\n\n\t/**\n\t * Stop the simulation timer.\n\t *\n\t * Indicates that the simulation has ended.\n\t */\n\tvoid stop();\n\n\t/**\n\t * Pause the simulation timer.\n\t *\n\t * Simulation time is updated before stopping.\n\t */\n\tvoid pause();\n\n\t/**\n\t * Resume the simulation timer if the clock is stopped.\n\t */\n\tvoid resume();\n\nprivate:\n\t/**\n\t * Status of the clock (init, running, stopped, ...).\n\t */\n\tClockState state;\n\n\t/**\n\t * Maximum length of a simulation iteration (in milliseconds).\n\t */\n\tuint16_t max_tick_time;\n\n\t/**\n\t * How fast time passes relative to real time.\n\t */\n\tspeed_t speed;\n\n\t/**\n\t * Last point in time where the clock was updated.\n\t */\n\ttimepoint_t last_check;\n\n\t/**\n\t * Start of the simulation in real time.\n\t */\n\ttimepoint_t start_time;\n\n\t/**\n\t * Stores how much time has passed inside the simulation (in milliseconds).\n\t */\n\ttime::time_t sim_time;\n\n\t/**\n\t * Stores how much time has passed inside the simulation (in milliseconds)\n\t * _without_ speed adjustments (i.e. it acts as if speed = 1.0).\n\t */\n\ttime::time_t sim_real_time;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/time/time.cpp",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#include \"time.h\"\n\n\nnamespace openage::time {\n\n// This file is intended to be empty\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/time/time.h",
    "content": "// Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n#include \"util/fixed_point.h\"\n\n\nnamespace openage::time {\n\n/**\n * Defines the type that is used as time index.\n * it has to implement all basic mathematically operations.\n */\nusing time_t = util::FixedPoint<int64_t, 16>;\n\n/**\n * Minimum time value.\n */\nstatic constexpr time_t TIME_MIN = std::numeric_limits<time_t>::min();\n\n/**\n * Maximum time value.\n */\nstatic constexpr time_t TIME_MAX = std::numeric_limits<time_t>::max();\n\n/**\n * Zero time value (start of simulation).\n */\nstatic constexpr time_t TIME_ZERO = time_t::zero();\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/time/time_loop.cpp",
    "content": "// Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n#include \"time_loop.h\"\n\n#include <mutex>\n\n#include \"log/log.h\"\n#include \"time/clock.h\"\n\n\nnamespace openage::time {\n\nTimeLoop::TimeLoop() :\n\trunning{false},\n\tclock{std::make_shared<Clock>()} {}\n\nTimeLoop::TimeLoop(const std::shared_ptr<Clock> clock) :\n\trunning{false},\n\tclock{clock} {}\n\nvoid TimeLoop::run() {\n\tthis->start();\n\twhile (this->running) {\n\t\tthis->clock->update_time();\n\t}\n\tlog::log(MSG(info) << \"Time loop exited\");\n}\n\nvoid TimeLoop::start() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->running = true;\n\n\tif (this->clock->get_state() == ClockState::INIT) {\n\t\tthis->clock->start();\n\t}\n\n\tlog::log(MSG(info) << \"Time loop started\");\n}\n\nvoid TimeLoop::stop() {\n\tstd::unique_lock lock{this->mutex};\n\n\tthis->running = false;\n\n\tlog::log(MSG(info) << \"Time loop stopped\");\n}\n\nconst std::shared_ptr<Clock> TimeLoop::get_clock() {\n\tstd::shared_lock lock{this->mutex};\n\n\treturn this->clock;\n}\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/time/time_loop.h",
    "content": "// Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <memory>\n#include <shared_mutex>\n\n\nnamespace openage::time {\nclass Clock;\n\n/**\n * Manages the passage of simulation time and real time.\n */\nclass TimeLoop {\npublic:\n\t/**\n\t * Create a new time loop with default values.\n\t */\n\tTimeLoop();\n\n\t/**\n\t * Create a new time loop from an existing and clock.\n\t */\n\tTimeLoop(const std::shared_ptr<Clock> clock);\n\t~TimeLoop() = default;\n\n\t/**\n\t * Run the time loop.\n\t *\n\t * Updates the clock and dispatches events that happened.\n\t */\n\tvoid run();\n\n\t/**\n\t * Start the time loop.\n\t */\n\tvoid start();\n\n\t/**\n\t * Exit the time loop.\n\t */\n\tvoid stop();\n\n\t/**\n\t * Get the clock used by this time loop.\n\t *\n\t * @return Simulation clock.\n\t */\n\tconst std::shared_ptr<Clock> get_clock();\n\nprivate:\n\t/**\n\t * State of the time loop.\n\t */\n\tbool running;\n\n\t/**\n\t * Manage time and speed inside the simulation.\n\t */\n\tstd::shared_ptr<Clock> clock;\n\n\t/**\n\t * Mutex for protecting threaded access.\n\t */\n\tstd::shared_mutex mutex;\n};\n\n\n} // namespace openage::time\n"
  },
  {
    "path": "libopenage/util/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tcolor.cpp\n\tcompiler.cpp\n\tconstinit_vector.cpp\n\tenum.cpp\n\tenum_test.cpp\n\texternalprofiler.cpp\n\texternalsstream.cpp\n\tfile.cpp\n\tfds.cpp\n\tfixed_point.cpp\n\tfixed_point_test.cpp\n\tfps.cpp\n\thash.cpp\n\thash_test.cpp\n\tinit.cpp\n\tlanguage.cpp\n\tmatrix.cpp\n\tmatrix_test.cpp\n\tmisc.cpp\n\tmisc_test.cpp\n\tos.cpp\n\tpath.cpp\n\tquaternion.cpp\n\tquaternion_test.cpp\n\trepr.cpp\n\tstringformatter.cpp\n\tstrings.cpp\n\tsubprocess.cpp\n\tthread_id.cpp\n\ttimer.cpp\n\ttiming.cpp\n\tunicode.cpp\n\tvector.cpp\n\tvector_test.cpp\n)\n\npxdgen(\n\tcompiler.h\n\tenum.h\n\tenum_test.h\n\tfile.h\n\tpath.h\n)\n\nadd_subdirectory(compress)\nadd_subdirectory(filelike)\nadd_subdirectory(fslike)\n"
  },
  {
    "path": "libopenage/util/algorithm.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n\n// TODO: make general transformation from iterator algorithm to container\n// algorithm. so much metaprogramming...\n\nnamespace openage {\nnamespace util {\n\n/**\n * std::for_each except just on containers.\n */\ntemplate <class Container, class Function>\ninline Function for_each(Container &&container, Function &&func) {\n\t// why cpp why...\n\treturn std::for_each(std::begin(std::forward<Container>(container)),\n\t                     std::end(std::forward<Container>(container)),\n\t                     std::forward<Function>(func));\n}\n\n/**\n * Filters items from a container which satisfy a certain predicate.\n */\ntemplate <class Container, class Function>\ninline void remove_from(Container &container, Function &&func) {\n\tcontainer.erase(std::remove_if(std::begin(container),\n\t                               std::end(container),\n\t                               std::forward<Function>(func)),\n\t                std::end(container));\n}\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/color.cpp",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#include \"color.h\"\n\n#include <epoxy/gl.h>\n\nnamespace openage::util {\n\nvoid col::use() {\n\t//TODO use glColor4b\n\tglColor4f(r / 255.f, g / 255.f, b / 255.f, a / 255.f);\n}\n\nvoid col::use(float alpha) {\n\tglColor4f(r / 255.f, g / 255.f, b / 255.f, alpha);\n}\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/color.h",
    "content": "// Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage {\nnamespace util {\n\nstruct col {\n\tcol(unsigned r, unsigned g, unsigned b, unsigned a) :\n\t\tr{r}, g{g}, b{b}, a{a} {}\n\n\tunsigned r, g, b, a;\n\n\tvoid use();\n\tvoid use(float alpha);\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/compiler.cpp",
    "content": "// Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n#include \"compiler.h\"\n\n#ifndef _WIN32\n\t#include <cxxabi.h>\n\t#include <dlfcn.h>\n#else\n\t#define WIN32_LEAN_AND_MEAN\n\t#include <Windows.h>\n\t#include <DbgHelp.h>\n#endif\n\n#include \"strings.h\"\n\n#include <array>\n#include <iostream>\n#include <mutex>\n#include <optional>\n\nnamespace openage {\nnamespace util {\n\n\nstd::string demangle(const char *symbol) {\n#ifdef _WIN32\n\t//  MSVC's typeid(T).name() already returns a demangled name\n\t// unlike clang and gcc the MSVC demangled name is prefixed with \"class \" or \"stuct \"\n\t// we remove the prefix to match the format of clang and gcc\n\treturn strchr(symbol, ' ') + 1;\n#else\n\tint status;\n\tchar *buf = abi::__cxa_demangle(symbol, nullptr, nullptr, &status);\n\n\tif (status != 0) {\n\t\treturn symbol;\n\t}\n\telse {\n\t\tstd::string result{buf};\n\t\tfree(buf);\n\t\treturn result;\n\t}\n#endif\n}\n\n\nstd::string addr_to_string(const void *addr) {\n\treturn sformat(\"[%p]\", addr);\n}\n\n#ifdef _WIN32\nnamespace {\n\n\nbool initialized_symbol_handler_successfully = false;\n\n\nstd::optional<std::string> symbol_name_win(const void *addr) {\n\t// Handle to the current process\n\tstatic HANDLE process_handle = INVALID_HANDLE_VALUE;\n\tstatic bool initialized_symbol_handler = false;\n\n\t// SymInitialize & SymFromAddr are, according to MSDN, not thread-safe.\n\tstatic std::mutex sym_mutex;\n\tstd::lock_guard<std::mutex> sym_lock_guard{sym_mutex};\n\n\t// Initialize symbol handler for process, if it has not yet been initialized\n\t// If we are not succesful on the first try, leave it, since MSDN says that searching for symbol files is very time consuming\n\tif (!initialized_symbol_handler) {\n\t\tinitialized_symbol_handler = true;\n\n\t\tprocess_handle = GetCurrentProcess();\n\t\tinitialized_symbol_handler_successfully = SymInitialize(process_handle, nullptr, TRUE);\n\t}\n\n\tif (initialized_symbol_handler_successfully) {\n\t\t// The magic of winapi\n\t\tconstexpr int name_buffer_size = 1024;\n\t\tconstexpr int buffer_size = sizeof(SYMBOL_INFO) + name_buffer_size * sizeof(char);\n\t\tstd::array<char, buffer_size> buffer;\n\n\t\tSYMBOL_INFO *symbol_info = reinterpret_cast<SYMBOL_INFO *>(buffer.data());\n\n\t\tsymbol_info->SizeOfStruct = sizeof(SYMBOL_INFO);\n\t\tsymbol_info->MaxNameLen = name_buffer_size;\n\n\t\tif (SymFromAddr(process_handle, reinterpret_cast<DWORD64>(addr), nullptr, symbol_info)) {\n\t\t\treturn std::string(symbol_info->Name);\n\t\t}\n\t}\n\n\treturn std::optional<std::string>();\n}\n\n\n} // namespace\n#endif\n\nstd::string symbol_name(const void *addr, bool require_exact_addr, bool no_pure_addrs) {\n#ifdef _WIN32\n\n\tauto symbol_name_result = symbol_name_win(addr);\n\n\tif (!initialized_symbol_handler_successfully || !symbol_name_result.has_value()) {\n\t\treturn no_pure_addrs ? \"\" : addr_to_string(addr);\n\t}\n\n\treturn symbol_name_result.value();\n\n#else\n\tDl_info addr_info;\n\n\tif (dladdr(addr, &addr_info) == 0) {\n\t\t// dladdr has... failed.\n\t\treturn no_pure_addrs ? \"\" : addr_to_string(addr);\n\t}\n\telse {\n\t\tsize_t symbol_offset = (size_t)addr - (size_t)addr_info.dli_saddr;\n\n\t\tif (addr_info.dli_sname == nullptr or (symbol_offset != 0 and require_exact_addr)) {\n\t\t\treturn no_pure_addrs ? \"\" : addr_to_string(addr);\n\t\t}\n\n\t\tif (symbol_offset == 0) {\n\t\t\t// this is our symbol name.\n\t\t\treturn demangle(addr_info.dli_sname);\n\t\t}\n\t\telse {\n\t\t\treturn util::sformat(\"%s+0x%lx\",\n\t\t\t                     demangle(addr_info.dli_sname).c_str(),\n\t\t\t                     symbol_offset);\n\t\t}\n\t}\n#endif\n}\n\n\nbool is_symbol(const void *addr) {\n#ifdef _WIN32\n\n\tif (!initialized_symbol_handler_successfully) {\n\t\treturn true;\n\t}\n\telse {\n\t\treturn symbol_name_win(addr).has_value();\n\t}\n\n#else\n\tDl_info addr_info;\n\n\tif (dladdr(addr, &addr_info) == 0) {\n\t\treturn false;\n\t}\n\n\treturn (addr_info.dli_saddr == addr);\n#endif\n}\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/compiler.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/*\n * Some general-purpose utilities related to the C++ compiler and standard\n * library implementations.\n *\n * May contain some platform-dependent code.\n */\n\n#include <ciso646>\n// pxd: from libcpp.string cimport string\n#include <string>\n#include <typeinfo>\n\n\n/**\n * DLL entry-point decorations.\n */\n// clang-format off\n// otherwise clang-format removes indentation\n#if defined(_WIN32)\n\t#if defined(libopenage_EXPORTS)\n\t\t#define OAAPI __declspec(dllexport)\n\t#else\n\t\t#define OAAPI __declspec(dllimport)\n\t#endif /* libopenage_EXPORTS */\n#else\n\t#define OAAPI\n#endif\n\n#if defined(_MSC_VER)\n\t#ifndef HAVE_SSIZE_T\n\t// ssize_t is defined the same as Python's definition it in pyconfig.h.\n\t// This is necessary to facilitate the build and link procedure using MSVC.\n\t\t#ifdef _WIN64\n\t\t\ttypedef __int64 ssize_t;\n\t\t#else\n\t\t\ttypedef int ssize_t;\n\t\t#endif\n\t\t#define HAVE_SSIZE_T 1\n\t#endif // HAVE_SSIZE_T\n#endif // _MSC_VER\n// clang-format on\n\n\n/**\n * Software breakpoint if you're too lazy\n * to add it in gdb but instead wanna add it into the code directly.\n */\n// clang-format off\n#ifdef _WIN32\n\t#define BREAKPOINT __debugbreak()\n#else\n\t#include <signal.h>\n\t#define BREAKPOINT raise(SIGTRAP)\n#endif\n// clang-format on\n\n\n/**\n * As C++ is such a shit language, we found this awesome\n * way to display a type of some variable as a warning.\n *\n * Other hacks like wrong assignments work as well, but crash\n * the compilation by error.\n */\n#define TYPEINFO(var) printf(\"%d\", var)\n\n\nnamespace openage {\nnamespace util {\n\n/**\n * Demangles a symbol name.\n *\n * On failure, the mangled symbol name is returned.\n */\nstd::string demangle(const char *symbol);\n\n\n/**\n * Returns the (demangled) symbol name for a given address.\n *\n * If no exact match can be found,\n * if require_exact_addr == true (default):\n *   if no_pure_addrs == false (default):\n *     a pure hex address string is returned\n *   else:\n *     an empty string is returned\n * if require_exact_addr == false:\n *   name of closest symbol + offset hex string is returned\n *\n * pxd: string symbol_name(const void *addr) except +\n */\nOAAPI std::string symbol_name(const void *addr, bool require_exact_addr = true, bool no_pure_addrs = false);\n\n\n/**\n * Returns true if the address is the (exact) address of a symbol.\n */\nbool is_symbol(const void *addr);\n\n\n/**\n * Returns the string representation of the given type.\n */\ntemplate <typename T>\nstd::string typestring() {\n\treturn demangle(typeid(T).name());\n}\n\n\n/**\n * Returns the string representation of type pointed to by the given pointer.\n * Example:\n * struct A {}; struct B : A {};\n * std::shared_ptr<A> ptr = std::make_shared<B>();\n * auto str = typestring(ptr.get());\n * assert(str == \"B\"s);\n */\ntemplate <typename T>\nstd::string typestring(const T *ptr) {\n\treturn demangle(typeid(*ptr).name());\n}\n\n/**\n * Returns the string representation of type of given reference\n */\ntemplate <typename T>\nstd::string typestring(const T &ref) {\n\treturn demangle(typeid(ref).name());\n}\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/compress/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tlzxd.cpp\n)\n\npxdgen(\n\tlzxd.h\n)\n"
  },
  {
    "path": "libopenage/util/compress/bitstream.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstring>\n\n#include <functional>\n\n#include \"../../error/error.h\"\n#include \"../compiler.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace compress {\n\n\n/**\n * Type of the 'read' callback function.\n *\n * See lzxd.h for documentation.\n */\nusing read_callback_t = std::function<size_t(unsigned char *buf, size_t size)>;\n\n\n/**\n * Bitstream that is stored in 16-bit little-endian numbers.\n * There are two modes:\n *\n * Bit stream\n * ----------\n *\n * This is the default mode.\n * Bits may be retrieved via read_bits().\n *\n * The input bytes\n *\n * 0babcdefgh\n * 0bijklmnop\n * 0bmnopqrst\n * 0buvwxyzAB\n *\n * are decoded to the following bitstream:\n *\n * ijkl mnop abcd efgh uvwx yzAB mnop qrst\n *\n * Someone should get a medal for this.\n *\n * Byte stream\n * -----------\n *\n * In bytestream mode, read_bytes() can be used to read any number of bytes,\n * verbatim, from the data source.\n *\n * Mode switching\n * --------------\n *\n * You can switch between modes via\n *\n *  - switch_to_bytestream_mode()\n *  - switch_to_bitstream_mode()\n *\n * Every modeswitch aligns the stream to a 16-bit boundary by discarding\n * the appropriate amount of nullbits/nullbytes.\n * Calling the modeswitch methods while already in the respective mode does\n * nothing.\n */\ntemplate <unsigned int inbuf_size>\nclass BitStream {\npublic:\n\t/**\n\t * true if the read callback has returned EOF.\n\t */\n\tunsigned char eof;\n\nprivate:\n\t/**\n\t * read callback function; invoked whenever the input buffer is empty.\n\t */\n\tread_callback_t read_callback;\n\n\t/**\n\t * Input byte buffer.\n\t * Used by both modes (via ensure_bits and read_bytes).\n\t */\n\tunsigned char inbuf[inbuf_size];\n\n\t/**\n\t * Pointer to current position in inbuf.\n\t */\n\tunsigned char *i_ptr;\n\n\t/**\n\t * Pointer to end of valid data in inbuf.\n\t */\n\tunsigned char *i_end;\n\n\t/**\n\t * Bit buffer; used in bitstream mode.\n\t * Filled by via get_next_byte() via ensure_bits(),\n\t * read by peak_bits(),\n\t * cleared by remove_bits().\n\t */\n\tunsigned int bit_buffer;\n\n\t/**\n\t * The number of valid bits in bit_buffer.\n\t * If e.g. the value is 2, only the two most significant bits of\n\t * bit_buffer are set.\n\t */\n\tunsigned int bits_left;\n\n\t/**\n\t * counts the number of bits (in bitstream mode) or bytes (in bytestream mode),\n\t * for determining the correct number of bits/bytes to discard.\n\t */\n\tsize_t stream_position;\n\n\t/**\n\t * If set, the bitstream is currently in bytestream mode.\n\t */\n\tbool bitstream_mode;\n\n\t/**\n\t * returns the number of bytes that are available in the input byte buffer.\n\t */\n\tunsigned int input_bytes_available() {\n\t\tif (this->i_ptr > this->i_end) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"input byte buffer state invalid: i_ptr > i_end\");\n\t\t}\n\n\t\treturn this->i_end - this->i_ptr;\n\t}\n\n\t/**\n\t * ensures that at least one byte is available in the input byte buffer.\n\t *\n\t * returns the amount of available bytes.\n\t */\n\tvoid ensure_input_bytes() {\n\t\t// check if we need to actually read some bytes.\n\t\tif (this->input_bytes_available() == 0) {\n\t\t\t// fill the entire input buffer.\n\t\t\tsize_t read_bytes = this->read_callback(this->inbuf, inbuf_size);\n\n\t\t\t// we might overrun the input stream by asking for bits we don't use,\n\t\t\t// so fake 2 more bytes at the end of input\n\t\t\tif (read_bytes == 0) [[unlikely]] {\n\t\t\t\tif (this->eof) {\n\t\t\t\t\tthrow Error(MSG(err) << \"Unexpected EOF in the middle of a block\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tread_bytes = 2;\n\t\t\t\t\tthis->inbuf[0] = 0;\n\t\t\t\t\tthis->inbuf[1] = 0;\n\t\t\t\t\tthis->eof = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (read_bytes > (int)inbuf_size) [[unlikely]] {\n\t\t\t\tthrow Error(MSG(err) << \"read() returned more data than requested\");\n\t\t\t}\n\n\t\t\t// update i_ptr and i_end\n\t\t\tthis->i_ptr = this->inbuf;\n\t\t\tthis->i_end = &this->inbuf[read_bytes];\n\t\t}\n\n\t\t// check if the reading was successful.\n\t\tif (this->i_ptr >= this->i_end) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"input byte buffer empty: failed to ensure_input_bytes\");\n\t\t}\n\t}\n\n\t/*\n\t * allow our friend HuffmanTable to directly use ensure_bits, peek_bits and remove_bits.\n\t */\n\ttemplate <unsigned maxsymbols_p, unsigned tablebits_p, bool allow_empty>\n\tfriend class HuffmanTable;\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * loads the next 16-bit value into the bitstream.\n\t */\n\tvoid load_next_16_bits() {\n\t\t// example: input stream contains bytes A, B. previous byte was J.\n\t\t//\n\t\t// 5 bits are left\n\t\t// bit buffer is:                     jjjjj000 00000000 00000000 00000000\n\t\t//\n\t\t// ensure_bits(9) is called.\n\t\t// b0 = aaaaaaaa\n\t\t// b1 = bbbbbbbb\n\t\t// bit_buffer |= bbbbbbbb aaaaaaaa << (32 - 16 - 5) == 11\n\t\t//\n\t\t// new bit buffer:                    jjjjjbbb bbbbbaaa aaaaa000 00000000\n\n\t\t// read two bytes to b0, b1\n\t\tthis->ensure_input_bytes();\n\t\tunsigned char b0 = *i_ptr++;\n\t\tthis->ensure_input_bytes();\n\t\tunsigned char b1 = *i_ptr++;\n\n\t\t// inject bits into bit_buffer\n\t\tbit_buffer |= ((b1 << 8) | b0) << (sizeof(bit_buffer) * 8 - 16 - bits_left);\n\t\tbits_left += 16;\n\t}\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * ensures there are at least nbits bits in the bit buffer.\n\t */\n\tinline void ensure_bits(unsigned int nbits) {\n\t\tif (!this->bitstream_mode) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"instream: attempted to ensure bits while in bytestream mode\");\n\t\t}\n\n\t\twhile (bits_left < nbits) {\n\t\t\tthis->load_next_16_bits();\n\t\t}\n\t}\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * returns nbits bits from the bit buffer, without removing them.\n\t */\n\tunsigned peek_bits(unsigned int nbits) {\n\t\t// example: bit buffer is:   abcdefgh ijkl0000 00000000 00000000\n\t\t//\n\t\t// peek_bits(3) is called.\n\t\t//\n\t\t// return (bit_buffer >> (32 - 3) == 29\n\t\t//\n\t\t// returned value is:        abc\n\t\tthis->ensure_bits(nbits);\n\n\t\treturn (bit_buffer >> (sizeof(bit_buffer) * 8 - nbits));\n\t}\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * removes nbits bits from the bit buffer.\n\t */\n\tvoid remove_bits(unsigned int nbits) {\n\t\t// example: bit buffer is:  abcdefgh ijkl0000 00000000 00000000\n\t\t//\n\t\t// remove_bits(3) is called.\n\t\t//\n\t\t// bit_buffer <<= 3\n\t\t//\n\t\t// resulting bit buffer is: defghijk l0000000 00000000 00000000\n\t\tthis->ensure_bits(nbits);\n\n\t\tbit_buffer <<= nbits;\n\t\tbits_left -= nbits;\n\t\tthis->stream_position += nbits;\n\t}\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * Aligns the bitstream to the next multiple of 2 bytes.\n\t *\n\t * If min_discard is given, at least that amount of bits is discarded.\n\t */\n\tvoid align_bitstream(unsigned int min_discard = 0) {\n\t\tunsigned int nbits = this->stream_position % 16;\n\t\tif (nbits != 0) {\n\t\t\tnbits = 16 - nbits;\n\t\t}\n\n\t\twhile (nbits < min_discard) [[unlikely]] {\n\t\t\tnbits += 16;\n\t\t}\n\n\t\tif (nbits == 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this->peek_bits(nbits)) {\n\t\t\tauto errmsg = MSG(err);\n\t\t\terrmsg << \"attempted to discard \" << nbits << \" non-null bits: \";\n\t\t\tfor (unsigned ctr = 1; ctr <= nbits; ctr++) {\n\t\t\t\terrmsg << (this->peek_bits(ctr) & 1);\n\t\t\t}\n\t\t\tthrow Error(errmsg);\n\t\t}\n\t\tthis->remove_bits(nbits);\n\t}\n\npublic:\n\tBitStream(read_callback_t read_callback) :\n\t\teof{false},\n\t\tread_callback{read_callback},\n\t\ti_ptr{inbuf},\n\t\ti_end{inbuf},\n\t\tbit_buffer{0},\n\t\tbits_left{0},\n\t\tstream_position{0},\n\t\tbitstream_mode{true} {\n\t\tstatic_assert(inbuf_size >= 2, \"inbuf size must be at least 2\");\n\t\tstatic_assert(inbuf_size % 2 == 0, \"inbuf size must be even\");\n\t}\n\n\t/**\n\t * for use in bitstream mode.\n\t *\n\t * takes nbits bits from the bit buffer and returns them.\n\t */\n\tunsigned read_bits(unsigned int nbits) {\n\t\tunsigned ret = peek_bits(nbits);\n\t\tremove_bits(nbits);\n\t\treturn ret;\n\t}\n\n\t/**\n\t * for use in bytestream mode.\n\t *\n\t * reads up to count verbatim bytes from the input byte buffer,\n\t * and writes them to *buf.\n\t *\n\t * reads at least 1 and at most count bytes.\n\t */\n\tunsigned read_bytes(unsigned char *buf, unsigned count) {\n\t\tif (this->bitstream_mode) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"attempt to read_bytes while in bitstream mode\");\n\t\t}\n\n\t\tthis->ensure_input_bytes();\n\n\t\tunsigned int available = this->input_bytes_available();\n\t\tif (available < count) {\n\t\t\tcount = available;\n\t\t}\n\n\t\tmemcpy(buf, this->i_ptr, count);\n\t\tthis->i_ptr += count;\n\t\tthis->stream_position += count;\n\n\t\treturn count;\n\t}\n\n\t/**\n\t * for use in bytestream mode.\n\t *\n\t * reads a single verbatim byte from the input byte buffer, and returns it.\n\t */\n\tunsigned char read_single_byte() {\n\t\tunsigned char buf;\n\n\t\tif (this->read_bytes(&buf, 1) != 1) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"failed to read single byte in bytestream mode\");\n\t\t}\n\n\t\treturn buf;\n\t}\n\n\t/**\n\t * for use in bytestream mode.\n\t *\n\t * reads a little-endian-encoded 4-byte number and returns it.\n\t */\n\tunsigned int read_4bytes_le() {\n\t\tunsigned int result;\n\n\t\tresult = this->read_single_byte() << 0;\n\t\tresult |= this->read_single_byte() << 8;\n\t\tresult |= this->read_single_byte() << 16;\n\t\tresult |= this->read_single_byte() << 24;\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Switches to bitstream mode.\n\t *\n\t * If the previous bytestream had an odd size, one byte is discarded first.\n\t */\n\tvoid switch_to_bitstream_mode() {\n\t\tif (this->bitstream_mode) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (this->stream_position & 1) {\n\t\t\t// bytestream hat odd length; discard one nullbyte.\n\t\t\tunsigned char data = this->read_single_byte();\n\t\t\tif (data != 0) {\n\t\t\t\tthrow Error(MSG(err).fmt(\"attempted to discard a non-zero byte at end of bytestream: %02x\", data));\n\t\t\t}\n\t\t}\n\n\t\tthis->bitstream_mode = true;\n\t\tthis->stream_position = 0;\n\t}\n\n\t/**\n\t * Switches to bytestream mode.\n\t *\n\t * Discards 1 to 16 bits to align the bitstream first.\n\t */\n\tvoid switch_to_bytestream_mode() {\n\t\tif (!this->bitstream_mode) {\n\t\t\treturn;\n\t\t}\n\n\t\t// for some weird reason, the alignment padding here is 1 to 16 bits, not 0 to 15.\n\t\t// thus, discard an additional bit.\n\t\tthis->align_bitstream(1);\n\n\t\tthis->bitstream_mode = false;\n\t\tthis->stream_position = 0;\n\n\t\tif (this->bits_left) {\n\t\t\tthrow Error(MSG(err) << \"bits left after switching to bytestream mode: \" << this->bits_left);\n\t\t}\n\t}\n\n\t/**\n\t * Aligns the bitstream - that is, _if_ we're currenlty in bitstream mode.\n\t *\n\t * Otherwise, a no-op.\n\t */\n\tvoid align_if_in_bitstream_mode() {\n\t\tif (this->bitstream_mode) {\n\t\t\tthis->align_bitstream();\n\t\t}\n\t}\n};\n\n\n} // namespace compress\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/compress/lzxd.cpp",
    "content": "// This file was adapted from cabextract/libmspack <http://www.cabextract.org.uk/>,\n// Copyright 2003-2013 the cabextract contributors.\n// It's licensed under the terms of the GNU Library General Public License version 2.\n// Modifications Copyright 2014-2023 the openage authors.\n// See copying.md for further legal info.\n\n/*\n * the original code was refactored almost beyond recognition;\n * all of the macros and optimizations were ripped out and replaced by\n * templated C++ classes.\n */\n\n#include \"lzxd.h\"\n\n#include <algorithm>\n#include <cerrno>\n#include <cinttypes>\n#include <climits>\n#include <cstdarg>\n#include <cstdint>\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n#include <utility>\n\n#include \"../../error/error.h\"\n#include \"../compiler.h\"\n\n#include \"bitstream.h\"\n\nnamespace openage::util::compress {\n\n\n// LZX specification constants\nconstexpr unsigned LZX_MIN_MATCH = 2;\n//constexpr unsigned LZX_MAX_MATCH = 257;             // seems to be unused. I'm scared.\nconstexpr unsigned LZX_NUM_CHARS = 256;\nconstexpr unsigned LZX_BLOCKTYPE_INVALID = 0; // also blocktypes 4-7 invalid\nconstexpr unsigned LZX_BLOCKTYPE_VERBATIM = 1;\nconstexpr unsigned LZX_BLOCKTYPE_ALIGNED = 2;\nconstexpr unsigned LZX_BLOCKTYPE_UNCOMPRESSED = 3;\nconstexpr unsigned LZX_PRETREE_NUM_ELEMENTS = 20;\nconstexpr unsigned LZX_ALIGNED_NUM_ELEMENTS = 8;\nconstexpr unsigned LZX_NUM_PRIMARY_LENGTHS = 7; // missing from spec!\nconstexpr unsigned LZX_NUM_SECONDARY_LENGTHS = 249; // length tree #elements\n\n// LZX huffman constants: tweak tablebits as desired\nconstexpr unsigned LZX_PRETREE_MAXSYMBOLS = LZX_PRETREE_NUM_ELEMENTS;\nconstexpr unsigned LZX_PRETREE_TABLEBITS = 6;\nconstexpr unsigned LZX_MAINTREE_MAXSYMBOLS = LZX_NUM_CHARS + 50 * 8;\nconstexpr unsigned LZX_MAINTREE_TABLEBITS = 12;\nconstexpr unsigned LZX_LENGTH_MAXSYMBOLS = LZX_NUM_SECONDARY_LENGTHS + 1;\nconstexpr unsigned LZX_LENGTH_TABLEBITS = 12;\nconstexpr unsigned LZX_ALIGNED_MAXSYMBOLS = LZX_ALIGNED_NUM_ELEMENTS;\nconstexpr unsigned LZX_ALIGNED_TABLEBITS = 7;\nconstexpr unsigned LZX_LENTABLE_SAFETY = 64; // table decoding overruns are allowed\n\n\n/*\n * LZX static data tables:\n *\n * LZX uses 'position slots' to represent match offsets. For every match,\n * a small 'position slot' number and a small offset from that slot are\n * encoded instead of one large offset.\n *\n * position_base[] is an index to the position slot bases\n *\n * extra_bits[] states how many bits of offset-from-base data is needed.\n *\n * They are generated like so:\n * for (i = 0;  i < 4; i++) extra_bits[i] = 0;\n * for (i = 4,  j = 0; i < 36; i+=2) extra_bits[i] = extra_bits[i+1] = j++;\n * for (i = 36; i < 51; i++) extra_bits[i] = 17;\n * for (i = 0,  j = 0; i < 51; j += 1 << extra_bits[i++]) position_base[i] = j;\n */\n\nstatic const unsigned position_base[] = {\n\t0, 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};\n\nstatic const unsigned char extra_bits[] = {\n\t0, 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};\n\n\ntemplate <unsigned int maxsymbols_p, unsigned int tablebits_p, bool allow_empty = false>\nclass HuffmanTable {\nprivate:\n\tclass LZXDStream *lzx;\n\npublic:\n\tstatic constexpr unsigned int HUFF_MAXBITS = 16;\n\tstatic constexpr unsigned int maxsymbols = maxsymbols_p;\n\tstatic constexpr unsigned int tablebits = tablebits_p;\n\n\t/**\n\t * table of code lengths (in bits) for each symbol\n\t */\n\tunsigned char len[maxsymbols_p + LZX_LENTABLE_SAFETY];\n\n\t/**\n\t * table to fill up with decoded values for symbols, and pointers.\n\t * symbols shorter than tablebits_p are decoded by direct lookup\n\t * (that's the first (1<<tablebits_p) entries).\n\t * longer symbols are decoded by following pointers to the appropriate\n\t * leaf in the upper nsyms*2 fields.\n\t */\n\tuint16_t table[(1 << tablebits_p) + (maxsymbols_p * 2)];\n\n\t/**\n\t * the table could not be constructed and is empty (try_make_decode_table failed).\n\t * this will lead to errors, should read_sym() be called.\n\t */\n\tbool is_empty;\n\n\texplicit HuffmanTable(class LZXDStream *lzx) :\n\t\tlzx{lzx}, is_empty{true} {}\n\n\t/**\n\t * Decodes the next huffman symbol from the input bitstream,\n\t * and returns it.\n\t * Do not use this function on a table unless build_decode_table() succeeded.\n\t */\n\tint read_sym();\n\n\t/**\n\t * calls try_make_decode_table, and, if allow_empty is false,\n\t * raises an exception on failure.\n\t * builds a huffman lookup table from code lengths.\n\t */\n\tvoid make_decode_table();\n\n\t/**\n\t * reads in code lengths for symbols first to last in the given table.\n\t * The code lengths are stored in their own special LZX way.\n\t */\n\tvoid read_lengths(unsigned int first, unsigned int last);\n\nprivate:\n\t/**\n\t * This function was originally coded by David Tritscher.\n\t * It builds a fast huffman decoding table from\n\t * a canonical huffman code lengths table.\n\t */\n\tbool try_make_decode_table();\n};\n\n\nclass LZXDStream {\npublic:\n\tssize_t output_pos; // number of output bytes\n\n\tunsigned char *window; // decoding window\n\tunsigned int window_size; // window size\n\tunsigned int window_posn; // decompression offset within window\n\tunsigned int frame_posn; // current frame offset within in window\n\tunsigned int frame; // the number of 32kb frames processed\n\tunsigned int reset_interval; // which frame do we reset the compressor?\n\n\tunsigned int R0, R1, R2; // for the LRU offset system\n\tunsigned int block_length; // uncompressed length of this LZX block\n\tunsigned int block_remaining; // uncompressed bytes still left to decode\n\n\tsigned int e8_magic; // magic value that's used by the intel E8 transform.\n\t\t// this field is read from the first 33 bits of the stream\n\t\t// after reset, and called intel_filesize in libmspack.\n\tbool header_read; // indicates whether the e8_magic header has been read.\n\n\tunsigned char block_type; // type of the current block\n\tunsigned char posn_slots; // how many posn slots in stream?\n\n\t// IO buffering\n\tBitStream<4096> bits;\n\n\t// huffman code lenghts\n\tHuffmanTable<LZX_PRETREE_MAXSYMBOLS, LZX_PRETREE_TABLEBITS> htpre;\n\tHuffmanTable<LZX_MAINTREE_MAXSYMBOLS, LZX_MAINTREE_TABLEBITS> htmain;\n\tHuffmanTable<LZX_LENGTH_MAXSYMBOLS, LZX_LENGTH_TABLEBITS, true> htlength;\n\tHuffmanTable<LZX_ALIGNED_MAXSYMBOLS, LZX_ALIGNED_TABLEBITS> htaligned;\n\n\t/** See the doc for LZXDecompressor::LZXDecompressor in lzxd.h */\n\tLZXDStream(read_callback_t callback, unsigned int window_bits, unsigned int reset_interval);\n\t~LZXDStream();\n\n\tLZXDStream(const LZXDStream &other) = delete;\n\tLZXDStream(LZXDStream &&other) = delete;\n\tLZXDStream &operator=(const LZXDStream &other) = delete;\n\tLZXDStream &operator=(LZXDStream &&other) = delete;\n\n\t/** See the doc for LZXDecompressor::decompress_next_frame in lzxd.h */\n\tunsigned decompress_next_frame(unsigned char *output_buf);\n\nprivate:\n\t/**\n\t * Initializes the next block.\n\t */\n\tvoid init_next_block();\n\n\t/**\n\t * Decodes one symbol from the current verbatim block.\n\t * Returns the number of bytes that were decoded.\n\t * The bytes are written to the window, and window_posn is advanced.\n\t */\n\tint decode_symbol_from_verbatim_block();\n\n\t/**\n\t * Decodes one symbol from the current aligned block.\n\t * Returns the number of bytes that were decoded.\n\t * The bytes are written to the window, and window_posn is advanced.\n\t */\n\tint decode_symbol_from_aligned_block();\n\n\t/**\n\t * Reads the given amount of bytes from the current uncompressed block.\n\t * Returns its argument (the number of bytes that were read).\n\t * The bytes are written to the window, and window_posn is advanced.\n\t */\n\tunsigned int read_data_from_uncompressed_block(unsigned int size);\n\n\t/**\n\t * Postprocesses the a frame's decoded data with E8 decoding.\n\t *\n\t * Before compressing data, LZX pre-processes it by translating the four bytes\n\t * following 0xE8-bytes; this optimizes intel x86 machine code.\n\t *\n\t * What the hell, Microsoft?\n\t */\n\tvoid postprocess_intel_e8(unsigned char *output_buf, int frame_size);\n\npublic:\n\tvoid reset_state();\n};\n\n\ntemplate <unsigned int maxsymbols_p, unsigned int tablebits_p, bool allow_empty>\nint HuffmanTable<maxsymbols_p, tablebits_p, allow_empty>::read_sym() {\n\tlzx->bits.ensure_bits(HUFF_MAXBITS);\n\tuint16_t sym = table[lzx->bits.peek_bits(tablebits)];\n\n\tunsigned i = 1 << (sizeof(lzx->bits.bit_buffer) * 8 - tablebits);\n\twhile (sym >= maxsymbols) {\n\t\t// huff_traverse\n\t\tif ((i >>= 1) == 0) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"huff_error in huff_traverse\");\n\t\t}\n\t\tsym = table[(sym << 1) | ((lzx->bits.bit_buffer & i) ? 1 : 0)];\n\t}\n\n\tlzx->bits.remove_bits(len[sym]);\n\treturn sym;\n}\n\n\ntemplate <unsigned int maxsymbols_p, unsigned int tablebits_p, bool allow_empty>\nvoid HuffmanTable<maxsymbols_p, tablebits_p, allow_empty>::make_decode_table() {\n\tif (this->try_make_decode_table()) {\n\t\tthis->is_empty = false;\n\t}\n\telse {\n\t\tif (allow_empty) {\n\t\t\t// allow an empty tree, but don't decode symbols with it\n\n\t\t\t// if any of the symbols has a greater-than-zero length, fail.\n\t\t\tfor (unsigned int i = 0; i < maxsymbols; i++) {\n\t\t\t\tif (this->len[i] > 0) [[unlikely]] {\n\t\t\t\t\tthrow Error(MSG(err) << \"failed to build HuffmanTable<allow_empty=true>\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis->is_empty = true;\n\t\t}\n\t\telse {\n\t\t\tthrow Error(MSG(err) << \"failed to build HuffmanTable<allow_empty=false>\");\n\t\t}\n\t}\n}\n\n\ntemplate <unsigned int maxsymbols_p, unsigned int tablebits_p, bool allow_empty>\nbool HuffmanTable<maxsymbols_p, tablebits_p, allow_empty>::try_make_decode_table() {\n\tuint16_t sym, next_symbol;\n\tunsigned int leaf, fill;\n\tunsigned char bit_num;\n\tunsigned int pos = 0; // the current position in the decode table\n\tunsigned int table_mask = 1 << tablebits;\n\tunsigned int bit_mask = table_mask >> 1; // don't do 0 length codes\n\n\t// fill entries for codes short enough for a direct mapping\n\tfor (bit_num = 1; bit_num <= tablebits; bit_num++) {\n\t\tfor (sym = 0; sym < maxsymbols; sym++) {\n\t\t\tif (len[sym] != bit_num) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tleaf = pos;\n\t\t\tif ((pos += bit_mask) > table_mask) {\n\t\t\t\treturn false; // table overrun\n\t\t\t}\n\n\t\t\t// fill all possible lookups of this symbol with the symbol itself\n\t\t\tfor (fill = bit_mask; fill-- > 0;) {\n\t\t\t\ttable[leaf++] = sym;\n\t\t\t}\n\t\t}\n\t\tbit_mask >>= 1;\n\t}\n\n\t// exit with success if table is now complete\n\tif (pos == table_mask) {\n\t\treturn true;\n\t}\n\n\t// mark all remaining table entries as unused\n\tfor (sym = pos; sym < table_mask; sym++) {\n\t\ttable[sym] = 0xFFFF;\n\t}\n\n\t// next_symbol = base of allocation for long codes\n\tnext_symbol = ((table_mask >> 1) < maxsymbols) ? maxsymbols : (table_mask >> 1);\n\n\t// give ourselves room for codes to grow by up to 16 more bits.\n\t// codes now start at bit (tablebits + 16) and end at (tablebits + 16 - codelength)\n\tpos <<= 16;\n\ttable_mask <<= 16;\n\tbit_mask = 1 << 15;\n\n\tfor (bit_num = tablebits + 1; bit_num <= HUFF_MAXBITS; bit_num++) {\n\t\tfor (sym = 0; sym < maxsymbols; sym++) {\n\t\t\tif (len[sym] != bit_num) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tleaf = pos >> 16;\n\t\t\tfor (fill = 0; fill < (bit_num - tablebits); fill++) {\n\t\t\t\t// if this path hasn't been taken yet, 'allocate' two entries\n\t\t\t\tif (table[leaf] == 0xFFFF) {\n\t\t\t\t\ttable[(next_symbol << 1) + 0] = 0xFFFF;\n\t\t\t\t\ttable[(next_symbol << 1) + 1] = 0xFFFF;\n\t\t\t\t\ttable[leaf] = next_symbol++;\n\t\t\t\t}\n\n\t\t\t\t// follow the path and select either left or right for next bit\n\t\t\t\tleaf = table[leaf] << 1;\n\t\t\t\tif ((pos >> (15 - fill)) & 1)\n\t\t\t\t\tleaf++;\n\t\t\t}\n\t\t\ttable[leaf] = sym;\n\n\t\t\tif ((pos += bit_mask) > table_mask) {\n\t\t\t\t// table overflow\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tbit_mask >>= 1;\n\t}\n\n\t// full table?\n\treturn pos == table_mask;\n}\n\n\ntemplate <unsigned int maxsymbols_p, unsigned int tablebits_p, bool allow_empty>\nvoid HuffmanTable<maxsymbols_p, tablebits_p, allow_empty>::read_lengths(unsigned int first, unsigned int last) {\n\t// bit buffer and huffman symbol decode variables\n\tunsigned int x, y;\n\tint z;\n\n\t// read lengths for pretree (20 symbols, lengths stored in fixed 4 bits)\n\tfor (x = 0; x < 20; x++) {\n\t\tlzx->htpre.len[x] = lzx->bits.read_bits(4);\n\t}\n\n\tlzx->htpre.make_decode_table();\n\n\tfor (x = first; x < last;) {\n\t\tz = lzx->htpre.read_sym();\n\t\tif (z == 17) {\n\t\t\t// code = 17, run of ([read 4 bits]+4) zeros\n\t\t\ty = lzx->bits.read_bits(4);\n\t\t\ty += 4;\n\t\t\twhile (y--) {\n\t\t\t\tlen[x++] = 0;\n\t\t\t}\n\t\t}\n\t\telse if (z == 18) {\n\t\t\t// code = 18, run of ([read 5 bits]+20) zeros\n\t\t\ty = lzx->bits.read_bits(5);\n\t\t\ty += 20;\n\t\t\twhile (y--) {\n\t\t\t\tlen[x++] = 0;\n\t\t\t}\n\t\t}\n\t\telse if (z == 19) {\n\t\t\t// code = 19, run of ([read 1 bit]+4) [read huffman symbol]\n\t\t\ty = lzx->bits.read_bits(1);\n\t\t\ty += 4;\n\t\t\tz = lzx->htpre.read_sym();\n\t\t\tz = len[x] - z;\n\t\t\tif (z < 0) {\n\t\t\t\tz += 17;\n\t\t\t}\n\t\t\twhile (y--) {\n\t\t\t\tlen[x++] = z;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// code = 0 to 16, delta current length entry\n\t\t\tz = len[x] - z;\n\t\t\tif (z < 0)\n\t\t\t\tz += 17;\n\n\t\t\tlen[x++] = z;\n\t\t}\n\t}\n}\n\n\nLZXDStream::LZXDStream(read_callback_t read_callback,\n                       unsigned int window_bits,\n                       unsigned int reset_interval) :\n\toutput_pos{0},\n\twindow_size{static_cast<unsigned int>(1) << window_bits},\n\twindow_posn{0},\n\tframe_posn{0},\n\tframe{0},\n\treset_interval{reset_interval},\n\tbits{std::move(read_callback)},\n\thtpre{this},\n\thtmain{this},\n\thtlength{this},\n\thtaligned{this} {\n\t// LZX supports window sizes of 2^15 (32 KiB) through 2^21 (2 MiB)\n\tif (window_bits < 15 || window_bits > 21) {\n\t\tthrow Error(MSG(err) << \"Bad requested window size: 2^\" << window_bits << \" bytes\");\n\t}\n\n\t// allocate decompression window\n\tthis->window = new unsigned char[window_size];\n\n\t// window bits:    15  16  17  18  19  20  21\n\t// position slots: 30  32  34  36  38  42  50\n\tthis->posn_slots = 2 * window_bits;\n\tif (window_bits == 21) {\n\t\tthis->posn_slots = 50;\n\t}\n\tif (window_bits == 20) {\n\t\tthis->posn_slots = 42;\n\t}\n\n\tthis->reset_state();\n}\n\n\nLZXDStream::~LZXDStream() {\n\tdelete[] this->window;\n}\n\n\nvoid LZXDStream::reset_state() {\n\tthis->R0 = 1;\n\tthis->R1 = 1;\n\tthis->R2 = 1;\n\tthis->header_read = false;\n\tthis->block_remaining = 0;\n\tthis->block_type = LZX_BLOCKTYPE_INVALID;\n\n\t// initialise tables to 0 (because deltas will be applied to them)\n\tfor (unsigned int i = 0; i < LZX_MAINTREE_MAXSYMBOLS; i++) {\n\t\tthis->htmain.len[i] = 0;\n\t}\n\n\tfor (unsigned int i = 0; i < LZX_LENGTH_MAXSYMBOLS; i++) {\n\t\tthis->htlength.len[i] = 0;\n\t}\n}\n\n\nvoid LZXDStream::init_next_block() {\n\t// we might still be in bytstream mode from the previous block.\n\tthis->bits.switch_to_bitstream_mode();\n\n\t// read block type (3 bits) and block length (24 bits)\n\tthis->block_type = this->bits.read_bits(3);\n\n\tthis->block_length = this->bits.read_bits(16) << 8;\n\tthis->block_length |= this->bits.read_bits(8);\n\tthis->block_remaining = this->block_length;\n\n\t// read individual block headers\n\tswitch (this->block_type) {\n\tcase LZX_BLOCKTYPE_ALIGNED:\n\t\t// read lengths of and build aligned huffman decoding tree\n\t\tfor (int i = 0; i < 8; i++) {\n\t\t\tthis->htaligned.len[i] = this->bits.read_bits(3);\n\t\t}\n\t\tthis->htaligned.make_decode_table();\n\n\t\t// Falls through.\n\t\t// Rest of aligned header is same as verbatim\n\tcase LZX_BLOCKTYPE_VERBATIM:\n\t\t// read lengths of and build main huffman decoding tree\n\t\tthis->htmain.read_lengths(0, 256);\n\t\tthis->htmain.read_lengths(256, LZX_NUM_CHARS + (this->posn_slots << 3));\n\t\tthis->htmain.make_decode_table();\n\t\t// read lengths of and build lengths huffman decoding tree\n\t\tthis->htlength.read_lengths(0, LZX_NUM_SECONDARY_LENGTHS);\n\t\tthis->htlength.is_empty = false;\n\t\tthis->htlength.make_decode_table();\n\t\tbreak;\n\n\tcase LZX_BLOCKTYPE_UNCOMPRESSED:\n\t\tthis->bits.switch_to_bytestream_mode();\n\n\t\t// read 12 bytes of stored R0 / R1 / R2 values\n\t\tthis->R0 = this->bits.read_4bytes_le();\n\t\tthis->R1 = this->bits.read_4bytes_le();\n\t\tthis->R2 = this->bits.read_4bytes_le();\n\n\t\tbreak;\n\n\tdefault:\n\t\tthrow Error(MSG(err) << \"decrunch: bad block type \" << this->block_type);\n\t}\n}\n\n\nint LZXDStream::decode_symbol_from_verbatim_block() {\n\tint main_element = this->htmain.read_sym();\n\tif (main_element < static_cast<int>(LZX_NUM_CHARS)) {\n\t\t// literal: 0 to LZX_NUM_CHARS-1\n\t\twindow[this->window_posn++] = main_element;\n\t\treturn 1;\n\t}\n\n\t// match: LZX_NUM_CHARS + ((slot<<3) | length_header (3 bits))\n\tmain_element -= LZX_NUM_CHARS;\n\n\t// get match length\n\tint match_length = main_element & LZX_NUM_PRIMARY_LENGTHS;\n\tif (match_length == LZX_NUM_PRIMARY_LENGTHS) {\n\t\tif (this->htlength.is_empty) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"decrunch: LENGTH symbol needed byt tree is empty\");\n\t\t}\n\t\tint length_footer = this->htlength.read_sym();\n\t\tmatch_length += length_footer;\n\t}\n\tmatch_length += LZX_MIN_MATCH;\n\n\t// get match offset\n\tunsigned match_offset = (main_element >> 3);\n\tswitch (match_offset) {\n\tcase 0:\n\t\tmatch_offset = this->R0;\n\t\tbreak;\n\tcase 1:\n\t\tmatch_offset = this->R1;\n\t\tthis->R1 = this->R0;\n\t\tthis->R0 = match_offset;\n\t\tbreak;\n\tcase 2:\n\t\tmatch_offset = this->R2;\n\t\tthis->R2 = this->R0;\n\t\tthis->R0 = match_offset;\n\t\tbreak;\n\tcase 3:\n\t\tmatch_offset = 1;\n\t\tthis->R2 = this->R1;\n\t\tthis->R1 = this->R0;\n\t\tthis->R0 = match_offset;\n\t\tbreak;\n\tdefault:\n\t\tint extra = extra_bits[match_offset];\n\t\tint verbatim_bits = this->bits.read_bits(extra);\n\t\tmatch_offset = position_base[match_offset] - 2 + verbatim_bits;\n\t\tthis->R2 = this->R1;\n\t\tthis->R1 = this->R0;\n\t\tthis->R0 = match_offset;\n\t}\n\n\tif ((this->window_posn + match_length) > this->window_size) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"decrunch: match ran over window wrap\");\n\t}\n\n\t// copy match\n\tunsigned char *rundest = &window[this->window_posn];\n\tint i = match_length;\n\t// does match offset wrap the window?\n\tif (match_offset > this->window_posn) {\n\t\t// j = length from match offset to end of window\n\t\tint j = match_offset - this->window_posn;\n\t\tif (j > (int)this->window_size) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"decrunch: match offset beyond window boundaries\");\n\t\t}\n\t\tunsigned char *runsrc = &window[this->window_size - j];\n\t\tif (j < i) {\n\t\t\t// if match goes over the window edge, do two copy runs\n\t\t\ti -= j;\n\t\t\twhile (j-- > 0) {\n\t\t\t\t*rundest++ = *runsrc++;\n\t\t\t}\n\t\t\trunsrc = window;\n\t\t}\n\t\twhile (i-- > 0) {\n\t\t\t*rundest++ = *runsrc++;\n\t\t}\n\t}\n\telse {\n\t\tunsigned char *runsrc = rundest - match_offset;\n\t\twhile (i-- > 0) {\n\t\t\t*rundest++ = *runsrc++;\n\t\t}\n\t}\n\n\tthis->window_posn += match_length;\n\treturn match_length;\n}\n\n\nint LZXDStream::decode_symbol_from_aligned_block() {\n\tint main_element = this->htmain.read_sym();\n\tif (main_element < static_cast<int>(LZX_NUM_CHARS)) {\n\t\t// literal: 0 to LZX_NUM_CHARS - 1\n\t\twindow[this->window_posn++] = main_element;\n\t\treturn 1;\n\t}\n\n\t// match: LZX_NUM_CHARS + ((slot<<3) | length_header (3 bits))\n\tmain_element -= LZX_NUM_CHARS;\n\n\t// get match length\n\tint match_length = main_element & LZX_NUM_PRIMARY_LENGTHS;\n\tif (match_length == LZX_NUM_PRIMARY_LENGTHS) {\n\t\tif (this->htlength.is_empty) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"decrunch: length symbol needed byt tree is empty\");\n\t\t}\n\t\tint length_footer = this->htlength.read_sym();\n\t\tmatch_length += length_footer;\n\t}\n\tmatch_length += LZX_MIN_MATCH;\n\n\t// get match offset\n\tunsigned match_offset = (main_element >> 3);\n\tswitch (match_offset) {\n\tcase 0:\n\t\tmatch_offset = this->R0;\n\t\tbreak;\n\tcase 1:\n\t\tmatch_offset = this->R1;\n\t\tthis->R1 = this->R0;\n\t\tthis->R0 = match_offset;\n\t\tbreak;\n\tcase 2:\n\t\tmatch_offset = this->R2;\n\t\tthis->R2 = this->R0;\n\t\tthis->R0 = match_offset;\n\t\tbreak;\n\tdefault:\n\t\tint extra = extra_bits[match_offset];\n\t\tmatch_offset = position_base[match_offset] - 2;\n\t\tif (extra > 3) {\n\t\t\t// verbatim and aligned bits\n\t\t\textra -= 3;\n\t\t\tint verbatim_bits = this->bits.read_bits(extra);\n\t\t\tmatch_offset += (verbatim_bits << 3);\n\t\t\tint aligned_bits = this->htaligned.read_sym();\n\t\t\tmatch_offset += aligned_bits;\n\t\t}\n\t\telse if (extra == 3) {\n\t\t\t// aligned bits only\n\t\t\tint aligned_bits = this->htaligned.read_sym();\n\t\t\tmatch_offset += aligned_bits;\n\t\t}\n\t\telse if (extra > 0) {\n\t\t\t// extra==1, extra==2\n\t\t\t// verbatim bits only\n\t\t\tint verbatim_bits = this->bits.read_bits(extra);\n\t\t\tmatch_offset += verbatim_bits;\n\t\t}\n\t\telse {\n\t\t\t// extra == 0\n\t\t\t// ??? not defined in LZX specification!\n\t\t\tmatch_offset = 1;\n\t\t}\n\n\t\t// update repeated offset LRU queue\n\t\tthis->R2 = this->R1;\n\t\tthis->R1 = this->R0;\n\t\tthis->R0 = match_offset;\n\t}\n\n\tif ((this->window_posn + match_length) > this->window_size) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"decrunch: match ran over window wrap\");\n\t}\n\n\t// copy match\n\tunsigned char *rundest = &window[this->window_posn];\n\tint i = match_length;\n\t// does match offset wrap the window?\n\tif (match_offset > this->window_posn) {\n\t\t// j = length from match offset to end of window\n\t\tint j = match_offset - this->window_posn;\n\t\tif (j > (int)this->window_size) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << \"decrunch: match offset beyond window boundaries\");\n\t\t}\n\t\tunsigned char *runsrc = &window[this->window_size - j];\n\t\tif (j < i) {\n\t\t\t// if match goes over the window edge, do two copy runs\n\t\t\ti -= j;\n\t\t\twhile (j-- > 0) {\n\t\t\t\t*rundest++ = *runsrc++;\n\t\t\t}\n\t\t\trunsrc = window;\n\t\t}\n\t\twhile (i-- > 0) {\n\t\t\t*rundest++ = *runsrc++;\n\t\t}\n\t}\n\telse {\n\t\tunsigned char *runsrc = rundest - match_offset;\n\t\twhile (i-- > 0) {\n\t\t\t*rundest++ = *runsrc++;\n\t\t}\n\t}\n\n\tthis->window_posn += match_length;\n\treturn match_length;\n}\n\n\nunsigned int LZXDStream::read_data_from_uncompressed_block(unsigned int size) {\n\tunsigned int remaining = size;\n\n\twhile (remaining) {\n\t\tunsigned int amount = this->bits.read_bytes(&this->window[this->window_posn], remaining);\n\t\tthis->window_posn += amount;\n\t\tremaining -= amount;\n\t}\n\n\treturn size;\n}\n\n\nunsigned LZXDStream::decompress_next_frame(unsigned char *output_buf) {\n\tif (this->bits.eof) [[unlikely]] {\n\t\treturn 0;\n\t}\n\n\t// have we reached the reset interval? (if there is one?)\n\tif (this->reset_interval && (this->frame % this->reset_interval) == 0) [[unlikely]] {\n\t\tif (this->block_remaining) [[unlikely]] {\n\t\t\tthrow Error(MSG(err) << this->block_remaining << \" bytes remaining at reset interval\");\n\t\t}\n\n\t\t// re-read the intel header and reset the huffman lengths\n\t\treset_state();\n\t}\n\n\t// read header if necessary (after initializing or resetting the stream).\n\tif (!this->header_read) [[unlikely]] {\n\t\t// the first bit of the header indicates whether the e8_magic field is present.\n\t\tif (this->bits.read_bits(1)) {\n\t\t\t// read e8_magic (32 bits).\n\t\t\tthis->e8_magic = this->bits.read_bits(16) << 16;\n\t\t\tthis->e8_magic |= this->bits.read_bits(16);\n\t\t}\n\t\telse {\n\t\t\t// e8_magic is zero.\n\t\t\tthis->e8_magic = 0;\n\t\t}\n\n\t\tthis->header_read = true;\n\t}\n\n\t// counter that stores the amount of data that has been accumulated for this frame.\n\tunsigned int frame_size = 0;\n\n\tif (this->frame_posn - this->window_posn) [[unlikely]] {\n\t\t// Warning: untested code.\n\t\t// In theory, a symbol may have overshot the last frame's boundary.\n\t\t// If it did, the amount of data would be available in frame_size.\n\n\t\tthrow Error(MSG(err) << \"untested code path: extra frame data available from last frame. \"\n\t\t                     << \"frame size = \" << this->frame_posn - this->window_posn);\n\t}\n\n\t// decode symbols until we have enough data for the frame.\n\twhile (frame_size < LZX_FRAME_SIZE) {\n\t\t// initialise next block, if one is needed\n\t\tif (this->block_remaining == 0) [[unlikely]] {\n\t\t\tif (this->bits.eof) {\n\t\t\t\t// EOF of input stream was reached at a block boundary.\n\t\t\t\t// there are no more blocks. Return the frame as-is.\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tthis->init_next_block();\n\t\t\t}\n\t\t\tcatch (Error &e) {\n\t\t\t\t// next block is beyond end of input stream\n\t\t\t\t// TODO: Figure out why this error happens\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tunsigned int symbol_size;\n\n\t\tswitch (this->block_type) {\n\t\tcase LZX_BLOCKTYPE_VERBATIM:\n\t\t\tsymbol_size = this->decode_symbol_from_verbatim_block();\n\t\t\tbreak;\n\t\tcase LZX_BLOCKTYPE_ALIGNED:\n\t\t\tsymbol_size = this->decode_symbol_from_aligned_block();\n\t\t\tbreak;\n\t\tcase LZX_BLOCKTYPE_UNCOMPRESSED:\n\t\t\tsymbol_size = this->read_data_from_uncompressed_block(std::min(this->block_remaining, LZX_FRAME_SIZE - frame_size));\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow Error(MSG(err) << \"this->blocktype neither verbatim nor aligned\");\n\t\t}\n\n\t\tif (symbol_size > this->block_remaining) [[unlikely]] {\n\t\t\t// we overshot the block boundary.\n\t\t\tthrow Error(MSG(err) << \"decrunch: overrun went past end of block by \" << symbol_size - this->block_remaining);\n\t\t}\n\n\t\tthis->block_remaining -= symbol_size;\n\t\tframe_size += symbol_size;\n\t}\n\n\tif (frame_size > LZX_FRAME_SIZE) [[unlikely]] {\n\t\t// Warning: untested code.\n\t\t// In theory, a symbol may overshoot a frame boundary. If it does, the data\n\t\t// will get re-used in the next frame, but we have to limit frame_size to LZX_FRAME_SIZE.\n\n\t\tthrow Error(MSG(err) << \"untested code path: frame_size > LZX_FRAME_SIZE\");\n\n\t\t// TODO: unreachable code\n\t\t// frame_size = LZX_FRAME_SIZE;\n\t}\n\n\t// streams don't extend over frame boundaries\n\tif (this->window_posn != this->frame_posn + frame_size) [[unlikely]] {\n\t\tthrow Error(MSG(err) << \"decrunch: decode beyond output frame limits\");\n\t}\n\n\t// copy frame data to the output buffer\n\tmemcpy(output_buf, &this->window[this->frame_posn], frame_size);\n\n\t// intel e8-postprocess the frame data\n\tthis->postprocess_intel_e8(output_buf, frame_size);\n\n\t// frame boundary; re-align the bitstream, if it's currently in bitstream mode.\n\tthis->bits.align_if_in_bitstream_mode();\n\n\t// advance frame start position\n\tthis->frame_posn += frame_size;\n\n\t// wrap window / frame position pointers\n\tif (this->window_posn == this->window_size) {\n\t\twindow_posn = 0;\n\t}\n\tif (this->frame_posn == this->window_size) {\n\t\tthis->frame_posn = 0;\n\t}\n\n\tthis->output_pos += frame_size;\n\tthis->frame++;\n\n\treturn frame_size;\n}\n\n\nvoid LZXDStream::postprocess_intel_e8(unsigned char *buf, int frame_size) {\n\tif (this->e8_magic == 0) {\n\t\t// this lzx stream is e8-processing-free\n\t\treturn;\n\t}\n\n\tif (this->frame >= 32768) {\n\t\t// in frames >= 32768 (>= 1GiB of plain data) there's no e8 processing (WTF WHY?)\n\t\treturn;\n\t}\n\n\t// search the block for occurances of '0xe8'\n\t// the last 10 bytes of the frame are not e8-handled, because reasons.\n\tfor (int pos = 0; pos < frame_size - 10; pos++) {\n\t\tbool is_e8 = (buf[pos] == 0xe8);\n\n\t\tif (!is_e8) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// we've found an E8 sequence.\n\t\t// the next 4 bytes shall be translated.\n\n\t\tint file_pos = pos + this->output_pos;\n\n\t\t// read the next 4 bytes as little endian\n\t\tint32_t abs_offset =\n\t\t\t(buf[pos + 1] << 0) | (buf[pos + 2] << 8) | (buf[pos + 3] << 16) | (buf[pos + 4] << 24);\n\n\t\tif (abs_offset >= -file_pos && abs_offset < this->e8_magic) {\n\t\t\tint32_t rel_offset;\n\t\t\tif (abs_offset >= 0) {\n\t\t\t\trel_offset = abs_offset - file_pos;\n\t\t\t}\n\t\t\telse {\n\t\t\t\trel_offset = abs_offset + this->e8_magic;\n\t\t\t}\n\n\t\t\t// write the next 4 bytes as little endian\n\t\t\tbuf[pos + 1] = static_cast<unsigned char>(rel_offset >> 0);\n\t\t\tbuf[pos + 2] = static_cast<unsigned char>(rel_offset >> 8);\n\t\t\tbuf[pos + 3] = static_cast<unsigned char>(rel_offset >> 16);\n\t\t\tbuf[pos + 4] = static_cast<unsigned char>(rel_offset >> 24);\n\t\t}\n\n\t\t// skip the next four bytes (they are what we just translated).\n\t\tpos += 4;\n\t}\n}\n\n\nLZXDecompressor::LZXDecompressor(read_callback_t read_callback,\n                                 unsigned int window_bits,\n                                 unsigned int reset_interval) :\n\tstream{new LZXDStream{std::move(read_callback), window_bits, reset_interval}} {}\n\n\nLZXDecompressor::~LZXDecompressor() {\n\tdelete stream;\n}\n\n\nunsigned LZXDecompressor::decompress_next_frame(unsigned char *output_buf) {\n\treturn this->stream->decompress_next_frame(output_buf);\n}\n\n\n} // namespace openage::util::compress\n"
  },
  {
    "path": "libopenage/util/compress/lzxd.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdlib>\n\n// pxd: from libopenage.pyinterface.functional cimport Func2\n#include \"bitstream.h\"\n\nnamespace openage {\nnamespace util {\nnamespace compress {\n\n\n// pxd: const unsigned LZX_FRAME_SIZE\nconstexpr unsigned LZX_FRAME_SIZE = 32768;\n\n\n/**\n * Wraps a LZXDStream object by reference; exposing LZXDStream directly would\n * cause too much header pollution due to its many member objects, and their\n * member objects.\n *\n * pxd:\n *\n * cppclass LZXDecompressor:\n *     LZXDecompressor(\n *         Func2[size_t, unsigned char *, size_t],\n *         unsigned int window_bits,\n *         unsigned int reset_interval\n *     ) except +\n *\n *     unsigned decompress_next_frame(unsigned char *out_buf) except +\n */\nclass OAAPI LZXDecompressor {\npublic:\n\t/*\n\t * Initialises LZX decompression state for decoding an LZX stream.\n\t *\n\t * @param read_callback      a callback method that is invoked whenever the decompressor\n\t *                           requires more data on the input stream.\n\t *                           first argument: buf (writable buffer),\n\t *                           second argument: size (amount of data requested).\n\t *                           must return: 0 on EOF, and 1 to size otherwise.\n\t *                           must fill buf [result] bytes.\n\t *                           must throw an exception on error.\n\t * @param window_bits        the size of the decoding window, which must be\n\t *                           between 15 and 21 inclusive.\n\t * @param reset_interval     the interval at which the LZX bitstream is\n\t *                           reset, in multiples of LZX frames (32678\n\t *                           bytes), e.g. a value of 2 indicates the input\n\t *                           stream resets after every 65536 output bytes.\n\t *                           A value of 0 indicates that the bitstream never\n\t *                           resets, such as in CAB LZX streams.\n\t */\n\tLZXDecompressor(read_callback_t read_callback,\n\t                unsigned int window_bits = 21,\n\t                unsigned int reset_interval = 0);\n\n\t/**\n\t * Frees the internally-allocated LZXDStream object.\n\t */\n\t~LZXDecompressor();\n\n\t/**\n\t * Decompresses a single 32-KiB frame.\n\t * Data from the input stream is read as required from read_callback.\n\t * The decoded frame is written to output_buf.\n\t * output_buf must be at least LZX_FRAME_SIZE (32768) bytes in size.\n\t *\n\t * Returns 0 in case of EOF, and the size of the frame otherwise.\n\t *\n\t * On error, an exception is thrown. After that, the object shall\n\t * be destroyed; every other operation on it may invoke undefined\n\t * behavior.\n\t */\n\tunsigned int decompress_next_frame(unsigned char *output_buf);\n\nprivate:\n\tclass LZXDStream *stream;\n\n\t// don't even think about it.\n\tLZXDecompressor(const LZXDecompressor &other) = delete;\n\tLZXDecompressor(LZXDecompressor &&other) = delete;\n\n\tLZXDecompressor &operator=(const LZXDecompressor &other) = delete;\n\tLZXDecompressor &operator=(LZXDecompressor &&other) = delete;\n};\n\n} // namespace compress\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/consteval.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdlib>\n\n\n/**\n * This namespace contains consteval functions, i.e. C++20 functions that are designed\n * to be evaluated at compile-time.\n */\nnamespace openage::util::consteval_ {\n\n/**\n * Returns true IFF the string literals have equal content.\n */\nconsteval bool streq(const char *a, const char *b) {\n\tfor (; *a == *b; ++a, ++b) {\n\t\tif (*a == '\\0') {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn false;\n}\n\n\n/**\n * Returns the length of the string literal, excluding the terminating NULL byte.\n */\nconsteval size_t strlen(const char *str) {\n\tfor (size_t len = 0;; ++len) {\n\t\tif (str[len] == '\\0') {\n\t\t\treturn len;\n\t\t}\n\t}\n}\n\n\n/**\n * Stores a string literal plus a \"length specifier\".\n *\n * Due to the nature of C strings, parts can only be cut off at the start of the string;\n * this struct represents a string literal that has parts cut off at its end.\n *\n * length represents the remaining length of the string;\n * Functions using an object of this type SHALL NOT access memory locations beyond &literal[length].\n */\nstruct truncated_string_literal {\n\tconst char *literal;\n\tsize_t length;\n};\n\n\n/**\n * Truncates a suffix from a string literal.\n *\n * Raises 'false' if str doesn't end in the given suffix.\n */\nconsteval truncated_string_literal get_prefix(const char *str, const char *suffix) {\n\tif (strlen(str) < strlen(suffix)) {\n\t\t// suffix is longer than str\n\t\tthrow false;\n\t}\n\telse if (streq(str + (strlen(str) - strlen(suffix)), suffix)) {\n\t\t// str ends with suffix\n\t\treturn truncated_string_literal{str, strlen(str) - strlen(suffix)};\n\t}\n\telse {\n\t\tthrow false;\n\t}\n}\n\n\n/**\n * Creates a truncated_string_literal from a regular string literal.\n */\nconsteval truncated_string_literal create_truncated_string_literal(const char *str) {\n\treturn truncated_string_literal{str, strlen(str)};\n}\n\n/**\n * Tests whether a string literal starts with the given prefix.\n */\nconsteval bool has_prefix(const char *str, const truncated_string_literal prefix) {\n\tfor (size_t pos = 0; pos < prefix.length; ++pos) {\n\t\tif (str[pos] != prefix.literal[pos]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n\n/**\n * Strips a prefix from a given string literal.\n *\n * If the string literal doesn't have that prefix, returns the string literal itself.\n */\nconsteval const char *strip_prefix(const char *str, const truncated_string_literal prefix) {\n\tif (has_prefix(str, prefix)) {\n\t\treturn str + prefix.length;\n\t}\n\telse {\n\t\treturn str;\n\t}\n}\n\n\n/**\n * Strips a prefix, given as const char *, from a given string literal.\n *\n * If the string literal doesn't have that prefix, returns the string literal itself.\n */\nconsteval const char *strip_prefix(const char *str, const char *prefix) {\n\treturn strip_prefix(str, create_truncated_string_literal(prefix));\n}\n\n} // namespace openage::util::consteval_\n"
  },
  {
    "path": "libopenage/util/constinit_vector.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"constinit_vector.h\"\n\n#include \"../testing/testing.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\n\n// no linkage for this test type\nnamespace {\n\n\nstruct S {\n\tint val;\n};\n\n\nclass T {\npublic:\n\texplicit T(int *ref)\n\t\t:\n\t\tref{ref} {\n\n\t\t*(this->ref) += 1;\n\t}\n\n\t~T() {\n\t\tif (this->ref) {\n\t\t\t*(this->ref) -= 1;\n\t\t}\n\t}\n\n\tT(const T &other) : ref{other.ref} {\n\t\t(*this->ref) += 1;\n\t}\n\n\tT &operator =(const T &other) {\n\t\tthis->ref = other.ref;\n\t\t(*this->ref) += 1;\n\t\treturn *this;\n\t}\n\nprivate:\n\tint *ref;\n};\n\n\n} // anonymous namespace\n\n\n\nvoid constinit_vector() {\n\tConstInitVector<S> sv;\n\n\tfor (int i = 0; i < 1337; i++) {\n\t\tsv.push_back(S{i});\n\t}\n\n\tsv.size() == 1337 or TESTFAIL;\n\tsv[235].val == 235 or TESTFAIL;\n\n\tint refctr = 0;\n\n\t{ // scope to test whether tv gets correctly de-inited.\n\n\tConstInitVector<T> tv;\n\tfor (int i = 0; i < 1337; i++) {\n\t\ttv.push_back(T(&refctr));\n\t}\n\n\trefctr == 1337 or TESTFAIL;\n\tsv.size() == 1337 or TESTFAIL;\n\n\t}\n\n\trefctr == 0 or TESTFAIL;\n}\n\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/constinit_vector.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n#include <memory>\n\n#include \"compiler.h\"\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * A std::vector-like object that has a constexpr constructor, and thus can be\n * initialized during the const- or zero-initialization phase, before any\n * dynamic initialization happens.\n *\n * Use this class if and only if you need that functionality (that should\n * be pretty uncommon; there generally are better ways of guaranteeing dynamic\n * initialization order, such as static function variables).\n */\ntemplate <typename T>\nclass ConstInitVector {\npublic:\n\tconstexpr ConstInitVector() noexcept :\n\t\tdata{nullptr}, capacity{16}, count{0} {}\n\n\n\t~ConstInitVector() {\n\t\tstd::allocator<T> alloc;\n\n\t\tif (this->data != nullptr) {\n\t\t\tfor (size_t i = 0; i < this->count; i++) {\n\t\t\t\t(this->data[i]).~T();\n\t\t\t}\n\t\t\talloc.deallocate(this->data, this->capacity);\n\t\t}\n\t}\n\n\n\t/**\n\t * Copying this is not supported.\n\t */\n\tConstInitVector(const ConstInitVector<T> &other) = delete;\n\tConstInitVector(ConstInitVector<T> &&other) = delete;\n\tConstInitVector &operator=(const ConstInitVector<T> &other) = delete;\n\tConstInitVector &operator=(ConstInitVector<T> &&other) = delete;\n\n\n\tvoid push_back(const T &val) {\n\t\tstd::allocator<T> alloc;\n\n\t\tif (this->data == nullptr) [[unlikely]] {\n\t\t\t// obj is fresh and needs to allocate memory.\n\t\t\tthis->data = alloc.allocate(this->capacity);\n\t\t}\n\n\t\tif (this->count == capacity) [[unlikely]] {\n\t\t\t// obj is full and needs to resize memory.\n\t\t\tsize_t newcapacity = capacity * 2;\n\t\t\tT *newdata = alloc.allocate(newcapacity);\n\t\t\tfor (size_t i = 0; i < this->capacity; i++) {\n\t\t\t\tnew (static_cast<void *>(&newdata[i])) T(std::move_if_noexcept(this->data[i]));\n\t\t\t\t(this->data[i]).~T();\n\t\t\t}\n\t\t\talloc.deallocate(this->data, this->capacity);\n\t\t\tthis->data = newdata;\n\t\t\tthis->capacity = newcapacity;\n\t\t}\n\n\t\t// add val at the end.\n\t\tnew (static_cast<void *>(&this->data[this->count])) T(val);\n\t\tthis->count += 1;\n\t}\n\n\n\t/**\n\t * The returned reference is invalid if n >= this->size().\n\t * It may be invalidated by a call to push_back().\n\t */\n\tconst T &operator[](size_t idx) const {\n\t\treturn this->data[idx];\n\t}\n\n\n\t/**\n\t * Returns the number of elements that have been pushed back.\n\t */\n\tsize_t size() {\n\t\treturn this->count;\n\t}\n\nprivate:\n\tT *data;\n\tsize_t capacity;\n\tsize_t count;\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/enum.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"enum.h\"\n\nnamespace openage {\nnamespace util {\n\n// Enum is all templated, so there's nothing to implement.\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/enum.h",
    "content": "// Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <iostream>\n#include <typeinfo>\n\n#include \"compiler.h\"\n\n// pxd: from libcpp cimport bool\n\n\nnamespace openage {\nnamespace util {\n\n/*\n * C++'s enum class has various deficits:\n *\n *  - Wrapping it via Cython requires hacks.\n *  - enum objects can't have any associated data.\n *  - they can't have member methods\n *\n * In cases where those features are required, we recommend to use references\n * to non-copyable objects instead of enum values. Which is what this class provides.\n *\n * This class provides is a simple base which\n *  - forbids copying\n *  - provides an equality operator which compares the memory addresses of the objects.\n *  - contains a const char *name, for use in operator <<\n *  - provides operator <<\n *  - contains a numeric value, as enums do\n *  - provides comparison operators which use the numeric value\n *\n * In order to contain the const references to these objects,\n * and as a namespace for static objects which contain the values,\n * use the Enum class below.\n *\n * For a full usage example, see enum_test.{h, cpp}.\n *\n * pxd:\n *\n * cppclass EnumValue[DerivedType, NumericType]:\n *     const char *name\n *     NumericType numeric\n */\ntemplate <typename DerivedType, typename NumericType = int>\nclass EnumValue {\npublic:\n\tconstexpr EnumValue(const char *value_name, NumericType numeric_value) :\n\t\tname(value_name), numeric(numeric_value) {}\n\n\t// enum values cannot be copied\n\tEnumValue(const EnumValue &other) = delete;\n\tEnumValue &operator=(const EnumValue &other) = delete;\n\n\t// an explicit deletion of the implicitly defined copy constructor and assignment operator\n\t// will implicitly delete the implicitly defined move constructor and assignment operator.\n\t// yay for C++\n\n\t// enum values are equal if the pointers are equal.\n\tconstexpr bool operator==(const DerivedType &other) const {\n\t\treturn (this == &other);\n\t}\n\n\tconstexpr bool operator!=(const DerivedType &other) const {\n\t\treturn !(*this == other);\n\t}\n\n\tconstexpr bool operator<=(const DerivedType &other) const {\n\t\treturn this->numeric <= other.numeric;\n\t}\n\n\tconstexpr bool operator<(const DerivedType &other) const {\n\t\treturn this->numeric < other.numeric;\n\t}\n\n\tconstexpr bool operator>=(const DerivedType &other) const {\n\t\treturn this->numeric >= other.numeric;\n\t}\n\n\tconstexpr bool operator>(const DerivedType &other) const {\n\t\treturn this->numeric > other.numeric;\n\t}\n\n\tfriend std::ostream &operator<<(std::ostream &os, const DerivedType &arg) {\n\t\tos << util::typestring<DerivedType>() << \"::\" << arg.name;\n\t\treturn os;\n\t}\n\n\tconst char *name;\n\tNumericType numeric;\n};\n\n\n/**\n * Container for possible enum values.\n * Inherit from this class with CRTP.\n *\n * This class should have the various values defined as `static constexpr`\n * members.\n * Due to the C++ standard, individual enum values must be declared\n * as `static constexpr` members of a class.\n * If they are declared as `static constexpr` constants in a namespace instead,\n * the linker will place the constant in the ELF file once per object file\n * instead of deduplicating it.\n *\n * Objects of this class contain a reference to a static constexpr value.\n *\n * The static constexpr values can be accessed through `operator ->`.\n *\n * See the documentation for the EnumValue class above.\n *\n * pxd:\n *\n * cppclass Enum[DerivedType]:\n *     const DerivedType &get() except +\n *\n *     bool operator ==(Enum[DerivedType] other) except +\n *     bool operator !=(Enum[DerivedType] other) except +\n *\n *     bool operator <(Enum[DerivedType] other) except +\n *     bool operator >(Enum[DerivedType] other) except +\n *     bool operator <=(Enum[DerivedType] other) except +\n *     bool operator >=(Enum[DerivedType] other) except +\n */\ntemplate <typename DerivedType>\nclass Enum {\n\tusing this_type = Enum<DerivedType>;\n\npublic:\n\t// disallow the empty constructor to ensure that value is always a valid pointer.\n\tconstexpr Enum() = delete;\n\tconstexpr Enum(const DerivedType &value) :\n\t\tvalue{&value} {}\n\n\tconstexpr explicit operator const DerivedType &() const {\n\t\treturn *this->value;\n\t}\n\n\tconstexpr Enum &operator=(const DerivedType &value) {\n\t\tthis->value = &value;\n\t\treturn *this;\n\t}\n\n\tconstexpr const DerivedType *operator->() const {\n\t\treturn this->value;\n\t}\n\n\tconstexpr bool operator==(const this_type &other) const {\n\t\treturn *this->value == *other.value;\n\t}\n\n\tconstexpr bool operator!=(const this_type &other) const {\n\t\treturn *this->value != *other.value;\n\t}\n\n\tconstexpr bool operator<=(const this_type &other) const {\n\t\treturn *this->value <= *other.value;\n\t}\n\n\tconstexpr bool operator<(const this_type &other) const {\n\t\treturn *this->value < *other.value;\n\t}\n\n\tconstexpr bool operator>=(const this_type &other) const {\n\t\treturn *this->value >= *other.value;\n\t}\n\n\tconstexpr bool operator>(const this_type &other) const {\n\t\treturn *this->value > *other.value;\n\t}\n\n\tconstexpr const DerivedType &get() const {\n\t\treturn *this->value;\n\t}\n\n\tfriend std::ostream &operator<<(std::ostream &os, const this_type &arg) {\n\t\tos << *arg.value;\n\t\treturn os;\n\t}\n\nprotected:\n\tconst DerivedType *value;\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/enum_test.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"enum_test.h\"\n\n#include \"../testing/testing.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\nstatic constexpr testenum_value undefined {{\"UNDEFINED\", 0}, \"undefined test string\"};\ntestenum::testenum() : Enum{undefined} {}\n\nvoid enum_() {\n\ttestenum tv0 = testenum::foo;\n\ttv0 == testenum::foo or TESTFAIL;\n\ttv0 != testenum::bar or TESTFAIL;\n\n\ttestenum tv1 = testenum::bar;\n\ttv0 == tv1 and TESTFAIL;\n\n\ttv1 = testenum::foo;\n\ttv0 == tv1 or TESTFAIL;\n\ttv0 != tv1 and TESTFAIL;\n\n\tTESTEQUALS(tv0->get_stuff(), std::string(\"foooooooooooooooooo\"));\n\tTESTEQUALS(tv0->stuff, std::string(\"foooooooooooooooooo\"));\n\n\t// \"default value\"\n\ttestenum tv2;\n\tTESTEQUALS(tv2->stuff, std::string(\"undefined test string\"));\n\n\ttv2 != tv0 or TESTFAIL;\n\ttv2 != tv1 or TESTFAIL;\n\ttv2 == tv2 or TESTFAIL;\n\n\tFString fstr;\n\tfstr << tv0;\n\tTESTEQUALS(fstr.buffer, \"openage::util::tests::testenum_value::foo\");\n\n\tfstr.reset();\n\tfstr << tv2;\n\tTESTEQUALS(fstr.buffer, \"openage::util::tests::testenum_value::UNDEFINED\");\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/enum_test.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libopenage.util.enum cimport Enum, EnumValue\n#include \"enum.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\n/**\n * pxd:\n *\n * cppclass testenum_value(EnumValue[testenum_value, int]):\n *     int numeric\n *     const char *name\n *     const char *stuff\n */\nstruct OAAPI testenum_value : EnumValue<testenum_value> {\n\tconst char *stuff;\n\n\tconst char *get_stuff() const {\n\t\treturn stuff;\n\t}\n};\n\n/**\n * pxd:\n *\n * cppclass testenum(Enum[testenum_value]):\n *     testenum_value *value\n *\n * testenum foo \"::openage::util::tests::testenum::foo\"\n * testenum bar \"::openage::util::tests::testenum::bar\"\n */\nstruct OAAPI testenum : Enum<testenum_value> {\n\t// this is always required for classes which inherit from Enum.\n\t// it allows the constructors to work as expected.\n\tusing util::Enum<testenum_value>::Enum;\n\n\t// a default constructor for testenum.\n\t// this is needed if we wish to allow use from Cython.\n\t// initializes the testenum to an internal UNDEFINED value.\n\ttestenum();\n\n\tstatic constexpr testenum_value foo{{\"foo\", 1}, \"foooooooooooooooooo\"};\n\tstatic constexpr testenum_value bar{{\"bar\", 2}, \"barrrrrrrrrrrrrrrrr\"};\n};\n\n} // namespace tests\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/externalprofiler.cpp",
    "content": "// Copyright 2014-2018 the openage authors. See copying.md for legal info.\n\n#include \"externalprofiler.h\"\n\n#include <string>\n\n#include \"config.h\"\n\n#if WITH_GPERFTOOLS_PROFILER\n#include <gperftools/profiler.h>\n#endif\n\n#include \"subprocess.h\"\n#include \"os.h\"\n#include \"../log/log.h\"\n\n\nnamespace openage {\nnamespace util {\n\n\nExternalProfiler::ExternalProfiler()\n\t:\n\tcurrently_profiling{false},\n\tcan_profile{WITH_GPERFTOOLS_PROFILER} {}\n\n\nconst char *const ExternalProfiler::profiling_filename = \"/tmp/openage-gperftools-cpuprofile\";\nconst char *const ExternalProfiler::profiling_pdf_filename = \"/tmp/openage-gperftools-cpuprofile.pdf\";\n\n\nvoid ExternalProfiler::start() {\n\tif (not this->can_profile) {\n\t\tlog::log(MSG(err) << \"Can not profile: gperftools is missing\");\n\t\treturn;\n\t}\n\n\tif (this->currently_profiling) {\n\t\tlog::log(MSG(info) << \"Profiler is already running\");\n\t\treturn;\n\t}\n\n\tlog::log(MSG(info) << \"Starting profiler; writing data to \" << this->profiling_filename);\n\n\tthis->currently_profiling = true;\n#if WITH_GPERFTOOLS_PROFILER\n\tProfilerStart(this->profiling_filename);\n#endif\n}\n\n\nvoid ExternalProfiler::stop() {\n\tif (not this->can_profile) {\n\t\tlog::log(MSG(err) << \"Can not profile: gperftools is missing\");\n\t\treturn;\n\t}\n\n\tif (not this->currently_profiling) {\n\t\tlog::log(MSG(err) << \"Profiler is not currently running\");\n\t\treturn;\n\t}\n\n\tthis->currently_profiling = false;\n#if WITH_GPERFTOOLS_PROFILER\n\tProfilerStop();\n#endif\n\n\tlog::log(MSG(info) << \"Profiler stopped; data written to \" << this->profiling_filename);\n}\n\n\nvoid ExternalProfiler::show_results() {\n\tif (not this->can_profile) {\n\t\tlog::log(MSG(err) << \"Can not profile: gperftools is missing\");\n\t\treturn;\n\t}\n\n\tif (this->currently_profiling) {\n\t\tlog::log(MSG(warn) << \"Profiler is currently running; trying to show results anyway\");\n\t}\n\n#if WITH_GPERFTOOLS_PROFILER\n\tstd::string pprof_path = subprocess::which(\"google-pprof\");\n\n\t// fallback to pprof\n\tif (pprof_path.size() == 0) {\n\t\tpprof_path = subprocess::which(\"pprof\");\n\t}\n\n\tif (pprof_path.size() == 0) {\n\t\tlog::log(ERR << \"Can not process profiling results: google-pprof or pprof not found in PATH\");\n\t\treturn;\n\t}\n\n\tint retval = subprocess::call(\n\t\t{\n\t\t\tpprof_path.c_str(),\n\t\t\t\"--pdf\",\n\t\t\t\"--\",\n\t\t\tos::self_exec_filename().c_str(),\n\t\t\tthis->profiling_filename,\n\t\t\tnullptr\n\t\t},\n\t\ttrue,\n\t\tthis->profiling_pdf_filename);\n\n\tif (retval != 0) {\n\t\tlog::log(MSG(err) << \"Profile analysis failed: \" << retval);\n\t\treturn;\n\t}\n\n\tretval = os::execute_file(this->profiling_pdf_filename);\n\n\tif (retval != 0) {\n\t\tlog::log(MSG(err) <<\n\t\t\t\"Could not view profiling visualization \" <<\n\t\t\tthis->profiling_pdf_filename << \": \" << retval);\n\t\treturn;\n\t}\n#endif\n}\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/externalprofiler.h",
    "content": "// Copyright 2014-2016 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage {\nnamespace util {\n\nclass ExternalProfiler {\npublic:\n\tExternalProfiler();\n\n\tstatic const char *const profiling_filename;\n\tstatic const char *const profiling_pdf_filename;\n\n\tbool currently_profiling;\n\tconst bool can_profile;\n\n\tvoid start();\n\tvoid stop();\n\n\tvoid show_results();\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/externalsstream.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"externalsstream.h\"\n\nnamespace openage {\nnamespace util {\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/externalsstream.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include <iostream>\n#include <string>\n\n#pragma once\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Used by ExternalOStringStream; data is written to the external accumulator.\n *\n * If this object's write functionality is accessed before output has been\n * manually set to a valid std::string object, or if the lifetime of that\n * object has been exceeded, undefined behavior will result.\n */\nclass ExternalStringBuf : public std::streambuf {\npublic:\n\tstd::string *output;\n\n\tint overflow(int chr) override {\n\t\toutput->push_back(static_cast<char>(chr));\n\t\treturn chr;\n\t}\n\n\tstd::streamsize xsputn(const char *s, std::streamsize count) override {\n\t\toutput->append(s, static_cast<size_t>(count));\n\t\treturn count;\n\t}\n};\n\n\n/**\n * Similar to std::ostringstream, but data is written to an external string\n * object.\n *\n * Using this stream before calling use_with will result in undefined behavior.\n */\nclass ExternalOStringStream : public std::ostream {\npublic:\n\t/**\n\t * Creates a stream without a valid accumulator.\n\t */\n\texplicit ExternalOStringStream() :\n\t\tstd::ostream{&this->buf} {}\n\n\t/**\n\t * Resets the stream's flags, and sets ptr as the internally-used\n\t * accumulator.\n\t */\n\tvoid use_with(std::string &output) {\n\t\tthis->buf.output = &output;\n\t}\n\nprivate:\n\tExternalStringBuf buf;\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fds.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include \"fds.h\"\n\n#include <cstdarg>\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n\n#include \"pty.h\"\n#include <fcntl.h>\n#ifdef _WIN32\n\t#include <io.h>\n#else\n\t#include <unistd.h>\n#endif\n\n#include \"unicode.h\"\n\nnamespace openage::util {\n\nFD::FD(int fd) {\n\tthis->fd = fd;\n}\n\nFD::FD(int fd, bool set_nonblocking) {\n\tthis->fd = ::dup(fd);\n\tthis->close_on_destroy = true;\n\n\tif (set_nonblocking) {\n#ifndef _WIN32\n\t\tint flags = ::fcntl(this->fd, F_GETFL, 0);\n\t\t::fcntl(this->fd, F_SETFL, flags | O_NONBLOCK);\n#endif\n\t}\n}\n\nFD::FD(FD *fd, bool set_nonblocking) :\n\tFD{fd->fd, set_nonblocking} {\n}\n\nFD::~FD() {\n\tif (this->restore_input_mode_on_destroy) {\n\t\tthis->restoreinputmode();\n\t}\n\n\tif (this->close_on_destroy) {\n\t\t::close(this->fd);\n\t}\n}\n\nint FD::write(const char *buf, size_t bytes) {\n\treturn ::write(this->fd, buf, bytes);\n}\n\nint FD::puts(const char *str) {\n\treturn this->write(str, strlen(str));\n}\n\nint FD::putbyte(char c) {\n\treturn this->write(&c, 1);\n}\n\nint FD::putcp(int cp) {\n\tchar utf8buf[5];\n\tif (util::utf8_encode(cp, utf8buf) == 0) {\n\t\t// unrepresentable character (question mark in black rhombus)\n\t\treturn this->puts(\"\\uFFFD\");\n\t}\n\telse {\n\t\treturn this->puts(utf8buf);\n\t}\n}\n\nint FD::printf(const char *format, ...) {\n\tconst unsigned buf_size = 16;\n\tchar *buf = static_cast<char *>(malloc(sizeof(char) * buf_size));\n\tif (!buf) {\n\t\treturn -1;\n\t}\n\n\tva_list vl;\n\n\t// first, try to vsnprintf to a buffer of length 16\n\tva_start(vl, format);\n\tunsigned len = vsnprintf(buf, buf_size, format, vl);\n\tva_end(vl);\n\n\t// if that wasn't enough, allocate more memory and try again\n\tif (len >= buf_size) {\n\t\tchar *oldbuf = buf;\n\t\tbuf = static_cast<char *>(realloc(oldbuf, sizeof(char) * (len + 1)));\n\t\tif (!buf) {\n\t\t\tfree(oldbuf);\n\t\t\treturn -1;\n\t\t}\n\n\t\tva_start(vl, format);\n\t\tvsnprintf(buf, len + 1, format, vl);\n\t\tva_end(vl);\n\t}\n\n\t// output buf to the socket\n\tint result = this->puts(buf);\n\n\t// free the buffer\n\tfree(buf);\n\n\treturn result;\n}\n\nvoid FD::setinputmodecanon() {\n#ifndef _WIN32\n\n\tif (::isatty(this->fd)) {\n\t\t// get the terminal settings for stdin\n\t\t::tcgetattr(this->fd, &this->old_tio);\n\t\t// backup old settings\n\t\tstruct termios new_tio = this->old_tio;\n\t\t// disable buffered i/o (canonical mode) and local echo\n\t\tnew_tio.c_lflag &= (~ICANON & ~ECHO & ~ISIG);\n\t\t// set the settings\n\t\t::tcsetattr(this->fd, TCSANOW, &new_tio);\n\t\tthis->restore_input_mode_on_destroy = true;\n\t}\n\n#endif /* _WIN32 */\n}\n\nvoid FD::restoreinputmode() {\n#ifndef _WIN32\n\n\tif (::isatty(this->fd)) {\n\t\t::tcsetattr(this->fd, TCSANOW, &this->old_tio);\n\t\tthis->restore_input_mode_on_destroy = false;\n\t}\n\n#endif /* _WIN32 */\n}\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/fds.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <stdlib.h>\n\n#ifndef _WIN32\n\t#include <termios.h>\n#endif\n\nnamespace openage {\nnamespace util {\n\n/**\n * wraps a POSIX file descriptor\n */\nclass FD {\npublic:\n\t/**\n\t * wraps an existing FD\n\t */\n\tFD(int fd);\n\n\t/**\n\t * duplicates an existing FD, and optionally\n\t * sets it to non-blocking mode\n\t */\n\tFD(int fd, bool set_nonblocking);\n\tFD(FD *fd, bool set_nonblocking);\n\n\t~FD();\n\n\tint fd;\n\n\t/**\n\t * if this is set to true, the destructor will close the fd.\n\t * will be set to true by the constructor iff the fd was duped.\n\t */\n\tbool close_on_destroy = false;\n\n\t/**\n\t * if this is set to true, the destructor will restore the input\n\t * mode. will be set on setinputmodecanon().\n\t */\n\tbool restore_input_mode_on_destroy = false;\n\n\t/**\n\t * writes 'bytes' bytes from buf\n\t */\n\tint write(const char *buf, size_t bytes);\n\n\t/**\n\t * writes the string (excluding the null terminator)\n\t */\n\tint puts(const char *str);\n\n\t/**\n\t * writes the char\n\t */\n\tint putbyte(char c);\n\n\t/**\n\t * writes the unicode codepoint, as utf-8\n\t */\n\tint putcp(int cp);\n\n\t/**\n\t * guess what this does. because I won't tell you.\n\t */\n\tint printf(const char *format, ...);\n\n\t/**\n\t * sets input to canonical mode.\n\t * this includes:\n\t * - disabling ECHO\n\t * - non-buffered input\n\t * - no generation of signals on ^C, ^Z, ...\n\t */\n\tvoid setinputmodecanon();\n\n\t/**\n\t * restores the input mode to the mode that was\n\t * backed up during the last setinputmodecanon() mode.\n\t */\n\tvoid restoreinputmode();\n\n#ifndef _WIN32\n\tstruct termios old_tio;\n#endif\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/file.cpp",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#include \"file.h\"\n\n#include <cstdio>\n#include <fstream>\n#include <string>\n#include <sys/stat.h>\n#include <sys/types.h>\n#include <utility>\n\n#include \"error/error.h\"\n#include \"log/log.h\"\n\n#include \"util/filelike/native.h\"\n#include \"util/filelike/python.h\"\n#include \"util/fslike/directory.h\"\n#include \"util/path.h\"\n#include \"util/strings.h\"\n\n\nnamespace openage::util {\n\n\nFile::File() = default;\n\n\n// yes. i'm sorry. but cython can't enum class yet.\nFile::File(const std::string &path, int mode) :\n\tFile{path, static_cast<mode_t>(mode)} {}\n\n\nFile::File(const std::string &path, mode_t mode) {\n\tthis->filelike = std::make_shared<filelike::Native>(path, mode);\n}\n\n\nFile::File(std::shared_ptr<filelike::FileLike> filelike) :\n\tfilelike{filelike} {}\n\n\nFile::File(const py::Obj &filelike) {\n\tthis->filelike = std::make_shared<filelike::Python>(filelike);\n}\n\n\nstd::string File::read(ssize_t max) {\n\treturn this->filelike->read(max);\n}\n\n\nsize_t File::read_to(void *buf, ssize_t max) {\n\treturn this->filelike->read_to(buf, max);\n}\n\n\nbool File::readable() {\n\treturn this->filelike->readable();\n}\n\n\nvoid File::write(const std::string &data) {\n\tthis->filelike->write(data);\n}\n\n\nbool File::writable() {\n\treturn this->filelike->writable();\n}\n\n\nvoid File::seek(ssize_t offset, seek_t how) {\n\tthis->filelike->seek(offset, how);\n}\n\n\nbool File::seekable() {\n\treturn this->filelike->seekable();\n}\n\n\nsize_t File::tell() {\n\treturn this->filelike->tell();\n}\n\n\nvoid File::close() {\n\tthis->filelike->close();\n}\n\n\nvoid File::flush() {\n\tthis->filelike->flush();\n}\n\n\nssize_t File::size() {\n\treturn this->filelike->get_size();\n}\n\n\nstd::vector<std::string> File::get_lines() {\n\t// TODO: relay the get_lines to the underlaying filelike\n\t//       which may do a better job in getting the lines.\n\t//       instead, we read everything and then split up into lines.\n\tstd::vector<std::string> result = util::split_newline(this->read());\n\n\treturn result;\n}\n\n\nstd::shared_ptr<filelike::FileLike> File::get_fileobj() const {\n\treturn this->filelike;\n}\n\n\nstd::ostream &operator<<(std::ostream &stream, const File &file) {\n\tstream << \"File(\";\n\tfile.filelike->repr(stream);\n\tstream << \")\";\n\n\treturn stream;\n}\n\nFile File::get_temp_file(bool executable) {\n\tfslike::Directory temp_dir = fslike::Directory::get_temp_directory();\n\tstd::string file_name = std::tmpnam(nullptr);\n\tstd::ostringstream dir_path;\n\ttemp_dir.repr(dir_path);\n\n\tif (executable) {\n\t\t// 0755 == rwxr-xr-x\n\t\tFile file_wrapper = File(dir_path.str() + file_name, 0755);\n\t\treturn file_wrapper;\n\t}\n\n\t// 0644 == rw-r--r--\n\tFile file_wrapper = File(dir_path.str() + file_name, 0644);\n\treturn file_wrapper;\n}\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/file.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp.memory cimport shared_ptr\n#include <memory>\n// pxd: from libcpp.string cimport string\n#include <string>\n#include <vector>\n\n// pxd: from libopenage.util.filelike.filelike cimport FileLike\n#include \"filelike/filelike.h\"\n// pxd: from libopenage.pyinterface.pyobject cimport PyObj\n#include \"../pyinterface/pyobject.h\"\n\n// pxd: from libopenage.util.path cimport Path\n\nnamespace openage {\nnamespace util {\n\nclass Path;\n\n\n/**\n * Generic File implementation, used in our filesystem-like and file-like\n * abtraction system. Can be created from Python :)\n *\n * TODO: maybe inherit from std::iostream so we can use it the c++-way\n *\n * pxd:\n *\n * cppclass File:\n *     File() noexcept\n *     File(const string &path, int mode) except +\n *     File(PyObj) except +\n *\n *     shared_ptr[FileLike] get_fileobj() except +\n */\nclass OAAPI File {\npublic:\n\tusing seek_t = filelike::FileLike::seek_t;\n\tusing mode_t = filelike::FileLike::mode_t;\n\n\t/**\n\t * Empty constructor, pls don't use.\n\t * It only exists because Cython needs it.\n\t */\n\tFile();\n\n\t/**\n\t * Open a filesystem path, int version.\n\t * this is just the integer value of the mode_t, because Cython...\n\t */\n\tFile(const std::string &path, int mode);\n\n\t/**\n\t * Open a filesystem path.\n\t */\n\tFile(const std::string &path, mode_t mode = mode_t::R);\n\n\t/**\n\t * Create a file from an already created filelike.\n\t */\n\tFile(std::shared_ptr<filelike::FileLike> filelike);\n\n\t/**\n\t * Wraps a python filelike object that is already open.\n\t * This is called from Cython.\n\t *\n\t * You will probably never call that manually.\n\t */\n\tFile(const py::Obj &filelike);\n\n\tvirtual ~File() = default;\n\n\t/**\n\t * Read data from the file and return a string.\n\t * If max is negative, return the full remaining file.\n\t */\n\tstd::string read(ssize_t max = -1);\n\n\t/**\n\t * Read data from the file into a buffer,\n\t * which has to be large enough to fit max bytes.\n\t * If max is negative, read the full remaining file,\n\t * so buf has to be as big as the remaining file size.\n\t *\n\t * Returns the number of bytes that were read.\n\t */\n\tsize_t read_to(void *buf, ssize_t max = -1);\n\tbool readable();\n\tvoid write(const std::string &data);\n\tbool writable();\n\tvoid seek(ssize_t offset, seek_t how = seek_t::SET);\n\tbool seekable();\n\tsize_t tell();\n\tvoid close();\n\tvoid flush();\n\tssize_t size();\n\tstd::vector<std::string> get_lines();\n\tstd::shared_ptr<filelike::FileLike> get_fileobj() const;\n\n\tstatic File get_temp_file(bool executable = false);\n\nprotected:\n\tstd::shared_ptr<filelike::FileLike> filelike;\n\n\tfriend std::ostream &operator<<(std::ostream &stream, const File &file);\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/filelike/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tfilelike.cpp\n\tnative.cpp\n\tpython.cpp\n)\n\npxdgen(\n\tfilelike.h\n\tpython.h\n)\n"
  },
  {
    "path": "libopenage/util/filelike/filelike.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"filelike.h\"\n\nnamespace openage::util::filelike {\n\n\nFileLike::FileLike() = default;\n\nbool FileLike::is_python_native() const noexcept {\n\treturn false;\n}\n\n} // openage::util::filelike\n"
  },
  {
    "path": "libopenage/util/filelike/filelike.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool\n\n#include <iostream>\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"../compiler.h\"\n\nnamespace openage {\nnamespace util {\nnamespace filelike {\n\n/**\n * File-like class that has the standard operations like\n * read, seek, write etc.\n *\n * pxd:\n * ctypedef enum seek_t \"openage::util::filelike::FileLike::seek_t\":\n *     seek_t_SET \"::openage::util::filelike::FileLike::seek_t::SET\" = 0\n *     seek_t_CUR \"::openage::util::filelike::FileLike::seek_t::CUR\" = 1\n *     seek_t_END \"::openage::util::filelike::FileLike::seek_t::END\" = 2\n *\n * cppclass FileLike:\n *     string read(ssize_t max) except +\n *     bool readable() except +\n *     void write(const string &data) except +\n *     bool writable() except +\n *     void seek(ssize_t offset, seek_t how) except +\n *     bool seekable() except +\n *     size_t tell() except +\n *     void close() except +\n *     void flush() except +\n *     ssize_t get_size() except +\n *\n *     bool is_python_native() noexcept\n */\nclass OAAPI FileLike {\npublic:\n\t/**\n\t * Seek reference point.\n\t *\n\t * Don't change the numbers, they're used from Cython.\n\t */\n\tenum class seek_t : int {\n\t\tSET = 0, //!< offset from file beginning\n\t\tCUR = 1, //!< offset from current position\n\t\tEND = 2  //!< offset from file end\n\t};\n\n\t/**\n\t * File access mode.\n\t *\n\t * Sync the numbers with the fslike/cpp.pyx\n\t * because Cython can't enum class yet.\n\t * TODO: once cython can handle enum class, add pxd annotation.\n\t */\n\tenum class mode_t : int {\n\t\tR = 0,\n\t\tW = 1,\n\t\tRW = 2,\n\t\tA = 3,\n\t\tAR = 4,\n\t};\n\n\tFileLike();\n\n\tvirtual ~FileLike() = default;\n\n\tvirtual std::string read(ssize_t max = -1) = 0;\n\tvirtual size_t read_to(void *buf, ssize_t max = -1) = 0;\n\n\tvirtual bool readable() = 0;\n\n\tvirtual void write(const std::string &data) = 0;\n\n\tvirtual bool writable() = 0;\n\n\tvirtual void seek(ssize_t offset, seek_t how = seek_t::SET) = 0;\n\tvirtual bool seekable() = 0;\n\tvirtual size_t tell() = 0;\n\tvirtual void close() = 0;\n\tvirtual void flush() = 0;\n\tvirtual ssize_t get_size() = 0;\n\n\tvirtual bool is_python_native() const noexcept;\n\n\t/** string representation of the filelike */\n\tvirtual std::ostream &repr(std::ostream &) = 0;\n};\n\n\n} // namespace filelike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/filelike/native.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"native.h\"\n\n#include <sstream>\n\n#include \"../../error/error.h\"\n\n\nnamespace openage::util::filelike {\n\nNative::Native(const std::string &path, mode_t mode)\n\t:\n\tpath{path},\n\tmode{mode} {\n\n\tstd::ios_base::openmode open_mode;\n\n\tswitch (this->mode) {\n\tcase mode_t::R:\n\t\topen_mode = std::ios_base::in;\n\t\tbreak;\n\tcase mode_t::W:\n\t\topen_mode = std::ios_base::out;\n\t\tbreak;\n\tcase mode_t::RW:\n\t\topen_mode = std::ios_base::in | std::ios_base::out;\n\t\tbreak;\n\tcase mode_t::A:\n\t\topen_mode = std::ios_base::out | std::ios_base::ate;\n\t\tbreak;\n\tcase mode_t::AR:\n\t\topen_mode = std::ios_base::in | std::ios_base::out | std::ios_base::ate;\n\t\tbreak;\n\tdefault:\n\t\tthrow Error{ERR << \"unknown open mode\"};\n\t}\n\n\t// Open in binary mode to avoid stupid behaviour on Windows\n\topen_mode |= std::ios_base::binary;\n\n\tthis->file.open(this->path, open_mode);\n\n\tif (not this->file.is_open()) {\n\t\tthrow Error{ERR << \"file not found: \" << path};\n\t}\n}\n\n\nNative::~Native() = default;\n\n\nstd::string Native::read(ssize_t max) {\n\t// read whole file:\n\tif (max < 0) {\n\t\tstd::string ret;\n\n\t\t// get remaining size and prepare the string size\n\t\t// to avoid resizing\n\t\tsize_t pos = this->file.tellg();\n\t\tthis->file.seekg(0, std::ios::end);\n\t\tret.reserve(static_cast<size_t>(this->file.tellg()) - pos);\n\t\tthis->file.seekg(pos, std::ios::beg);\n\n\t\tret.assign(std::istreambuf_iterator<char>(this->file),\n\t\t           std::istreambuf_iterator<char>());\n\n\t\treturn ret;\n\t}\n\t// read `max` bytes\n\telse {\n\t\t// create string with required size\n\t\tstd::string ret;\n\t\tret.resize(max);\n\n\t\t// and read the data\n\t\tthis->file.read(&ret[0], max);\n\t\treturn ret;\n\t}\n}\n\n\nsize_t Native::read_to(void *buf, ssize_t max) {\n\tthis->file.readsome(reinterpret_cast<std::fstream::char_type *>(buf), max);\n\treturn this->file.gcount();\n}\n\n\nbool Native::readable() {\n\treturn (this->mode == mode_t::R or\n\t        this->mode == mode_t::RW);\n}\n\n\nvoid Native::write(const std::string &data) {\n\tthis->file.write(data.c_str(), data.size());\n}\n\n\nbool Native::writable() {\n\treturn (this->mode == mode_t::W or\n\t        this->mode == mode_t::RW);\n}\n\n\nvoid Native::seek(ssize_t offset, seek_t how) {\n\tstd::ios::seekdir where;\n\n\tswitch (how) {\n\tcase seek_t::SET:\n\t\twhere = std::ios::beg;\n\t\tbreak;\n\tcase seek_t::CUR:\n\t\twhere = std::ios::cur;\n\t\tbreak;\n\tcase seek_t::END:\n\t\twhere = std::ios::end;\n\t\tbreak;\n\tdefault:\n\t\tthrow Error{ERR << \"invalid seek mode\"};\n\t}\n\n\tthis->file.seekg(offset, where);\n}\n\n\nbool Native::seekable() {\n\treturn true;\n}\n\n\nsize_t Native::tell() {\n\treturn this->file.tellg();\n}\n\n\nvoid Native::close() {\n\tthis->file.close();\n}\n\n\nvoid Native::flush() {\n\tthis->file.flush();\n}\n\n\nssize_t Native::get_size() {\n\t// remember where we were\n\tsize_t pos = this->file.tellg();\n\n\t// go to the end\n\tthis->file.seekg(0, std::ios::end);\n\tssize_t size = static_cast<ssize_t>(this->file.tellg());\n\n\t// return to position\n\tthis->file.seekg(pos, std::ios::beg);\n\n\treturn size;\n}\n\n\nstd::ostream &Native::repr(std::ostream &stream) {\n\tstream << \"Native(\" << this->path  << \")\";\n\treturn stream;\n}\n\n} // openage::util::filelike\n"
  },
  {
    "path": "libopenage/util/filelike/native.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <fstream>\n#include <iostream>\n#include <memory>\n#include <string>\n\n#include \"filelike.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace filelike {\n\n/**\n * File-like class that uses native stdlib functions to access data.\n */\nclass Native : public FileLike {\npublic:\n\tNative(const std::string &path, mode_t mode = mode_t::R);\n\tvirtual ~Native();\n\n\tstd::string read(ssize_t max) override;\n\tsize_t read_to(void *buf, ssize_t max) override;\n\n\tbool readable() override;\n\n\tvoid write(const std::string &data) override;\n\n\tbool writable() override;\n\n\tvoid seek(ssize_t offset, seek_t how = seek_t::SET) override;\n\tbool seekable() override;\n\tsize_t tell() override;\n\tvoid close() override;\n\tvoid flush() override;\n\tssize_t get_size() override;\n\n\tstd::ostream &repr(std::ostream &) override;\n\nprotected:\n\tstd::string path;\n\n\tmode_t mode;\n\n\tstd::fstream file;\n};\n\n} // namespace filelike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/filelike/python.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"python.h\"\n\n#include <type_traits>\n\n\nnamespace openage::util::filelike {\n\nPython::Python(const py::Obj &fileobj)\n\t:\n\tfileobj{std::make_shared<py::Obj>(fileobj)} {}\n\n\nstd::string Python::read(ssize_t max) {\n\treturn pyx_file_read.call(this->fileobj->get_ref(), max);\n}\n\n\nsize_t Python::read_to(void *buf, ssize_t max) {\n\treturn pyx_file_read_to.call(this->fileobj->get_ref(), buf, max);\n}\n\n\nbool Python::readable() {\n\treturn pyx_file_readable.call(this->fileobj->get_ref());\n}\n\n\nvoid Python::write(const std::string &data) {\n\tpyx_file_write.call(this->fileobj->get_ref(), data);\n}\n\n\nbool Python::writable() {\n\treturn pyx_file_writable.call(this->fileobj->get_ref());\n}\n\n\nvoid Python::seek(ssize_t offset, seek_t how) {\n\t// transform the enum class...\n\tint how_i = static_cast<std::underlying_type<seek_t>::type>(how);\n\n\tpyx_file_seek.call(this->fileobj->get_ref(), offset, how_i);\n}\n\n\nbool Python::seekable() {\n\treturn pyx_file_seekable.call(this->fileobj->get_ref());\n}\n\n\nsize_t Python::tell() {\n\treturn pyx_file_tell.call(this->fileobj->get_ref());\n}\n\n\nvoid Python::close() {\n\tpyx_file_close.call(this->fileobj->get_ref());\n}\n\n\nvoid Python::flush() {\n\tpyx_file_flush.call(this->fileobj->get_ref());\n}\n\n\nssize_t Python::get_size() {\n\treturn pyx_file_size.call(this->fileobj->get_ref());\n}\n\n\nbool Python::is_python_native() const noexcept {\n\treturn true;\n}\n\n\npy::Obj &Python::get_py_fileobj() const {\n\treturn *this->fileobj.get();\n}\n\n\nstd::ostream &Python::repr(std::ostream &stream) {\n\tstream << this->fileobj->repr();\n\treturn stream;\n}\n\n\npyinterface::PyIfFunc<std::string, PyObject *, ssize_t> pyx_file_read;\npyinterface::PyIfFunc<size_t, PyObject *, void *, ssize_t> pyx_file_read_to;\npyinterface::PyIfFunc<bool, PyObject *> pyx_file_readable;\npyinterface::PyIfFunc<void, PyObject *, const std::string &> pyx_file_write;\npyinterface::PyIfFunc<bool, PyObject *> pyx_file_writable;\npyinterface::PyIfFunc<void, PyObject *, ssize_t, int> pyx_file_seek;\npyinterface::PyIfFunc<bool, PyObject *> pyx_file_seekable;\npyinterface::PyIfFunc<size_t, PyObject *> pyx_file_tell;\npyinterface::PyIfFunc<void, PyObject *> pyx_file_close;\npyinterface::PyIfFunc<void, PyObject *> pyx_file_flush;\npyinterface::PyIfFunc<ssize_t, PyObject *> pyx_file_size;\n\n\n} // openage::util::filelike\n"
  },
  {
    "path": "libopenage/util/filelike/python.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool\n\n#include <iostream>\n#include <memory>\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"filelike.h\"\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc1, PyIfFunc2, PyIfFunc3\n#include \"../../pyinterface/functional.h\"\n// pxd: from libopenage.pyinterface.pyobject cimport PyObjectPtr, PyObj\n#include \"../../pyinterface/pyobject.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace filelike {\n\n/**\n * File-like class that wraps a python open() file-like object.\n *\n * pxd:\n * cppclass Python:\n *     PyObj &get_py_fileobj() except +\n */\nclass Python : public FileLike {\npublic:\n\tPython(const py::Obj &fileobj);\n\n\tstd::string read(ssize_t max) override;\n\tsize_t read_to(void *buf, ssize_t max) override;\n\n\tbool readable() override;\n\n\tvoid write(const std::string &data) override;\n\n\tbool writable() override;\n\n\tvoid seek(ssize_t offset, seek_t how = seek_t::SET) override;\n\tbool seekable() override;\n\tsize_t tell() override;\n\tvoid close() override;\n\tvoid flush() override;\n\tssize_t get_size() override;\n\n\tbool is_python_native() const noexcept override;\n\tOAAPI py::Obj &get_py_fileobj() const;\n\n\tstd::ostream &repr(std::ostream &) override;\n\nprotected:\n\tstd::shared_ptr<py::Obj> fileobj;\n};\n\n\n// now follow python interface functions which are\n// used to call to python\n\n// pxd: PyIfFunc2[string, PyObjectPtr, ssize_t] pyx_file_read\nextern OAAPI pyinterface::PyIfFunc<std::string, PyObject *, ssize_t> pyx_file_read;\n\n// pxd: PyIfFunc3[size_t, PyObjectPtr, void *, ssize_t] pyx_file_read_to\nextern OAAPI pyinterface::PyIfFunc<size_t, PyObject *, void *, ssize_t> pyx_file_read_to;\n\n// pxd: PyIfFunc1[bool, PyObjectPtr] pyx_file_readable\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *> pyx_file_readable;\n\n// pxd: PyIfFunc2[void, PyObjectPtr, const string&] pyx_file_write\nextern OAAPI pyinterface::PyIfFunc<void, PyObject *, const std::string &> pyx_file_write;\n\n// pxd: PyIfFunc1[bool, PyObjectPtr] pyx_file_writable\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *> pyx_file_writable;\n\n// pxd: PyIfFunc3[void, PyObjectPtr, ssize_t, int] pyx_file_seek\nextern OAAPI pyinterface::PyIfFunc<void, PyObject *, ssize_t, int> pyx_file_seek;\n\n// pxd: PyIfFunc1[bool, PyObjectPtr] pyx_file_seekable\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *> pyx_file_seekable;\n\n// pxd: PyIfFunc1[size_t, PyObjectPtr] pyx_file_tell\nextern OAAPI pyinterface::PyIfFunc<size_t, PyObject *> pyx_file_tell;\n\n// pxd: PyIfFunc1[void, PyObjectPtr] pyx_file_close\nextern OAAPI pyinterface::PyIfFunc<void, PyObject *> pyx_file_close;\n\n// pxd: PyIfFunc1[void, PyObjectPtr] pyx_file_flush\nextern OAAPI pyinterface::PyIfFunc<void, PyObject *> pyx_file_flush;\n\n// pxd: PyIfFunc1[ssize_t, PyObjectPtr] pyx_file_size\nextern OAAPI pyinterface::PyIfFunc<ssize_t, PyObject *> pyx_file_size;\n\n\n} // namespace filelike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fixed_point.cpp",
    "content": "// Copyright 2016-2016 the openage authors. See copying.md for legal info.\n\n#include \"fixed_point.h\"\n\nnamespace openage {\nnamespace util {\n\n// FixedPoint is all templated, so there's nothing to implement.\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/fixed_point.h",
    "content": "// Copyright 2015-2025 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n#include <bit>\n#include <climits>\n#include <cmath>\n#include <iomanip>\n#include <limits>\n#include <ostream>\n#include <type_traits>\n\n#include \"compiler.h\"\n#include \"misc.h\"\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Helper function that performs a left shift without causing undefined\n * behavior.\n * regular left-shift is undefined if amount >= bitwidth,\n * or amount >= bitwidth - 1 for signed integers.\n */\ntemplate <unsigned int amount, typename T>\nconstexpr static\n\ttypename std::enable_if<(amount + (std::is_signed<T>::value ? 1 : 0) < sizeof(T) * CHAR_BIT), T>::type\n\tsafe_shiftleft(T value) {\n\treturn static_cast<T>(\n\t\tstatic_cast<typename std::make_unsigned<T>::type>(value) << amount);\n}\n\n\n/**\n * Helper function that performs a right shift without causing undefined\n * behavior.\n * right-shift is usually undefined if amount >= bit size.\n */\ntemplate <unsigned int amount, typename T>\nconstexpr static\n\ttypename std::enable_if<(amount >= sizeof(T) * CHAR_BIT), T>::type\n\tsafe_shiftright(T value) {\n\treturn value < 0 ? -1 : 0;\n}\n\ntemplate <unsigned int amount, typename T>\nconstexpr static\n\ttypename std::enable_if<(amount < sizeof(T) * CHAR_BIT), T>::type\n\tsafe_shiftright(T value) {\n\treturn value >> amount;\n}\n\n\n/**\n * Helper function that performs either a safe shift-right (amount < 0),\n * or a safe shift-left (amount >= 0).\n */\ntemplate <int amount, typename T>\nconstexpr static\n\ttypename std::enable_if<(amount < 0), T>::type\n\tsafe_shift(T value) {\n\treturn safe_shiftright<-amount>(value);\n}\n\n\ntemplate <int amount, typename T>\nconstexpr static\n\ttypename std::enable_if<(amount >= 0), T>::type\n\tsafe_shift(T value) {\n\treturn safe_shiftleft<amount>(value);\n}\n\n\n/**\n * Fixed-point integer class;\n *\n * this is designed to be used instead of floats in places where guaranteed\n * precision is required.\n *\n * For example,\n * FixedPoint<int64_t, 32>\n * can store values from -2**32 to +2**32 with a constant precision of 2**-32.\n *\n * If you change this class, remember to update the gdb pretty printers\n * in etc/gdb_pretty/printers.py.\n */\ntemplate <typename int_type, unsigned int fractional_bits, typename intermediate_type = int_type>\nclass FixedPoint {\npublic:\n\tusing raw_type = int_type;\n\tusing this_type = FixedPoint<int_type, fractional_bits, intermediate_type>;\n\tusing unsigned_int_type = typename std::make_unsigned<int_type>::type;\n\tusing unsigned_intermediate_type = typename std::make_unsigned<intermediate_type>::type;\n\n\tusing same_type_but_unsigned = FixedPoint<typename FixedPoint::unsigned_int_type,\n\t                                          fractional_bits, typename FixedPoint::unsigned_intermediate_type>;\n\nprivate:\n\t// Helper function to create the scaling factors that are used below.\n\tstatic constexpr double power_of_two(unsigned int power) {\n\t\tdouble result = 1.0;\n\t\twhile (power--) {\n\t\t\tresult *= 2.0;\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Storage of the fixed point data.\n\t */\n\tint_type raw_value;\n\n\tstatic constexpr const double from_double_factor = power_of_two(fractional_bits);\n\tstatic constexpr const double to_double_factor = 1 / from_double_factor;\n\tstatic constexpr const float from_float_factor = from_double_factor;\n\tstatic constexpr const float to_float_factor = to_double_factor;\n\n\tstatic constexpr const unsigned int approx_decimal_places = static_cast<unsigned int>(\n\t\tstatic_cast<double>(fractional_bits) * 0.30103 + 1);\n\n\t// constexpr helper function for get_fractional_part()\n\tstatic constexpr typename FixedPoint::unsigned_int_type fractional_part_bitmask() {\n\t\t// return ~(MAX_VAL << fractional_bits);\n\t\treturn static_cast<FixedPoint::unsigned_int_type>(\n\t\t\t~(\n\t\t\t\tsafe_shiftleft<fractional_bits, FixedPoint::unsigned_int_type>(\n\t\t\t\t\tstd::numeric_limits<FixedPoint::unsigned_int_type>::max())));\n\t}\n\n\tfriend std::hash<openage::util::FixedPoint<int_type, fractional_bits>>;\n\n\tstatic constexpr int_type raw_value_from_double(double n) {\n\t\treturn static_cast<int_type>(n * from_double_factor);\n\t}\n\npublic:\n\t// obligatory copy constructor / assignment operator.\n\tconstexpr FixedPoint(const FixedPoint &other) :\n\t\traw_value(other.raw_value) {}\n\n\tconstexpr FixedPoint(FixedPoint &&other) noexcept\n\t\t:\n\t\traw_value(std::move(other.raw_value)) {}\n\n\tconstexpr FixedPoint &operator=(const FixedPoint &other) {\n\t\tthis->raw_value = other.raw_value;\n\t\treturn *this;\n\t}\n\n\tconstexpr FixedPoint &operator=(FixedPoint &&other) noexcept {\n\t\tthis->raw_value = std::move(other.raw_value);\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Empty constructor. Initializes the number to 0.\n\t */\n\tconstexpr FixedPoint() :\n\t\traw_value(0) {}\n\n\t/**\n\t * floating-point constructor. Initializes the number from a double.\n\t */\n\t// implicitly construct from double.\n\t// for other creations, use the factory methods below.\n\tconstexpr FixedPoint(double n) :\n\t\traw_value(FixedPoint::raw_value_from_double(n)) {}\n\n\t/**\n\t * FixedPoint value that is preinitialized to zero.\n\t */\n\tstatic constexpr FixedPoint zero() {\n\t\treturn FixedPoint::from_int(0);\n\t}\n\n\t/**\n\t * Math constants represented in FixedPoint\n\t */\n\t// naming, definition and value are kept compatible with `math_constants.h`\n\tstatic constexpr FixedPoint e() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(6267931151224907085ll));\n\t}\n\n\tstatic constexpr FixedPoint log2e() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(3326628274461080622ll));\n\t}\n\n\tstatic constexpr FixedPoint log10e() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(1001414895036696345ll));\n\t}\n\n\tstatic constexpr FixedPoint ln2() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(1598288580650331957ll));\n\t}\n\n\tstatic constexpr FixedPoint ln10() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(5309399739799983627ll));\n\t}\n\n\tstatic constexpr FixedPoint pi() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(7244019458077122842ll));\n\t}\n\n\tstatic constexpr FixedPoint pi_2() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(3622009729038561421ll));\n\t}\n\n\tstatic constexpr FixedPoint pi_4() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(1811004864519280710ll));\n\t}\n\n\tstatic constexpr FixedPoint inv_pi() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(733972625820500306ll));\n\t}\n\n\tstatic constexpr FixedPoint inv2_pi() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(1467945251641000613ll));\n\t}\n\n\tstatic constexpr FixedPoint inv2_sqrt_pi() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(2601865214189558307ll));\n\t}\n\n\tstatic constexpr FixedPoint tau() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 60>::from_raw_value(7244019458077122842ll));\n\t}\n\n\tstatic constexpr FixedPoint degs_per_rad() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(40244552544872904ll));\n\t}\n\n\tstatic constexpr FixedPoint rads_per_deg() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 57>::from_raw_value(8257192040480628449ll));\n\t}\n\n\tstatic constexpr FixedPoint sqrt_2() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(3260954456333195553ll));\n\t}\n\n\tstatic constexpr FixedPoint inv_sqrt_2() {\n\t\treturn from_fixedpoint(FixedPoint<int64_t, 61>::from_raw_value(1630477228166597776ll));\n\t}\n\n\t/**\n\t * Factory function to get a fixed-point number from an integer.\n\t */\n\tstatic constexpr FixedPoint from_int(int_type n) {\n\t\treturn FixedPoint::from_raw_value(safe_shiftleft<fractional_bits, int_type>(n));\n\t}\n\n\t/**\n\t * Factory function to get a fixed-point number from a float.\n\t */\n\tstatic constexpr FixedPoint from_float(float n) {\n\t\treturn FixedPoint::from_raw_value(static_cast<int_type>(n * from_float_factor));\n\t}\n\n\t/**\n\t * Factory function to get a fixed-point number from a double.\n\t */\n\tstatic constexpr FixedPoint from_double(double n) {\n\t\treturn FixedPoint::from_raw_value(FixedPoint::raw_value_from_double(n));\n\t}\n\n\t/**\n\t * Factory function to get a fixed-point number from a fixed-point number of different type.\n\t */\n\ttemplate <typename other_int_type, unsigned int other_fractional_bits, typename other_intermediate_type, typename std::enable_if<(fractional_bits > other_fractional_bits)>::type * = nullptr>\n\tstatic constexpr FixedPoint from_fixedpoint(const FixedPoint<other_int_type, other_fractional_bits, other_intermediate_type> &other) {\n\t\treturn FixedPoint::from_raw_value(\n\t\t\tsafe_shift<fractional_bits - other_fractional_bits, int_type>(static_cast<int_type>(other.get_raw_value())));\n\t}\n\n\ttemplate <typename other_int_type, unsigned int other_fractional_bits, typename other_intermediate_type, typename std::enable_if<(fractional_bits <= other_fractional_bits)>::type * = nullptr>\n\tstatic constexpr FixedPoint from_fixedpoint(const FixedPoint<other_int_type, other_fractional_bits, other_intermediate_type> &other) {\n\t\treturn FixedPoint::from_raw_value(\n\t\t\tstatic_cast<int_type>(other.get_raw_value() / safe_shiftleft<other_fractional_bits - fractional_bits, other_int_type>(1)));\n\t}\n\n\t/**\n\t * The minimum possible value of this type.\n\t */\n\tstatic constexpr const FixedPoint min_value() {\n\t\treturn FixedPoint::from_raw_value(std::numeric_limits<int_type>::min());\n\t}\n\n\t/**\n\t * The maximum possible value of this type.\n\t */\n\tstatic constexpr const FixedPoint max_value() {\n\t\treturn FixedPoint::from_raw_value(std::numeric_limits<int_type>::max());\n\t}\n\n\t/**\n\t * Factory function to construct a fixed-point number with a given raw value.\n\t * Don't use this.\n\t */\n\tstatic constexpr FixedPoint from_raw_value(int_type raw_value) {\n\t\tFixedPoint result;\n\t\tresult.raw_value = raw_value;\n\t\treturn result;\n\t}\n\n\t/**\n\t * Converter to retrieve the raw value of the fixed-point number.\n\t * Don't use this.\n\t */\n\tconstexpr int_type get_raw_value() const {\n\t\treturn this->raw_value;\n\t}\n\n\t/**\n\t * Converter to retrieve the int (pre-decimal) part of the number.\n\t */\n\tconstexpr int_type to_int() const {\n\t\treturn safe_shiftright<fractional_bits, int_type>(this->raw_value);\n\t}\n\n\tconstexpr explicit operator int() const {\n\t\treturn this->to_int();\n\t}\n\n\t/**\n\t * Converter to retrieve the number as float.\n\t */\n\tconstexpr float to_float() const {\n\t\treturn static_cast<float>(this->raw_value) * FixedPoint::to_float_factor;\n\t}\n\n\tconstexpr explicit operator float() const {\n\t\treturn this->to_float();\n\t}\n\n\t/**\n\t * Converter to retrieve the number as double.\n\t */\n\tconstexpr double to_double() const {\n\t\treturn static_cast<double>(this->raw_value) * FixedPoint::to_double_factor;\n\t}\n\n\tconstexpr explicit operator double() const {\n\t\treturn this->to_double();\n\t}\n\n\t/**\n\t * Show a string representation. Useful for debugging in gdb.\n\t */\n\tstd::string str() const {\n\t\tstd::ostringstream builder;\n\t\tbuilder << \"FixedPoint(\" << this->to_double()\n\t\t\t\t<< \", fracbits=\" << fractional_bits\n\t\t\t\t<< \", raw=\" << this->raw_value\n\t\t\t\t<< \")\";\n\t\treturn builder.str();\n\t};\n\n\t/**\n\t * Converter to retrieve the fractional (post-decimal) part of the number.\n\t */\n\tconstexpr typename FixedPoint::same_type_but_unsigned get_fractional_part() const {\n\t\t// returns a new variable with only the bits from\n\t\t// fractional_part_bitmask set.\n\t\treturn FixedPoint::same_type_but_unsigned::from_raw_value(\n\t\t\tstatic_cast<FixedPoint::unsigned_int_type>(this->raw_value) & std::integral_constant<int_type, FixedPoint::fractional_part_bitmask()>::value);\n\t}\n\n\t// Comparison operators for comparison with other\n\tconstexpr auto operator<=>(const FixedPoint &o) const = default;\n\n\t// Unary operators\n\tconstexpr FixedPoint operator+() const {\n\t\treturn *this;\n\t}\n\n\t// the inner_int_type template is required for enable_if.\n\ttemplate <typename inner_int_type = int_type>\n\tconstexpr\n\t\ttypename std::enable_if<std::is_signed<inner_int_type>::value, typename FixedPoint::this_type>::type\n\t\toperator-() const {\n\t\tstatic_assert(std::is_same<inner_int_type, int_type>::value, \"inner_int_type must == int_type\");\n\t\treturn FixedPoint::this_type::from_raw_value(-this->raw_value);\n\t}\n\n\ttemplate <typename I, unsigned F, typename Inter>\n\tconstexpr double hypot(const FixedPoint<I, F, Inter> rhs) {\n\t\treturn std::hypot(this->to_double(), rhs.to_double());\n\t}\n\n\ttemplate <typename I, unsigned F, typename Inter>\n\tconstexpr FixedPoint<I, F> hypotfp(const FixedPoint<I, F, Inter> rhs) {\n\t\treturn FixedPoint<I, F, Inter>(this->hypot(rhs));\n\t}\n\n\t// Basic operators\n\tconstexpr FixedPoint &operator+=(const FixedPoint &n) {\n\t\tthis->raw_value += n.raw_value;\n\t\treturn *this;\n\t}\n\n\tconstexpr FixedPoint &operator-=(const FixedPoint &n) {\n\t\tthis->raw_value -= n.raw_value;\n\t\treturn *this;\n\t}\n\n\t/**\n\t * FixedPoint *= N, where N is not a FixedPoint.\n\t */\n\ttemplate <typename N>\n\ttypename std::enable_if<std::is_arithmetic<N>::value, FixedPoint &>::type constexpr operator*=(const N &rhs) {\n\t\tthis->raw_value *= rhs;\n\t\treturn *this;\n\t}\n\n\t/**\n\t * FixedPoint /= N\n\t */\n\ttemplate <typename N>\n\tconstexpr FixedPoint &operator/=(const N &rhs) {\n\t\tthis->raw_value = div(this->raw_value, static_cast<int_type>(rhs));\n\t\treturn *this;\n\t}\n\n\tvoid swap(FixedPoint &rhs) {\n\t\tstd::swap(this->raw_value, rhs.raw_value);\n\t}\n\n\t// I/O operators\n\tfriend std::ostream &operator<<(std::ostream &os, const FixedPoint &n) {\n\t\tos << std::fixed << std::setprecision(FixedPoint::approx_decimal_places) << double(n);\n\n\t\tif (n == FixedPoint::max_value()) [[unlikely]] {\n\t\t\tos << \"[MAX]\";\n\t\t}\n\t\telse if (n != 0 and n == FixedPoint::min_value()) [[unlikely]] {\n\t\t\tos << \"[MIN]\";\n\t\t}\n\n\t\treturn os;\n\t}\n\n\tfriend std::istream &operator>>(std::istream &is, FixedPoint &n) {\n\t\tdouble temp;\n\t\tis >> temp;\n\t\tn = temp;\n\t\treturn is;\n\t}\n\n\t/**\n\t * Pure FixedPoint sqrt implementation using Heron's Algorithm.\n\t *\n\t * Note that this function is undefined for negative values.\n\t *\n\t * There's a small loss in precision depending on the value of fractional_bits and the position of\n\t * the most significant bit: if the integer portion is very large, we won't have as much (absolute)\n\t * precision. Ideally you would want the intermediate_type to be twice the size of raw_type to avoid\n\t * any losses.\n\t */\n\tconstexpr FixedPoint sqrt() {\n\t\t// Zero can cause issues later, so deal with now.\n\t\tif (this->raw_value == 0) {\n\t\t\treturn zero();\n\t\t}\n\n\t\t// Check for negative values\n\t\tif constexpr (std::is_signed<raw_type>()) {\n\t\t\tENSURE(this->raw_value > 0, \"FixedPoint::sqrt() is undefined for negative values.\");\n\t\t}\n\n\t\t// A greater shift = more precision, but can overflow the intermediate type if too large.\n\t\tsize_t max_shift = std::countl_zero(static_cast<unsigned_intermediate_type>(this->raw_value)) - 1;\n\t\tsize_t shift = max_shift > fractional_bits ? fractional_bits : max_shift;\n\n\t\t// shift + fractional bits must be an even number\n\t\tif ((shift + fractional_bits) % 2) {\n\t\t\tshift -= 1;\n\t\t}\n\n\t\t// We can't use the safe shift since the shift value is unknown at compile time.\n\t\tintermediate_type n = static_cast<intermediate_type>(this->raw_value) << shift;\n\t\tintermediate_type guess = static_cast<intermediate_type>(1) << fractional_bits;\n\n\t\tfor (size_t i = 0; i < fractional_bits; i++) {\n\t\t\tintermediate_type prev = guess;\n\t\t\tguess = (guess + n / guess) / 2;\n\t\t\tif (guess == prev) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// The sqrt operation halves the number of bits, so we'll we'll have to calculate a shift back\n\t\tsize_t unshift = fractional_bits - (shift + fractional_bits) / 2;\n\n\t\treturn from_raw_value(guess << unshift);\n\t}\n\n\tconstexpr double atan2(const FixedPoint &n) {\n\t\treturn std::atan2(this->to_double(), n.to_double());\n\t}\n\n\tconstexpr double sin() {\n\t\treturn std::sin(this->to_double());\n\t}\n\n\tconstexpr double cos() {\n\t\treturn std::cos(this->to_double());\n\t}\n\n\tconstexpr double tan() {\n\t\treturn std::tan(this->to_double());\n\t}\n};\n\n\n// Binary operators\n\n/**\n * FixedPoint + FixedPoint\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator+(const FixedPoint<I, F, Inter> &lhs, const FixedPoint<I, F, Inter> &rhs) {\n\treturn FixedPoint<I, F, Inter>::from_raw_value(lhs.get_raw_value() + rhs.get_raw_value());\n}\n\n/**\n * FixedPoint + double\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator+(const FixedPoint<I, F, Inter> &lhs, const double &rhs) {\n\treturn FixedPoint<I, F, Inter>{lhs} + FixedPoint<I, F, Inter>::from_double(rhs);\n}\n\n/**\n * FixedPoint - FixedPoint\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator-(const FixedPoint<I, F, Inter> &lhs, const FixedPoint<I, F, Inter> &rhs) {\n\treturn FixedPoint<I, F, Inter>::from_raw_value(lhs.get_raw_value() - rhs.get_raw_value());\n}\n\n/**\n * FixedPoint - double\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator-(const FixedPoint<I, F, Inter> &lhs, const double &rhs) {\n\treturn FixedPoint<I, F, Inter>{lhs} - FixedPoint<I, F, Inter>::from_double(rhs);\n}\n\n\n/**\n * FixedPoint * N\n */\ntemplate <typename I, unsigned F, typename Inter, typename N>\ntypename std::enable_if<std::is_arithmetic<N>::value, FixedPoint<I, F, Inter>>::type constexpr operator*(const FixedPoint<I, F, Inter> lhs, const N &rhs) {\n\treturn FixedPoint<I, F, Inter>::from_raw_value(lhs.get_raw_value() * rhs);\n}\n\n/*\n\n*/\n\n/**\n * FixedPoint * FixedPoint\n *\n * FixedPoint * FixedPoint can result in surprising overflows.\n *\n * using fp = FixedPoint<uint64_t, 16>;\n * fp a = fp.from_int(1 << 16);\n * => a * a will overflow because:\n *    a.rawvalue == 2^(16+16) == 2^32\n *    -> a.rawvalue * a.rawvalue == 2^64 => pwnt\n *\n * Use a larger intermediate type to prevent overflow\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator*(const FixedPoint<I, F, Inter> lhs, const FixedPoint<I, F, Inter> rhs) {\n\tInter ret = static_cast<Inter>(lhs.get_raw_value()) * static_cast<Inter>(rhs.get_raw_value());\n\tret >>= F;\n\n\treturn FixedPoint<I, F, Inter>::from_raw_value(static_cast<I>(ret));\n}\n\n\n/**\n * FixedPoint / FixedPoint\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator/(const FixedPoint<I, F, Inter> lhs, const FixedPoint<I, F, Inter> rhs) {\n\tInter ret = div((static_cast<Inter>(lhs.get_raw_value()) << F), static_cast<Inter>(rhs.get_raw_value()));\n\treturn FixedPoint<I, F, Inter>::from_raw_value(static_cast<I>(ret));\n}\n\n\n/**\n * FixedPoint / N\n */\ntemplate <typename I, unsigned F, typename Inter, typename N>\nconstexpr FixedPoint<I, F, Inter> operator/(const FixedPoint<I, F, Inter> lhs, const N &rhs) {\n\treturn FixedPoint<I, F, Inter>::from_raw_value(div(lhs.get_raw_value(), static_cast<I>(rhs)));\n}\n\n/**\n * FixedPoint % FixedPoint (modulo)\n */\ntemplate <typename I, unsigned int F, typename Inter>\nconstexpr FixedPoint<I, F, Inter> operator%(const FixedPoint<I, F, Inter> lhs, const FixedPoint<I, F, Inter> rhs) {\n\tauto div = (lhs / rhs);\n\tauto n = div.to_int();\n\treturn lhs - (rhs * n);\n}\n\n} // namespace util\n} // namespace openage\n\n\n// std function overloads\nnamespace std {\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double sqrt(openage::util::FixedPoint<I, F, Inter> n) {\n\treturn static_cast<double>(n.sqrt());\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double atan2(openage::util::FixedPoint<I, F, Inter> x, openage::util::FixedPoint<I, F, Inter> y) {\n\treturn x.atan2(y);\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double sin(openage::util::FixedPoint<I, F, Inter> n) {\n\treturn n.sin();\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double cos(openage::util::FixedPoint<I, F, Inter> n) {\n\treturn n.cos();\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double tan(openage::util::FixedPoint<I, F, Inter> n) {\n\treturn n.tan();\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr openage::util::FixedPoint<I, F, Inter> min(openage::util::FixedPoint<I, F, Inter> x, openage::util::FixedPoint<I, F, Inter> y) {\n\treturn openage::util::FixedPoint<I, F, Inter>::from_raw_value(\n\t\tstd::min(x.get_raw_value(),\n\t             y.get_raw_value()));\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr openage::util::FixedPoint<I, F, Inter> max(openage::util::FixedPoint<I, F, Inter> x, openage::util::FixedPoint<I, F, Inter> y) {\n\treturn openage::util::FixedPoint<I, F, Inter>::from_raw_value(\n\t\tstd::max(x.get_raw_value(),\n\t             y.get_raw_value()));\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr openage::util::FixedPoint<I, F, Inter> abs(openage::util::FixedPoint<I, F, Inter> n) {\n\treturn openage::util::FixedPoint<I, F, Inter>::from_raw_value(\n\t\tstd::abs(n.get_raw_value()));\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nconstexpr double hypot(openage::util::FixedPoint<I, F, Inter> x, openage::util::FixedPoint<I, F, Inter> y) {\n\treturn x.hypot(y);\n}\n\ntemplate <typename I, unsigned F, typename Inter>\nstruct hash<openage::util::FixedPoint<I, F, Inter>> {\n\tconstexpr size_t operator()(const openage::util::FixedPoint<I, F, Inter> &n) const {\n\t\treturn std::hash<I>{}(n.raw_value);\n\t}\n};\n\ntemplate <typename I, unsigned F, typename Inter>\nstruct numeric_limits<openage::util::FixedPoint<I, F, Inter>> {\n\tconstexpr static openage::util::FixedPoint<I, F, Inter> min() {\n\t\treturn openage::util::FixedPoint<I, F, Inter>::min_value();\n\t}\n\n\tconstexpr static openage::util::FixedPoint<I, F, Inter> max() {\n\t\treturn openage::util::FixedPoint<I, F, Inter>::max_value();\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/util/fixed_point_test.cpp",
    "content": "// Copyright 2016-2025 the openage authors. See copying.md for legal info.\n\n#include \"fixed_point.h\"\n\n#include <cstdint>\n\n#include \"../testing/testing.h\"\n#include \"stringformatter.h\"\n#include \"math_constants.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\n\nvoid fixed_point() {\n\t// test construction and assignments\n\tFixedPoint<int8_t, 4> a(4.3);\n\tTESTEQUALS(a.to_int(), 4);\n\tTESTEQUALS(a.to_float(), 4.25);\n\tTESTEQUALS(a.to_double(), 4.25);\n\tTESTEQUALS(a.get_raw_value(), 68);\n\tTESTEQUALS(a.get_fractional_part().to_float(), 0.25);\n\n\tFixedPoint<int8_t, 4> b(a);\n\tTESTEQUALS(b.get_raw_value(), 68);\n\n\tauto c = FixedPoint<uint16_t, 8>::from_fixedpoint<int8_t, 4>(a);\n\tTESTEQUALS(c.get_raw_value(), 1088);\n\tTESTEQUALS(c.to_int(), 4);\n\tTESTEQUALS(c.to_double(), 4.25);\n\n\ta = FixedPoint<int8_t, 4>::from_int(1);\n\tTESTEQUALS(a.get_raw_value(), 16);\n\n\ta = FixedPoint<int8_t, 4>::from_float(1.3);\n\tTESTEQUALS(a.get_raw_value(), 20);\n\n\ta = FixedPoint<int8_t, 4>::from_double(1.4);\n\tTESTEQUALS(a.get_raw_value(), 22);\n\n\tauto d = FixedPoint<uint64_t, 48>::from_double(2016);\n\t// this raw value was calculated in python: 2016 * 2**48\n\tTESTEQUALS(d.get_raw_value(), 567453553048682496UL);\n\n\t// test arithmetics\n\tusing TestType = FixedPoint<int64_t, 32>;\n\n\tTestType e(120.7);\n\tTestType f(-12.4);\n\te += f;\n\n\tTESTEQUALS_FLOAT(e.to_double(), 108.3, 1e-7);\n\tTESTEQUALS_FLOAT((-e).to_double(), -108.3, 1e-7);\n\tTESTEQUALS_FLOAT((+e).to_double(), 108.3, 1e-7);\n\tTESTEQUALS_FLOAT((e - f).to_double(), 120.7, 1e-7);\n\tTESTEQUALS_FLOAT((e + f).to_double(), 95.9, 1e-7);\n\tTESTEQUALS_FLOAT((e * 1.02).to_double(), 108.3 * 1.02, 1e-7);\n\tTESTEQUALS_FLOAT((e * 10).to_double(), 108.3 * 10, 1e-7);\n\tTESTEQUALS_FLOAT((e / 10).to_double(), 108.3 / 10, 1e-7);\n\tTESTEQUALS_FLOAT(std::sqrt(e), sqrt(108.3), 1e-7);\n\tTESTEQUALS_FLOAT(std::atan2(e, f), atan2(108.3, -12.4), 1e-7);\n\tTESTEQUALS_FLOAT(std::abs(-e).to_double(), 108.3, 1e-7);\n\tTESTEQUALS_FLOAT(std::hypot(e, f), hypot(108.3, -12.4), 1e-7);\n\tTESTEQUALS_FLOAT(std::min(e, f), -12.4, 1e-7);\n\tTESTEQUALS_FLOAT(std::max(e, f), 108.3, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::min_value().to_double(), -2147483648.0, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::max_value().to_double(), 2147483648.0, 1e-7);\n\n\te -= f;\n\n\tTESTEQUALS_FLOAT(e.to_double(), 120.7, 1e-7);\n\n\tTESTEQUALS(e == f, false);\n\tTESTEQUALS(e != f, true);\n\tTESTEQUALS(e == e, true);\n\tTESTEQUALS(e <= e, true);\n\tTESTEQUALS(e >= e, true);\n\tTESTEQUALS(e >= f, true);\n\tTESTEQUALS(e <= f, false);\n\tTESTEQUALS(e < e, false);\n\tTESTEQUALS(e > e, false);\n\tTESTEQUALS(e < f, false);\n\tTESTEQUALS(e > f, true);\n\n\t// test the string I/O functions\n\tFString s;\n\ts << a;\n\tTESTEQUALS(std::string(s), \"1.38\");\n\n\ts.reset();\n\ts << c;\n\tTESTEQUALS(std::string(s), \"4.250\");\n\n\ts.reset();\n\ts << d;\n\tTESTEQUALS(std::string(s), \"2016.000000000000000\");\n\n\tstd::stringstream sstr(\"1234.5678\");\n\tsstr >> e;\n\tTESTEQUALS_FLOAT(e.to_double(), 1234.5678, 1e-7);\n\n\tTESTEQUALS_FLOAT(TestType::e().to_double(), math::E, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::log2e().to_double(), math::LOG2E, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::log10e().to_double(), math::LOG10E, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::ln2().to_double(), math::LN2, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::ln10().to_double(), math::LN10, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::pi().to_double(), math::PI, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::pi_2().to_double(), math::PI_2, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::pi_4().to_double(), math::PI_4, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::inv_pi().to_double(), math::INV_PI, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::inv2_pi().to_double(), math::INV2_PI, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::inv2_sqrt_pi().to_double(), math::INV2_SQRT_PI, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::tau().to_double(), math::TAU, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::degs_per_rad().to_double(), math::DEGSPERRAD, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::rads_per_deg().to_double(), math::RADSPERDEG, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::sqrt_2().to_double(), math::SQRT_2, 1e-7);\n\tTESTEQUALS_FLOAT(TestType::inv_sqrt_2().to_double(), math::INV_SQRT_2, 1e-7);\n\n\n\tusing TestTypeShort = FixedPoint<int32_t, 16>;\n\tTESTEQUALS_FLOAT(TestTypeShort::e().to_double(), math::E, 1e-3);\n\tTESTEQUALS_FLOAT(TestTypeShort::pi().to_double(), math::PI, 1e-3);\n\n\t{\n\t\tusing S = FixedPoint<uint16_t, 7U>;\n\t\tusing T = FixedPoint<uint16_t, 7U, uint64_t>;\n\n\t\tauto a = S::from_int(16U);\n\t\tTESTNOTEQUALS((a*a).to_int(), 256U);\n\n\t\tauto b = T::from_int(16U);\n\t\tTESTEQUALS((b*b).to_int(), 256U);\n\n\t\tauto c = T::from_int(17U);\n\t\tTESTEQUALS((c*c).to_int(), 289U);\n\t}\n\t{\n\t\tusing S = FixedPoint<int32_t, 12U>;\n\t\tauto a = S::from_int(256);\n\t\tauto b = S::from_int(8);\n\t\tTESTNOTEQUALS((a/b).to_int(), 32);\n\n\n\t\tusing T = FixedPoint<int32_t, 12, int64_t>;\n\t\tauto c = T::from_int(256);\n\t\tauto d = T::from_int(8);\n\t\tTESTEQUALS((c/d).to_int(), 32);\n\t}\n\t{\n\t\tusing T = FixedPoint<int32_t, 12, int64_t>;\n\t\tauto a = T::from_double(4.75);\n\t\tauto b = T::from_double(3.5);\n\t\tauto c = -a;\n\t\tTESTEQUALS_FLOAT((a/b).to_double(), 4.75/3.5, 0.1);\n\t\tTESTEQUALS_FLOAT((c/b).to_double(), -4.75/3.5, 0.1);\n\t}\n\n\t// Pure FixedPoint sqrt tests\n\t{\n\t\tusing T = FixedPoint<int64_t, 32, int64_t>;\n\t\tTESTEQUALS_FLOAT(T(41231.131).sqrt(), 203.0545025356, 1e-7);\n\t\tTESTEQUALS_FLOAT(T(547965.116).sqrt(), 740.2466588915, 1e-7);\n\n\t\tTESTEQUALS_FLOAT(T(2).sqrt(), T::sqrt_2(), 1e-9);\n\t\tTESTEQUALS_FLOAT(2 / std::sqrt(T::pi()), T::inv2_sqrt_pi(), 1e-9);\n\n\t\t// Powers of two (anything over 2^15 will overflow (2^16)^2 = 2^32 >).\n\t\tfor (size_t i = 0; i < 15; i++) {\n\t\t\tint64_t value = 1 << i;\n\t\t\tTESTEQUALS_FLOAT(T(value * value).sqrt(), value, 1e-7);\n\t\t}\n\n\t\tfor (size_t i = 0; i < 100; i++) {\n\t\t\tdouble value = 14.25 * i;\n\t\t\tTESTEQUALS_FLOAT(T(value * value).sqrt(), value, 1e-7);\n\t\t}\n\n\t\t// This one can go up to 2^63, but that would take years.\n\t\tfor (uint32_t i = 0; i < 65536; i++) {\n\t\t\tT value = T::from_raw_value(i * i);\n\t\t\tTESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-7);\n\t\t}\n\n\t\t// We lose some precision when raw_type == intermediate_type\n\t\tfor (uint64_t i = 1; i < std::numeric_limits<uint64_t>::max(); i = (i * 2) ^ i) {\n\t\t\tT value = T::from_raw_value(i * i);\n\t\t\tif (value < 0) {\n\t\t\t\tvalue = -value;\n\t\t\t}\n\t\t\tTESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4);\n\t\t}\n\n\t\tusing FP16_16 = FixedPoint<uint32_t, 16, uint64_t>;\n\t\tfor (uint32_t i = 1; i < 65536; i++) {\n\t\t\tFP16_16 value = FP16_16::from_raw_value(i);\n\t\t\tTESTEQUALS_FLOAT(value.sqrt(), std::sqrt(value.to_double()), 1e-4);\n\t\t}\n\n\n\t\t// Test with negative number\n\t\tTESTTHROWS((FixedPoint<int64_t, 32>::from_float(-3.25).sqrt()));\n\t\tTESTNOEXCEPT((FixedPoint<int64_t, 32>::from_float(3.25).sqrt()));\n\t\tTESTNOEXCEPT((FixedPoint<uint64_t, 32>::from_float(-3.25).sqrt()));\n\t}\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/fps.cpp",
    "content": "// Copyright 2013-2019 the openage authors. See copying.md for legal info.\n\n#include \"fps.h\"\n\n#include <cmath>\n\n#include \"timing.h\"\n\nnamespace openage::util {\n\nFrameCounter::FrameCounter()\n\t:\n\tfps{0},\n\tcount{0},\n\tnsec_lastframe{0},\n\tframe_count_weighted{0},\n\tframe_length_sum_weighted{0},\n\tframe_timer{false} {\n}\n\n\nvoid FrameCounter::frame() {\n\tthis->nsec_lastframe = this->frame_timer.getandresetval();\n\tif (this->nsec_lastframe <= 1) {\n\t\t// never can be safe enough\n\t\tthis->nsec_lastframe = 1;\n\t}\n\n\t// frames that lie 0.5 seconds in the past have 1/e of the weight\n\tfloat previous_frames_weight_adjustment = exp((-0.001 / 0.5) * this->nsec_lastframe);\n\n\tthis->frame_length_sum_weighted *= previous_frames_weight_adjustment;\n\tthis->frame_length_sum_weighted += this->nsec_lastframe;\n\n\tthis->frame_count_weighted *= previous_frames_weight_adjustment;\n\tthis->frame_count_weighted += 1;\n\n\tif (this->frame_length_sum_weighted <= 0 || this->frame_count_weighted <= 0) {\n\t\tfps = 0.0;\n\t} else {\n\t\tfps = 1e9 * this->frame_count_weighted / this->frame_length_sum_weighted;\n\t}\n\n\tif (count % 20 == 0) {\n\t\tdisplay_fps = fps;\n\t}\n\n\tcount += 1;\n}\n\n} // openage::util\n"
  },
  {
    "path": "libopenage/util/fps.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include \"timer.h\"\n\nnamespace openage {\nnamespace util {\n\nclass FrameCounter {\npublic:\n\tFrameCounter();\n\t~FrameCounter() = default;\n\n\t/** to be called each time a frame has been completed */\n\tvoid frame();\n\n\t/** auto-updated to always contain the current FPS value */\n\tfloat fps;\n\n\t/** auto-updated every 20 frames to always contain the current FPS value */\n\tfloat display_fps;\n\n\t/** contains the number of completed frames */\n\tuint64_t count;\n\n\t/** nanoseconds used for the last frame */\n\ttime_nsec_t nsec_lastframe;\n\nprivate:\n\tfloat frame_count_weighted;\n\tfloat frame_length_sum_weighted;\n\n\tTimer frame_timer;\n};\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fslike/CMakeLists.txt",
    "content": "add_sources(libopenage\n\tdirectory.cpp\n\tfslike.cpp\n\tnative.cpp\n\tpython.cpp\n)\n\npxdgen(\n\tfslike.h\n\tpython.h\n)\n"
  },
  {
    "path": "libopenage/util/fslike/directory.cpp",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#include \"directory.h\"\n\n// HACK: windows.h defines max and min as macros. This results in compile errors.\n#ifdef _WIN32\n\t// defining `NOMINMAX` disables the definition of those macros.\n\t#define NOMINMAX\n#endif\n\n#include <cstdio>\n#include <dirent.h>\n#include <fcntl.h>\n#include <filesystem>\n#include <iostream>\n#include <sys/stat.h>\n#include <sys/types.h>\n#include <utility>\n\n#ifdef __APPLE__\n\t#include <sys/time.h>\n#endif\n#ifdef _WIN32\n\t#include <direct.h>\n\t#include <io.h>\n\t#include <sys/utime.h>\n\t// HACK: What the heck? I want the std::filesystem library!\n\t#define O_NOCTTY 0\n\t#define O_NONBLOCK 0\n\t#define W_OK 2\n#else // ! _MSC_VER\n\t#include <unistd.h>\n#endif\n\n#include \"../file.h\"\n#include \"../filelike/native.h\"\n#include \"../misc.h\"\n#include \"../path.h\"\n#include \"./native.h\"\n\n\nnamespace openage::util::fslike {\n\n\nDirectory::Directory(std::string basepath, bool create_if_missing) :\n\tbasepath{std::move(basepath)} {\n\tif (create_if_missing) {\n\t\tthis->mkdirs({});\n\t}\n}\n\n\n// We don't need a resolve_r and resolve_w here!\n// If the underlying fslike system is a Directory (i.e. this.)\n// then we don't have any overlay possibility!\n// -> Always resolve just the real system filename.\nstd::string Directory::resolve(const Path::parts_t &parts) const {\n\tstd::string ret = this->basepath;\n\tfor (auto &part : parts) {\n\t\tret += PATHSEP + part;\n\t}\n\treturn ret;\n}\n\n\n// TODO: use std::optional when available\nstd::tuple<struct stat, int> Directory::do_stat(const Path::parts_t &parts) const {\n\tconst std::string path = this->resolve(parts);\n\tstruct stat buf;\n\tint result = stat(path.c_str(), &buf);\n\n\treturn std::make_tuple(buf, result);\n}\n\n\nbool Directory::is_file(const Path::parts_t &parts) {\n\tauto stat_result = this->do_stat(parts);\n\n\t// test for regular file\n\tif (std::get<1>(stat_result) == 0 and S_ISREG(std::get<0>(stat_result).st_mode)) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n\nbool Directory::is_dir(const Path::parts_t &parts) {\n\tauto stat_result = this->do_stat(parts);\n\n\t// test for regular file\n\tif (std::get<1>(stat_result) == 0 and S_ISDIR(std::get<0>(stat_result).st_mode)) {\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n\nbool Directory::writable(const Path::parts_t &parts) {\n\tPath::parts_t parts_test = parts;\n\n\t// try to find the first existing path-part\n\twhile (not(this->is_dir(parts_test) or this->is_file(parts_test))) {\n\t\tif (parts_test.size() == 0) {\n\t\t\tthrow Error{ERR << \"file not found\"};\n\t\t}\n\n\t\tparts_test.pop_back();\n\t}\n\tconst std::string path = this->resolve(parts_test);\n\n\treturn access(path.c_str(), W_OK);\n}\n\n\nstd::vector<Path::part_t> Directory::list(const Path::parts_t &parts) {\n\tconst std::string path = this->resolve(parts);\n\tstd::vector<Path::part_t> ret;\n\n\tDIR *dir;\n\tstruct dirent *ent;\n\n\tdir = opendir(path.c_str());\n\n\tif (dir == nullptr) {\n\t\tthrow Error(ERR << \"could not list contents of '\" << path << \"'\");\n\t}\n\n\t// walk over dir contents\n\twhile ((ent = readdir(dir)) != nullptr) {\n\t\tret.emplace_back(ent->d_name);\n\t}\n\n\tclosedir(dir);\n\n\treturn ret;\n}\n\n\nbool Directory::mkdirs(const Path::parts_t &parts) {\n\tPath::parts_t all_parts = util::split(this->basepath, PATHSEP);\n\n\tvector_extend(all_parts, parts);\n\n\tstd::string dirpath;\n\n\tfor (auto &part : all_parts) {\n\t\tdirpath += PATHSEP + part;\n\n\t\tstruct stat buf;\n\n\t\t// it it exists already, try creating the next one\n\t\tif (stat(dirpath.c_str(), &buf) == 0 and S_ISDIR(buf.st_mode)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// create the folder, umask will turn it to 755.\n#ifdef _MSC_VER\n\t\tbool dir_created = _mkdir(dirpath.c_str()) == 0;\n#elif __MINGW32__\n\t\tbool dir_created = mkdir(dirpath.c_str()) == 0;\n#else\n\t\tbool dir_created = mkdir(dirpath.c_str(), 0777) == 0;\n#endif\n\n\t\tif (not dir_created) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n\nFile Directory::open_r(const Path::parts_t &parts) {\n\treturn File{\n\t\tstd::make_shared<filelike::Native>(this->resolve(parts),\n\t                                       filelike::Native::mode_t::R)};\n}\n\n\nFile Directory::open_w(const Path::parts_t &parts) {\n\treturn File{\n\t\tstd::make_shared<filelike::Native>(this->resolve(parts),\n\t                                       filelike::Native::mode_t::W)};\n}\n\n\nFile Directory::open_rw(const Path::parts_t &parts) {\n\treturn File{\n\t\tstd::make_shared<filelike::Native>(this->resolve(parts),\n\t                                       filelike::Native::mode_t::RW)};\n}\n\n\nFile Directory::open_a(const Path::parts_t &parts) {\n\treturn File{\n\t\tstd::make_shared<filelike::Native>(this->resolve(parts),\n\t                                       filelike::Native::mode_t::A)};\n}\n\n\nFile Directory::open_ar(const Path::parts_t &parts) {\n\treturn File{\n\t\tstd::make_shared<filelike::Native>(this->resolve(parts),\n\t                                       filelike::Native::mode_t::AR)};\n}\n\n\nstd::string Directory::get_native_path(const Path::parts_t &parts) {\n\treturn this->resolve(parts);\n}\n\n\nbool Directory::rename(const Path::parts_t &parts,\n                       const Path::parts_t &target_parts) {\n\treturn std::rename(this->resolve(parts).c_str(),\n\t                   this->resolve(target_parts).c_str())\n\t       == 0;\n}\n\n\nbool Directory::rmdir(const Path::parts_t &parts) {\n\treturn ::rmdir(this->resolve(parts).c_str()) == 0;\n}\n\n\nbool Directory::touch(const Path::parts_t &parts) {\n\tconst std::string path = this->resolve(parts);\n\n\t// create the file if missing\n\tint fd = open(\n\t\tpath.c_str(),\n\t\tO_WRONLY | O_CREAT | O_NOCTTY | O_NONBLOCK,\n\t\t0666);\n\n\tif (fd < 0) {\n\t\treturn false;\n\t}\n\n\tclose(fd);\n\n\t// update the timestamp\n#ifdef __APPLE__\n\tint result = utimes(path.c_str(), nullptr) == 0;\n#elif defined _WIN32\n\tint result = _utime(path.c_str(), nullptr) == 0;\n#else\n\tint result = utimensat(AT_FDCWD, path.c_str(), nullptr, 0) == 0;\n#endif\n\n\treturn result;\n}\n\n\nbool Directory::unlink(const Path::parts_t &parts) {\n\treturn std::remove(this->resolve(parts).c_str()) == 0;\n}\n\n\nint Directory::get_mtime(const Path::parts_t &parts) {\n\tauto stat_result = this->do_stat(parts);\n\n\t// return the mtime\n\tif (std::get<1>(stat_result) == 0) {\n#ifdef __APPLE__\n\t\treturn std::get<0>(stat_result).st_mtimespec.tv_sec;\n#elif defined _WIN32\n\t\treturn std::get<0>(stat_result).st_mtime;\n#else\n\t\treturn std::get<0>(stat_result).st_mtim.tv_sec;\n#endif\n\t}\n\n\tthrow Error{ERR << \"can't get mtime\"};\n}\n\n\nuint64_t Directory::get_filesize(const Path::parts_t &parts) {\n\tauto stat_result = this->do_stat(parts);\n\n\t// return the mtime\n\tif (std::get<1>(stat_result) == 0) {\n\t\treturn std::get<0>(stat_result).st_size;\n\t}\n\n\tthrow Error{ERR << \"can't get filesize\"};\n}\n\n\nstd::ostream &Directory::repr(std::ostream &stream) {\n\tstream << this->basepath;\n\treturn stream;\n}\n\nDirectory Directory::get_temp_directory() {\n\tstd::filesystem::path path = std::filesystem::temp_directory_path() / std::tmpnam(nullptr);\n\tstd::string temp_dir_path = path.string();\n\tbool create = true;\n\tDirectory directory = Directory(temp_dir_path, create);\n\treturn directory;\n}\n\n} // namespace openage::util::fslike\n"
  },
  {
    "path": "libopenage/util/fslike/directory.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n#include <string>\n#include <sys/stat.h>\n#include <tuple>\n#include <vector>\n\n#include \"fslike.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace fslike {\n\n\n/**\n * Filesystem-like object which uses native libc calls.\n * It is used to directly access your real filesystem\n * that the kernel mounted for you.\n */\nclass Directory : public FSLike {\npublic:\n\tDirectory(std::string basepath, bool create_if_missing = false);\n\n\tbool is_file(const Path::parts_t &parts) override;\n\tbool is_dir(const Path::parts_t &parts) override;\n\tbool writable(const Path::parts_t &parts) override;\n\tstd::vector<Path::part_t> list(const Path::parts_t &parts) override;\n\tbool mkdirs(const Path::parts_t &parts) override;\n\tFile open_r(const Path::parts_t &parts) override;\n\tFile open_w(const Path::parts_t &parts) override;\n\tFile open_rw(const Path::parts_t &parts) override;\n\tFile open_a(const Path::parts_t &parts) override;\n\tFile open_ar(const Path::parts_t &parts) override;\n\t// inherit the resolve_r/resolve_w functions\n\tstd::string get_native_path(const Path::parts_t &parts) override;\n\tbool rename(const Path::parts_t &parts,\n\t            const Path::parts_t &target_parts) override;\n\tbool rmdir(const Path::parts_t &parts) override;\n\tbool touch(const Path::parts_t &parts) override;\n\tbool unlink(const Path::parts_t &parts) override;\n\n\tint get_mtime(const Path::parts_t &parts) override;\n\tuint64_t get_filesize(const Path::parts_t &parts) override;\n\n\tstd::ostream &repr(std::ostream &) override;\n\n\tstatic Directory get_temp_directory();\n\nprotected:\n\t/**\n\t * resolve the path to an actually usable one.\n\t * basically basepath + \"/\".join(parts)\n\t */\n\tstd::string resolve(const Path::parts_t &parts) const;\n\n\tstd::tuple<struct stat, int> do_stat(const Path::parts_t &parts) const;\n\n\tstd::string basepath;\n};\n} // namespace fslike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fslike/fslike.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"fslike.h\"\n\n#include \"../path.h\"\n\n\nnamespace openage::util::fslike {\n\nFSLike::FSLike() = default;\n\nPath FSLike::root() {\n\treturn Path{this->shared_from_this(), {}};\n}\n\n\nstd::pair<bool, Path> FSLike::resolve_r(const Path::parts_t &parts) {\n\tif (this->is_file(parts) or this->is_dir(parts)) {\n\t\treturn std::make_pair(true, Path{this->shared_from_this(), parts});\n\t}\n\telse {\n\t\treturn std::make_pair(false, Path{});\n\t}\n}\n\n\nstd::pair<bool, Path> FSLike::resolve_w(const Path::parts_t &parts) {\n\tif (this->writable(parts)) {\n\t\treturn std::make_pair(true, Path{this->shared_from_this(), parts});\n\t}\n\telse {\n\t\treturn std::make_pair(false, Path{});\n\t}\n}\n\n\nbool FSLike::is_python_native() const noexcept {\n\treturn false;\n}\n\n\n} // openage::util::fslike\n"
  },
  {
    "path": "libopenage/util/fslike/fslike.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp cimport bool\n// pxd: from libcpp.vector cimport vector\n\n// pxd: from libc.stdint cimport uint64_t\n#include <cstdint>\n#include <iostream>\n// pxd: from libcpp.memory cimport shared_ptr\n#include <memory>\n// pxd: from libcpp.string cimport string\n#include <string>\n// pxd: from libcpp.utility cimport pair\n#include <utility>\n\n// pxd: from libopenage.util.path cimport Path\n// pxd: ctypedef vector[string] parts_t\n#include \"../path.h\"\n// pxd: from libopenage.util.file cimport File\n#include \"../file.h\"\n\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Contains the filesystem-like object abstraction for C++.\n * With that, filesystem access can be dispatched for\n * transparent archive access, union mounts and more.\n */\nnamespace fslike {\n\n/**\n * Base class for possible path implementations.\n *\n * pxd:\n * cppclass FSLike:\n *     Path root() except +\n *     bool is_file(const parts_t &parts) except +\n *     bool is_dir(const parts_t &parts) except +\n *     bool writable(const parts_t &parts) except +\n *     parts_t list(const parts_t &parts) except +\n *     bool mkdirs(const parts_t &parts) except +\n *\n *     File open_r(const parts_t &parts) except +\n *     File open_w(const parts_t &parts) except +\n *     File open_rw(const parts_t &parts) except +\n *     File open_a(const parts_t &parts) except +\n *     File open_ar(const parts_t &parts) except +\n *     pair[bool, Path] resolve_r(const parts_t &parts) except +\n *     pair[bool, Path] resolve_w(const parts_t &parts) except +\n *     string get_native_path(const parts_t &parts) except +\n *\n *     bool rename(const parts_t &parts,\n *                 const parts_t &target_parts) except +\n *     bool rmdir(const parts_t &parts) except +\n *     bool touch(const parts_t &parts) except +\n *     bool unlink(const parts_t &parts) except +\n *\n *     int get_mtime(const parts_t &parts) except +\n *     uint64_t get_filesize(const parts_t &parts) except +\n *\n *     bool is_python_native() noexcept\n *     shared_ptr[FSLike] shared_from_this() except +\n */\nclass OAAPI FSLike : public std::enable_shared_from_this<FSLike> {\npublic:\n\tFSLike();\n\n\tvirtual ~FSLike() = default;\n\n\tvirtual Path root();\n\n\tvirtual bool is_file(const Path::parts_t &parts) = 0;\n\tvirtual bool is_dir(const Path::parts_t &parts) = 0;\n\tvirtual bool writable(const Path::parts_t &parts) = 0;\n\tvirtual Path::parts_t list(const Path::parts_t &parts) = 0;\n\tvirtual bool mkdirs(const Path::parts_t &parts) = 0;\n\tvirtual File open_r(const Path::parts_t &parts) = 0;\n\tvirtual File open_w(const Path::parts_t &parts) = 0;\n\tvirtual File open_rw(const Path::parts_t &parts) = 0;\n\tvirtual File open_a(const Path::parts_t &parts) = 0;\n\tvirtual File open_ar(const Path::parts_t &parts) = 0;\n\n\t// provide a default implementation that resolves the path\n\t// by checking if it is readable/writable:\n\tvirtual std::pair<bool, Path> resolve_r(const Path::parts_t &parts);\n\tvirtual std::pair<bool, Path> resolve_w(const Path::parts_t &parts);\n\tvirtual std::string get_native_path(const Path::parts_t &parts) = 0;\n\tvirtual bool rename(const Path::parts_t &parts,\n\t                    const Path::parts_t &target_parts) = 0;\n\tvirtual bool rmdir(const Path::parts_t &parts) = 0;\n\tvirtual bool touch(const Path::parts_t &parts) = 0;\n\tvirtual bool unlink(const Path::parts_t &parts) = 0;\n\n\tvirtual int get_mtime(const Path::parts_t &parts) = 0;\n\tvirtual uint64_t get_filesize(const Path::parts_t &parts) = 0;\n\n\tvirtual bool is_python_native() const noexcept;\n\n\tvirtual std::ostream &repr(std::ostream &) = 0;\n};\n\n} // namespace fslike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fslike/native.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"native.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace fslike {\n\n}}} // openage::util::fslike\n"
  },
  {
    "path": "libopenage/util/fslike/native.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\nnamespace openage {\nnamespace util {\nnamespace fslike {\n\n\n// TODO: at compiletime, figure out the path separator.\n//       this is most likely a job of the buildsystem configuration.\nconstexpr char PATHSEP = '/';\n\n\n} // namespace fslike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/fslike/python.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"python.h\"\n\n#include \"../../log/log.h\"\n\nnamespace openage::util::fslike {\n\n\nPython::Python(const py::Obj &fsobj)\n\t:\n\tfsobj{std::make_shared<py::Obj>(fsobj)} {}\n\n\nbool Python::is_file(const Path::parts_t &parts) {\n\treturn pyx_fs_is_file.call(this->fsobj->get_ref(), parts);\n}\n\nbool Python::is_dir(const Path::parts_t &parts) {\n\treturn pyx_fs_is_dir.call(this->fsobj->get_ref(), parts);\n}\n\nbool Python::writable(const Path::parts_t &parts) {\n\treturn pyx_fs_writable.call(this->fsobj->get_ref(), parts);\n}\n\nstd::vector<Path::part_t> Python::list(const Path::parts_t &parts) {\n\treturn pyx_fs_list.call(this->fsobj->get_ref(), parts);\n}\n\nbool Python::mkdirs(const Path::parts_t &parts) {\n\treturn pyx_fs_mkdirs.call(this->fsobj->get_ref(), parts);\n}\n\nFile Python::open_r(const Path::parts_t &parts) {\n\treturn pyx_fs_open_r.call(this->fsobj->get_ref(), parts);\n}\n\nFile Python::open_w(const Path::parts_t &parts) {\n\treturn pyx_fs_open_w.call(this->fsobj->get_ref(), parts);\n}\n\nFile Python::open_rw(const Path::parts_t &parts) {\n\treturn pyx_fs_open_rw.call(this->fsobj->get_ref(), parts);\n}\n\nFile Python::open_a(const Path::parts_t &parts) {\n\treturn pyx_fs_open_a.call(this->fsobj->get_ref(), parts);\n}\n\nFile Python::open_ar(const Path::parts_t &parts) {\n\treturn pyx_fs_open_ar.call(this->fsobj->get_ref(), parts);\n}\n\nstd::pair<bool, Path> Python::resolve_r(const Path::parts_t &parts) {\n\tauto path = pyx_fs_resolve_r.call(this->fsobj->get_ref(), parts);\n\treturn std::make_pair(path.get_fsobj() != nullptr, path);\n}\n\nstd::pair<bool, Path> Python::resolve_w(const Path::parts_t &parts) {\n\tauto path = pyx_fs_resolve_w.call(this->fsobj->get_ref(), parts);\n\treturn std::make_pair(path.get_fsobj() != nullptr, path);\n}\n\nstd::string Python::get_native_path(const Path::parts_t &parts) {\n\tpy::Obj ret = pyx_fs_get_native_path.call(this->fsobj->get_ref(), parts);\n\tif (ret.is(py::None)) {\n\t\treturn \"\";\n\t}\n\telse {\n\t\treturn ret.bytes();\n\t}\n}\n\nbool Python::rename(const Path::parts_t &parts,\n                    const Path::parts_t &target_parts) {\n\treturn pyx_fs_rename.call(this->fsobj->get_ref(), parts, target_parts);\n}\n\n\nbool Python::rmdir(const Path::parts_t &parts) {\n\treturn pyx_fs_rmdir.call(this->fsobj->get_ref(), parts);\n}\n\nbool Python::touch(const Path::parts_t &parts) {\n\treturn pyx_fs_touch.call(this->fsobj->get_ref(), parts);\n}\n\nbool Python::unlink(const Path::parts_t &parts) {\n\treturn pyx_fs_unlink.call(this->fsobj->get_ref(), parts);\n}\n\n\nint Python::get_mtime(const Path::parts_t &parts) {\n\treturn pyx_fs_get_mtime.call(this->fsobj->get_ref(), parts);\n}\n\nuint64_t Python::get_filesize(const Path::parts_t &parts) {\n\treturn pyx_fs_get_filesize.call(this->fsobj->get_ref(), parts);\n}\n\n\nbool Python::is_python_native() const noexcept {\n\treturn true;\n}\n\n\npy::Obj &Python::get_py_fsobj() const {\n\treturn *this->fsobj.get();\n}\n\n\nstd::ostream &Python::repr(std::ostream &stream) {\n\tstream << this->fsobj->str();\n\treturn stream;\n}\n\n\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_is_file;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_is_dir;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_writable;\npyinterface::PyIfFunc<std::vector<std::string>, PyObject *, const std::vector<std::string>&> pyx_fs_list;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_mkdirs;\npyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string>&> pyx_fs_open_r;\npyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string>&> pyx_fs_open_w;\npyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string>&> pyx_fs_open_rw;\npyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string>&> pyx_fs_open_a;\npyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string>&> pyx_fs_open_ar;\npyinterface::PyIfFunc<Path, PyObject *, const std::vector<std::string>&> pyx_fs_resolve_r;\npyinterface::PyIfFunc<Path, PyObject *, const std::vector<std::string>&> pyx_fs_resolve_w;\n\npyinterface::PyIfFunc<py::Obj, PyObject *, const std::vector<std::string>&> pyx_fs_get_native_path;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&, const std::vector<std::string>&> pyx_fs_rename;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_rmdir;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_touch;\npyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string>&> pyx_fs_unlink;\npyinterface::PyIfFunc<int, PyObject *, const std::vector<std::string>&> pyx_fs_get_mtime;\npyinterface::PyIfFunc<uint64_t, PyObject *, const std::vector<std::string>&> pyx_fs_get_filesize;\npyinterface::PyIfFunc<bool, PyObject *> pyx_fs_is_fslike_directory;\n\n} // openage::util::fslike\n"
  },
  {
    "path": "libopenage/util/fslike/python.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n// pxd: from libcpp cimport bool\n// pxd: from libc.stdint cimport uint64_t\n#include <cstdint>\n#include <memory>\n// pxd: from libcpp.string cimport string\n#include <string>\n// pxd: from libcpp.vector cimport vector\n#include <vector>\n\n// pxd: from libopenage.pyinterface.functional cimport PyIfFunc1, PyIfFunc2, PyIfFunc3\n#include \"../../pyinterface/functional.h\"\n// pxd: from libopenage.pyinterface.pyobject cimport PyObj, PyObjectPtr, PyObjectRef\n#include \"../../pyinterface/pyobject.h\"\n\n// pxd: from libopenage.util.fslike.fslike cimport FSLike\n#include \"fslike.h\"\n// pxd: from libopenage.util.path cimport Path\n#include \"../path.h\"\n// pxd: from libopenage.util.file cimport File\n#include \"../file.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace fslike {\n\n\n/**\n * Filesystem-like object that wraps a filesystem-like object from Python.\n * Calls are relayed via Cython, which performs the data conversion.\n *\n * pxd:\n * cppclass Python(FSLike):\n *     PyObj &get_py_fsobj() except +\n */\nclass Python : public FSLike {\npublic:\n\tPython(const py::Obj &fsobj);\n\n\tbool is_file(const Path::parts_t &parts) override;\n\tbool is_dir(const Path::parts_t &parts) override;\n\tbool writable(const Path::parts_t &parts) override;\n\tstd::vector<Path::part_t> list(const Path::parts_t &parts) override;\n\tbool mkdirs(const Path::parts_t &parts) override;\n\tFile open_r(const Path::parts_t &parts) override;\n\tFile open_w(const Path::parts_t &parts) override;\n\tFile open_rw(const Path::parts_t &parts) override;\n\tFile open_a(const Path::parts_t &parts) override;\n\tFile open_ar(const Path::parts_t &parts) override;\n\t// specialize the resolve functions to relay them to python.\n\tstd::pair<bool, Path> resolve_r(const Path::parts_t &parts) override;\n\tstd::pair<bool, Path> resolve_w(const Path::parts_t &parts) override;\n\tstd::string get_native_path(const Path::parts_t &parts) override;\n\tbool rename(const Path::parts_t &parts,\n\t            const Path::parts_t &target_parts) override;\n\tbool rmdir(const Path::parts_t &parts) override;\n\tbool touch(const Path::parts_t &parts) override;\n\tbool unlink(const Path::parts_t &parts) override;\n\n\tint get_mtime(const Path::parts_t &parts) override;\n\tuint64_t get_filesize(const Path::parts_t &parts) override;\n\n\tbool is_python_native() const noexcept override;\n\tOAAPI py::Obj &get_py_fsobj() const;\n\n\tstd::ostream &repr(std::ostream &) override;\n\nprotected:\n\tstd::shared_ptr<py::Obj> fsobj;\n};\n\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_is_file\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_is_file;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_is_dir\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_is_dir;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_writable\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_writable;\n\n// pxd: PyIfFunc2[vector[string], PyObjectPtr, const vector[string]] pyx_fs_list\nextern OAAPI pyinterface::PyIfFunc<std::vector<std::string>, PyObject *, const std::vector<std::string> &> pyx_fs_list;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_mkdirs\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_mkdirs;\n\n// pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_r\nextern OAAPI pyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string> &> pyx_fs_open_r;\n\n// pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_w\nextern OAAPI pyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string> &> pyx_fs_open_w;\n\n// pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_rw\nextern OAAPI pyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string> &> pyx_fs_open_rw;\n\n// pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_a\nextern OAAPI pyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string> &> pyx_fs_open_a;\n\n// pxd: PyIfFunc2[File, PyObjectPtr, const vector[string]] pyx_fs_open_ar\nextern OAAPI pyinterface::PyIfFunc<File, PyObject *, const std::vector<std::string> &> pyx_fs_open_ar;\n\n// pxd: PyIfFunc2[Path, PyObjectPtr, const vector[string]] pyx_fs_resolve_r\nextern OAAPI pyinterface::PyIfFunc<Path, PyObject *, const std::vector<std::string> &> pyx_fs_resolve_r;\n\n// pxd: PyIfFunc2[Path, PyObjectPtr, const vector[string]] pyx_fs_resolve_w\nextern OAAPI pyinterface::PyIfFunc<Path, PyObject *, const std::vector<std::string> &> pyx_fs_resolve_w;\n\n// pxd: PyIfFunc2[PyObjectRef, PyObjectPtr, const vector[string]] pyx_fs_get_native_path\nextern OAAPI pyinterface::PyIfFunc<py::Obj, PyObject *, const std::vector<std::string> &> pyx_fs_get_native_path;\n\n// pxd: PyIfFunc3[bool, PyObjectPtr, const vector[string], const vector[string]] pyx_fs_rename\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &, const std::vector<std::string> &> pyx_fs_rename;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_rmdir\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_rmdir;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_touch\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_touch;\n\n// pxd: PyIfFunc2[bool, PyObjectPtr, const vector[string]] pyx_fs_unlink\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *, const std::vector<std::string> &> pyx_fs_unlink;\n\n// pxd: PyIfFunc2[int, PyObjectPtr, const vector[string]] pyx_fs_get_mtime\nextern OAAPI pyinterface::PyIfFunc<int, PyObject *, const std::vector<std::string> &> pyx_fs_get_mtime;\n\n// pxd: PyIfFunc2[uint64_t, PyObjectPtr, const vector[string]] pyx_fs_get_filesize\nextern OAAPI pyinterface::PyIfFunc<uint64_t, PyObject *, const std::vector<std::string> &> pyx_fs_get_filesize;\n\n\n// pxd: PyIfFunc1[bool, PyObjectPtr] pyx_fs_is_fslike_directory\nextern OAAPI pyinterface::PyIfFunc<bool, PyObject *> pyx_fs_is_fslike_directory;\n\n\n} // namespace fslike\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/hash.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include <array>\n#include <climits>\n\n#include \"hash.h\"\n#include \"misc.h\"\n\nnamespace openage::util {\n\nsize_t hash_combine(size_t hash1, size_t hash2) {\n\t// https://www.boost.org/doc/libs/1_70_0/doc/html/hash/reference.html#boost.hash_combine\n\treturn hash1 ^ (hash2 + 0x9e3779b9 + ((hash1 << 6) + (hash1 >> 2)));\n}\n\n\nSiphash::Siphash(std::array<uint8_t, 16> key) {\n\tset_key(key);\n}\n\n\nSiphash& Siphash::set_key(std::array<uint8_t, 16> key) {\n\tthis->key[0] = array8_to_uint64(key.data(), 8, false);\n\tthis->key[1] = array8_to_uint64(key.data() + 8, 8, false);\n\n\treturn *this;\n}\n\n\n/**\n * Siphash implementation\n *\n * https://131002.net/siphash/\n */\nuint64_t hash(const uint64_t key[2], const uint8_t *data, size_t len_data) {\n\n\tstatic auto sipround = [](uint64_t *v) {\n\t\tv[0] += v[1];\n\t\tv[1] = rol<uint64_t, 13>(v[1]);\n\t\tv[1] ^= v[0];\n\t\tv[0] = rol<uint64_t, 32>(v[0]);\n\t\tv[2] += v[3];\n\t\tv[3] = rol<uint64_t, 16>(v[3]);\n\t\tv[3] ^= v[2];\n\t\tv[0] += v[3];\n\t\tv[3] = rol<uint64_t, 21>(v[3]);\n\t\tv[3] ^= v[0];\n\t\tv[2] += v[1];\n\t\tv[1] = rol<uint64_t, 17>(v[1]);\n\t\tv[1] ^= v[2];\n\t\tv[2] = rol<uint64_t, 32>(v[2]);\n\t};\n\n\t// (len_data mod 256) ending to append later at the end of the data\n\tuint64_t ending = static_cast<uint64_t>(len_data & 0xff) << 56;\n\n\t// Initialization\n\n\tuint64_t v[4];\n\t// These values were taken from the paper: https://131002.net/siphash/siphash.pdf\n\tv[0] = key[0] ^ 0x736f6d6570736575ull;\n\tv[1] = key[1] ^ 0x646f72616e646f6dull;\n\tv[2] = key[0] ^ 0x6c7967656e657261ull;\n\tv[3] = key[1] ^ 0x7465646279746573ull;\n\n\t// Compression\n\n\t{\n\t\tsize_t rem_bytes{len_data};\n\t\tconst uint8_t *idx{data}; // Points to the start of the current block being copied\n\t\tuint64_t block;\n\t\tbool finished{false};\n\n\t\tdo {\n\t\t\tif (rem_bytes < 8) {\n\t\t\t\tblock = array8_to_uint64(idx, rem_bytes, false);\n\t\t\t\tblock |= ending;\n\t\t\t\tfinished = true;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tblock = array8_to_uint64(idx, 8, false);\n\t\t\t}\n\n\t\t\tv[3] ^= block;\n\t\t\tsipround(v);\n\t\t\tsipround(v);\n\t\t\tv[0] ^= block;\n\n\t\t\trem_bytes -= 8;\n\t\t\tidx += 8;\n\t\t} while (!finished);\n\t}\n\n\t// Finalization\n\n\tv[2] ^= 0xff;\n\tsipround(v);\n\tsipround(v);\n\tsipround(v);\n\tsipround(v);\n\n\treturn v[0] ^ v[1] ^ v[2] ^ v[3];\n}\n\n\nuint64_t Siphash::digest(const uint8_t *data, size_t count) {\n\treturn hash(key, data, count);\n}\n\n\nuint64_t Siphash::digest(uint64_t value) {\n\tstd::vector<uint8_t> data = uint64_to_array8(value, true);\n\treturn hash(key, data.data(), 8);\n}\n\n\n} // openage::util\n"
  },
  {
    "path": "libopenage/util/hash.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <array>\n#include <cstdint>\n#include <cstdlib>\n#include <typeindex>\n#include <typeinfo>\n\nnamespace openage {\nnamespace util {\n\n/**\n * Helper function to generate hash of certain type T\n */\n\ntemplate <typename T>\nsize_t type_hash() {\n\treturn std::hash<std::type_index>()(std::type_index(typeid(T)));\n}\n\n/**\n * A hash combination function\n * Creates a hash value as a combination of two other hashes. Can be called incrementally to create\n * hash value from several variables. Will always produce the same result for the same combination of\n * hash1 and hash2 during a single run of a program.\n */\nsize_t hash_combine(size_t hash1, size_t hash2);\n\n\n/** \\class Siphash\n * Contains a Siphash implementration.\n *\n * https://131002.net/siphash/\n */\nclass Siphash {\npublic:\n\t/**\n\t * Creates a hash generator.\n\t * @param key Key to use with this hasher.\n\t */\n\tSiphash(std::array<uint8_t, 16> key);\n\n\n\t/**\n\t * Set the key for subsequent hashes.\n\t * @param k Key to use with this hasher.\n\t * @return Reference to itself, for method chaining.\n\t */\n\tSiphash &set_key(std::array<uint8_t, 16> key);\n\n\n\t/**\n\t * Hashes the input data as uint8_t array and returns the result.\n\t * @param data Start of the input data.\n\t * @param len_data Number of bytes to read.\n\t * @return Hash.\n\t */\n\tuint64_t digest(const uint8_t *data, size_t len_data);\n\n\n\t/**\n\t * Hashes a single uint64_t number and returns the result.\n\t *\n\t * The 64-bit word is interpreted as big-endian.\n\t * e.g. 0x0123456789abcdef -> {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}\n\t *\n\t * @param value uint64_t to hash.\n\t * @return Hash.\n\t */\n\tuint64_t digest(const uint64_t);\n\n\nprivate:\n\t/**\n\t * The key to use for hashing. As two 64-bit little-endian words.\n\t */\n\tuint64_t key[2];\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/hash_test.cpp",
    "content": "// Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n#include \"hash.h\"\n\n#include <array>\n\n#include \"../testing/testing.h\"\n#include \"misc.h\"\n\n\nnamespace openage::util::tests {\n\nstatic const std::array<uint8_t, 16> test_key {{\n\t0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,\n\t0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f\n}};\n\n\nvoid siphash() {\n\n\t/*\n\t * Hash results for some Siphash inputs\n\t *\n\t * The test key is 0x000102...0d0e0f\n\t * The inputs are:\n\t *    - nothing (zero length)\n\t *    - 0x00\n\t *    - 0x0001\n\t *    - 0x000102\n\t *    - 0x00010203\n\t *    - ...\n\t */\n\tstatic const std::array<uint64_t, 64> expected_hashes {{\n\t\t0x726fdb47dd0e0e31,0x74f839c593dc67fd,0x0d6c8009d9a94f5a,0x85676696d7fb7e2d,\n\t\t0xcf2794e0277187b7,0x18765564cd99a68d,0xcbc9466e58fee3ce,0xab0200f58b01d137,\n\t\t0x93f5f5799a932462,0x9e0082df0ba9e4b0,0x7a5dbbc594ddb9f3,0xf4b32f46226bada7,\n\t\t0x751e8fbc860ee5fb,0x14ea5627c0843d90,0xf723ca908e7af2ee,0xa129ca6149be45e5,\n\t\t0x3f2acc7f57c29bdb,0x699ae9f52cbe4794,0x4bc1b3f0968dd39c,0xbb6dc91da77961bd,\n\t\t0xbed65cf21aa2ee98,0xd0f2cbb02e3b67c7,0x93536795e3a33e88,0xa80c038ccd5ccec8,\n\t\t0xb8ad50c6f649af94,0xbce192de8a85b8ea,0x17d835b85bbb15f3,0x2f2e6163076bcfad,\n\t\t0xde4daaaca71dc9a5,0xa6a2506687956571,0xad87a3535c49ef28,0x32d892fad841c342,\n\t\t0x7127512f72f27cce,0xa7f32346f95978e3,0x12e0b01abb051238,0x15e034d40fa197ae,\n\t\t0x314dffbe0815a3b4,0x027990f029623981,0xcadcd4e59ef40c4d,0x9abfd8766a33735c,\n\t\t0x0e3ea96b5304a7d0,0xad0c42d6fc585992,0x187306c89bc215a9,0xd4a60abcf3792b95,\n\t\t0xf935451de4f21df2,0xa9538f0419755787,0xdb9acddff56ca510,0xd06c98cd5c0975eb,\n\t\t0xe612a3cb9ecba951,0xc766e62cfcadaf96,0xee64435a9752fe72,0xa192d576b245165a,\n\t\t0x0a8787bf8ecb74b2,0x81b3e73d20b49b6f,0x7fa8220ba3b2ecea,0x245731c13ca42499,\n\t\t0xb78dbfaf3a8d83bd,0xea1ad565322a1a0b,0x60e61c23a3795013,0x6606d7e446282b93,\n\t\t0x6ca4ecb15c5f91e1,0x9f626da15c9625f3,0xe51b38608ef25f57,0x958a324ceb064572\n\t}};\n\n\tstd::array<uint8_t, 64> data;\n\topenage::util::Siphash siphash(test_key);\n\n\tfor (int i = 0; i < 64; i++) {\n\t\tTESTEQUALS(siphash.digest(data.data(), i), expected_hashes[i]);\n\t\tdata.at(i) = i;\n\t}\n\n\t// Now test the 64bit input digest\n\tconst uint8_t data8[8] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef};\n\tconst uint64_t data64 = 0x0123456789abcdef;\n\tTESTEQUALS(siphash.digest(data8, 8), siphash.digest(data64));\n}\n\n} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/init.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"init.h\"\n\n#include \"../testing/testing.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\n// those test variables need no linkage.\nnamespace {\n\tint testval;\n\n\tOnInit init_testval([]() {\n\t\ttestval = 5;\n\t});\n}\n\nvoid init() {\n\tTESTEQUALS(testval, 5);\n\n\tint i = 6;\n\n\tTESTEQUALS(i, 6);\n\n\t{\n\t\tOnInit init_i([&]() {\n\t\t\ti = 7;\n\t\t});\n\n\t\tOnDeInit deinit_i([&]() {\n\t\t\ti = 8;\n\t\t});\n\n\t\tTESTEQUALS(i, 7);\n\t}\n\n\tTESTEQUALS(i, 8);\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/init.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <functional>\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Runs code() on construction.\n *\n * Designed for use as a global object.\n * Note that your regular dynamic initialization order concerns apply.\n *\n * Example:\n *\n * // anonymous namespace as container for these global objects\n * namespace {\n * util::OnInit stuff_to_be_done_on_initialization([]() { do_stuff(1 * 2 + 3); });\n * }\n */\nclass OnInit {\npublic:\n\texplicit OnInit(std::function<void()> code) {\n\t\tcode();\n\t}\n\n\t~OnInit() = default;\n\nprivate:\n\t// nope.\n\tOnInit(const OnInit &) = delete;\n\tOnInit(OnInit &&) = delete;\n\n\tOnInit &operator=(const OnInit &) = delete;\n\tOnInit &operator=(OnInit &&) = delete;\n};\n\n\n/**\n * Runs code() on destruction.\n *\n * Designed mostly for use as a global object, though theoretically\n * it might be useful as a stack-allocated object in some circumstances.\n */\nclass OnDeInit {\npublic:\n\texplicit OnDeInit(std::function<void()> code) :\n\t\tcode{code} {}\n\n\n\t~OnDeInit() {\n\t\tthis->code();\n\t}\n\nprivate:\n\tstd::function<void()> code;\n\n\t// nope.\n\tOnDeInit(const OnDeInit &) = delete;\n\tOnDeInit(OnDeInit &&) = delete;\n\n\tOnDeInit &operator=(const OnDeInit &) = delete;\n\tOnDeInit &operator=(OnDeInit &&) = delete;\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/language.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"language.h\"\n"
  },
  {
    "path": "libopenage/util/language.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n\n/*\n * Some general-purpose utilities related to the C++ language.\n */\n\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Simple wrapper type that contains a function pointer.\n * Needed as workaround for http://stackoverflow.com/questions/31040075\n */\ntemplate <typename ReturnType, typename... ArgTypes>\nclass FunctionPtr {\npublic:\n\t/**\n\t * Implicit conversion from raw type.\n\t */\n\tFunctionPtr(ReturnType (*ptr)(ArgTypes...)) :\n\t\tptr{ptr} {}\n\n\tReturnType (*ptr)(ArgTypes...);\n};\n\n\n/**\n * Wrapping a function call with ignore_result makes it more clear to\n * readers, compilers and linters that you are, in fact, ignoring the\n * function's return value on purpose.\n */\ntemplate <typename T>\ninline void ignore_result(T /* unused result */) {}\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/macro/concat.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#define CONCAT_1(OP, X) (X)\n#define CONCAT_2(OP, X, Y) (X) OP(Y)\n#define CONCAT_3(OP, X, Y, Z) (X) OP(Y) OP(Z)\n\n#define CONCAT_N(_1, _2, _3, NAME, ...) NAME\n\n#ifdef _MSC_VER\n\t#define CONCAT_COUNT_ARGS_IMPL(args) CONCAT_N args\n\t#define CONCAT_COUNT_ARGS(...) CONCAT_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1))\n\t#define CONCAT_HELPER2(count) CONCAT_##count\n\t#define CONCAT_HELPER1(count) CONCAT_HELPER2(count)\n\t#define CONCAT_HELPER(count) CONCAT_HELPER1(count)\n\t#define CONCAT_GLUE(x, y) x y\n\t#define CONCAT(OP, ...) CONCAT_GLUE(CONCAT_HELPER(CONCAT_COUNT_ARGS(__VA_ARGS__)), (OP, __VA_ARGS__))\n#else\n\t#define CONCAT(OP, ...) CONCAT_N(__VA_ARGS__, \\\n\t\t                             CONCAT_3, \\\n\t\t                             CONCAT_2, \\\n\t\t                             CONCAT_1)(OP, __VA_ARGS__)\n#endif\n"
  },
  {
    "path": "libopenage/util/macro/loop.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#define LOOP_1(MACRO, X) MACRO(X)\n#define LOOP_2(MACRO, X, Y) MACRO(X), MACRO(Y)\n#define LOOP_3(MACRO, X, Y, Z) MACRO(X), MACRO(Y), MACRO(Z)\n\n#define LOOP_N(_1, _2, _3, NAME, ...) NAME\n\n#ifdef _MSC_VER\n\t#define LOOP_COUNT_ARGS_IMPL(args) LOOP_N args\n\t#define LOOP_COUNT_ARGS(...) LOOP_COUNT_ARGS_IMPL((__VA_ARGS__, 3, 2, 1))\n\t#define LOOP_HELPER2(count) LOOP_##count\n\t#define LOOP_HELPER1(count) LOOP_HELPER2(count)\n\t#define LOOP_HELPER(count) LOOP_HELPER1(count)\n\t#define LOOP_GLUE(x, y) x y\n\t#define LOOP(MACRO, ...) LOOP_GLUE(LOOP_HELPER(LOOP_COUNT_ARGS(__VA_ARGS__)), (MACRO, __VA_ARGS__))\n#else\n\t#define LOOP(MACRO, ...) LOOP_N(__VA_ARGS__, \\\n\t\t                            LOOP_3, \\\n\t\t                            LOOP_2, \\\n\t\t                            LOOP_1)(MACRO, __VA_ARGS__)\n#endif\n"
  },
  {
    "path": "libopenage/util/math.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cmath>\n\nnamespace openage {\nnamespace math {\n\ntemplate <typename T>\nT square(T arg) {\n\treturn arg * arg;\n}\n\n\ntemplate <typename T>\nT hypot3(T x, T y, T z) {\n\treturn sqrt(x * x + y * y + z * z);\n}\n\n} // namespace math\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/math_constants.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <limits>\n#include <numbers>\n\nnamespace openage {\nnamespace math {\n\n// TODO: use std::numbers instead of these where appropriate\n// clang-format off\n// keep equal signs aligned for readability\nconstexpr double E            = std::numbers::e;  //!< e\nconstexpr double LOG2E        = std::numbers::log2e;  //!< log_2 e\nconstexpr double LOG10E       = std::numbers::log10e; //!< log_10 e\nconstexpr double LN2          = std::numbers::ln2; //!< log_e 2\nconstexpr double LN10         = 2.30258509299404568402;  //!< log_e 10\nconstexpr double PI           = std::numbers::pi;  //!< pi\nconstexpr double PI_2         = 1.57079632679489661923;  //!< pi/2\nconstexpr double PI_4         = 0.785398163397448309616; //!< pi/4\nconstexpr double INV_PI       = std::numbers::inv_pi; //!< 1/pi\nconstexpr double INV2_PI      = 0.636619772367581343076; //!< 2/pi\nconstexpr double INV2_SQRT_PI = 1.12837916709551257390;  //!< 2/sqrt(pi)\nconstexpr double TAU          = 6.28318530717958647693;  //!< 2*pi\nconstexpr double DEGSPERRAD   = 0.017453292519943295769; //!< tau/360\nconstexpr double RADSPERDEG   = 57.29577951308232087679; //!< 360/tau\nconstexpr double SQRT_2       = std::numbers::sqrt2;  //!< sqrt(2)\nconstexpr double INV_SQRT_2   = 0.707106781186547524401; //!< 1/sqrt(2)\n// clang-format on\n\nconstexpr unsigned int UINT_INF = std::numeric_limits<unsigned int>::max();\nconstexpr int INT_INF = std::numeric_limits<int>::max();\nconstexpr double DOUBLE_INF = std::numeric_limits<double>::max();\n\n} // namespace math\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/matrix.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"matrix.h\"\n\nnamespace openage {\nnamespace util {\n\n// Matrix is all templated, so there's nothing to implement.\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/matrix.h",
    "content": "// Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <array>\n#include <cmath>\n#include <cstring>\n#include <iostream>\n#include <type_traits>\n\n#include \"vector.h\"\n\nnamespace openage::util {\n\n/**\n * Matrix class with arithmetic. M rows, N columns.\n * T = underlying single value type (float, double, ...)\n */\ntemplate <size_t M, size_t N, typename T>\nclass Matrix : public std::array<std::array<T, N>, M> {\npublic:\n\tstatic_assert(M > 0 and N > 0, \"0-dimensional matrix not allowed\");\n\n\tusing this_type = Matrix<M, N, T>;\n\n\tstatic constexpr float default_eps = 1e-5;\n\n\tstatic constexpr size_t rows = M;\n\tstatic constexpr size_t cols = N;\n\tstatic constexpr bool is_square = (M == N);\n\tstatic constexpr bool is_row_vector = (M == 1);\n\tstatic constexpr bool is_column_vector = (N == 1);\n\n\t/**\n\t * Initialize the matrix to zeroes.\n\t */\n\tMatrix() {\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\t(*this)[i][j] = 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t~Matrix() = default;\n\n\t/**\n\t * Constructor from Vector\n\t */\n\ttemplate <bool cond = is_column_vector,\n\t          typename = typename std::enable_if<cond>::type>\n\tMatrix(const Vector<M, T> &vec) {\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\t(*this)[i][0] = vec[i];\n\t\t}\n\t}\n\n\t/**\n\t * Constructor with N*M values\n\t */\n\ttemplate <typename... Ts>\n\tMatrix(Ts... args) {\n\t\tstatic_assert(sizeof...(args) == N * M, \"not all values supplied\");\n\n\t\tstd::array<float, N * M> temp{{static_cast<T>(args)...}};\n\t\tsize_t index = 0;\n\t\tfor (size_t row = 0; row < M; row++) {\n\t\t\tfor (size_t col = 0; col < N; col++) {\n\t\t\t\t(*this)[row][col] = temp[index];\n\t\t\t\tindex += 1;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Constructs the identity matrix for the current size.\n\t */\n\ttemplate <bool cond = is_square,\n\t          typename = typename std::enable_if<cond>::type>\n\tstatic this_type identity() {\n\t\tthis_type res;\n\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\tres[i][i] = 1;\n\t\t}\n\n\t\treturn res;\n\t}\n\n\t/**\n\t * Test if both matrices contain the same values within epsilon.\n\t */\n\tbool equals(const this_type &other, float eps = default_eps) const {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\tfor (size_t j = 0; j < M; j++) {\n\t\t\t\tif (std::abs((*this)[i][j] - other[i][j]) >= eps) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Matrix multiplication\n\t */\n\ttemplate <size_t P>\n\tMatrix<M, P, T> operator*(const Matrix<N, P, T> &other) const {\n\t\tMatrix<M, P, T> res;\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < P; j++) {\n\t\t\t\tres[i][j] = 0;\n\t\t\t\tfor (size_t k = 0; k < N; k++) {\n\t\t\t\t\tres[i][j] += (*this)[i][k] * other[k][j];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Matrix-Vector multiplication\n\t */\n\tMatrix<M, 1, T> operator*(const Vector<M, T> &vec) const {\n\t\treturn (*this) * static_cast<Matrix<M, 1, T>>(vec);\n\t}\n\n\t/**\n\t * Matrix addition\n\t */\n\tthis_type operator+(const this_type &other) const {\n\t\tthis_type res;\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\tres[i][j] = (*this)[i][j] + other[i][j];\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Matrix subtraction\n\t */\n\tthis_type operator-(const this_type &other) const {\n\t\tthis_type res;\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\tres[i][j] = (*this)[i][j] - other[i][j];\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Scalar multiplication with assignment\n\t */\n\tvoid operator*=(T other) {\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\t(*this)[i][j] *= other;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Scalar multiplication\n\t */\n\tthis_type operator*(T other) const {\n\t\tthis_type res(*this);\n\t\tres *= other;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Scalar division with assignment\n\t */\n\tvoid operator/=(T other) {\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\t(*this)[i][j] /= other;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Scalar division\n\t */\n\tthis_type operator/(T other) const {\n\t\tthis_type res(*this);\n\t\tres /= other;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Transposition\n\t */\n\tMatrix<N, M, T> transpose() const {\n\t\tMatrix<N, M, T> res;\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tfor (size_t j = 0; j < N; j++) {\n\t\t\t\tres[j][i] = (*this)[i][j];\n\t\t\t}\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Conversion to Vector\n\t */\n\ttemplate <bool cond = is_column_vector,\n\t          typename = typename std::enable_if<cond>::type>\n\tVector<M, T> to_vector() const {\n\t\tVector<M, T> res{};\n\t\tfor (size_t i = 0; i < M; i++) {\n\t\t\tres[i] = (*this)[i][0];\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Matrix trace: the sum of all diagonal entries\n\t */\n\ttemplate <bool cond = is_square,\n\t          typename = typename std::enable_if<cond>::type>\n\tT trace() const {\n\t\tT res = 0;\n\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\tres += (*this)[i][i];\n\t\t}\n\n\t\treturn res;\n\t}\n\n\t/**\n\t * Print to output stream using '<<'\n\t */\n\tfriend std::ostream &operator<<(std::ostream &o,\n\t                                const this_type &mat) {\n\t\to << \"(\";\n\t\tfor (size_t j = 0; j < M - 1; j++) {\n\t\t\to << \"(\";\n\t\t\tfor (size_t i = 0; i < N - 1; i++) {\n\t\t\t\to << mat[j][i] << \",\\t\";\n\t\t\t}\n\t\t\to << mat[j][N - 1] << \")\";\n\t\t\to << \",\" << std::endl\n\t\t\t  << \" \";\n\t\t}\n\t\to << \"(\";\n\t\tfor (size_t i = 0; i < N - 1; i++) {\n\t\t\to << mat[M - 1][i] << \",\\t\";\n\t\t}\n\t\to << mat[M - 1][N - 1] << \"))\";\n\t\treturn o;\n\t}\n};\n\n/**\n * Scalar multiplication with swapped arguments\n */\ntemplate <size_t M, size_t N, typename T>\nMatrix<M, N, T> operator*(T a, const Matrix<M, N, T> &mat) {\n\treturn mat * a;\n}\n\n/**\n * Scalar multiplication with swapped arguments for int. This was just added\n * because otherwise the above float-multiplication function might not match to\n * the template deduction.\n */\ntemplate <size_t M, size_t N, typename T>\nMatrix<M, N, T> operator*(int64_t a, const Matrix<M, N, T> &mat) {\n\treturn mat * a;\n}\n\ntemplate <typename T = float>\nusing Matrix2t = Matrix<2, 2, T>;\n\ntemplate <typename T = float>\nusing Matrix3t = Matrix<3, 3, T>;\n\ntemplate <typename T = float>\nusing Matrix4t = Matrix<4, 4, T>;\n\ntemplate <size_t M, size_t N>\nusing Matrixf = Matrix<M, N, float>;\n\ntemplate <size_t M, size_t N>\nusing Matrixd = Matrix<M, N, double>;\n\nusing Matrix2f = Matrix<2, 2, float>;\nusing Matrix3f = Matrix<3, 3, float>;\nusing Matrix4f = Matrix<4, 4, float>;\n\nusing Matrix2d = Matrix<2, 2, double>;\nusing Matrix3d = Matrix<3, 3, double>;\nusing Matrix4d = Matrix<4, 4, double>;\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/matrix_test.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"matrix.h\"\n#include \"vector.h\"\n\n#include \"../testing/testing.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\nvoid matrix() {\n\t{ // matrix multiplication\n\t\tconst Matrix<5, 3, float> a(0.0, 0.5, 1.0,\n\t\t                            1.5, 2.0, 2.5,\n\t\t                            3.0, 3.5, 4.0,\n\t\t                            4.5, 5.0, 5.5,\n\t\t                            6.0, 6.5, 7.0);\n\t\tconst Matrix<3, 4, float> b(1, 0, 0, 0,\n\t\t                            0, 1, 0, 0,\n\t\t                            0, 0, 1, 0);\n\t\tMatrix<5, 4, float> c = a * b;\n\t\tTESTEQUALS_FLOAT(c[0][0], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[0][1], 0.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[0][2], 1, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[0][3], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][0], 1.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][1], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][2], 2.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][3], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[2][0], 3, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[2][1], 3.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[2][2], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[2][3], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[3][0], 4.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[3][1], 5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[3][2], 5.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[3][3], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[4][0], 6, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[4][1], 6.5, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[4][2], 7, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[4][3], 0, 1e-7);\n\t}\n\n\t{ // addition and subtraction\n\t\tconst Matrix2f a(1, 2, 3, 4);\n\t\tconst Matrix2f b(5, 6, 7, 8);\n\t\tMatrix2f c = a + b;\n\t\tTESTEQUALS_FLOAT(c[0][0], 6, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[0][1], 8, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][0], 10, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][1], 12, 1e-7);\n\n\t\tc = b - a;\n\t\tTESTEQUALS_FLOAT(c[0][0], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[0][1], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][0], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(c[1][1], 4, 1e-7);\n\t}\n\n\t{ // scalar multiplication and division\n\t\tconst Matrix2f a(0, 1, 2, 3);\n\t\tMatrix2f b = a * 2;\n\t\tTESTEQUALS_FLOAT(b[0][0], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[0][1], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][0], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][1], 6, 1e-7);\n\n\t\tb = 2 * a;\n\t\tTESTEQUALS_FLOAT(b[0][0], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[0][1], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][0], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][1], 6, 1e-7);\n\n\t\tb = a / 0.5;\n\t\tTESTEQUALS_FLOAT(b[0][0], 0, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[0][1], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][0], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][1], 6, 1e-7);\n\t}\n\n\t{ // transposition\n\t\tconst Matrix2f a(1, 2, 3, 4);\n\t\tconst Matrix2f b = a.transpose();\n\t\tTESTEQUALS_FLOAT(b[0][0], 1, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[0][1], 3, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][0], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(b[1][1], 4, 1e-7);\n\t}\n\n\t{ // vector interaction\n\t\tconst Matrix3f a(2, 0, 0,\n\t\t                 0, 2, 0,\n\t\t                 0, 0, 2);\n\t\tconst Vector3f v(1, 2, 3);\n\t\tauto u = (a * v).to_vector();\n\t\tTESTEQUALS_FLOAT(u[0], 2, 1e-7);\n\t\tTESTEQUALS_FLOAT(u[1], 4, 1e-7);\n\t\tTESTEQUALS_FLOAT(u[2], 6, 1e-7);\n\t}\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/misc.cpp",
    "content": "// Copyright 2013-2017 the openage authors. See copying.md for legal info.\n\n#include \"misc.h\"\n\nnamespace openage {\nnamespace util {\n\nstd::string empty_string;\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/misc.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n#include <cmath>\n#include <cstring>\n#include <limits.h>\n#include <memory>\n#include <string>\n#include <vector>\n\n#include \"../error/error.h\"\n#include \"compiler.h\"\n\n\nnamespace openage::util {\n\n\n/**\n * global empty string, sometimes needed\n * as return value in some `std::string &function()` return values.\n */\nextern std::string empty_string;\n\n\n/**\n * modulo operation that guarantees to return positive values.\n */\ntemplate <typename T>\nconstexpr T mod(T x, T m) {\n\tT r = x % m;\n\n\tif (r < 0) {\n\t\treturn r + m;\n\t}\n\telse {\n\t\treturn r;\n\t}\n}\n\n/**\n * compiletime defined modulo function.\n */\ntemplate <typename T, unsigned int modulo>\nconstexpr T mod(T x) {\n\tT r = x % modulo;\n\n\tif (r < 0) {\n\t\treturn r + modulo;\n\t}\n\telse {\n\t\treturn r;\n\t}\n}\n\n\n/**\n * compiletime defined rotate left function\n */\ntemplate <typename T, int amount>\nconstexpr T rol(T x) {\n\tstatic_assert(sizeof(T) * CHAR_BIT > amount && amount > 0, \"invalid rotation amount\");\n\treturn (x << amount) | (x >> (sizeof(T) * CHAR_BIT - amount));\n}\n\n\n/**\n * implements the 'correct' version of the division operator,\n * which always rounds to -inf\n */\ntemplate <typename T>\nconstexpr inline T div(T x, T m) {\n\treturn (x - mod<T>(x, m)) / m;\n}\n\n/**\n * generic callable, that compares any types for creating a total order.\n *\n * use for stdlib structures like std::set.\n * the template parameter has to be a pointer type.\n */\ntemplate <typename T>\nstruct less {\n\tbool operator()(const T x, const T y) const {\n\t\treturn *x < *y;\n\t}\n};\n\n\n// Size of uint64_t measured in bytes\nstatic constexpr size_t uint64_s = 8;\n\n\n/**\n * Convert a C-style array of uint8_t to a uint64_t.\n * Uses little-endianness as default.\n * Fills the remaining bytes with zeroes.\n *\n * e.g (big-endian): {0x01, 0x23, 0x45, 0x67} -> 0x0123456700000000\n * e.g (little-endian): {0x01, 0x23, 0x45, 0x67} -> 0x0000000067452301\n *\n * @param start Pointer to start of the input data.\n * @param count Number of bytes to read.\n * @param big_endian Endianness of byte array.\n * @return Input data as a 64 bit number.\n */\ninline uint64_t\narray8_to_uint64(const uint8_t *start, size_t count, bool big_endian = false) {\n\tif (count > uint64_s) {\n\t\tthrow Error(MSG(err) << \"Tried to copy more than \" << uint64_s << \" bytes\");\n\t}\n\n\tuint64_t result{0};\n\n\tif (big_endian) {\n\t\tfor (size_t i = 0; i < count; i++) {\n\t\t\tresult |= static_cast<uint64_t>(*(start + i)) << (7 - i) * 8;\n\t\t}\n\t}\n\telse {\n\t\tfor (size_t i = 0; i < count; i++) {\n\t\t\tresult |= static_cast<uint64_t>(*(start + i)) << i * 8;\n\t\t}\n\t}\n\treturn result;\n}\n\n\n/**\n * Convert a uint64_t to an array of uint8_t. Uses little-endianness as default.\n *\n * e.g (big-endian):\n *     0x0123456789abcdef -> {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}\n * e.g (little-endian):\n *     0x0123456789abcdef -> {0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01}\n *\n * @param value 64 bit number to convert.\n * @param big_endian Endianness of byte array.\n * @return Input data as a 8 bit number array.\n */\ninline std::vector<uint8_t>\nuint64_to_array8(const uint64_t value, bool big_endian = false) {\n\tstd::vector<uint8_t> result(uint64_s, 0);\n\n\tif (big_endian) {\n\t\tfor (size_t i = 0; i < uint64_s; i++) {\n\t\t\tresult[i] = (value >> ((7 - i) * 8)) & 0xff;\n\t\t}\n\t}\n\telse {\n\t\tfor (size_t i = 0; i < uint64_s; i++) {\n\t\t\tresult[i] = (value >> (i * 8)) & 0xff;\n\t\t}\n\t}\n\treturn result;\n}\n\n\n/**\n * Get the number of uint64-elements\n * that result from converting a count-sized uint8_t array.\n *\n * Used to determine the size for array8_to_array64()\n *\n * @return ceil(count/8)\n */\ninline constexpr size_t array64_size(size_t count) {\n\tsize_t partial = mod<size_t, uint64_s>(count);\n\treturn ((count - partial) / uint64_s) + (partial ? 1 : 0);\n}\n\n\n/**\n * Convert a C-style array of uint8_t to a std::vector of uint64_t.\n * Uses little-endianness as default.\n * Fills the remaining bytes of the last uint64_t with zeroes.\n *\n * e.g (big-endian): {0x01, 0x23, 0x45, 0x67} -> 0x0123456700000000\n * e.g (little-endian): {0x01, 0x23, 0x45, 0x67} -> 0x0000000067452301\n *\n * @param start Start of the input data.\n * @param count Number of bytes to convert.\n * @param big_endian Endianness of byte array.\n * @return Input data as a 64 bit number vector.\n */\ninline std::vector<uint64_t>\narray8_to_array64(const uint8_t *start, size_t count, bool big_endian = false) {\n\tsize_t size{array64_size(count)};\n\tstd::vector<uint64_t> result(size, 0);\n\n\tsize_t rem_bytes;\n\tfor (size_t i = 0; i < size; i++) {\n\t\trem_bytes = count - i * uint64_s;\n\t\tresult[i] = array8_to_uint64(\n\t\t\tstart + (i * uint64_s),\n\t\t\tstd::min(rem_bytes, uint64_s),\n\t\t\tbig_endian);\n\t}\n\treturn result;\n}\n\n\n/**\n * Convert a C-style array of uint64_t to a std::vector of uint8_t.\n * Uses little-endianness as default.\n *\n * e.g (big-endian):\n *     0x0123456789abcdef -> {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}\n * e.g (little-endian):\n *     0x0123456789abcdef -> {0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01}\n *\n * @param Start of the input data.\n * @param count Number of uint64_t to convert.\n * @param big_endian Endianness of byte array.\n * @return Input data as a 8 bit number vector.\n */\ninline std::vector<uint8_t>\narray64_to_array8(const uint64_t *start, size_t count, bool big_endian = false) {\n\tstd::vector<uint8_t> result;\n\tresult.reserve(count * uint64_s);\n\n\tfor (size_t i = 0; i < count; i++) {\n\t\tstd::vector<uint8_t> block = uint64_to_array8(*(start + i), big_endian);\n\t\tresult.insert(result.end(), block.begin(), block.end());\n\t}\n\n\treturn result;\n}\n\n\n/**\n * Extend a vector with elements, without destroying source one.\n */\ntemplate <typename T>\nvoid vector_extend(std::vector<T> &vec, const std::vector<T> &ext) {\n\tvec.reserve(vec.size() + ext.size());\n\tvec.insert(std::end(vec), std::begin(ext), std::end(ext));\n}\n\n\n/*\n * Extend a vector with elements with move semantics.\n */\ntemplate <typename T>\nvoid vector_extend(std::vector<T> &vec, std::vector<T> &&ext) {\n\tif (vec.empty()) {\n\t\tvec = std::move(ext);\n\t}\n\telse {\n\t\tvec.reserve(vec.size() + ext.size());\n\t\tstd::move(std::begin(ext), std::end(ext), std::back_inserter(vec));\n\t\text.clear();\n\t}\n}\n\n\n/**\n * Remove the given element index in the vector.\n * May swap with the end element for efficient removing.\n *\n * If the element is not in the vector, do nothing.\n */\ntemplate <typename T>\nvoid vector_remove_swap_end(std::vector<T> &vec, size_t idx) {\n\t// is at the end\n\tif (idx == vec.size() - 1) {\n\t\tvec.pop_back();\n\t}\n\t// is in the middle\n\telse if (idx < vec.size()) {\n\t\tstd::swap(vec[idx], vec.back());\n\t\tvec.pop_back();\n\t}\n\telse {\n\t\treturn;\n\t}\n}\n\n\n/**\n * Comparator that returns true\n * if the contained value of the left sharedptr is `<`\n * than the value contained in the right sharedptr.\n */\ntemplate <typename T>\nstruct SharedPtrLess {\n\tbool operator()(const std::shared_ptr<T> &left,\n\t                const std::shared_ptr<T> &right) {\n\t\tif (not left or not right) [[unlikely]] {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn *left < *right;\n\t}\n};\n\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/misc_test.cpp",
    "content": "// Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n#include \"misc.h\"\n\n#include \"../testing/testing.h\"\n\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\n\nvoid array_conversion() {\n\t{\n\t\tstatic const std::vector<uint8_t> test_array8_big{\n\t\t\t0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef\n\t\t};\n\t\tstatic const std::vector<uint8_t> test_array8_little{\n\t\t\t0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01\n\t\t};\n\t\tstatic const uint64_t test_uint64{0x0123456789abcdef};\n\n\t\t// Big-endian conversions\n\t\tarray8_to_uint64(test_array8_big.data(), 8, true) == test_uint64 or TESTFAIL;\n\t\tuint64_to_array8(test_uint64, true) == test_array8_big or TESTFAIL;\n\t\t// Little-endian conversions\n\t\tarray8_to_uint64(test_array8_little.data(), 8, false) == test_uint64 or TESTFAIL;\n\t\tuint64_to_array8(test_uint64, false) == test_array8_little or TESTFAIL;\n\t}\n\n\t{\n\t\tstatic const std::vector<uint8_t> test_array8_big{\n\t\t\t0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,\n\t\t\t0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7\n\t\t};\n\t\tstatic const std::vector<uint8_t> test_array8_little{\n\t\t\t0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,\n\t\t\t0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0\n\t\t};\n\t\tstatic const std::vector<uint64_t> test_array64{\n\t\t\t0x0123456789abcdef, 0xf0f1f2f3f4f5f6f7\n\t\t};\n\t\t// Big-endian conversions\n\t\tarray8_to_array64(test_array8_big.data(), 16, true) == test_array64 or TESTFAIL;\n\t\tarray64_to_array8(test_array64.data(), 2, true) == test_array8_big or TESTFAIL;\n\t\t// Little-endian conversions\n\t\tarray8_to_array64(test_array8_little.data(), 16, false) == test_array64 or TESTFAIL;\n\t\tarray64_to_array8(test_array64.data(), 2, false) == test_array8_little or TESTFAIL;\n\t}\n\n\t// Now I'm converting 14 instead of 16 bytes, when using array8_to_array64 the\n\t// remaining bytes should be filled with zeroes.\n\t{\n\t\tstatic const std::vector<uint8_t> test_array8_big{\n\t\t\t0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,\n\t\t\t0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0x00, 0x00\n\t\t};\n\t\tstatic const std::vector<uint64_t> test_array64_big{\n\t\t\t0x0123456789abcdef, 0xf0f1f2f3f4f50000\n\t\t};\n\n\t\tstatic const std::vector<uint8_t> test_array8_little{\n\t\t\t0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,\n\t\t\t0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0x00, 0x00\n\t\t};\n\t\tstatic const std::vector<uint64_t> test_array64_little{\n\t\t\t0xefcdab8967452301, 0x0000f2f3f4f5f6f7\n\t\t};\n\n\t\t// Big-endian conversions\n\t\tarray8_to_array64(test_array8_big.data(), 14, true) == test_array64_big or TESTFAIL;\n\t\tarray64_to_array8(test_array64_big.data(), 2, true) == test_array8_big or TESTFAIL;\n\n\t\t// Little-endian conversions\n\t\tarray8_to_array64(test_array8_little.data(), 14, false) == test_array64_little or TESTFAIL;\n\t\tarray64_to_array8(test_array64_little.data(), 2, false) == test_array8_little or TESTFAIL;\n\t}\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/os.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include \"os.h\"\n\n#include <memory>\n\n#ifdef _WIN32\n// TODO not yet implemented\n#else\n\t#include <unistd.h>\n#endif\n\n#ifdef __APPLE__\n\t#include <mach-o/dyld.h>\n#endif\n\n#ifdef __FreeBSD__\n\t#include <sys/sysctl.h>\n\t#include <sys/types.h>\n#endif\n\n#include \"../log/log.h\"\n#include \"subprocess.h\"\n\nnamespace openage::os {\n\nstd::string read_symlink(const char *path) {\n#ifdef _WIN32\n\t// TODO not yet implemented\n\treturn \"\";\n#else\n\tsize_t bufsize = 1024;\n\n\twhile (true) {\n\t\tbufsize *= 2;\n\t\tstd::unique_ptr<char[]> buf{new char[bufsize]};\n\n\t\tssize_t len = readlink(path, buf.get(), bufsize);\n\n\t\tif (len < 0) {\n\t\t\tlog::log(MSG(err) << \"could not read link \" << path);\n\t\t\treturn \"\";\n\t\t}\n\n\t\tif (static_cast<size_t>(len) >= bufsize) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tbuf[len] = '\\0';\n\n\t\treturn std::string{buf.get()};\n\t}\n#endif\n}\n\nstd::string self_exec_filename() {\n\t// nice overview of crossplatform methods:\n\t// http://stackoverflow.com/questions/5919996\n\n#ifdef __linux\n\treturn read_symlink(\"/proc/self/exe\");\n#elif __APPLE__\n\tuint32_t bufsize = 1024;\n\n\twhile (true) {\n\t\tstd::unique_ptr<char[]> buf{new char[bufsize]};\n\n\t\tif (_NSGetExecutablePath(buf.get(), &bufsize) < 0) {\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn std::string{buf.get()};\n\t}\n#elif __FreeBSD__\n\tsize_t bufsize = 1024;\n\tint mib[4] = {\n\t\tCTL_KERN,\n\t\tKERN_PROC,\n\t\tKERN_PROC_PATHNAME,\n\t\t-1};\n\n\twhile (true) {\n\t\tstd::unique_ptr<char[]> buf{new char[bufsize]};\n\n\t\tif (sysctl(mib, 4, buf.get(), &bufsize, nullptr, 0) < 0) {\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn std::string{buf.get()};\n\t}\n\n#elif _WIN32\n\t// TODO not yet implemented\n\treturn std::string(\"openage.exe\"); // FIXME: wild guess though\n#else\n\tstatic_assert(false, \"subprocess::self_filename is not yet implemented for... whatever platform you're using right now.\");\n#endif\n}\n\nint execute_file(const char *path, bool background) {\n#ifdef _WIN32\n\t// TODO some sort of shell-open, not yet implemented\n\treturn -1; // failure\n#else\n\tstd::string runner = \"\";\n\t#ifdef __APPLE__\n\trunner = subprocess::which(\"open\");\n\t#else\n\trunner = subprocess::which(\"xdg-open\");\n\t// fallback\n\tif (runner.size() == 0) {\n\t\trunner = subprocess::which(\"gnome-open\");\n\t}\n\t#endif\n\tif (runner.size() == 0) {\n\t\tlog::log(MSG(err) << \"could not locate file-opener\");\n\t\treturn -1;\n\t}\n\n\treturn subprocess::call({runner.c_str(), path, nullptr}, not background);\n#endif\n}\n\n} // namespace openage::os\n"
  },
  {
    "path": "libopenage/util/os.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\nnamespace openage {\n\n/**\n * similar in its goals to Python's os module\n */\nnamespace os {\n\n/**\n * reads a symlink\n */\nstd::string read_symlink(const char *path);\n\n/**\n * returns openage's executable name\n */\nstd::string self_exec_filename();\n\n/**\n * tries to xdg-open the file\n */\nint execute_file(const char *path, bool background = true);\n\n} // namespace os\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/path.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"path.h\"\n\n#include <utility>\n\n#include \"../error/error.h\"\n#include \"compiler.h\"\n#include \"fslike/directory.h\"\n#include \"fslike/native.h\"\n#include \"fslike/python.h\"\n#include \"misc.h\"\n#include \"strings.h\"\n\n\nnamespace openage::util {\n\n\n/**\n * Strip out ../ etc\n */\nvoid path_normalizer(Path::parts_t &output, const Path::parts_t &input) {\n\toutput.reserve(input.size());\n\n\t// normalize the path parts\n\tfor (auto &part : input) {\n\t\tif (part == \".\" or part == \"\") {\n\t\t\tcontinue;\n\t\t}\n\t\telse if (part == \"..\") {\n\t\t\tif (output.size() > 0) [[likely]] {\n\t\t\t\toutput.pop_back();\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\toutput.push_back(part);\n\t\t}\n\t}\n}\n\n\nPath::Path() = default;\n\n\nPath::Path(const py::Obj &fsobj_in,\n           const std::vector<std::string> &parts) {\n\tpath_normalizer(this->parts, parts);\n\n\t// optimization: the fsobj from python may be convertible to\n\t//               a native c++ implementation.\n\t//               then calls don't need to be relayed any more.\n\n\t// test if fsobj is fslike.Directory\n\tif (fslike::pyx_fs_is_fslike_directory.call(fsobj_in.get_ref())) {\n\t\tthis->fsobj = std::make_shared<fslike::Directory>(\n\t\t\tfsobj_in.getattr(\"path\").bytes());\n\t}\n\telse {\n\t\t// we can't create a c++-variant of the path,\n\t\t// so just wrap the python path.\n\t\tthis->fsobj = std::make_shared<fslike::Python>(fsobj_in);\n\t}\n}\n\n\nPath::Path(std::shared_ptr<fslike::FSLike> fsobj,\n           const parts_t &parts) :\n\tfsobj{std::move(fsobj)} {\n\tpath_normalizer(this->parts, parts);\n}\n\n\nbool Path::exists() const {\n\treturn this->is_file() or this->is_dir();\n}\n\nbool Path::is_file() const {\n\treturn this->fsobj->is_file(this->parts);\n}\n\nbool Path::is_dir() const {\n\treturn this->fsobj->is_dir(this->parts);\n}\n\nbool Path::writable() const {\n\treturn this->fsobj->writable(this->parts);\n}\n\nstd::vector<Path::part_t> Path::list() {\n\treturn this->fsobj->list(this->parts);\n}\n\nstd::vector<Path> Path::iterdir() {\n\tstd::vector<Path> ret;\n\n\t// return a new path object for each element in that directory\n\tfor (auto &entry : this->fsobj->list(this->parts)) {\n\t\tret.push_back(this->joinpath(entry));\n\t}\n\n\treturn ret;\n}\n\nbool Path::mkdirs() {\n\treturn this->fsobj->mkdirs(this->parts);\n}\n\nFile Path::open(const std::string &mode) const {\n\tif (mode == \"r\") {\n\t\treturn this->open_r();\n\t}\n\telse if (mode == \"w\") {\n\t\treturn this->open_w();\n\t}\n\telse if (mode == \"rw\" or mode == \"r+\") {\n\t\treturn this->open_w();\n\t}\n\telse if (mode == \"a\") {\n\t\treturn this->open_a();\n\t}\n\telse if (mode == \"a+\" or mode == \"ar\") {\n\t\treturn this->open_ar();\n\t}\n\telse {\n\t\tthrow Error{ERR << \"unsupported open mode: \" << mode};\n\t}\n}\n\n\nFile Path::open_r() const {\n\treturn this->fsobj->open_r(this->parts);\n}\n\n\nFile Path::open_w() const {\n\treturn this->fsobj->open_w(this->parts);\n}\n\n\nFile Path::open_rw() const {\n\treturn this->fsobj->open_rw(this->parts);\n}\n\n\nFile Path::open_a() const {\n\treturn this->fsobj->open_a(this->parts);\n}\n\n\nFile Path::open_ar() const {\n\treturn this->fsobj->open_ar(this->parts);\n}\n\n\nstd::string Path::get_native_path() const {\n\treturn this->fsobj->get_native_path(this->parts);\n}\n\n\nstd::string Path::resolve_native_path(const std::string &mode) const {\n\t// TODO: Catch other errors that can occur when fsobj is not a native path.\n\tif (this->fsobj == nullptr) {\n\t\tthrow Error{ERR << \"fsobj is nullptr and cannot be resolved\"};\n\t}\n\n\tif (mode == \"r\") {\n\t\treturn this->resolve_native_path_r();\n\t}\n\telse if (mode == \"w\") {\n\t\treturn this->resolve_native_path_w();\n\t}\n\telse {\n\t\tthrow Error{ERR << \"unsupported resolve mode: \" << mode};\n\t}\n}\n\n\nstd::string Path::resolve_native_path_r() const {\n\tauto resolved_path = this->fsobj->resolve_r(this->parts);\n\n\tif (resolved_path.first) {\n\t\treturn resolved_path.second.get_native_path();\n\t}\n\telse {\n\t\tthrow Error{ERR << \"failed to locate readable path: \" << *this};\n\t}\n}\n\n\nstd::string Path::resolve_native_path_w() const {\n\tauto resolved_path = this->fsobj->resolve_w(this->parts);\n\n\tif (resolved_path.first) {\n\t\treturn resolved_path.second.get_native_path();\n\t}\n\telse {\n\t\tthrow Error{ERR << \"failed to locate writable path: \" << *this};\n\t}\n}\n\n\nbool Path::rename(const Path &target_path) {\n\t// check if both fsobj pointers are the same\n\t// maybe a real equality check is better?\n\tif (this->fsobj != target_path.fsobj) {\n\t\tthrow Error{\n\t\t\tERR << \"can't rename across two different filesystem like objects\"};\n\t}\n\treturn this->fsobj->rename(this->parts, target_path.parts);\n}\n\nbool Path::rmdir() {\n\treturn this->fsobj->rmdir(this->parts);\n}\n\nbool Path::touch() {\n\treturn this->fsobj->touch(this->parts);\n}\n\nbool Path::unlink() {\n\treturn this->fsobj->unlink(this->parts);\n}\n\nvoid Path::removerecursive() {\n\tif (this->is_dir()) {\n\t\tfor (auto &path : this->iterdir()) {\n\t\t\tpath.removerecursive();\n\t\t}\n\t\tthis->rmdir();\n\t}\n\telse {\n\t\tthis->unlink();\n\t}\n}\n\n\nint Path::get_mtime() const {\n\treturn this->fsobj->get_mtime(this->parts);\n}\n\nuint64_t Path::get_filesize() const {\n\treturn this->fsobj->get_filesize(this->parts);\n}\n\n// int Path::watch();\n// void Path::poll_fs_watches();\n\nPath Path::get_parent() const {\n\treturn this->joinpath(\"..\");\n}\n\n\nconst std::string &Path::get_name() const {\n\tif (this->parts.size() > 0) {\n\t\treturn this->parts.back();\n\t}\n\telse {\n\t\treturn util::empty_string;\n\t}\n}\n\nstd::string Path::get_suffix() const {\n\tauto &name = this->get_name();\n\tsize_t pos = name.rfind('.');\n\tif (pos == std::string::npos) {\n\t\treturn \"\";\n\t}\n\n\treturn name.substr(pos);\n}\n\nstd::vector<std::string> Path::get_suffixes() const {\n\tstd::string name = this->get_name();\n\tif (name.size() > 0 and name[0] == '.') {\n\t\tname = name.substr(1);\n\t}\n\n\tstd::vector<std::string> ret;\n\t// ret = ['.' + suffix for suffix in name.split('.')[1:]]\n\treturn ret;\n}\n\nstd::string Path::get_stem() const {\n\tconst std::string &name = this->get_name();\n\tsize_t pos = name.rfind('.');\n\tif (pos == std::string::npos) {\n\t\treturn name;\n\t}\n\n\treturn name.substr(0, pos);\n}\n\n\nPath Path::joinpath(const parts_t &subpaths) const {\n\tparts_t new_parts = this->parts;\n\tfor (auto &part : subpaths) {\n\t\tif (part.size() > 0) [[likely]] {\n\t\t\tnew_parts.push_back(part);\n\t\t}\n\t}\n\treturn Path{this->fsobj, new_parts};\n}\n\nPath Path::joinpath(const part_t &subpath) const {\n\treturn this->joinpath(util::split(subpath, '/'));\n}\n\nPath Path::operator[](const parts_t &subpaths) const {\n\treturn this->joinpath(subpaths);\n}\n\nPath Path::operator[](const part_t &subpath) const {\n\treturn this->joinpath(subpath);\n}\n\nPath Path::operator/(const part_t &subpath) const {\n\treturn this->joinpath(subpath);\n}\n\nPath Path::with_name(const part_t &name) const {\n\treturn this->get_parent().joinpath(name);\n}\n\nPath Path::with_suffix(const part_t &suffix) const {\n\treturn this->with_name(this->get_stem() + suffix);\n}\n\nbool Path::operator==(const Path &other) const {\n\treturn this->fsobj == other.fsobj and this->parts == other.parts;\n}\n\nbool Path::operator!=(const Path &other) const {\n\treturn not(*this == other);\n}\n\n\nfslike::FSLike *Path::get_fsobj() const {\n\treturn this->fsobj.get();\n}\n\n\nconst Path::parts_t &Path::get_parts() const {\n\treturn this->parts;\n}\n\nsize_t Path::get_hash() const {\n\tif (not this->hash.has_value()) {\n\t\tthis->hash = std::hash<std::string>{}(this->get_native_path());\n\t}\n\treturn this->hash.value();\n}\n\n\nstd::ostream &operator<<(std::ostream &stream, const Path &path) {\n\tif (path.fsobj == nullptr) {\n\t\tstream << \"INVALID PATH (fsobj == nullptr)\";\n\t\treturn stream;\n\t}\n\n\tstream << \"Path(\";\n\tpath.fsobj->repr(stream);\n\tstream << \":\";\n\n\tfor (auto &part : path.parts) {\n\t\tstream << \"/\" << part;\n\t}\n\n\tstream << \")\";\n\n\treturn stream;\n}\n\n\nstd::string filename(const std::string &fullpath) {\n\tsize_t rsep_pos = fullpath.rfind(fslike::PATHSEP);\n\tif (rsep_pos == std::string::npos) {\n\t\treturn \"\";\n\t}\n\telse {\n\t\treturn fullpath.substr(rsep_pos + 1);\n\t}\n}\n\n\nstd::string dirname(const std::string &fullpath) {\n\tsize_t rsep_pos = fullpath.rfind(fslike::PATHSEP);\n\tif (rsep_pos == std::string::npos) {\n\t\treturn \"\";\n\t}\n\telse {\n\t\treturn fullpath.substr(0, rsep_pos);\n\t}\n}\n\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/path.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n/*\n * C++ wrappers for openage.util.fslikeobject\n */\n\n\n// pxd: from libcpp.string cimport string\n#include <string>\n// pxd: from libcpp.vector cimport vector\n#include <vector>\n\n#include <optional>\n\n// pxd: from libopenage.pyinterface.pyobject cimport PyObj\n#include \"../pyinterface/pyobject.h\"\n#include \"file.h\"\n#include \"fslike/native.h\"\n\n// pxd: from libopenage.util.fslike.fslike cimport FSLike\n\nnamespace openage::util {\n\nnamespace fslike {\nclass FSLike;\n}\n\n\n/**\n * C++ pendant to the python util.fslike.path.Path\n *\n * Contains a filesystem-like object and path-parts.\n *\n * pxd:\n * cppclass Path:\n *     ctypedef string part_t\n *     ctypedef vector[string] parts_t\n *\n *     Path() noexcept\n *     Path(PyObj, const vector[string]&) except +\n *\n *     FSLike *get_fsobj() except +\n *     const vector[string] &get_parts() except +\n */\nclass OAAPI Path {\npublic:\n\t/**\n\t * Storage type for a part of a path access.\n\t * Basically this is the name of a file or directory.\n\t */\n\tusing part_t = std::string;\n\n\t/**\n\t * Storage type for the filesystem access parts\n\t * this is basically a list/of/elements/in/a/path.\n\t */\n\tusing parts_t = std::vector<part_t>;\n\n\t/**\n\t * Nullary constructor, pls don't use.\n\t * It only exists because Cython can't RAII.\n\t * It should be just friend of cython.\n\t */\n\tPath();\n\n\t/**\n\t * Construct a path object from a python fslike\n\t * object. This is called from Cython.\n\t *\n\t * You will probably never call that manually.\n\t */\n\tPath(const py::Obj &fslike,\n\t     const parts_t &parts = {});\n\n\t/**\n\t * Construct a path from a fslike pointer.\n\t */\n\tPath(std::shared_ptr<fslike::FSLike> fslike,\n\t     const parts_t &parts = {});\n\n\tvirtual ~Path() = default;\n\npublic:\n\tbool exists() const;\n\tbool is_file() const;\n\tbool is_dir() const;\n\tbool writable() const;\n\tstd::vector<part_t> list();\n\tstd::vector<Path> iterdir();\n\tbool mkdirs();\n\tFile open(const std::string &mode = \"r\") const;\n\tFile open_r() const;\n\tFile open_w() const;\n\tFile open_rw() const;\n\tFile open_a() const;\n\tFile open_ar() const;\n\n\t/**\n\t * Resolve the native path by flattening all underlying\n\t * filesystem objects (like unions).\n\t * Returns the /native/path/on/disk.\n\t * Throws up if there is no native path.\n\t *\n\t * This basically is the same as resolve_*().get_native_path().\n\t */\n\tstd::string resolve_native_path(const std::string &mode = \"r\") const;\n\n\tstd::string resolve_native_path_r() const;\n\tstd::string resolve_native_path_w() const;\n\n\tbool rename(const Path &target_path);\n\tbool rmdir();\n\tbool touch();\n\tbool unlink();\n\tvoid removerecursive();\n\n\tint get_mtime() const;\n\tuint64_t get_filesize() const;\n\n\t// TODO: watching of path with inotify or similar\n\t//       this should get a Watcher*, which manages the multiple events\n\t//       otherwise, each file would require an inotify fd.\n\t//       => see the AssetManager and move functionality from there.\n\t// int watch(std::function<> callback);\n\t// void poll_fs_watches(); // call triggered callbacks\n\n\tPath get_parent() const;\n\tconst std::string &get_name() const;\n\tstd::string get_suffix() const;\n\tstd::vector<std::string> get_suffixes() const;\n\tstd::string get_stem() const;\n\n\tPath joinpath(const parts_t &subpaths) const;\n\tPath joinpath(const part_t &subpath) const;\n\tPath operator[](const parts_t &subpaths) const;\n\tPath operator[](const part_t &subpath) const;\n\tPath operator/(const part_t &subpath) const;\n\n\tPath with_name(const part_t &name) const;\n\tPath with_suffix(const part_t &suffix) const;\n\n\tbool operator==(const Path &other) const;\n\tbool operator!=(const Path &other) const;\n\n\tfslike::FSLike *get_fsobj() const;\n\tconst parts_t &get_parts() const;\n\n\t// compute hash from the resolved native path\n\t// the hash value is cashed after first access to avoid\n\t// further Python calls\n\tsize_t get_hash() const;\n\nprivate:\n\t/**\n\t * Return the native path\n\t * (something like /your/folder/with/file)\n\t * for this path.\n\t * returns emptystring (\"\") if there is no native path.\n\t *\n\t * This does _not_ minimize/flatten (= \"resolve\") the path!\n\t *\n\t * You probably want to use `resolve_native_path` instead.\n\t */\n\tstd::string get_native_path() const;\n\n\t// TODO: Find a better method that works without Python access\n\t// Cached hash value based on resolved native path\n\t// computed lazily on first access to avoid expensive\n\t// Python calls\n\tmutable std::optional<size_t> hash;\n\nprotected:\n\tstd::shared_ptr<fslike::FSLike> fsobj;\n\n\tparts_t parts;\n\n\tfriend std::ostream &operator<<(std::ostream &stream, const Path &path);\n};\n\n\n// helper functions, needed for some parts of convert/\n\n/**\n * get the filename (last part) of a given path\n */\nstd::string filename(const std::string &fullpath);\n\n/**\n * return the head (dirname) part of a path.\n */\nstd::string dirname(const std::string &fullpath);\n\n\n} // namespace openage::util\n\nnamespace std {\n\ntemplate <>\nstruct hash<openage::util::Path> {\n\tsize_t operator()(const openage::util::Path &path) const {\n\t\treturn path.get_hash();\n\t}\n};\n\n} // namespace std\n"
  },
  {
    "path": "libopenage/util/pty.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#ifdef __APPLE__\n\t#include <util.h>\n#elif defined(__FreeBSD__)\n\t#include <libutil.h>\n\t#include <sys/ioctl.h>\n\t#include <sys/types.h>\n\t#include <termios.h>\n#elif _WIN32\n// TODO not yet implemented\n#else\n\t#include <pty.h>\n#endif\n"
  },
  {
    "path": "libopenage/util/quaternion.cpp",
    "content": "// Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n#include \"quaternion.h\"\n\nnamespace openage {\nnamespace util {\n\n// Quaternion is all templated, so there's nothing to implement.\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/quaternion.h",
    "content": "// Copyright 2017-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cmath>\n\n#include \"math_constants.h\"\n#include \"matrix.h\"\n#include \"vector.h\"\n\n#include \"../error/error.h\"\n#include \"../log/log.h\"\n\nnamespace openage {\nnamespace util {\n\n/**\n * Implements Quaternions to represent 3d rotations.\n *\n * The 4 components stores 3 components as rotation axis,\n * and one component as the rotation amount.\n *\n * This is mainly Ken Shoemake stuff from:\n * http://www.cs.ucr.edu/~vbz/resources/quatut.pdf\n *\n * Also:\n * From Quaternion to Matrix and Back\n * J.M.P. van Waveren, id Software, 2005\n */\ntemplate <typename T>\nclass Quaternion {\npublic:\n\tusing this_type = Quaternion<T>;\n\tusing type = T;\n\n\tstatic constexpr T default_eps = 1e-4;\n\n\tQuaternion(T w, T x, T y, T z) :\n\t\tw{w}, x{x}, y{y}, z{z} {}\n\n\t/**\n\t * Create a identity quaternion.\n\t */\n\tQuaternion() :\n\t\tw{1}, x{0}, y{0}, z{0} {}\n\n\t/**\n\t * Constructs a quaternion from a rotation matrix.\n\t * mat is assumed to be a left-matrix:\n\t * vec_transformed = mat * vec_orig.\n\t *\n\t * mat can be 3x3 or 4x4.\n\t *\n\t * This tries to avoid float-fuckups in near-zero divides\n\t * by using larger components first: w, then x, y, or z.\n\t *\n\t * trace(mat) >= 0 => |w| > 1/2\n\t * => as small as a largest component can be.\n\t * else:\n\t * max diagonal entry <=> max (|x|, |y|, |z|)\n\t * which is larger than |w| and >= 1/2\n\t */\n\ttemplate <size_t N = 4>\n\tQuaternion(const Matrix<N, N, T> &mat) {\n\t\tstatic_assert(N == 3 or N == 4, \"only 3 and 4 dimensional matrices can be converted to a quaternion!\");\n\n\t\tT trace = mat.trace();\n\t\tT trace_cmp = trace;\n\n\t\tif (N == 4) {\n\t\t\ttrace_cmp -= mat[3][3];\n\t\t}\n\t\telse {\n\t\t\ttrace += 1.0;\n\t\t}\n\n\t\tif (trace_cmp > 0) {\n\t\t\tT trace_root = std::sqrt(trace); // = 2w\n\t\t\tthis->w = trace_root * 0.5;      // = w\n\t\t\ttrace_root = 0.5 / trace_root;   // = 1/4w\n\n\t\t\tthis->x = (mat[2][1] - mat[1][2]) * trace_root;\n\t\t\tthis->y = (mat[0][2] - mat[2][0]) * trace_root;\n\t\t\tthis->z = (mat[1][0] - mat[0][1]) * trace_root;\n\t\t}\n\t\telse {\n\t\t\t// determine highest diagonal component:\n\t\t\tint n0 = 0;\n\n\t\t\tif (mat[1][1] > mat[0][0]) {\n\t\t\t\tn0 = 1;\n\t\t\t}\n\n\t\t\tif (mat[2][2] > mat[n0][n0]) {\n\t\t\t\tn0 = 2;\n\t\t\t}\n\n\t\t\t// following indices to 0, 1, 2\n\t\t\tint next_index[] = {1, 2, 0};\n\n\t\t\t// indexable pointers\n\t\t\tT *ptrs[] = {&this->x, &this->y, &this->z};\n\n\t\t\tint n1 = next_index[n0];\n\t\t\tint n2 = next_index[n1];\n\n\t\t\t// this avoids float fuckups:\n\t\t\tT trace_ordered = (mat[n0][n0] - (mat[n1][n1] + mat[n2][n2]));\n\n\t\t\tif (N == 4) {\n\t\t\t\ttrace_ordered += mat[3][3];\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttrace_ordered += 1.0;\n\t\t\t}\n\n\t\t\tT trace_root = std::sqrt(trace_ordered);\n\n\t\t\t*ptrs[n0] = trace_root * 0.5;  // = w\n\t\t\ttrace_root = 0.5 / trace_root; // = 1/4w\n\n\t\t\t*ptrs[n1] = (mat[n0][n1] + mat[n1][n0]) * trace_root;\n\t\t\t*ptrs[n2] = (mat[n2][n0] + mat[n0][n2]) * trace_root;\n\n\t\t\tthis->w = (mat[n2][n1] - mat[n1][n2]) * trace_root;\n\t\t}\n\n\t\tif (N == 4) {\n\t\t\t// normalize the quaternion by the matrix[3][3] entry\n\t\t\t(*this) *= 1.0 / std::sqrt(mat[3][3]);\n\t\t}\n\t}\n\n\tQuaternion(const this_type &other) = default;\n\tQuaternion(this_type &&other) = default;\n\tQuaternion &operator=(const this_type &other) = default;\n\tQuaternion &operator=(this_type &&other) = default;\n\n\tvirtual ~Quaternion() = default;\n\n\t/**\n\t * Create a quaternion from a rotation in radians around a given axis.\n\t */\n\tstatic this_type from_rad(T rad, Vector3t<T> axis) {\n\t\tT rot = rad / 2.0;\n\t\tthis_type q{\n\t\t\tstd::cos(rot),\n\t\t\taxis[0] * std::sin(rot),\n\t\t\taxis[1] * std::sin(rot),\n\t\t\taxis[2] * std::sin(rot)};\n\t\treturn q;\n\t}\n\n\t/**\n\t * Create a quaternion from a rotation in degree around a given axis.\n\t */\n\tstatic this_type from_deg(T deg, Vector3t<T> axis) {\n\t\treturn this_type::from_rad(deg * math::DEGSPERRAD, axis);\n\t}\n\n\t/**\n\t * Perform a dot product with another quaternion.\n\t */\n\tT dot(const this_type &o) const {\n\t\treturn this->w * o.w + this->x * o.x + this->y * o.y + this->z * o.z;\n\t}\n\n\t/**\n\t * Calculate the length of the quaternion.\n\t */\n\tT norm() const {\n\t\treturn std::sqrt(this->dot(*this));\n\t}\n\n\t/**\n\t * Ensure that the quaternion's length equals 1.\n\t */\n\tT normalize() {\n\t\tT len = this->norm();\n\t\t*this /= len;\n\t\treturn len;\n\t}\n\n\t/**\n\t * Return the normalized version of this quaternion.\n\t */\n\tthis_type normalized() const {\n\t\tthis_type q{*this};\n\t\tq.normalize();\n\t\treturn q;\n\t}\n\n\t/**\n\t * Inverted the quaternion:\n\t * Flip the rotation axes and scale by\n\t * inverse sum of all components squared.\n\t *\n\t * inv(q)= conj(q)/(w*w + x*x + y*y + z*z)\n\t */\n\tvoid inverse() {\n\t\tT dot = this->dot(*this);\n\t\tif (dot > 0.0) {\n\t\t\t*this = this->conjugated() / dot;\n\t\t}\n\t\telse {\n\t\t\tthrow Error(MSG(err) << \"tried inverting \" << *this);\n\t\t}\n\t}\n\n\t/**\n\t * Return the inverted quaternion.\n\t */\n\tthis_type inversed() const {\n\t\tthis_type q{*this};\n\t\tq.inverse();\n\t\treturn q;\n\t}\n\n\t/**\n\t * Conjugate the quaternion by additive inverting\n\t * the x, y and z components.\n\t */\n\tvoid conjugate() {\n\t\tthis->x = -this->x;\n\t\tthis->y = -this->y;\n\t\tthis->z = -this->z;\n\t}\n\n\t/**\n\t * Return the conjugated quaternion.\n\t */\n\tthis_type conjugated() const {\n\t\tthis_type q{*this};\n\t\tq.conjugate();\n\t\treturn q;\n\t}\n\n\t/**\n\t * Test if the rotation of both quaternions is the same.\n\t */\n\tbool equals(const this_type &other, T eps = default_eps) const {\n\t\tT ori = this->dot(other);\n\t\treturn (1 - (ori * ori)) < eps;\n\t}\n\n\t/**\n\t * Test if both quaternion store the same numbers.\n\t */\n\tbool equals_number(const this_type &other, T eps = default_eps) const {\n\t\tbool result = true;\n\t\t(this->w - other.w) < eps or (result = false);\n\t\t(this->x - other.x) < eps or (result = false);\n\t\t(this->y - other.y) < eps or (result = false);\n\t\t(this->z - other.z) < eps or (result = false);\n\t\treturn result;\n\t}\n\n\t/**\n\t * Test rotation equality with another quaternion\n\t * with given precision in radians.\n\t */\n\tbool equals_rad(const this_type &other, T rad_eps = default_eps) const {\n\t\tT ori = this->dot(other);\n\t\tT angle = std::acos((2.0 * (ori * ori)) - 1.0);\n\n\t\treturn std::abs(angle) < rad_eps;\n\t}\n\n\t/**\n\t * Test rotation equality with another quaternion\n\t * with given precision in degree.\n\t */\n\tbool equals_deg(const this_type &other, T deg_eps = default_eps) const {\n\t\treturn this->equals_rad(other, deg_eps * math::RADSPERDEG);\n\t}\n\n\t/**\n\t * Generate the corresponding rotation matrix.\n\t */\n\tMatrix3t<T> to_matrix() const {\n\t\tT x2 = this->x * 2;\n\t\tT y2 = this->y * 2;\n\t\tT z2 = this->z * 2;\n\n\t\tT x2w = x2 * this->w;\n\t\tT y2w = y2 * this->w;\n\t\tT z2w = z2 * this->w;\n\n\t\tT x3 = x2 * this->x;\n\t\tT y2x = y2 * this->x;\n\t\tT z2x = z2 * this->x;\n\n\t\tT y3 = y2 * this->y;\n\t\tT z2y = z2 * this->y;\n\t\tT z3 = z2 * this->z;\n\n\t\t// clang-format off\n\t\tMatrix3t<T> m{\n\t\t\t1.0 - (y3 + z3), y2x - z2w, z2x + y2w,\n\t\t\ty2x + z2w, 1.0 - (x3 + z3), z2y - x2w,\n\t\t\tz2x - y2w, z2y + x2w, 1.0 - (x3 + y3)\n\t\t};\n\t\t// clang-format on\n\n\t\treturn m;\n\t}\n\n\t/**\n\t * Transforms a vector by this quaternion.\n\t */\n\tVector3t<T> operator*(const Vector3t<T> &vec) const {\n\t\tVector3t<T> axis{this->x, this->y, this->z};\n\n\t\tVector3t<T> axis_vec_normal = axis.cross_product(vec);\n\t\tVector3t<T> axis_vec_inplane = axis.cross_product(axis_vec_normal);\n\n\t\taxis_vec_normal *= 2.0f * this->w;\n\t\taxis_vec_inplane *= 2.0f;\n\n\t\treturn vec + axis_vec_normal + axis_vec_inplane;\n\t}\n\n\tconst this_type &operator+=(const this_type &other) {\n\t\tthis->w += other.w;\n\t\tthis->x += other.x;\n\t\tthis->y += other.y;\n\t\tthis->z += other.z;\n\n\t\treturn *this;\n\t}\n\n\tthis_type operator+(const this_type &other) const {\n\t\tthis_type q{*this};\n\t\tq += other;\n\t\treturn q;\n\t}\n\n\tconst this_type &operator-=(const this_type &other) {\n\t\tthis->w -= other.w;\n\t\tthis->x -= other.x;\n\t\tthis->y -= other.y;\n\t\tthis->z -= other.z;\n\n\t\treturn *this;\n\t}\n\n\tthis_type operator-(const this_type &other) const {\n\t\tthis_type q{*this};\n\t\tq -= other;\n\t\treturn q;\n\t}\n\n\tconst this_type &operator*=(const T &fac) {\n\t\tthis->w *= fac;\n\t\tthis->x *= fac;\n\t\tthis->y *= fac;\n\t\tthis->z *= fac;\n\n\t\treturn *this;\n\t}\n\n\tthis_type operator*(const T &fac) const {\n\t\tthis_type q{*this};\n\t\tq *= fac;\n\t\treturn q;\n\t}\n\n\tconst this_type &operator*=(const this_type &other) {\n\t\tT w_new = (this->w * other.w - this->x * other.x\n\t\t           - this->y * other.y - this->z * other.z);\n\t\tT x_new = (this->w * other.x + this->x * other.w\n\t\t           + this->y * other.z - this->z * other.y);\n\t\tT y_new = (this->w * other.y - this->x * other.z\n\t\t           + this->y * other.w + this->z * other.x);\n\t\tT z_new = (this->w * other.z + this->x * other.y\n\t\t           - this->y * other.x + this->z * other.w);\n\n\t\tthis->w = w_new;\n\t\tthis->x = x_new;\n\t\tthis->y = y_new;\n\t\tthis->z = z_new;\n\n\t\treturn *this;\n\t}\n\n\tthis_type operator*(const this_type &other) const {\n\t\tthis_type q{*this};\n\t\tq *= other;\n\t\treturn q;\n\t}\n\n\tconst this_type &operator/=(const T &fac) {\n\t\tthis->w /= fac;\n\t\tthis->x /= fac;\n\t\tthis->y /= fac;\n\t\tthis->z /= fac;\n\n\t\treturn *this;\n\t}\n\n\tthis_type operator/(const T &fac) const {\n\t\tthis_type q{*this};\n\t\tq /= fac;\n\t\treturn q;\n\t}\n\n\tconst this_type operator-() const {\n\t\treturn this_type{-this->w, -this->x, -this->y, -this->z};\n\t}\n\n\tbool operator==(const this_type &other) const {\n\t\treturn ((this->w == other.w) and (this->x == other.x) and (this->y == other.y) and (this->z == other.z));\n\t}\n\n\tbool operator!=(const this_type &other) const {\n\t\treturn not(*this == other);\n\t}\n\n\tfriend std::ostream &operator<<(std::ostream &o, const this_type &q) {\n\t\to << \"Quaternion(\" << q.w << \", \" << q.x;\n\t\to << \", \" << q.y << \", \" << q.z << \")\";\n\t\treturn o;\n\t}\n\nprotected:\n\t/**\n\t * Stored quaternion number components.\n\t */\n\tT w, x, y, z;\n};\n\nusing Quaternionf = Quaternion<float>;\nusing Quaterniond = Quaternion<double>;\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/quaternion_test.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"quaternion.h\"\n\n#include <cmath>\n\n#include \"../error/error.h\"\n#include \"../log/log.h\"\n#include \"../testing/testing.h\"\n\n\nnamespace openage::util::tests {\n\nvoid quaternion() {\n\t{\n\t\t// Quaternion construction tests\n\t\tconst Quaternionf id{};\n\t\tQuaternionf id_explicit{1.0, 0.0, 0.0, 0.0};\n\n\t\tid.equals(id_explicit) or TESTFAIL;\n\t\tid.equals_deg(id_explicit, 1e-5f) or TESTFAIL;\n\n\t\tQuaternionf wrong{0.0, 0.0, 1.0, 0.0};\n\t\tnot id.equals(wrong) or TESTFAIL;\n\n\t\tMatrix3f id_mat3 = Matrix3f::identity();\n\t\tQuaternionf q_id_mat3{id_mat3};\n\t\tid.equals(q_id_mat3) or TESTFAIL;\n\n\t\tMatrix4f id_mat4 = Matrix4f::identity();\n\t\tQuaternionf q_id_mat4{id_mat4};\n\t\tid.equals(q_id_mat4) or TESTFAIL;\n\n\t\tMatrix4f neg_mat4{\n\t\t\t-1, 0, 0, 0,\n\t\t\t0, 1, 0, 0,\n\t\t\t0, 0, -1, 0,\n\t\t\t0, 0, 0, 1,\n\t\t};\n\n\t\tQuaternionf q_neg{neg_mat4};\n\t\tQuaternionf q_nex_exp{0, 0, 1, 0};\n\n\t\tq_neg.equals(q_nex_exp) or TESTFAIL;\n\t}\n\t{\n\t\t// member functions\n\t\tQuaternionf q0{1, 2, 3, 4};\n\t\tQuaternionf q1{5, 6, 7, 8};\n\n\t\tTESTEQUALS_FLOAT(q0.dot(q1), 5 + 12 + 21 + 32, 1e-5f);\n\t\tTESTEQUALS_FLOAT(q0.norm(), 5.4772255 /*= sqrt(30) */, 1e-4f);\n\n\t\tQuaternionf q2{2, 8, 4, 16};\n\n\t\tQuaternionf q2_normd = q2.normalized();\n\t\tq2_normd.equals(q2) or TESTFAIL;\n\n\t\tQuaternionf q2conj_exp{2, -8, -4, -16};\n\t\tq2conj_exp.equals(q2.conjugated()) or TESTFAIL;\n\n\t\tQuaternionf q2inv_exp{\n\t\t\t0.0058823529411764705f,\n\t\t\t-0.023529411764705882f,\n\t\t\t-0.011764705882352941f,\n\t\t\t-0.047058823529411764f\n\t\t};\n\n\t\tQuaternionf q2inv = q2.inversed();\n\t\tq2inv.normalized().equals(q2inv_exp.normalized()) or TESTFAIL;\n\n\t\tq2.normalize();\n\t\tQuaternionf q2norm_exp{\n\t\t\t0.10846522f, 0.433860915f, 0.216930457f, 0.8677218312f\n\t\t};\n\t\tq2norm_exp.equals(q2) or TESTFAIL;\n\t}\n\t{\n\t\t// Operator tests.\n\t\tQuaternionf id{};\n\t\tQuaternionf bla0{13, 37, 42, 235};\n\t\tbla0.equals(id * bla0) or TESTFAIL;\n\n\t\tQuaternionf bla1{8, 16, 24, 32};\n\t\tQuaternionf bla0_1_exp{21, 45, 50, 243};\n\t\tbla0_1_exp.equals(bla0 + bla1) or TESTFAIL;\n\n\t\tQuaternionf bla1_2_exp{2, 8, 12, 16};\n\t\tbla1_2_exp.equals(bla1 / 4) or TESTFAIL;\n\n\t\tQuaternionf bla1_3_exp{16, 64, 96, 128};\n\t\tbla1_3_exp.equals(bla1 * 8) or TESTFAIL;\n\n\t\tQuaternionf bla1_4_exp{21, 53, 66, 267};\n\t\tbla1_4_exp.equals(bla0 + bla1) or TESTFAIL;\n\n\t\tQuaternionf bla1_5_exp{5, 21, 18, 203};\n\t\tbla1_5_exp.equals(bla0 - bla1) or TESTFAIL;\n\n\t\tQuaternionf bla2_exp{\n\t\t\t13 * 21 - 37 * 45 - 42 * 50 - 235 * 243,\n\t\t\t13 * 45 + 37 * 21 + 42 * 243 - 235 * 50,\n\t\t\t13 * 50 - 37 * 243 + 42 * 21 + 235 * 45,\n\t\t\t13 * 243 + 37 * 50 - 42 * 45 + 235 * 21\n\t\t};\n\t\tbla2_exp.equals(bla0 * bla0_1_exp) or TESTFAIL;\n\n\t\t(bla2_exp == bla2_exp) or TESTFAIL;\n\t\t(bla2_exp != bla0) or TESTFAIL;\n\n\t\tQuaternionf bla0_neg_exp{-13, -37, -42, -235};\n\t\tbla0_neg_exp.equals(-bla0) or TESTFAIL;\n\t}\n\t{\n\t\tenum class axis {\n\t\t\tx, y, z\n\t\t};\n\n\t\t// Rotation tests\n\t\tauto rot_mat = [&](axis a, float am, bool deg=true) -> Matrix3f {\n\t\t\tif (deg) {\n\t\t\t\tam = (am * math::PI) / 180.0;\n\t\t\t}\n\n\t\t\tswitch (a) {\n\n\t\t\tcase axis::x:\n\t\t\t\treturn {\n\t\t\t\t\t1, 0, 0,\n\t\t\t\t\t0, std::cos(am), -std::sin(am),\n\t\t\t\t\t0, std::sin(am), std::cos(am)\n\t\t\t\t};\n\n\t\t\tcase axis::y:\n\t\t\t\treturn {\n\t\t\t\t\tstd::cos(am), 0, std::sin(am),\n\t\t\t\t\t0, 1, 0,\n\t\t\t\t\t-std::sin(am), 0, std::cos(am)\n\t\t\t\t};\n\n\t\t\tcase axis::z:\n\t\t\t\treturn {\n\t\t\t\t\tstd::cos(am), -std::sin(am), 0,\n\t\t\t\t\tstd::sin(am), std::cos(am), 0,\n\t\t\t\t\t0, 0, 1,\n\t\t\t\t};\n\t\t\tdefault:\n\t\t\t\tthrow Error{ERR << \"wtf unreachable code reached\"};\n\t\t\t}\n\t\t};\n\n\t\t// zero rotation = identity.\n\t\tMatrix3f rot = rot_mat(axis::x, 0, false);\n\t\trot.equals(Matrix3f::identity()) or TESTFAIL;\n\t\tQuaternionf q_rot{rot};\n\t\tQuaternionf q_rot_deg = Quaternionf::from_rad(0, {1, 0, 0});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// 10 rad rotation\n\t\trot = rot_mat(axis::x, 10, false);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_rad(10, {1, 0, 0});\n\t\tq_rot.equals_rad(q_rot_deg) or TESTFAIL;\n\n\t\t// wrong 90-rad-rotation:\n\t\tq_rot_deg = Quaternionf::from_rad(90, {1, 0, 0});\n\t\tnot q_rot.equals_rad(q_rot_deg) or TESTFAIL;\n\n\t\t// 10 deg rotation\n\t\trot = rot_mat(axis::x, 10);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(10, {1, 0, 0});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// 90 deg rotation\n\t\trot = rot_mat(axis::x, 90);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(90, {1, 0, 0});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\trot = rot_mat(axis::y, 90);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(90, {0, 1, 0});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\trot = rot_mat(axis::z, 90);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(90, {0, 0, 1});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// -90 deg rotation\n\t\trot = rot_mat(axis::y, -90);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(-90, {0, 1, 0});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// 45 deg rotation\n\t\trot = rot_mat(axis::z, 45);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = Quaternionf::from_deg(45, {0, 0, 1});\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// rotation combination\n\t\trot = rot_mat(axis::z, 45) * rot_mat(axis::y, 60);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = (Quaternionf::from_deg(45, {0, 0, 1}) *\n\t\t             Quaternionf::from_deg(60, {0, 1, 0}));\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\trot = rot_mat(axis::z, 45) *\n\t\t      rot_mat(axis::y, 60) *\n\t\t      rot_mat(axis::x, -200);\n\t\tq_rot = Quaternionf{rot};\n\t\tq_rot_deg = (Quaternionf::from_deg(45, {0, 0, 1}) *\n\t\t             Quaternionf::from_deg(60, {0, 1, 0}) *\n\t\t             Quaternionf::from_deg(-200, {1, 0, 0}));\n\t\tq_rot.equals_deg(q_rot_deg) or TESTFAIL;\n\n\t\t// to_matrix tests\n\t\trot = rot_mat(axis::x, 235);\n\t\tq_rot = Quaternionf::from_deg(235, {1, 0, 0});\n\t\trot.equals(q_rot.to_matrix()) or TESTFAIL;\n\n\t\trot = rot_mat(axis::y, -55);\n\t\tq_rot = Quaternionf::from_deg(-55, {0, 1, 0});\n\t\trot.equals(q_rot.to_matrix()) or TESTFAIL;\n\n\t\trot = rot_mat(axis::z, 64);\n\t\tq_rot = Quaternionf::from_deg(64, {0, 0, 1});\n\t\trot.equals(q_rot.to_matrix()) or TESTFAIL;\n\t}\n\t{\n\t\tVector3f vec{5, 0, 0};\n\t\tVector3f turned = Quaternionf::from_deg(180, {0, 0, 1}) * vec;\n\t\tVector3f turned_exp{-5, 0, 0};\n\t\tturned.equals(turned_exp) or TESTFAIL;\n\n\t\t// intentional fail:\n\t\tturned_exp = Vector3f{-42, -42, -42};\n\t\tnot turned.equals(turned_exp) or TESTFAIL;\n\n\t\t// another turn\n\t\tvec = Vector3f{1337, 42, 235};\n\t\tturned = Quaternionf::from_deg(90, {1, 0, 0}) * vec;\n\t\tturned_exp = Vector3f{1337, -235, 42};\n\t\tturned.equals(turned_exp) or TESTFAIL;\n\t}\n}\n\n} // openage::util::tests\n"
  },
  {
    "path": "libopenage/util/repr.cpp",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#include \"repr.h\"\n\nnamespace openage {\nnamespace util {\n\n\nconstexpr const char *HEX_DIGITS = \"0123456789abcdef\";\n\n\nstd::string repr(const std::string &arg) {\n\tstd::string result;\n\tresult.push_back('\"');\n\tfor (char c : arg) {\n\t\tif (c >= 0x20 && c < 0x7f) {\n\t\t\t// printable\n\t\t\tif (c == '\\\\' || c == '\"') {\n\t\t\t\t// but it must be escaped\n\t\t\t\tresult.push_back('\\\\');\n\t\t\t}\n\t\t\tresult.push_back(c);\n\t\t} else {\n\t\t\t// non-printable\n\t\t\tresult.push_back('\\\\');\n\t\t\tresult.push_back('x');\n\t\t\tresult.push_back(HEX_DIGITS[(c & 0xf0) >> 4]);\n\t\t\tresult.push_back(HEX_DIGITS[(c & 0x0f) >> 0]);\n\t\t}\n\t}\n\tresult.push_back('\"');\n\treturn result;\n}\n\n\n}} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/repr.h",
    "content": "// Copyright 2017-2018 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * method that behaves similar to python's \"repr\".\n * puts the string in quotes and escapes all sorts of stuff inside.\n */\nstd::string repr(const std::string &arg);\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/signal.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <signal.h>\n\n// TODO: change these to ifndef __linux ?\n#ifdef __APPLE__\ntypedef void (*sighandler_t)(int);\n#endif\n#ifdef _WIN32\ntypedef void (*sighandler_t)(int);\n#endif\n"
  },
  {
    "path": "libopenage/util/stringformatter.cpp",
    "content": "// Copyright 2015-2019 the openage authors. See copying.md for legal info.\n\n#include \"stringformatter.h\"\n\n#include <array>\n\nnamespace openage::util {\n\n\nCachableOSStream::CachableOSStream() = default;\n\n\nCachableOSStream::CachableOSStream(std::string &output)\n\t:\n\tCachableOSStream{} {\n\n\tthis->stream.use_with(output);\n}\n\n\nCachableOSStream *CachableOSStream::acquire(std::string &output) {\n\tstatic std::array<CachableOSStream, 128> cache;\n\n\t// Find an available cache element.\n\tfor (CachableOSStream &elem : cache) {\n\t\tif (elem.flag.test_and_set() == false) {\n\t\t\telem.stream.use_with(output);\n\t\t\treturn &elem;\n\t\t}\n\t}\n\n\t// All cached objects are currently in use... that probably means that\n\t// your code is broken.\n\n\t// Anyways, have fun with your dynamically-allocated object.\n\t// TODO Print a warning debug message about this?\n\t// then again, printing a log message about the logging system being\n\t// overloaded might not be the most clever idea.\n\treturn new CachableOSStream{output};\n}\n\n\nvoid CachableOSStream::release(CachableOSStream *cs) {\n\tif (cs == nullptr) {\n\t\treturn;\n\t}\n\n\t// std::atomic_flag doesn't have a simple way of getting the truth\n\t// value, so we have to do a \"full\" test_and_set() instead,\n\t// even though thread-safety is not a concern here.\n\tif (cs->flag.test_and_set()) {\n\t\tcs->stream.clear();\n\t\tcs->flag.clear();\n\t} else {\n\t\t// test_and_set returned false -> the flag was not set.\n\t\t// This is because the element was not acquired from the cache,\n\t\t// but instead dynamically allocated. de-alloc it.\n\t\tdelete cs;\n\t}\n}\n\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/stringformatter.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <atomic>\n#include <memory>\n#include <type_traits>\n\n#include \"../util/compiler.h\"\n\n#include \"externalsstream.h\"\n#include \"strings.h\"\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Designed for usage by StringFormatter.\n *\n * The problem with iostreams, including ExternalOStringStream, is that\n * they use lots of memory, and are quite slow to construct, due to some\n * pesky implementation details.\n *\n * This class fixes this by keeping a reservoir of such objects,\n * and providing access to them via acquire(), in a thread-safe manner.\n * If the cache is exhausted, new objects are constructed on the heap as needed.\n *\n * release() makes a cached object available for acquiring again (cache) or\n * destroys it (heap).\n */\nclass CachableOSStream {\npublic:\n\tExternalOStringStream stream;\n\n\t/*\n\t * Does not call stream.use_with(); for use by cache objects.\n\t */\n\tCachableOSStream();\n\n\t/**\n\t * Calls stream.use_with(output); for use by heap objects.\n\t */\n\tCachableOSStream(std::string &output);\n\n\t/**\n\t * Returns a brand-new(*) CachableOSStream object.\n\t * Origin may vary (static internal cache, heap allocation).\n\t *\n\t * After you're done, pass the pointer to release().\n\t *\n\t * (*) Disclaimer: Object might not actually be brand-new.\n\t *\n\t * @param output:\n\t *     The string object that the stream should output its data to.\n\t */\n\tstatic CachableOSStream *acquire(std::string &output);\n\n\t/**\n\t * Resets the stream to a brand-new state, and clears the flag.\n\t *\n\t * no-op if cs is nullptr.\n\t */\n\tstatic OAAPI void release(CachableOSStream *cs);\n\nprivate:\n\t/**\n\t * true if all of the following are true:\n\t *  - object is part of a larger cache\n\t *  - object is currently in use (-> unavailable)\n\t * http://en.cppreference.com/w/cpp/atomic/ATOMIC_FLAG_INIT\n\t * https://stackoverflow.com/questions/24437396/stdatomic-flag-as-member-variable\n\t */\n\tstd::atomic_flag flag = ATOMIC_FLAG_INIT;\n};\n\n\n/**\n * Wraps an output string stream, and provides all sorts of overloads\n * for operator <<, plus some other formatting methods.\n *\n * Designed mainly for usage by the Formatter / logging / MSG system.\n *\n * The operator << methods return references to ChildType.\n *\n * If possible, input data is written directly to the buffer,\n * but if needed, a CachableOSStream is acquired (and later released).\n * As an optimization, instead of creating a new ExternalOStringStream object,\n * CachableOSStream.acquire() is used internally.\n */\ntemplate <typename ChildType>\nclass StringFormatter {\npublic:\n\t/**\n\t * @param buffer\n\t *     All input data is appended to this object.\n\t */\n\tStringFormatter(std::string &output) :\n\t\toutput{&output},\n\t\tstream_ptr{nullptr} {}\n\n\t/**\n\t * Releases the CachableOSStream object (if it was acquired).\n\t */\n\tvirtual ~StringFormatter() {\n\t\tCachableOSStream::release(this->stream_ptr);\n\t}\n\n\tinline ChildType &child_type_ref() {\n\t\treturn *static_cast<ChildType *>(this);\n\t}\n\n\tStringFormatter(StringFormatter<ChildType> &&other) noexcept\n\t\t:\n\t\toutput{other.output},\n\t\tstream_ptr{other.stream_ptr} {\n\t\tother.stream_ptr = nullptr;\n\t}\n\n\tStringFormatter<ChildType> &operator=(StringFormatter<ChildType> &&other) noexcept {\n\t\tthis->output = other.output;\n\n\t\tthis->stream_ptr = other.stream_ptr;\n\t\tother.stream_ptr = nullptr;\n\t\treturn *this;\n\t}\n\n\t// no copy construction!\n\tStringFormatter(const StringFormatter &) = delete;\n\tStringFormatter &operator=(const StringFormatter &) = delete;\n\n\t// These methods allow usage like an ostream object.\n\ttemplate <typename T>\n\tChildType &operator<<(const T &t) {\n\t\tif (this->should_format()) {\n\t\t\tthis->ensure_stream_obj();\n\t\t\tthis->stream_ptr->stream << t;\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\tChildType &operator<<(std::ios &(*x)(std::ios &)) {\n\t\tif (this->should_format()) {\n\t\t\tthis->ensure_stream_obj();\n\t\t\tthis->stream_ptr->stream << x;\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\tChildType &operator<<(std::ostream &(*x)(std::ostream &)) {\n\t\tif (this->should_format()) {\n\t\t\tthis->ensure_stream_obj();\n\t\t\tthis->stream_ptr->stream << x;\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\t// Optimizations to prevent needless stream-acquiring if just a simple\n\t// string is printed.\n\tChildType &operator<<(const char *s) {\n\t\tif (this->should_format()) {\n\t\t\tthis->output->append(s);\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\tChildType &operator<<(const std::string &s) {\n\t\tif (this->should_format()) {\n\t\t\tthis->output->append(s);\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\t// Printf-style formatting\n\tChildType &fmt(const char *fmt, ...) ATTRIBUTE_FORMAT(2, 3) {\n\t\tif (this->should_format()) {\n\t\t\tva_list ap;\n\t\t\tva_start(ap, fmt);\n\t\t\tutil::vsformat(fmt, ap, *this->output);\n\t\t\tva_end(ap);\n\t\t}\n\n\t\treturn this->child_type_ref();\n\t}\n\n\n\t// Allow direct inputting of stuff that's wrapped in the C++11 pointer types.\n\ttemplate <typename T>\n\tChildType &operator<<(const std::unique_ptr<T> &ptr) {\n\t\tif (this->should_format()) {\n\t\t\t*this << ptr.get();\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\ttemplate <typename T>\n\tChildType &operator<<(const std::shared_ptr<T> &ptr) {\n\t\tif (this->should_format()) {\n\t\t\t*this << ptr.get();\n\t\t}\n\t\treturn this->child_type_ref();\n\t}\n\n\n\t/**\n\t * Clears the underlying stream object's flags, if such a stream\n\t * object exists.\n\t */\n\tvoid reset_flags() {\n\t\tif (this->stream_ptr != nullptr) {\n\t\t\tthis->stream_ptr->stream.clear();\n\t\t}\n\t}\n\n\t/**\n\t * Returns if formatting should actually occur.\n\t */\n\tvirtual bool should_format() const {\n\t\treturn true;\n\t}\n\nprivate:\n\t/**\n\t * Ensures that we have a valid CachableOSStream object in stream_ptr.\n\t */\n\tinline void ensure_stream_obj() {\n\t\tif (this->stream_ptr == nullptr) [[unlikely]] {\n\t\t\tthis->stream_ptr = CachableOSStream::acquire(*this->output);\n\t\t}\n\t}\n\n\tstd::string *output;\n\tCachableOSStream *stream_ptr;\n};\n\n\n/**\n * Allows simple direct usage of StringFormatter.\n */\nclass Formatter : public StringFormatter<Formatter> {};\n\n\n/**\n * A self-formatting string! Get yours today!\n *\n * Use like this:\n *\n * std::string stuff() {\n *     FString s;\n *     s << \"test: \" << 5 << std::cout;\n *     return s;\n * }\n *\n * Use as a faster, care-free replacement for stringstream:\n *\n * std::string stuff() {\n *     std::stringstream sstr;\n *     sstr << \"test: \" << 5 << std::cout;\n *     return sstr.str();\n * }\n */\nclass FString : public StringFormatter<FString> {\npublic:\n\tFString() :\n\t\tStringFormatter<FString>{this->buffer} {}\n\n\t// allow assignment and construction from std::string.\n\n\tFString(const std::string &other) :\n\t\tStringFormatter<FString>{this->buffer},\n\t\tbuffer{other} {}\n\n\tFString(std::string &&other) noexcept\n\t\t:\n\t\tStringFormatter<FString>{this->buffer},\n\t\tbuffer{std::move(other)} {}\n\n\tFString &operator=(const std::string &other) {\n\t\tthis->buffer = other;\n\t\treturn *this;\n\t}\n\n\tFString &operator=(std::string &&other) noexcept {\n\t\tthis->buffer = std::move(other);\n\t\treturn *this;\n\t}\n\n\t// allow conversion to std::string.\n\n\toperator std::string &() & {\n\t\treturn this->buffer;\n\t}\n\n\toperator const std::string &() const {\n\t\treturn this->buffer;\n\t}\n\n\toperator std::string() && {\n\t\treturn std::move(this->buffer);\n\t}\n\n\tvoid reset() {\n\t\tbuffer.clear();\n\t\tthis->reset_flags();\n\t}\n\n\t// users may directly access this.\n\tstd::string buffer;\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/strings.cpp",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#include \"strings.h\"\n\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n#include <memory>\n#include <string>\n#include <vector>\n\n#include \"../error/error.h\"\n#include \"compiler.h\"\n#include \"config.h\"\n\nnamespace openage::util {\n\n\nstd::string sformat(const char *fmt, ...) {\n\tstd::string ret;\n\n\tva_list ap;\n\tva_start(ap, fmt);\n\tvsformat(fmt, ap, ret);\n\tva_end(ap);\n\n\treturn ret;\n}\n\n\nsize_t vsformat(const char *fmt, va_list ap, std::string &output) {\n#if HAVE_THREAD_LOCAL_STORAGE\n\tstatic thread_local std::vector<char> buf(64);\n#else\n\tstd::vector<char> buf(64);\n#endif\n\n\tva_list aq;\n\tva_copy(aq, ap);\n\tsize_t length = vsnprintf(buf.data(), buf.size(), fmt, aq);\n\tva_end(aq);\n\n\tif (length >= buf.size()) [[unlikely]] {\n\t\tsize_t target_size = buf.size();\n\t\tif (target_size < 64) [[unlikely]] {\n\t\t\ttarget_size = 64;\n\t\t}\n\t\twhile (length >= target_size) {\n\t\t\ttarget_size *= 2;\n\t\t}\n\n\t\tbuf.resize(target_size);\n\n\t\tvsnprintf(buf.data(), buf.size(), fmt, ap);\n\t}\n\n\toutput.append(buf.data(), length);\n\n\treturn length;\n}\n\n\nstd::unique_ptr<char[]> copy_string(const char *s) {\n\tsize_t sz = strlen(s) + 1;\n\tauto ret = std::make_unique<char[]>(sz);\n\tmemcpy(ret.get(), s, sz);\n\treturn ret;\n}\n\n\nsize_t rstrip(char *s) {\n\tsize_t strippedlen = strlen(s);\n\n\twhile (strippedlen > 0) {\n\t\tif (s[strippedlen - 1] == '\\n' || s[strippedlen - 1] == ' ' || s[strippedlen - 1] == '\\t') {\n\t\t\tstrippedlen -= 1;\n\t\t}\n\t\telse {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\ts[strippedlen] = '\\0';\n\n\treturn strippedlen;\n}\n\n\nbool string_matches_pattern(const char *str, const char *pattern) {\n\twhile (true) {\n\t\tif (*pattern == '*') {\n\t\t\t// skip all wildcard chars\n\t\t\twhile (*pattern == '*') {\n\t\t\t\tpattern++;\n\t\t\t}\n\n\t\t\t// performance optimization: if the wildcard was the\n\t\t\t// last char of the pattern, it's a sure match.\n\t\t\tif (*pattern == '\\0') {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// search for all places in str that equal *pattern;\n\t\t\t// those are possible places of continuation.\n\t\t\twhile (*str != '\\0') {\n\t\t\t\tif (*str == *pattern) {\n\t\t\t\t\tif (string_matches_pattern(str, pattern)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// no match was found\n\t\t\treturn false;\n\t\t}\n\n\t\tif (*str != *pattern) {\n\t\t\t// chars don't match\n\t\t\treturn false;\n\t\t}\n\n\t\tif (*pattern == '\\0') {\n\t\t\t// comparision done\n\t\t\treturn true;\n\t\t}\n\n\t\tstr += 1;\n\t\tpattern += 1;\n\t}\n}\n\n\nstd::vector<std::string> split(const std::string &txt, char delimiter) {\n\tstd::vector<std::string> items;\n\t// use the back inserter iterator and the templated split function.\n\tsplit(txt, delimiter, std::back_inserter(items));\n\treturn items;\n}\n\n\nstd::vector<std::string> split_escape(const std::string &txt, char delim, size_t size_hint) {\n\t// output vector\n\tstd::vector<std::string> items;\n\tif (size_hint) [[likely]] {\n\t\titems.reserve(size_hint);\n\t}\n\n#if HAVE_THREAD_LOCAL_STORAGE\n\tstatic thread_local std::vector<char> buf;\n#else\n\tstd::vector<char> buf;\n\tbuf.reserve(256);\n#endif\n\n\t// string reading pointer\n\tconst char *r = txt.c_str();\n\n\t// copy characters to buf, and a buf is emitted as a token\n\t// when the delimiter or end is reached.\n\twhile (true) {\n\t\t// end of input string\n\t\tif (*r == '\\0') {\n\t\t\titems.emplace_back(std::begin(buf), std::end(buf));\n\t\t\tbuf.clear();\n\t\t\tbreak;\n\t\t}\n\n\t\t// delimiter found\n\t\tif (*r == delim) {\n\t\t\titems.emplace_back(std::begin(buf), std::end(buf));\n\t\t\tbuf.clear();\n\n\t\t\tr++;\n\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (*r == '\\\\') {\n\t\t\t// an escaped char: increment the read pointer to point\n\t\t\t// at the escape code.\n\t\t\tr++;\n\n\t\t\t// analyze the escape code\n\t\t\tswitch (*r) {\n\t\t\tcase '\\0':\n\t\t\t\t// string ended in the middle of an escape code\n\t\t\t\t// error!\n\t\t\t\tthrow Error{ERR << \"string ends after escape\"};\n\n\t\t\tcase 'n':\n\t\t\t\t// a newline\n\t\t\t\tbuf.push_back('\\n');\n\t\t\t\tcontinue;\n\n\t\t\tdefault:\n\t\t\t\t// the escape code already represents the literal\n\t\t\t\t// character (e.g.: \"\\\\\" = '\\', \"\\,\" = ',').\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tbuf.push_back(*r);\n\t\tr++;\n\t}\n\n\treturn items;\n}\n\nstd::vector<std::string> split_newline(const std::string &txt) {\n\tauto lines = split(txt, '\\n');\n\n\t// remove the '\\r' from the end of each line\n\tfor (auto &line : lines) {\n\t\tif (not line.empty() and line.back() == '\\r') {\n\t\t\tline.pop_back();\n\t\t}\n\t}\n\n\treturn lines;\n}\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/strings.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdarg>\n#include <functional>\n#include <iomanip>\n#include <memory>\n#include <sstream>\n#include <string>\n#include <vector>\n\n#if defined(__GNUC__)\n\t#define ATTRIBUTE_FORMAT(i, j) __attribute__((format(printf, i, j)))\n#else\n\t#define ATTRIBUTE_FORMAT(i, j)\n#endif\n\nnamespace openage {\nnamespace util {\n\n\n/**\n * Quick-formatter for floats when working with string streams.\n * Usage: cout << FormatFloat{1.0, 10};\n */\ntemplate <unsigned decimals, unsigned w = 0>\nstruct FloatFixed {\n\tfloat value;\n};\n\n\ntemplate <unsigned decimals, unsigned w>\nstd::ostream &operator<<(std::ostream &os, FloatFixed<decimals, w> f) {\n\tstatic_assert(decimals < 50, \"Refusing to print float with >= 50 decimals\");\n\tstatic_assert(w < 70, \"Refusing to print float with a width >= 70\");\n\n\tos.precision(decimals);\n\tos << std::fixed;\n\n\tif (w) {\n\t\tos << std::setw(w);\n\t}\n\n\tos << f.value;\n\n\treturn os;\n}\n\n\n/**\n * printf-style to-string formatting.\n */\nstd::string sformat(const char *fmt, ...) ATTRIBUTE_FORMAT(1, 2);\n\n\n/**\n * printf-style valist-to-string formatting; the result is appended to output.\n */\nsize_t vsformat(const char *fmt, va_list ap, std::string &output);\n\n\n/**\n * Copies the given string to the returned unique_ptr, including 0 byte.\n */\nstd::unique_ptr<char[]> copy_string(const char *s);\n\n\n/**\n * Returns the number of whitespace characters on the right of the string.\n */\nsize_t rstrip(char *s);\n\n\n/**\n * returns true if str matches the basic globbing pattern\n * in the pattern, '*' matches any number of characters, while all other\n * characters are interpreted as literal.\n */\nbool string_matches_pattern(const char *str, const char *pattern);\n\n\n/**\n * Split a string at a delimiter, push the result back in an iterator.\n * Why doesn't the fucking standard library have std::string::split(delimiter)?\n */\ntemplate <typename ret_t>\nvoid split(const std::string &txt, char delimiter, ret_t result) {\n\tstd::stringstream splitter;\n\tsplitter.str(txt);\n\tstd::string part;\n\n\twhile (std::getline(splitter, part, delimiter)) {\n\t\t*result = part;\n\t\tresult++;\n\t}\n}\n\n\n/**\n * Split a string at a delimiter into a vector.\n * Internally, uses the above iterator splitter.\n */\nstd::vector<std::string> split(const std::string &txt, char delim);\n\n\n/**\n * Split a string at a delimiter into a vector.\n * size_hint is to give a predicted size of the vector already.\n *\n * tokenizes txt by splitting it up to substrings at the deliminiters.\n * \"\\n\" is evaluated to '\\n'; all other '\\X' escape sequences are evaluated\n * to literal X, including the deliminiter.\n */\nstd::vector<std::string> split_escape(const std::string &txt,\n                                      char delim,\n                                      size_t size_hint = 0);\n\n/**\n * Newline splitter that works with both \\n and \\r\\n.\n */\nstd::vector<std::string> split_newline(const std::string &txt);\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/subprocess.cpp",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#include \"subprocess.h\"\n\n#include <cerrno>\n#include <cstdlib>\n#include <cstring>\n\n#ifdef _WIN32\n// TODO not yet implemented\n#else\n\t#include <fcntl.h>\n\t#include <sys/stat.h>\n\t#include <sys/types.h>\n\t#include <sys/wait.h>\n\t#include <unistd.h>\n#endif\n\n#include \"../log/log.h\"\n#include \"strings.h\"\n\nnamespace openage::subprocess {\n\nbool is_executable(const char *filename) {\n#ifdef _WIN32\n\t// TODO not yet implemented\n\treturn false;\n#else\n\tstruct stat sb;\n\treturn (stat(filename, &sb) == 0\n\t        and S_ISREG(sb.st_mode)\n\t        and sb.st_mode & 0111);\n#endif\n}\n\nstd::string which(const char *name) {\n#ifdef _WIN32\n\t// TODO not yet implemented\n\treturn name;\n#else\n\n\t// when it's an absolute name\n\tif (is_executable(name)) {\n\t\treturn name;\n\t}\n\n\tchar *env_path = getenv(\"PATH\");\n\n\tif (env_path == nullptr) {\n\t\tlog::log(WARN << \"no PATH environment variable found!\");\n\t\treturn \"\";\n\t}\n\n\tstd::unique_ptr<char[]> path = util::copy_string(env_path);\n\n\tfor (char *dir = strtok(path.get(), \":\"); dir; dir = strtok(nullptr, \":\")) {\n\t\tstd::string filename;\n\t\tfilename.append(dir);\n\t\tfilename.push_back('/');\n\t\tfilename.append(name);\n\n\t\tif (is_executable(filename.c_str())) {\n\t\t\treturn filename;\n\t\t}\n\t}\n\n\treturn \"\";\n#endif\n}\n\nint call(const std::vector<const char *> &argv, bool wait, const char *redirect_stdout_to) {\n#ifdef _WIN32\n\t// TODO not yet implemented\n\treturn -1; // nope\n#else\n\n\t// used by child to communicate execve() to its parent.\n\t// on success, the child auto-closes the pipe; the parent reads 0 bytes.\n\t// on failure, however, the child writes errno; the parent reads this.\n\tint pipefd[2];\n\n\tif (pipe(pipefd) < 0) {\n\t\t// the pipe could not be created\n\t\tlog::log(MSG(err) << \"could not create pipe: \" << strerror(errno));\n\n\t\treturn -1;\n\t}\n\n\tint replacement_stdout_fd = -1;\n\tif (redirect_stdout_to != nullptr) {\n\t\treplacement_stdout_fd = open(\n\t\t\tredirect_stdout_to,\n\t\t\tO_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,\n\t\t\t0644);\n\n\t\tif (replacement_stdout_fd < 0) {\n\t\t\tlog::log(MSG(err) << \"could not open output redirection file \" << redirect_stdout_to << \": \" << strerror(errno));\n\n\t\t\tclose(pipefd[0]);\n\t\t\tclose(pipefd[1]);\n\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\t// mark write end of pipe as close-on-exec\n\tif (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) < 0) {\n\t\tlog::log(MSG(err) << \"could not fcntl write-end of pipe: \" << strerror(errno));\n\n\t\tclose(pipefd[0]);\n\t\tclose(pipefd[1]);\n\n\t\tif (replacement_stdout_fd > -1) {\n\t\t\tclose(replacement_stdout_fd);\n\t\t}\n\n\t\treturn -1;\n\t}\n\n\tpid_t child_pid = fork();\n\n\tif (child_pid == -1) {\n\t\t// the fork has failed\n\t\tlog::log(MSG(err) << \"could not fork: \" << strerror(errno));\n\n\t\tclose(pipefd[0]);\n\t\tclose(pipefd[1]);\n\n\t\tif (replacement_stdout_fd > -1) {\n\t\t\tclose(replacement_stdout_fd);\n\t\t}\n\n\t\treturn -1;\n\t}\n\n\tif (child_pid == 0) {\n\t\t// we're the child\n\n\t\t// close read end of pipe\n\t\tclose(pipefd[0]);\n\n\t\t// replace stdout\n\t\tif (replacement_stdout_fd > -1) {\n\t\t\tdup2(replacement_stdout_fd, 1);\n\t\t\tclose(replacement_stdout_fd);\n\t\t}\n\n\t\tconst char *const *argv_data = argv.data();\n\n\t\t// this is horrible.\n\t\t// \"(...) these objects are completely constant.\"\n\t\t// \"Due to a limitation of the ISO C standard,\"\n\t\t// \"it is not possible to state that idea in standard C.\"\n\t\t// wtf is your problem, guys...\n\t\texecv(argv[0], const_cast<char *const *>(argv_data));\n\n\t\tint child_errno = errno;\n\n\t\t// execv has failed. write errno to parent\n\n\t\t// welcome to our little adventure of writing _4_ bytes to our parent :)\n\t\tchar *write_buf = reinterpret_cast<char *>(&child_errno);\n\t\tssize_t remaining = sizeof(child_errno);\n\n\t\twhile (remaining > 0) {\n\t\t\tssize_t written = write(pipefd[1], write_buf, remaining);\n\n\t\t\tif (written <= 0) {\n\t\t\t\t// we didn't even succeed at passing errno to our parent.\n\t\t\t\t// truly, all hope is lost.\n\t\t\t\texit(2);\n\t\t\t}\n\n\t\t\tremaining -= written;\n\t\t\twrite_buf += written;\n\t\t}\n\t\texit(1);\n\t}\n\n\t// we're the parent.\n\tclose(pipefd[1]);\n\tif (replacement_stdout_fd > -1) {\n\t\tclose(replacement_stdout_fd);\n\t}\n\n\t// welcome to our little adventure of reading _4_ bytes from our child :)\n\tint child_errno;\n\tchar *read_buf = reinterpret_cast<char *>(&child_errno);\n\tsize_t total = 0;\n\n\t// we expect to read either 0, or sizeof(child_errno) bytes.\n\twhile (total < sizeof(child_errno)) {\n\t\tssize_t read_count = read(pipefd[0], read_buf + total, sizeof(child_errno) - total);\n\n\t\tif (read_count == 0) {\n\t\t\t// no more data from child\n\t\t\tbreak;\n\t\t}\n\n\t\tif (read_count < 0) {\n\t\t\tlog::log(MSG(err) << \"read from child pipe failed\" << strerror(errno));\n\t\t\tclose(pipefd[0]);\n\t\t\treturn -1;\n\t\t}\n\n\t\ttotal += read_count;\n\t}\n\n\tclose(pipefd[0]);\n\n\tif (total > 0) {\n\t\tif (total != sizeof(int)) {\n\t\t\tlog::log(MSG(err) << \"wrong number of bytes read from child pipe: \" << total);\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (child_errno > 0) {\n\t\t\tlog::log(MSG(err) << \"execv has failed: \" << strerror(child_errno));\n\t\t\treturn -1;\n\t\t}\n\t}\n\n\tif (!wait) {\n\t\t// leave the child process to its fate\n\t\t// TODO wait in thread to avoid zombification?\n\t\treturn 0;\n\t}\n\n\t// wait for the child process to finish\n\tint status;\n\tif (waitpid(child_pid, &status, 0) < 0) {\n\t\tlog::log(MSG(err) << \"could not wait for child process\");\n\t\treturn -1;\n\t}\n\n\t// everything went well.\n\treturn status;\n#endif\n}\n\n} // namespace openage::subprocess\n"
  },
  {
    "path": "libopenage/util/subprocess.h",
    "content": "// Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <string>\n#include <vector>\n\nnamespace openage {\n\n/**\n * similar in its goals to Python's subprocess module\n */\nnamespace subprocess {\n\n/**\n * returns true IFF the file is executable\n */\nbool is_executable(const char *filename);\n\n/**\n * finds the path to an executable by evaluating env[PATH]\n */\nstd::string which(const char *name);\n\n/**\n * starts the process at argv[0]\n * passes argv as argv\n *\n * on failure, -1 is returned.\n *\n * @param argv               must consist of at least one non-empty string,\n *                           and end with a nullptr. this is not verified.\n *\n * @param wait               decides whether we wait for the process to finish.\n *                           if true, returns the process's return value\n *                           if false, returns 0\n *\n * @param redirect_stdout_to if not nullptr, the given file is opened and\n *                           substituted for the process's stdout.\n *                           the existing file is overwritten.\n */\nint call(const std::vector<const char *> &argv,\n         bool wait = false,\n         const char *redirect_stdout_to = nullptr);\n\n} // namespace subprocess\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/thread_id.cpp",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#include \"thread_id.h\"\n\n#include \"config.h\"\n\n#if HAVE_THREAD_LOCAL_STORAGE\n\t#include <atomic>\n#else\n\t#include <thread>\n#endif\n\nnamespace openage {\nnamespace util {\n\n#if HAVE_THREAD_LOCAL_STORAGE\n\n/**\n * Designed for usage as a global, thread-local object.\n *\n * The first time it's accessed from a new thread, its constructor will\n * run, assigning a value to 'val'.\n */\nclass ThreadIdSupplier {\npublic:\n\tThreadIdSupplier() :\n\t\tval{ThreadIdSupplier::counting_id++} {}\n\n\tconst size_t val;\n\nprivate:\n\t/**\n\t * Used internally to keep track of already-used thread ids.\n\t * Holds the id that will be used for the next thread.\n\t */\n\tstatic std::atomic<size_t> counting_id;\n};\n\nstd::atomic<size_t> ThreadIdSupplier::counting_id{0};\n\n#endif\n\nsize_t get_current_thread_id() {\n#if HAVE_THREAD_LOCAL_STORAGE\n\tstatic thread_local ThreadIdSupplier current_thread_id;\n\treturn current_thread_id.val;\n#else\n\treturn std::hash<std::thread::id>()(std::this_thread::get_id());\n#endif\n}\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/thread_id.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <stddef.h>\n\nnamespace openage {\nnamespace util {\n\n/**\n * Returns the unique identifier of the current thread.\n *\n * For compilers with support for thread_local storage, the function also guarantees\n * to return strictly monotonically increasing identifier (no collision).\n */\nsize_t get_current_thread_id();\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/timer.cpp",
    "content": "// Copyright 2013-2017 the openage authors. See copying.md for legal info.\n\n#include \"timer.h\"\n\n#include <ciso646>\n\nnamespace openage {\nnamespace util {\n\nTimer::Timer(bool stopped) {\n\tthis->reset(stopped);\n}\n\nvoid Timer::reset(bool stopped) {\n\tthis->stopped = stopped;\n\tif (this->stopped) {\n\t\tthis->stoppedat = 0;\n\t} else {\n\t\tthis->starttime = timing::get_monotonic_time();\n\t}\n}\n\nvoid Timer::stop() {\n\tif (not stopped) {\n\t\tthis->stopped = true;\n\t\tthis->stoppedat = timing::get_monotonic_time() - this->starttime;\n\t}\n}\n\nvoid Timer::start() {\n\tif (this->stopped) {\n\t\tthis->stopped = false;\n\t\tthis->starttime = timing::get_monotonic_time() - this->stoppedat;\n\t}\n}\n\ntime_nsec_t Timer::getval() const {\n\tif (this->stopped) {\n\t\treturn this->stoppedat;\n\t} else {\n\t\treturn timing::get_monotonic_time() - this->starttime;\n\t}\n}\n\ntime_nsec_t Timer::getandresetval() {\n\ttime_nsec_t result;\n\n\tif (this->stopped) {\n\t\tresult = this->stoppedat;\n\t\tthis->stoppedat = 0;\n\t}\n\telse {\n\t\ttime_nsec_t now = timing::get_monotonic_time();\n\t\tresult = now - this->starttime;\n\t\tthis->starttime = now;\n\t}\n\n\treturn result;\n}\n\nbool Timer::isstopped() const {\n\treturn this->stopped;\n}\n\n}}  // openage::util\n"
  },
  {
    "path": "libopenage/util/timer.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\n#include \"timing.h\"\n\n\nnamespace openage {\nnamespace util {\n\n/**\n * Time measurement class.\n */\nclass Timer {\n\tbool stopped;\n\tunion {\n\t\t/**\n\t\t * while paused, stores the current timer value\n\t\t */\n\t\ttime_nsec_t stoppedat;\n\n\t\t/**\n\t\t * while running, stores the time the timer is counting from\n\t\t */\n\t\ttime_nsec_t starttime;\n\t};\n\npublic:\n\t/**\n\t * creates the timer, in either stopped or running state.\n\t */\n\tTimer(bool stopped = true);\n\n\t/**\n\t * resets the timer, in either stopped or running state.\n\t */\n\tvoid reset(bool stopped = true);\n\n\t/**\n\t * stops/pauses the timer.\n\t */\n\tvoid stop();\n\n\t/**\n\t * starts/unpauses the timer.\n\t */\n\tvoid start();\n\n\t/**\n\t * reads the current timer value, in nanoseconds.\n\t */\n\ttime_nsec_t getval() const;\n\n\t/**\n\t * reads the current timer value, in nanoseconds,\n\t * and resets the timer to zero (preserving started/stopped state).\n\t */\n\ttime_nsec_t getandresetval();\n\n\t/**\n\t * returns whether the timer is currently running.\n\t */\n\tbool isstopped() const;\n};\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/timing.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"timing.h\"\n\n#include <chrono>\n#include <cstring>\n\n#include \"../error/error.h\"\n\nnamespace openage {\nnamespace timing {\n\ntime_nsec_t get_real_time() {\n\tstd::chrono::system_clock::time_point t;\n\tt = std::chrono::system_clock::now();\n\tauto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(t.time_since_epoch()).count();\n\treturn static_cast<time_nsec_t>(ns);\n}\n\ntime_nsec_t get_monotonic_time() {\n\tstd::chrono::steady_clock::time_point t;\n\tt = std::chrono::steady_clock::now();\n\tauto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(t.time_since_epoch()).count();\n\treturn static_cast<time_nsec_t>(ns);\n}\n\n}} // namespace openage::timing\n"
  },
  {
    "path": "libopenage/util/timing.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <cstdint>\n\nnamespace openage {\n\n/**\n * Type to store time in nanoseconds.\n */\nusing time_nsec_t = uint64_t;\n\n\nnamespace timing {\n\n/**\n * returns a monotonically-increasing time that returns the number of\n * nanoseconds since an unspecified event.\n *\n * not influenced by system time changes.\n */\ntime_nsec_t get_monotonic_time();\n\n/**\n * returns the number of nanoseconds since the UNIX epoch.\n */\ntime_nsec_t get_real_time();\n\n} // namespace timing\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/unicode.cpp",
    "content": "// Copyright 2013-2019 the openage authors. See copying.md for legal info.\n\n#include \"unicode.h\"\n\n#include <cstring>\n\nnamespace openage {\nnamespace util {\n\nutf8_decoder::utf8_decoder() {\n\treset();\n}\n\nvoid utf8_decoder::reset() {\n\tout = -1;\n\tremaining = 0;\n}\n\nbool utf8_decoder::feed(char c) {\n\tbool result = true;\n\n\tif ((c & (1 << 7)) == 0) {\n\t\t// single-byte character\n\t\tif (remaining) {\n\t\t\t// successful re-synchronization\n\t\t\t// (current multi-byte character discarded)\n\t\t\tresult = false;\n\t\t}\n\n\t\tremaining = 0;\n\t\tout = c;\n\t} else if (c & (1 << 6)) {\n\t\tif (remaining) {\n\t\t\t// successful re-synchronization\n\t\t\t// (current multi-byte character discarded)\n\t\t\tresult = false;\n\t\t}\n\n\t\t// beginning of a multi-byte character\n\t\tif ((c & (1 << 5)) == 0) {\n\t\t\t// 2-byte character\n\t\t\tremaining = 1;\n\t\t\tout = c & ((1 << 5) - 1);\n\t\t} else if ((c & (1 << 4)) == 0) {\n\t\t\t// 3-byte character\n\t\t\tremaining = 2;\n\t\t\tout = c & ((1 << 4) - 1);\n\t\t} else if ((c & (1 << 3)) == 0) {\n\t\t\t// 4-byte character\n\t\t\tremaining = 3;\n\t\t\tout = c & ((1 << 3) - 1);\n\t\t} else {\n\t\t\t// no 5- or 6-byte characters exist in utf8\n\t\t\tremaining = 0;\n\t\t\tout = -1;\n\t\t\tresult = false;\n\t\t}\n\t} else {\n\t\t// inside a multi-byte character\n\t\tif (!remaining) {\n\t\t\t// we expected the start of a new character\n\t\t\tresult = false;\n\t\t}\n\n\t\tremaining -= 1;\n\t\tout = (out << 6) | (c & ((1 << 6) - 1));\n\t}\n\n\treturn result;\n}\n\nsize_t utf8_decode(const unsigned char *s, size_t len, int32_t *outbuf) {\n\tsize_t advance;\n\twchar_t w;\n\tsize_t result = 0;\n\n\twhile (len > 0) {\n\t\tif (s[0] < 0x80) {\n\t\t\t// 1-byte (ASCII) character\n\t\t\tw = *s;\n\t\t\tadvance = 1;\n\t\t} else if (len >= 2 && s[0] >= 0xc2 && s[0] <= 0xdf && (s[1] & 0xc0) == 0x80) {\n\t\t\t// 2-byte character\n\t\t\tw = ((s[0] & 0x1f) << 6) | (s[1] & 0x3f);\n\t\t\tadvance = 2;\n\t\t} else if (len >= 3 &&\n\t\t\t  ((s[0] == 0xe0  && s[1] >= 0xa0 && s[1] <= 0xbf)\n\t\t\t|| (s[0] >= 0xe1 && s[0] <= 0xec && s[1] >= 0x80 && s[1] <= 0xbf)\n\t\t\t|| (s[0] == 0xed && s[1] >= 0x80 && s[1] <= 0x9f)\n\t\t\t|| (s[0] >= 0xee && s[0] <= 0xef && s[1] >= 0x80 && s[1] <= 0xbf))\n\t\t\t&& (s[2] & 0xc0) == 0x80) {\n\t\t\t// 3-byte character\n\t\t\tw = ((s[0] & 0x0f) << 12) | ((s[1] & 0x3f) << 6) | (s[2] & 0x3f);\n\t\t\tadvance = 3;\n\t\t} else if (len >= 4 &&\n\t\t\t  ((s[0] == 0xf0 && s[1] >= 0x90 && s[1] <= 0xbf)\n\t\t\t|| (s[0] >= 0xf1 && s[0] <= 0xf3 && s[1] >= 0x80 && s[1] <= 0xbf)\n\t\t\t|| (s[0] == 0xf4 && s[1] >= 0x80 && s[1] <= 0x8f))\n\t\t\t&& (s[2] & 0xc0) == 0x80\n\t\t\t&& (s[3] & 0xc0) == 0x80) {\n\t\t\t// 4-byte character\n\t\t\tw = ((s[0] & 0x07) << 18) | ((s[1] & 0x3f) << 12) | ((s[2] & 0x3f) << 6) | (s[3] & 0x3f);\n\t\t\tadvance = 4;\n\t\t} else {\n\t\t\t// decoding error; try with next byte\n\t\t\tw = 0xfffd;\n\t\t\tadvance = 1;\n\t\t}\n\n\t\tlen -= advance;\n\t\ts += advance;\n\n\t\t*outbuf++ = w;\n\t\tresult++;\n\t}\n\n\treturn result;\n}\n\nsize_t utf8_encode(int cp, char *outbuf) {\n\tif (cp < 0) {\n\t\t// illegal codepoint (negative)\n\t\toutbuf[0] = '\\0';\n\t\treturn 0;\n\t} else if (cp < 0x80) {\n\t\toutbuf[0] = cp;\n\t\toutbuf[1] = '\\0';\n\t\treturn 1;\n\t} else if (cp < 0x800) {\n\t\toutbuf[2] = '\\0';\n\t\toutbuf[1] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[0] = 0xc0 | cp;\n\t\treturn 2;\n\t} else if (cp < 0x10000) {\n\t\toutbuf[3] = '\\0';\n\t\toutbuf[2] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[1] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[0] = 0xe0 | cp;\n\t\treturn 3;\n\t} else if (cp < 0x200000) {\n\t\toutbuf[4] = '\\0';\n\t\toutbuf[3] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[2] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[1] = 0x80 | (cp & 0x3f); cp >>= 6;\n\t\toutbuf[0] = 0xf0 | cp;\n\t\treturn 4;\n\t} else {\n\t\t// illegal codepoint: unicode is only defined up to 0x1fffff\n\t\toutbuf[0] = '\\0';\n\t\treturn 0;\n\t}\n}\n\nsize_t utf8_last_char_size(char *str) {\n\tint r = 0;\n\tint i = strlen(str) - 1;\n\twhile ((i >= 0) && (str[i] & 0x80) && !(str[i] & 0x40)) {\n\t\ti--;\n\t\tr++;\n\t}\n\tif (i >= 0) {\n\t\tr++;\n\t}\n\treturn r;\n}\n\nvoid utf8_pop_back(std::string &str) {\n\tstr.erase(str.size() - utf8_last_char_size(&str[0]));\n}\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/unicode.h",
    "content": "// Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <stdint.h>\n#include <stdlib.h>\n#include <string>\n\nnamespace openage {\nnamespace util {\n\n// we could simply use wchar_t here, but legend says there are operating\n// systems where for some sick reason wchar_t might be only 16 bits wide.\n// also, a signed type is useful here to mark errors/illegal characters.\nusing codepoint_t = int32_t;\n\nclass utf8_decoder {\npublic:\n\tutf8_decoder();\n\n\t/**\n\t * current result character code point;\n\t * overwritten on feed().\n\t *\n\t * if no valid output character currently exists,\n\t * this is negative or remaining is not 0.\n\t */\n\tcodepoint_t out;\n\n\t/**\n\t * number of remaining characters\n\t */\n\tunsigned remaining;\n\n\t/**\n\t * feeds one char to the decoder state machine\n\t * returns false on decoding error, true else\n\t *\n\t * once a character has been completely decoded,\n\t * remaining is set to false and out is set to the\n\t * decoded character.\n\t * note that even if true is returned, remaining MAY\n\t * be false (e.g. multi-byte characters),\n\t * and remaining MAY be true even if false is returned\n\t * (successful re-synchronization).\n\t *\n\t * thus, after each call to feed(), you'll first want\n\t * to evaluate the return value and print an error\n\t * message or add the U+FFFD replacement character.\n\t * then, you'll want to check for\n\t *   remaining == 0\n\t *   out >= 0\n\t */\n\tbool feed(char c);\n\n\t/**\n\t * resets the decoder to its initial state\n\t */\n\tvoid reset();\n};\n\n/**\n * decodes a UTF-8 character string of given length\n *\n * the results are written to outbuf.\n * no leading 0 is written to outbuf (nor is one expected at s).\n * outbuf MUST be large enough to hold all characters.\n * to ensure that outbuf is large enough, it SHOULD have a size of at least len.\n *\n * the number of actual unicode characters is returned.\n * it might be anywhere in the range [len/4; len].\n *\n * in case of decoding errors, the special unicode character 0xfffd is written.\n *\n * code logic gratefully borrowed from rxvt-unicode.\n */\nsize_t utf8_decode(const unsigned char *s, size_t len, codepoint_t *outbuf);\n\n/**\n * encodes one Unicode codepoint to a null-terminated UTF-8 character string.\n * due to the nature of UTF-8, the result string is at most 4 bytes long.\n * on error, the empty string is returned.\n *\n * cp\n *   the codepoint\n * outbuf\n *   a output char buffer. outbuf[0] to outbuf[4] MUST be writeable.\n *   outbuf[0] will ALWAYS be written to.\n * returns\n *   the number of non-NULL bytes that have been written, i.e.\n *   strlen(outbuf)\n */\nsize_t utf8_encode(int cp, char *outbuf);\n\n/**\n * computes the length of the last character in a given UTF-8 string.\n *\n * str\n *   the UTF-8 string\n * returns\n *   the length of the last character in bytes\n */\nsize_t utf8_last_char_size(char *str);\n\n/**\n * pops back (deletes) the last UTF-8 character in a std::string.\n */\nvoid utf8_pop_back(std::string &str);\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/variable.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\nnamespace openage {\nnamespace util {\n\n/**\n * A typed value holder\n */\nclass VariableBase {\npublic:\n\tvirtual ~VariableBase() {}\n\n\t/**\n\t * sets the type and value\n\t */\n\ttemplate <class T, class V>\n\tvoid set(const V &value);\n\n\t/**\n\t * returns the stored value\n\t * throws an exception if the template\n\t * does not match the set type\n\t */\n\ttemplate <class T>\n\tconst T &get() const;\n};\n\n\ntemplate <class T>\nclass Variable : public VariableBase {\npublic:\n\tVariable(const T &initial_value) :\n\t\tvalue(initial_value) {}\n\n\n\tconst T &get() const {\n\t\treturn this->value;\n\t}\n\n\tvoid set(const T &v) {\n\t\tthis->value = v;\n\t}\n\n\t/**\n\t * accessable typed value\n\t */\n\tT value;\n};\n\n\ntemplate <class T>\nconst T &VariableBase::get() const {\n\treturn dynamic_cast<const Variable<T> &>(*this).get();\n}\n\ntemplate <class T, class V>\nvoid VariableBase::set(const V &value) {\n\treturn dynamic_cast<Variable<T> &>(*this).set(value);\n}\n\n\n} // namespace util\n} // namespace openage\n"
  },
  {
    "path": "libopenage/util/vector.cpp",
    "content": "// Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n#include \"vector.h\"\n\nnamespace openage {\nnamespace util {\n\n// Vector is all templated, so there's nothing to implement.\n\n}} // openage::util\n"
  },
  {
    "path": "libopenage/util/vector.h",
    "content": "// Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n#include <algorithm>\n#include <array>\n#include <cmath>\n#include <cstring>\n#include <iostream>\n#include <type_traits>\n\n#include \"../error/error.h\"\n#include \"../log/log.h\"\n\n\nnamespace openage::util {\n\n/**\n * Vector class with arithmetic.\n *\n * N = dimensions\n * T = underlying single value type (double, float, ...)\n *\n * If you change this class, remember to update the gdb pretty printers\n * in etc/gdb_pretty/printers.py.\n */\ntemplate <size_t N, typename T>\nclass Vector : public std::array<T, N> {\npublic:\n\tstatic_assert(N > 0, \"0-dimensional vector not allowed\");\n\n\tusing this_type = Vector<N, T>;\n\n\t/**\n\t * Default comparison epsilon.\n\t */\n\tstatic constexpr T default_eps = 1e-4;\n\n\t/**\n\t * Default, zero-value constructor.\n\t */\n\tVector() {\n\t\tthis->fill(0);\n\t}\n\n\t~Vector() = default;\n\n\t/**\n\t * Constructor for initialisation with N T values\n\t */\n\ttemplate <typename... Ts>\n\tVector(Ts... args) :\n\t\tstd::array<T, N>{static_cast<T>(args)...} {\n\t\tstatic_assert(sizeof...(args) == N, \"not all values supplied.\");\n\t}\n\n\t/**\n\t * Cast every value to NT and return the new Vector.\n\t */\n\ttemplate <typename NT>\n\tVector<N, NT> casted() const {\n\t\tVector<N, NT> ret;\n\t\tstd::copy(std::begin(*this), std::end(*this), std::begin(ret));\n\t\treturn ret;\n\t}\n\n\t/**\n\t * Equality test with given precision.\n\t */\n\tbool equals(const this_type &other, T eps = default_eps) {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\tT diff = std::abs((*this)[i] - other[i]);\n\t\t\tif (diff >= eps) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Vector addition with assignment\n\t */\n\tthis_type &operator+=(const this_type &other) {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\t(*this)[i] += other[i];\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Vector addition\n\t */\n\tthis_type operator+(const this_type &other) const {\n\t\tthis_type res(*this);\n\t\tres += other;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Vector subtraction with assignment\n\t */\n\tthis_type &operator-=(const this_type &other) {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\t(*this)[i] -= other[i];\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Vector subtraction\n\t */\n\tthis_type operator-(const this_type &other) const {\n\t\tthis_type res(*this);\n\t\tres -= other;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Scalar multiplication with assignment\n\t */\n\tthis_type &operator*=(T a) {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\t(*this)[i] *= a;\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Scalar multiplication\n\t */\n\tthis_type operator*(T a) const {\n\t\tthis_type res(*this);\n\t\tres *= a;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Scalar division with assignment\n\t */\n\tthis_type &operator/=(T a) {\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\t(*this)[i] /= a;\n\t\t}\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Scalar division\n\t */\n\tthis_type operator/(T a) const {\n\t\tthis_type res(*this);\n\t\tres /= a;\n\t\treturn res;\n\t}\n\n\t/**\n\t * Dot product of two Vectors\n\t */\n\tT dot(const this_type &other) const {\n\t\tT res = 0;\n\t\tfor (size_t i = 0; i < N; i++) {\n\t\t\tres += (*this)[i] * other[i];\n\t\t}\n\t\treturn res;\n\t}\n\n\t/**\n\t * Euclidian norm aka length\n\t */\n\tT norm() const {\n\t\treturn std::sqrt(this->dot(*this));\n\t}\n\n\t/**\n\t * Scales the Vector so that its norm is 1\n\t */\n\tthis_type &normalize() {\n\t\t*this /= this->norm();\n\t\treturn *this;\n\t}\n\n\t/**\n\t * Cross-product of two 3-dimensional vectors\n\t */\n\ttemplate <typename U = this_type>\n\ttypename std::enable_if<N == 3, U>::type\n\t/*Vector<N>*/\n\tcross_product(const this_type &other) const {\n\t\treturn this_type(\n\t\t\t((*this)[1] * other[2] - (*this)[2] * other[1]),\n\t\t\t((*this)[2] * other[0] - (*this)[0] * other[2]),\n\t\t\t((*this)[0] * other[1] - (*this)[1] * other[0]));\n\t}\n\n\t/**\n\t * Scalar multiplication with swapped arguments\n\t */\n\tfriend this_type operator*(T a, const this_type &v) {\n\t\treturn v * a;\n\t}\n\n\t/**\n\t * Print to output stream using '<<'\n\t */\n\tfriend std::ostream &operator<<(std::ostream &o, const this_type &v) {\n\t\to << \"(\";\n\t\tfor (size_t i = 0; i < N - 1; i++) {\n\t\t\to << v[i] << \", \";\n\t\t}\n\t\to << v[N - 1] << \")\";\n\t\treturn o;\n\t}\n};\n\n\ntemplate <typename T = float>\nusing Vector2t = Vector<2, T>;\n\ntemplate <typename T = float>\nusing Vector3t = Vector<3, T>;\n\ntemplate <typename T = float>\nusing Vector4t = Vector<4, T>;\n\ntemplate <size_t N>\nusing Vectorf = Vector<N, float>;\n\ntemplate <size_t N>\nusing Vectord = Vector<N, double>;\n\nusing Vector2f = Vector<2, float>;\nusing Vector3f = Vector<3, float>;\nusing Vector4f = Vector<4, float>;\n\nusing Vector2d = Vector<2, double>;\nusing Vector3d = Vector<3, double>;\nusing Vector4d = Vector<4, double>;\n\nusing Vector2i = Vector<2, int>;\nusing Vector3i = Vector<3, int>;\nusing Vector4i = Vector<4, int>;\n\nusing Vector2s = Vector<2, size_t>;\nusing Vector3s = Vector<3, size_t>;\nusing Vector4s = Vector<4, size_t>;\n\nusing Vector2ss = Vector<2, ssize_t>;\nusing Vector3ss = Vector<3, ssize_t>;\nusing Vector4ss = Vector<4, ssize_t>;\n\n} // namespace openage::util\n"
  },
  {
    "path": "libopenage/util/vector_test.cpp",
    "content": "// Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n#include \"vector.h\"\n\n#include \"../testing/testing.h\"\n\nnamespace openage {\nnamespace util {\nnamespace tests {\n\nvoid vector() {\n\t{\n\t\t// zero-initialization test.\n\t\tVectorf<5> zero_explicit{0, 0, 0, 0, 0};\n\t\tVectorf<5> zero;\n\n\t\tzero.equals(zero_explicit) or TESTFAIL;\n\t}\n\t{\n\t\t// tests in 2 dimensions.\n\t\t// we want to be able to reuse the variable names later.\n\t\tconst Vectorf<2> a(1.0, 2.0);\n\t\tconst Vectorf<2> b(3.0, 4.0);\n\t\tVectorf<2> c;\n\n\t\t// test basic operators.\n\t\tc = a + b;\n\t\tc.equals({4.0, 6.0}) or TESTFAIL;\n\n\t\tc = a - b;\n\t\tc.equals({-2.0, -2.0}) or TESTFAIL;\n\n\t\tc = 5 * a;\n\t\tc.equals({5.0, 10.0}) or TESTFAIL;\n\n\t\tc = a / 8;\n\t\tc.equals({0.125, 0.25}) or TESTFAIL;\n\n\t\tc.equals({13, 37}) and TESTFAIL;\n\n\t\t// test dot product, norm and normalization.\n\t\tTESTEQUALS_FLOAT(a.dot(b), 11, 1e-7);\n\n\t\tc = b;\n\t\tTESTEQUALS_FLOAT(c.norm(), 5, 1e-7);\n\n\t\tc.normalize();\n\t\tTESTEQUALS_FLOAT(c.norm(), 1, 1e-7);\n\t}\n\n\t{\n\t\t// test for the cross_product\n\t\tconst Vectorf<3> a(1.0, 2.0, 3.0);\n\t\tconst Vectorf<3> b(4.0, 5.0, 6.0);\n\t\tVectorf<3> c = a.cross_product(b);\n\n\t\tc.equals({-3.0, 6.0, -3.0}) or TESTFAIL;\n\t}\n}\n\n}}} // openage::util::tests\n"
  },
  {
    "path": "libopenage/version.cpp.in",
    "content": "// Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#include \"version.h\"\n\nnamespace openage::version {\n\nconst char *const version = \"${VERSION_FULL_STRING}\";\n\n} // openage::version\n"
  },
  {
    "path": "libopenage/version.h.in",
    "content": "// Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#pragma once\n\nnamespace openage::version {\n\nextern const char *const version;\n\n} // openage::version\n"
  },
  {
    "path": "libopenage/versions/CMakeLists.txt",
    "content": "configure_file(compiletime.h.in compiletime.h)\nconfigure_file(compiletime.cpp.in compiletime.cpp)\n\nadd_sources(libopenage\n\t${CMAKE_CURRENT_BINARY_DIR}/compiletime.cpp\n\tversions.cpp\n)\n\npxdgen(\n\tversions.h\n)\n"
  },
  {
    "path": "libopenage/versions/compiletime.cpp.in",
    "content": "// Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#include \"compiletime.h\"\n\nnamespace openage::versions {\n\nconst char *const engine_version = \"${VERSION_FULL_STRING}\";\nconst char *const nyan_version = \"${nyan_VERSION}\";\n\n} // namespace openage::versions\n"
  },
  {
    "path": "libopenage/versions/compiletime.h.in",
    "content": "// Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n// ${AUTOGEN_WARNING}\n\n#pragma once\n\nnamespace openage::versions {\n\nextern const char *const engine_version;\nextern const char *const nyan_version;\n\n} // namespace openage::versions\n"
  },
  {
    "path": "libopenage/versions/versions.cpp",
    "content": "// Copyright 2020-2024 the openage authors. See copying.md for legal info.\n\n#include \"versions.h\"\n\n#ifdef __linux__\n\t#include <gnu/libc-version.h>\n#endif\n\n#include <eigen3/Eigen/Dense>\n#include <epoxy/gl.h>\n#include <harfbuzz/hb.h>\n#include <opus_defines.h>\n#include <QtGlobal>\n#include <sstream>\n\n#include \"../util/strings.h\"\n#include \"versions/compiletime.h\"\n\nnamespace openage::versions {\n\nstd::map<std::string, std::string> get_version_numbers() {\n\tstd::map<std::string, std::string> version_numbers;\n\n\t// Eigen compiletime version number\n\tversion_numbers.emplace(\"Eigen\", util::sformat(\"%d.%d.%d\", EIGEN_WORLD_VERSION, EIGEN_MAJOR_VERSION, EIGEN_MINOR_VERSION));\n\n\t// Harfbuzz compiletime version number\n\tversion_numbers.emplace(\"Harfbuzz\", util::sformat(\"%d.%d.%d\", HB_VERSION_MAJOR, HB_VERSION_MINOR, HB_VERSION_MICRO));\n\n\t// Add Qt version number\n\tversion_numbers.emplace(\"Qt\", QT_VERSION_STR);\n\n\t// Add nyan version number\n\tversion_numbers.emplace(\"nyan\", nyan_version);\n\n\t// Add Opus version number\n\tstd::string opus_version = opus_get_version_string();\n\tversion_numbers.emplace(\"Opus\", opus_version.substr(opus_version.find(' ') + 1));\n\n\t// TODO: Add OpenGL version number\n\n#ifdef __linux__\n\t// Add libc version number if not MacOSX\n\tversion_numbers.emplace(\"libc-runtime\", gnu_get_libc_version());\n\n\tversion_numbers.emplace(\"libc-compile\", util::sformat(\"%d.%d\", __GLIBC__, __GLIBC_MINOR__));\n#endif\n\n#ifdef __APPLE__\n\tversion_numbers.emplace(\"libc-runtime\", \"Apple\");\n#endif\n\n#ifdef __WINDOWS__\n\tversion_numbers.emplace(\"libc-runtime\", \"Windows\");\n#endif\n\n\treturn version_numbers;\n}\n\n} // namespace openage::versions\n"
  },
  {
    "path": "libopenage/versions/versions.h",
    "content": "// Copyright 2020-2024 the openage authors. See copying.md for legal info.\n\n#pragma once\n\n// pxd: from libcpp.map cimport map\n#include <map>\n// pxd: from libcpp.string cimport string\n#include <string>\n\n#include \"../util/compiler.h\"\n\n\nnamespace openage::versions {\n\n/**\n * return a mapping of tool name to tool version,\n * for various components of the engine.\n *\n * pxd:\n * map[string,string] get_version_numbers() except +\n */\nOAAPI std::map<std::string, std::string> get_version_numbers();\n\n} // namespace openage::versions\n"
  },
  {
    "path": "nix/nyan.nix",
    "content": "{ lib\n, stdenv\n, fetchFromGitHub\n, clang\n, cmake\n, flex\n}:\nlet\n  pname = \"nyan\";\n  version = \"0.3\";\nin\nstdenv.mkDerivation\n{\n  inherit pname version;\n\n  src = fetchFromGitHub {\n    owner = \"SFTtech\";\n    repo = pname;\n    rev = \"v${version}\";\n    hash = \"sha256-bjz4aSS5RO+QuLd7kyblTPNcuQFhYK7sW1csNXHL4Qs=\";\n  };\n\n  nativeBuildInputs = [\n    clang\n    cmake\n  ];\n\n  buildInputs = [\n    flex\n  ];\n\n  meta = with lib; {\n    description = \"A data description language\";\n    longDescription = ''\n      Nyan stores hierarchical objects with key-value pairs in a database with the key idea that changes in a parent affect all children. We created nyan because there existed no suitable language to properly represent the enormous complexity of storing the data for openage.\n    '';\n    homepage = \"https://openage.sft.mx\";\n    license = licenses.lgpl3Plus;\n    platforms = platforms.unix;\n    mainProgram = \"nyancat\";\n  };\n}\n\n\n"
  },
  {
    "path": "nix/openage.nix",
    "content": "# This file contains the definition of a nix expression that builds openage.\n# A similar one exists from nyan, the configuration language.\n#\n# The expression in this file is a function (to be called by callPackage)\n# that returns a derivation. callPackage is used inside the flake.nix file.\n# \n# To be compliant with nixpkgs, the dependencies can be passed via function\n# arguments, similarly to how python3 or nyan is used here. The available\n# packages that can be used when building/developing can be found among nixpkgs\n# (https://search.nixos.org/) -as done with python3- or passed from the flake\n# as done with nyan in the flake.nix file.\n{ pkgs\n, lib\n, stdenv\n, fetchFromGitHub\n, nyan\n, python3\n, ...\n}:\nlet\n  pname = \"openage\";\n  version = \"0.5.3\";\n\n  # Python libraries needed at build and run time. This function creates a\n  # python3 package configured with some packages (similar to a python venv).\n  # The current python3 version is used, which is python3.11 at the moment of\n  # writing.\n  # If more packages are needed, they can be added here. The available packages\n  # are listed at\n  # https://search.nixos.org/packages?channel=unstable&query=python311Packages\n  # For example, to add python311Packages.pybind11 to the dependencies, we could\n  # add to this list ps.pybind11\n  pyEnv = python3.withPackages (ps: [\n    ps.mako\n    ps.pillow\n    ps.numpy\n    ps.lz4\n    ps.pygments\n    ps.cython\n    ps.pylint\n    ps.toml\n  ]);\nin\nstdenv.mkDerivation {\n  inherit pname version;\n\n  # This uses the current checked-out repository as source code\n  src = ../.;\n\n  # This fetches the code on github: one would use this when submitting the\n  # package to nixpkgs.\n  # src = fetchFromGitHub {\n  #   owner = \"SFTtech\";\n  #   repo = pname;\n  #   rev = \"v${version}\";\n  #   hash = \"sha256-/ag4U7nnZbvkInnLOZ6cB2VWUb+n/HgM1wpo1y/mUHQ=\";\n  # };\n\n  # Dependencies that are used only at build time\n  nativeBuildInputs = [\n    # This is needed for qt applications\n    pkgs.qt6.wrapQtAppsHook\n    pkgs.toml11\n  ];\n\n  # Dependencies that are used at build and run time\n  buildInputs = with pkgs; [\n    nyan\n    pyEnv\n\n    gcc\n    clang\n    cmake\n    gnumake\n    qt6.full\n\n    eigen\n    libepoxy\n    libogg\n    libpng\n    dejavu_fonts\n    ftgl\n    fontconfig\n    harfbuzz\n    opusfile\n    libopus\n    qt6.qtdeclarative\n    qt6.qtmultimedia\n  ];\n\n  # openage requires access to both python dependencies and openage bindings\n  # Since nix places the binary somewhere in the nix store (/nix/store/blah),\n  # it needs to know where to find the python environment build above. This\n  # is done by setting the PYTHONPATH: the first part contains the path to \n  # the environment, the second part contains the path to the openage library\n  # build with this code.\n  postInstall = ''\n    wrapProgram $out/bin/openage --set PYTHONPATH \"${pyEnv}/${pyEnv.sitePackages}:$out/lib/python3.11/site-packages/\"\n  '';\n\n  # Metadata\n  meta = with lib; {\n    description = \"Free (as in freedom) open source clone of the Age of Empires II engine\";\n    longDescription = ''\n      openage: a volunteer project to create a free engine clone of the Genie Engine used by Age of Empires, Age of Empires II (HD) and Star Wars: Galactic Battlegrounds, comparable to projects like OpenMW, OpenRA, OpenSAGE, OpenTTD and OpenRCT2.\n\n      openage uses the original game assets (such as sounds and graphics), but (for obvious reasons) doesn't ship them\n    '';\n    homepage = \"https://openage.sft.mx\";\n    license = licenses.lgpl3Plus;\n    platforms = platforms.unix;\n  };\n}\n"
  },
  {
    "path": "openage/.gitignore",
    "content": "# generated by cmake\n/config.py\n# generated by cython\n*.cpp\n*.html\n"
  },
  {
    "path": "openage/CMakeLists.txt",
    "content": "# python module configurations\n\n# python config file is created in libopenage\n# in order to get options from libopenage\n\nadd_py_modules(\n\t__init__.py\n\t__main__.py\n\tassets.py\n\t${CMAKE_CURRENT_BINARY_DIR}/config.py\n\tdefault_dirs.py\n\tNOINSTALL devmode.py\n)\n\nadd_cython_modules(\n\tcython_check.pyx\n)\n\nadd_subdirectory(cabextract)\nadd_subdirectory(codegen)\nadd_subdirectory(convert)\nadd_subdirectory(cppinterface)\nadd_subdirectory(cvar)\nadd_subdirectory(event)\nadd_subdirectory(game)\nadd_subdirectory(gamestate)\nadd_subdirectory(log)\nadd_subdirectory(main)\nadd_subdirectory(nyan)\nadd_subdirectory(pathfinding)\nadd_subdirectory(renderer)\nadd_subdirectory(testing)\nadd_subdirectory(util)\nadd_subdirectory(versions)\n"
  },
  {
    "path": "openage/__init__.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThe Python part of openage, a free engine re-write of\nAge of Empires II.\n\nSee https://openage.dev and http://github.com/SFTtech/openage\n\nRequires Python 3.6.\n\"\"\"\n\nfrom sys import version_info as py_version\n\nfrom .log import setup_logging\n\nif py_version < (3, 9):\n    raise RuntimeError(\"openage requires python 3.9 or higher.\")\n\n\ntry:\n    # TODO pylint: disable=wrong-import-position,import-self\n    from . import config\n\nexcept ImportError:\n    VERSION = \"< unknown version; ./configure incomplete >\"\n    LONGVERSION = VERSION\n\nelse:\n    VERSION = config.VERSION\n\n    LONGVERSION = (\n        f\"openage {config.VERSION}\"\n        f\"{' [devmode]' if config.DEVMODE else ''}\\n\"\n        f\"{config.CONFIG_OPTIONS}, ci-cfg {config.CICFGVERSION}\\n\"\n        f\"{config.COMPILER} [{config.COMPILERFLAGS}]\\n\"\n        \"\\n\"\n        \"== Python ==\\n\"\n        f\"Python        {config.PYTHONINTERPRETER}\\n\"\n        f\"Python C API  {config.PYTHONCAPI}\\n\"\n        f\"Cython        {config.CYTHONVERSION}\\n\"\n        f\"Mako          {config.MAKOVERSION}\\n\"\n        f\"NumPy         {config.NUMPYVERSION}\\n\"\n        f\"Pillow        {config.PILVERSION}\\n\"\n        f\"Pygments      {config.PYGMENTSVERSION}\\n\"\n        \"\\n\"\n        \"== C++ ==\"\n    )\n\n\nsetup_logging()\n"
  },
  {
    "path": "openage/__main__.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-statements\n\"\"\"\nBehold: The central entry point for all of openage.\n\nThis module mostly does argparsing.\nSubparsers are initialized by their respective modules.\n\"\"\"\n\nimport argparse\n# TODO remove this once all multiprocessing has been eliminated:\nimport multiprocessing\nimport os\nimport sys\n\nfrom .log import set_loglevel, verbosity_to_level, ENV_VERBOSITY\n\n\ndef print_version():\n    \"\"\"\n    The default version printer, unfortunately, inserts newlines.\n    This is the easiest way around.\n    \"\"\"\n    from . import LONGVERSION\n    print(LONGVERSION)\n    from .versions.versions import get_version_numbers\n    version_numbers = get_version_numbers()\n    for key in version_numbers:\n        print(key.decode(\"utf8\") + \" \" + version_numbers[key].decode(\"utf8\"))\n    sys.exit(0)\n\n\ndef add_dll_search_paths(dll_paths: list[str]):\n    \"\"\"\n    This function adds DLL search paths.\n    \"\"\"\n    from .util.dll import DllDirectoryManager\n\n    manager = DllDirectoryManager(dll_paths)\n\n    return manager\n\n\ndef main(argv=None):\n    \"\"\" Top-level argparsing; invokes subparser for all submodules. \"\"\"\n    cli = argparse.ArgumentParser(\n        \"openage\",\n        description=(\"free age of empires II engine clone\")\n    )\n\n    if sys.platform == 'win32':\n        from .util.dll import default_paths\n        cli.add_argument(\n            \"--add-dll-search-path\", action='append', dest='dll_paths',\n            # use path of current openage executable as default\n            default=default_paths(),\n            help=\"(Windows only) provide additional DLL search path\")\n\n    cli.add_argument(\"--version\", \"-V\", action='store_true', dest='print_version',\n                     help=\"print version info and exit\")\n\n    # shared arguments for all subcommands\n    global_cli = argparse.ArgumentParser(add_help=False)\n    global_cli.add_argument(\"--verbose\", \"-v\", action='count',\n                            default=ENV_VERBOSITY,\n                            help=\"increase verbosity\")\n    global_cli.add_argument(\"--quiet\", \"-q\", action='count', default=0,\n                            help=\"decrease verbosity\")\n    global_cli.add_argument(\"--trap-exceptions\", action=\"store_true\",\n                            help=(\"upon throwing an exception a debug break is \"\n                                  \"triggered. this will crash openage if no \"\n                                  \"debugger is present\"))\n\n    devmodes = global_cli.add_mutually_exclusive_group()\n    devmodes.add_argument(\"--devmode\", action=\"store_true\",\n                          help=\"force-enable development mode\")\n    devmodes.add_argument(\"--no-devmode\", action=\"store_true\",\n                          help=\"force-disable development mode\")\n\n    # shared directory arguments for most subcommands\n    cfg_cli = argparse.ArgumentParser(add_help=False)\n\n    cfg_cli.add_argument(\"--asset-dir\",\n                         help=\"Use this as an additional asset directory.\")\n    cfg_cli.add_argument(\"--cfg-dir\",\n                         help=\"Use this as an additional config directory.\")\n\n    subparsers = cli.add_subparsers(dest=\"subcommand\")\n\n    # enable reimports for \"init_subparser\"\n    # pylint: disable=reimported\n\n    from .main.main import init_subparser\n    main_cli = subparsers.add_parser(\n        \"main\",\n        parents=[global_cli, cfg_cli])\n    init_subparser(main_cli)\n\n    from .game.main import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"game\",\n        parents=[global_cli, cfg_cli]))\n\n    from .testing.main import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"test\",\n        parents=[global_cli, cfg_cli]))\n\n    from .convert.main import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"convert\",\n        parents=[global_cli]))\n\n    from .convert.tool.singlefile import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"convert-file\",\n        parents=[global_cli]))\n\n    from .convert.tool.api_export import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"convert-export-api\",\n        parents=[global_cli]))\n\n    from .codegen.main import init_subparser\n    init_subparser(subparsers.add_parser(\n        \"codegen\",\n        parents=[global_cli]))\n\n    args, remaining_args = cli.parse_known_args(argv)\n\n    dll_manager = None\n    if sys.platform == 'win32':\n        dll_manager = add_dll_search_paths(args.dll_paths)\n        dll_manager.add_directories()\n\n    if args.print_version:\n        print_version()\n\n    if not args.subcommand:\n        # the user didn't specify a subcommand. default to 'main'.\n        args = main_cli.parse_args(remaining_args)\n\n    args.dll_manager = dll_manager\n\n    # process the shared args\n    set_loglevel(verbosity_to_level(args.verbose - args.quiet))\n\n    try:\n        from . import config\n\n        if args.no_devmode:\n            config.DEVMODE = False\n        if args.devmode:\n            config.DEVMODE = True\n            from . import cython_check\n            cython_check.this_is_true()\n    except ImportError:\n        cli.error(\"code was not yet generated. \"\n                  \"Did you run the command from the build directory (bin/)?\\n\"\n                  \"See doc/building.md for more information.\")\n\n    if \"asset_dir\" in args and args.asset_dir:\n        if not os.path.exists(args.asset_dir):\n            cli.error(\"asset directory does not exist: \" + args.asset_dir)\n\n    # call the entry point for the subcommand.\n    return args.entrypoint(args, cli.error)\n\n\nif __name__ == '__main__':\n    # Required for Windows executables (and apparently macOS too)\n    # https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support\n    # https://pyinstaller.org/en/latest/common-issues-and-pitfalls.html#multi-processing\n    multiprocessing.freeze_support()\n\n    # openage is complicated and multithreaded; better not use fork.\n    multiprocessing.set_start_method('spawn')\n\n    sys.exit(main())\n"
  },
  {
    "path": "openage/assets.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCode for locating the game assets.\n\"\"\"\nfrom __future__ import annotations\n\n\nimport typing\nimport os\nfrom pathlib import Path\n\nfrom .util.fslike.directory import Directory\nfrom .util.fslike.union import Union\nfrom .util.fslike.wrapper import WriteBlocker\n\nfrom . import config\nfrom . import default_dirs\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.union import UnionPath\n\n\ndef get_asset_path(custom_asset_dir: str = None) -> UnionPath:\n    \"\"\"\n    Returns a Path object for the game assets.\n\n    `custom_asset_dir` can a custom asset directory, which is mounted at the\n    top of the union filesystem (i.e. has highest priority).\n\n    This function is used by the both the conversion process\n    and the game startup. The conversion uses it for its output,\n    the game as its data source(s).\n    \"\"\"\n\n    # mount the possible locations in an union:\n    result = Union().root\n\n    # if we're in devmode, use only the in-repo asset folder\n    if not custom_asset_dir and config.DEVMODE:\n        result.mount(Directory(os.path.join(config.BUILD_SRC_DIR, \"assets\")).root)\n        return result\n\n    # else overlay the global dir and the user dir.\n\n    # the cmake-determined folder for storing assets\n    global_data = Path(config.GLOBAL_ASSET_DIR)\n    if global_data.is_dir():\n        result.mount(WriteBlocker(Directory(global_data).root).root)\n\n    # user-data directory as provided by environment variables\n    # and platform standards\n    # we always create this!\n    home_data = default_dirs.get_dir(\"data_home\") / \"openage\"\n    result.mount(\n        Directory(\n            home_data,\n            create_if_missing=True\n        ).root / \"assets\"\n    )\n\n    # the program argument overrides it all\n    if custom_asset_dir:\n        result.mount(Directory(custom_asset_dir).root)\n\n    return result\n\n\ndef test():\n    \"\"\"\n    Tests whether a specific asset exists.\n    \"\"\"\n    from .testing.testing import assert_value\n    import argparse\n\n    fakecli = argparse.ArgumentParser()\n    fakecli.add_argument(\"--asset-dir\", default=None)\n    args = fakecli.parse_args([])\n\n    assert_value(get_asset_path(args.asset_dir)['test']['textures']['missing.png'].filesize, 580)\n"
  },
  {
    "path": "openage/cabextract/CMakeLists.txt",
    "content": "add_cython_modules(\n\tlzxd.pyx\n\tcabchecksum.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n\tcab.py\n\tlzxdstream.py\n\ttest.py\n)\n"
  },
  {
    "path": "openage/cabextract/__init__.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThis package contains modules that allow extracting LZX-compressed MSCAB\nfiles (not to be confused with InstallShield CAB files, which are\ntotally different).\n\nIt allows converting media directly from the Age of Empires setup disk.\n\"\"\"\n"
  },
  {
    "path": "openage/cabextract/cab.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides CABFile, an extractor for the MSCAB format.\n\nStruct definitions are according to the documentation at\nhttps://msdn.microsoft.com/en-us/library/bb417343.aspx.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom bisect import bisect\nfrom calendar import timegm\nfrom collections import OrderedDict\nfrom typing import Generator, NoReturn, Union\n\nfrom ..log import dbg\nfrom ..util.filelike.readonly import PosSavingReadOnlyFileLikeObject\nfrom ..util.filelike.stream import StreamFragment\nfrom ..util.files import read_guaranteed, read_nullterminated_string\nfrom ..util.fslike.filecollection import FileCollection, FileEntry\nfrom ..util.math import INF\nfrom ..util.strings import try_decode\nfrom ..util.struct import NamedStruct, Flags\nfrom .cabchecksum import mscab_csum\n\nif typing.TYPE_CHECKING:\n    from openage.util.filelike.abstract import FileLikeObject\n\n\nclass CFHeaderFlags(Flags):\n    \"\"\" Cabinet file option indicators. Found in the header. \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    specstr         = \"H\"\n\n    # this cabfile is not the first of the set.\n    # prev_cab and prev_disk are present after the header.\n    prev_cabinet    = 0\n\n    # this cabfile is not the last of the set.\n    # next_cab and next_disk are present after the header.\n    next_cabinet    = 1\n\n    # this cabinet file has reserved fields.\n    # cbCFHeader, cbCFFolder, and cbCFData are present after the header,\n    # followed by cbCFHeader bytes of reserved data.\n    # if false, cbCFHeader, cbCFFolder, and cbCFData default to 0.\n    reserve_present = 2\n\n\nclass CFHeader(NamedStruct):\n    \"\"\" Global CAB file header; found at the very beginning of the file. \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness      = \"<\"\n\n    signature       = \"4s\"  # magic number: MSCF\n    reserved1       = \"I\"   #\n    cbCabinet       = \"I\"   # size of this cabinet file in bytes\n    reserved2       = \"I\"   #\n    coffFiles       = \"I\"   # absolute offset of the first CFFILE entry\n    reserved3       = \"I\"   #\n    versionMinor    = \"B\"   # cab file format version (minor)\n    versionMajor    = \"B\"   # cab file format version (major)\n    cFolders        = \"H\"   # number of CFFOLDER entries in this cabinet\n    cFiles          = \"H\"   # number of CFFILES entries in this cabinet\n    flags           = CFHeaderFlags\n    setID           = \"H\"   # must be same in all cabinets of a set\n    iCabinet        = \"H\"   # number of this cabinet file in the set\n\n    # those fields are set manually later.\n    reserved_data   = None  # CFHeaderReservedFields\n\n    reserved        = None  # bytes object of size reserved_data.cbCFHeader\n\n    # strings that hold the disk labels/file names where to find the prev/next\n    # CAB files.\n    prev_cab        = None\n    prev_disk       = None\n    next_cab        = None\n    next_disk       = None\n\n\nclass CFHeaderReservedFields(NamedStruct):\n    \"\"\"\n    Optionally found after the header.\n    The fields indicate the size of the reserved data blocks in the header,\n    folder and data structs.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness      = \"<\"\n\n    cbCFHeader      = \"H\"   # size of per-cabinet reserved area\n    cbCFFolder      = \"B\"   # size of per-folder reserved area\n    cbCFData        = \"B\"   # size of per-datablock reserved area\n\n\nclass CFFolder(NamedStruct):\n    \"\"\"\n    CAB folder header; A CAB folder is a data stream consisting of\n    (compressed) concatenated file contents.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness      = \"<\"\n\n    coffCabStart    = \"I\"   # offset of first CFDATA block of this folder.\n    cCFData         = \"H\"   # number of CFDATA blocks.\n\n    typeCompress    = \"H\"   # compression algorithm for this folder.\n\n    # filled in later manually\n    reserved        = None  # bytes object of size reserved_data.cbCFFolder\n\n    comp_name       = None  # human-readable compression name\n    plain_stream    = None  # file-like object for decompressed folder.\n\n\nclass CFFileAttributes(Flags):\n    \"\"\"\n    File flags; found in the CFFile struct.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    specstr         = \"H\"\n\n    rdonly          = 0     # read-only\n    hidden          = 1     # hidden\n    system          = 2     # system file\n    arch            = 5     # archive flag: modified since last backup\n    exec            = 6     # run file after extraction (lol, we won't.)\n    name_is_utf     = 7     # name is UTF-8, not \"current locale\" (8859-1)\n\n\nclass CFFile(NamedStruct):\n    \"\"\"\n    Header for a single file.\n\n    Describes the file's metadata,\n    as well as the location of its content (which CAB folder, at what offset).\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness      = \"<\"\n\n    size            = \"I\"   # uncompressed filesize\n    pos             = \"I\"   # offset of file in uncompressed folder\n\n    # index of the folder that contains this file.\n    # there are several reserved indices with special meanings:\n    #\n    #  0xFFFD continued_from_prev       (acutal id: 0)\n    #  0xFFFE continued_to_next         (acutal id: last)\n    #  0xFFFF continued_prev_and_next:  (actual id: 0)\n    folderid        = \"H\"\n\n    date            = \"H\"   # date stamp ((y–1980) << 9)+(m << 5)+(d) wtf.\n    time            = \"H\"   # time stamp (h << 11)+(m << 5)+(s >> 1) røfl.\n    attribs         = CFFileAttributes\n\n    # filled in later manually\n    path            = None  # array of path parts\n\n    continued       = None  # file continued from previous CAB file\n    continues       = None  # file continues in next CAB file\n\n    folder          = None  # CFFolder object for folderid\n\n    timestamp       = None  # UNIX timestamp\n\n\nclass CFData(NamedStruct):\n    \"\"\"\n    CAB folders are concatenations of data blocks; this is the header\n    of one such data block.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness      = \"<\"\n\n    csum            = \"I\"   # checksum of this block (cbData through payload)\n    cbData          = \"H\"   # number of compressed bytes\n    cbUncomp        = \"H\"   # number of uncompressed bytes\n\n    # filled in later manually\n    reserved        = None  # bytes object of size reserved_data.cbCFData\n    payload         = None  # compressed folder stream data block\n\n    def verify_checksum(self) -> Union[None, NoReturn]:\n        \"\"\"\n        Checks whether csum contains the correct checksum for the block.\n        Raises ValueError otherwise.\n        \"\"\"\n        # the first part of the checksum is simply the checksum of cbData and\n        # cbUncomp, which is simply the little-endian value of their\n        # concatenation.\n        checksum = (self.cbUncomp << 16) | self.cbData\n\n        # the next part of the checksum is from reserved.\n        if self.reserved:\n            checksum ^= mscab_csum(self.reserved)\n\n        # the final part is from the actual data\n        checksum ^= mscab_csum(self.payload)\n\n        if checksum != self.csum:\n            raise ValueError(\"checksum error in MSCAB data block\")\n\n\nclass CABEntry(FileEntry):\n    \"\"\"\n    Entry in a CAB file.\n    \"\"\"\n\n    def __init__(self, fileobj: CFFile):\n        self.fileobj = fileobj\n\n    def open_r(self):\n        return StreamFragment(\n            self.fileobj.folder.plain_stream,\n            self.fileobj.pos,\n            self.fileobj.size\n        )\n\n    def size(self) -> int:\n        return self.fileobj.size\n\n    def mtime(self) -> float:\n        return self.fileobj.timestamp\n\n\nclass CABFile(FileCollection):\n    \"\"\"\n    The actual file system-like CAB object.\n\n    Constructor arguments:\n\n    @param cab:\n        A file-like object that must implement read() and seek() with\n        whence=os.SEEK_SET.\n\n    The constructor reads the entire header, including the folder and file\n    descriptions. Most CAB file issues should cause the constructor to fail.\n    \"\"\"\n\n    def __init__(self, cab: FileLikeObject, offset: int = 0):\n        super().__init__()\n\n        # read header\n        cab.seek(offset)\n        header = CFHeader.read(cab)\n\n        # verify magic number\n        if header.signature != b\"MSCF\":\n            raise SyntaxError(\"invalid CAB file signature: \" +\n                              repr(header.signature))\n\n        # read reserve header, if present\n        if header.flags.reserve_present:\n            header.reserved_data = CFHeaderReservedFields.read(cab)\n        else:\n            header.reserved_data = CFHeaderReservedFields.from_nullbytes()\n\n        # read reserved\n        header.reserved = read_guaranteed(cab, header.reserved_data.cbCFHeader)\n\n        # read previous cabinet info\n        if header.flags.prev_cabinet:\n            header.prev_cab = try_decode(read_nullterminated_string(cab))\n            header.prev_disk = try_decode(read_nullterminated_string(cab))\n\n        # read next cabinet info\n        if header.flags.next_cabinet:\n            header.next_cab = try_decode(read_nullterminated_string(cab))\n            header.next_disk = try_decode(read_nullterminated_string(cab))\n\n        dbg(header)\n        self.header = header\n\n        self.folders = tuple(self.read_folder_headers(cab, offset))\n\n        # {filename: fileobj}, {subdirname: subdir}\n        self.rootdir = OrderedDict(), OrderedDict()\n\n        for fileobj in self.read_file_headers(cab, offset):\n            if self.is_file(fileobj.path) or self.is_dir(fileobj.path):\n                raise ValueError(\n                    \"CABFile has multiple entries with the same path: \" +\n                    b'/'.join(fileobj.path).decode())\n\n            file_entry = CABEntry(fileobj)\n\n            self.add_fileentry(fileobj.path, file_entry)\n\n    def __repr__(self):\n        return \"CABFile\"\n\n    def read_folder_headers(\n        self,\n        cab: FileLikeObject,\n        offset: int\n    ) -> Generator[CFFolder, None, None]:\n        \"\"\"\n        Called during the constructor run.\n\n        Reads the folder headers and initializes the folder's plain stream\n        file-like objects.\n\n        Yields all folders.\n        \"\"\"\n        # read folder headers\n        for _ in range(self.header.cFolders):\n            folder = CFFolder.read(cab)\n\n            # read reserved\n            folder.reserved = read_guaranteed(\n                cab,\n                self.header.reserved_data.cbCFFolder)\n\n            # create compressed data stream\n            compressed_data_stream = CABFolderStream(\n                cab,\n                folder.coffCabStart + offset,\n                folder.cCFData,\n                self.header.reserved_data.cbCFData)\n\n            # determine compression type and create plain data stream\n            compression_type = folder.typeCompress & 0x000f\n\n            if compression_type == 0:\n                folder.comp_name = \"Plain\"\n                folder.plain_stream = compressed_data_stream\n\n            elif compression_type == 1:\n                raise SyntaxError(\"MSZIP compression is unsupported\")\n\n            elif compression_type == 2:\n                raise SyntaxError(\"Quantum compression is unsupported\")\n\n            elif compression_type == 3:\n                window_bits = (folder.typeCompress >> 8) & 0x1f\n                folder.comp_name = f\"LZX (window_bits = {window_bits:d})\"\n\n                from .lzxdstream import LZXDStream\n                from ..util.filelike.stream import StreamSeekBuffer\n\n                unseekable_plain_stream = LZXDStream(\n                    compressed_data_stream,\n                    window_bits=window_bits,\n                    reset_interval=0)\n\n                folder.plain_stream = StreamSeekBuffer(unseekable_plain_stream)\n\n            else:\n                raise SyntaxError(f\"Unknown compression type {compression_type:d}\")\n\n            dbg(folder)\n            yield folder\n\n    def read_file_headers(self, cab: FileLikeObject, offset: int) -> Generator[CFFile, None, None]:\n        \"\"\"\n        Called during the constructor run.\n\n        Reads the headers for all files and yields CFFile objects.\n        \"\"\"\n        # seek to the correct position\n        if cab.tell() != self.header.coffFiles + offset:\n            cab.seek(self.header.coffFiles + offset)\n            dbg(\"cabfile has nonstandard format: seek to header.coffFiles was required\")\n\n        for _ in range(self.header.cFiles):\n            fileobj = CFFile.read(cab)\n\n            # read filename\n            rpath = read_nullterminated_string(cab)\n\n            # decode filename according to flags\n            if fileobj.attribs.name_is_utf:\n                path = rpath.decode('utf-8')\n            else:\n                path = rpath.decode('iso-8859-1')\n\n            fileobj.path = path.replace('\\\\', '/').lower().encode().split(b'/')\n\n            # interpret the special values of folderid\n            if fileobj.folderid == 0xFFFD:\n                fileobj.folderid = 0\n                fileobj.continued = True\n            elif fileobj.folderid == 0xFFFE:\n                fileobj.folderid = len(self.folders) - 1\n                fileobj.continues = True\n            elif fileobj.folderid == 0xFFFF:\n                fileobj.folderid = 0\n                fileobj.continued = True\n                fileobj.continues = True\n\n            fileobj.folder = self.folders[fileobj.folderid]\n\n            # decode file timestamp\n            # beware: reading this may give you internal bleedings.\n            year = (fileobj.date >> 9) + 1980\n            month = (fileobj.date >> 5) & 0x000f\n            day = (fileobj.date >> 0) & 0x001f\n\n            # it's sort of sad that there's no bit for AM/PM.\n            hour = fileobj.time >> 11\n            minute = (fileobj.time >> 5) & 0x003f\n            sec = (fileobj.time << 1) & 0x003f\n\n            # CAB files have no timezone info; assume UTC.\n            fileobj.timestamp = timegm((year, month, day, hour, minute, sec))\n\n            yield fileobj\n\n\nclass CABFolderStream(PosSavingReadOnlyFileLikeObject):\n    \"\"\"\n    Read-only, seekable, file-like stream object that represents\n    a compressed MSCAB folder (MSCAB folders are just compressed streams\n    of concatenated file contents, and are not to be confused with file system\n    folders).\n\n    Constructor arguments:\n\n    @param fileobj:\n        Seekable file-like object that represents the CAB file.\n\n        CABFolderStream explicitly positions the file cursor before every read;\n        this is to make sure that multiple CABFolderStreams can work on the\n        same file, in parallel.\n\n        The file object must implement read() and seek() with SEEK_SET.\n\n    @param offset:\n        Offset of the first of the folder's data blocks in the CAB file.\n\n    @param blockcount:\n        Number of data blocks in the folder.\n    \"\"\"\n\n    def __init__(\n        self,\n        fileobj: FileLikeObject,\n        offset: int,\n        blockcount: int,\n        blockreserved: int\n    ):\n        super().__init__()\n\n        self.fileobj = fileobj\n        self.blockcount = blockcount\n        self.blockreserved = blockreserved\n\n        # positions of the blocks in fileobj. block 0 starts at offset.\n        self.blockoffsets = [offset]\n\n        # positions in the stream of the start of each block.\n        self.streamindex = [0]\n\n    def next_block_size(self, payloadsize: int) -> None:\n        \"\"\"\n        adds metadata for the next block\n        \"\"\"\n        self.blockoffsets.append(self.blockoffsets[-1] +\n                                 CFData.size() + self.blockreserved +\n                                 payloadsize)\n\n        self.streamindex.append(self.streamindex[-1] + payloadsize)\n\n    def read_block_data(self, block_id: int) -> bytes:\n        \"\"\"\n        reads the data of block block_id.\n\n        if necessary, the metadata info in self.blockvalues and\n        self.blockoffsets is updated.\n\n        returns the block data.\n        \"\"\"\n        if block_id >= self.blockcount:\n            raise EOFError()\n\n        while block_id >= len(self.blockoffsets):\n            # We do not yet know where the block starts. Seek forwards.\n\n            # read info for the rightmost known block to get its size.\n            offset = self.blockoffsets[-1]\n            self.fileobj.seek(self.blockoffsets[-1])\n            datablock = CFData.read(self.fileobj)\n\n            # add starting position of next block to metadata.\n            self.next_block_size(datablock.cbData)\n\n        # we now know the starting position of the block.\n        offset = self.blockoffsets[block_id]\n        self.fileobj.seek(offset)\n        datablock = CFData.read(self.fileobj)\n        datablock.reserved = read_guaranteed(self.fileobj, self.blockreserved)\n        datablock.payload = read_guaranteed(self.fileobj, datablock.cbData)\n\n        # verify the datablock's checksum.\n        datablock.verify_checksum()\n\n        # add starting data of next block to metadata, if required.\n        if block_id + 1 == len(self.blockoffsets):\n            self.next_block_size(datablock.cbData)\n\n        # finally, return the data.\n        return datablock.payload\n\n    def read_blocks(self, size: int = -1) -> Generator[bytes, None, None]:\n        \"\"\"\n        Similar to read, bit instead of a single bytes object,\n        returns an iterator of multiple bytes objects, one for each block.\n\n        Used internally be read(), but you may use it directly.\n        \"\"\"\n        if size < 0:\n            size = INF\n\n        # use self.streamindex to determine the block id for pos.\n        blockid = bisect(self.streamindex, self.pos) - 1\n\n        # bytes to discard (because we're still reading data that lie\n        # before self.pos).\n        discard = self.pos - self.streamindex[blockid]\n\n        while size > 0:\n            try:\n                block_data = self.read_block_data(blockid)\n            except EOFError:\n                return\n\n            blockid += 1\n\n            if discard != 0:\n                if discard >= len(block_data):\n                    # we're actually still seeking through the file,\n                    # reading it block by block, approaching the stream cursor\n                    # position.\n\n                    discard -= len(block_data)\n                    continue\n\n                # discard the first few bytes of the block's data.\n                block_data = block_data[discard:]\n                discard = 0\n\n            if len(block_data) > size:\n                # less than the entire block was requested.\n                block_data = block_data[:size]\n\n            size -= len(block_data)\n            self.pos += len(block_data)\n\n            yield block_data\n\n    def read(self, size: int = -1) -> bytes:\n        return b\"\".join(self.read_blocks(size))\n\n    def get_size(self) -> int:\n        del self  # unused\n        return -1\n\n    def close(self) -> None:\n        self.closed = True\n        del self.fileobj\n        del self.blockoffsets\n        del self.streamindex\n"
  },
  {
    "path": "openage/cabextract/cabchecksum.pyx",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nImplements the MSCAB checksum algorithm.\n\nThe algorithm is used for calculating checksums of data blocks, and was\ndesigned by a true genius:\n\nGiven e.g. 11 bytes of data, ABCDEFGHIJK, it creates three four-byte integers\n\nM  L\nS  S\nB  B\n\nDCBA    <- little endian\nHGFE    <- little endian\n IJK    <- big endian\n\nand XORs them.\n\nFor the original doc, see\n\nhttps://msdn.microsoft.com/en-us/library/bb417343.aspx#chksum\n\"\"\"\n\ncdef unsigned int as_little_endian(unsigned char *data) nogil:\n    \"\"\"\n    Given a character pointer, decodes the next four bytes as a little-endian\n    value.\n    \"\"\"\n    return (\n        ((<unsigned int> data[0]) <<  0) |\n        ((<unsigned int> data[1]) <<  8) |\n        ((<unsigned int> data[2]) << 16) |\n        ((<unsigned int> data[3]) << 24))\n\n\ncdef unsigned int as_big_endian(unsigned char *data, unsigned int bytecount) nogil:\n    \"\"\"\n    Given a character pointer, decodes the next bytecount bytes as a\n    big-endian value.\n\n    Won't work properly for bytecount > 4.\n    \"\"\"\n    cdef unsigned int result = 0\n\n    for i in range(bytecount):\n        result <<= 8\n        result |= <unsigned int> data[i]\n\n    return result\n\n\ndef mscab_csum(bytes data):\n    \"\"\"\n    Implements the checksum algorithm that is described in the module doc.\n\n    The for loop gets optimized to C-level performance - wheeee!\n    \"\"\"\n    cdef unsigned int result = 0\n\n    cdef size_t bufsize\n    cdef unsigned char *buf\n    bufsize, buf = len(data), data\n    if buf == NULL:\n        raise Exception(\"invalid data for checksum\")\n\n    cdef unsigned int *data_ptr = <unsigned int *> buf\n\n    cdef unsigned int count = bufsize // 4\n    cdef unsigned int remainder = bufsize % 4\n\n    with nogil:\n        for i in range(count):\n            result ^= data_ptr[i]\n\n    # we have so far ignored endianness issues.\n    # on a non-little endian system, the interpretation is wrong.\n    # thus, interpret it as binary data and decode it as little endian.\n    result = (\n        as_little_endian(<unsigned char *> &result)\n        ^\n        as_big_endian(<unsigned char *> &data_ptr[count], remainder)\n    )\n\n    return result\n"
  },
  {
    "path": "openage/cabextract/gen_test_arc.sh",
    "content": "#!/bin/bash\n# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n# Used to create the test archive file for openage.cabextract.test\n# (this shell script does not actually run at test time;\n#  the tests use a pre-prepared CAB file that was created by this script).\n\ncd `mktemp -d`\n\n# get cabarc.exe tool\necho \"downloading CABARC.EXE\"\ncurl http://download.microsoft.com/download/d/3/8/d38066aa-4e37-4ae8-bce3-a4ce662b2024/WindowsXP-KB838079-SupportTools-ENU.exe > tmp.exe\n7z e tmp.exe support.cab\n7z e support.cab cabarc.exe\n\n# create test files\ndd if=/dev/urandom bs=512k count=1 > testfilea\ndd if=/dev/zero bs=64M count=1 > testfileb\ntouch testfilec\nmkdir -p testdir\necho \"testfiled\" > testdir/testfiled\ncp /bin/bash testdir/testfilee\n\n# compress test files\ntestfiles=\"testfilea testfileb testfilec testdir/testfiled testdir/testfilee\"\necho\nwine cabarc.exe -m LZX:21 n openage_testarc.cab $testfiles\necho\necho \"creating openage_testarc.tar.xz\"\ntar cJvf openage_testarc.tar.xz $testfiles\n\n# show info\necho\necho \"md5sums\"\nmd5sum $testfiles\necho\necho \"sizes\"\nwc -c $testfiles\necho\necho \"archive sizes\"\nwc -c openage_testarc.cab openage_testarc.tar.xz\necho\necho \"folder: `pwd`\"\n\n# clean up\nrm cabarc.exe\nrm tmp.exe\nrm support.cab\n"
  },
  {
    "path": "openage/cabextract/lzxd.pyx",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\nfrom libc.string cimport memcpy\nfrom libcpp cimport bool\n\nfrom cpython.ref cimport PyObject\nfrom cpython.bytes cimport PyBytes_FromStringAndSize\n\nfrom libopenage.util.compress.lzxd cimport (\n    LZXDecompressor as c_LZXDecompressor,\n    LZX_FRAME_SIZE\n)\n\nfrom libopenage.pyinterface.functional cimport Func2\n\nfrom openage.cppinterface.typedefs cimport voidptr\n\n\ncdef class LZXDecompressor:\n    \"\"\"\n    Decompresses an LZX-compressed stream.\n\n    Its constructor takes a callback method that shall provide the compressed\n    data stream.\n\n    decompress_next_frame() decodes the next frame and returns the\n    decompressed data as a bytes object.\n\n    Constructor arguments:\n\n    @param py_read_callback\n        The read callback method. On EOF, b\"\" should be returned.\n        Otherwise, at least one and at most the number of requested bytes\n        should be returned.\n    @param window_bits\n        The LZX window size; for MSCAB archives, this is indicated in the\n        CAB file header.\n    @param reset_interval\n        The LZX stream reset interval; for MSCAB archives, this is 0.\n    \"\"\"\n\n    \"\"\" The C++ object. \"\"\"\n    cdef c_LZXDecompressor *thisptr\n\n    # \"public\" makes those fields available from python (__getattr__).\n\n    \"\"\" The python read callback function. \"\"\"\n    cdef public object py_read_callback\n\n    \"\"\"\n    If a C function throws an exception, the c_LZXDecompressor object\n    becomes invalid (using it again is undefined behavior).\n    \"\"\"\n    cdef public bool invalid\n\n    @staticmethod\n    cdef size_t read_callback(void *context, unsigned char *buf, size_t size) except * with gil:\n        \"\"\"\n        Read callback function that's passed to c_LZXDecompressor.\n\n        context is a pointer to this LZXDecompressor object; the callback\n        uses it to determine the py_read_callback function.\n        \"\"\"\n        cdef object lzxdecompressor_obj = <object> <PyObject *> context\n        cdef bytes result\n\n        result = lzxdecompressor_obj.py_read_callback(size)\n\n        cdef size_t resultsize\n        cdef unsigned char *resultbuf\n        resultsize, resultbuf = len(result), <unsigned char *> result\n\n        if resultsize > size:\n            raise Exception(\n                \"read callback returned more data then requested.\")\n\n        if resultbuf == NULL:\n            raise Exception(\n                \"internal error in read callback\")\n\n        memcpy(buf, resultbuf, resultsize)\n        return resultsize\n\n    def __cinit__(self, py_read_callback,\n                  unsigned int window_bits=21,\n                  unsigned int reset_interval=0):\n\n        \"\"\" Invokes the C++ constructor. \"\"\"\n\n        self.invalid = False\n        self.py_read_callback = py_read_callback\n\n        cdef Func2[size_t, unsigned char *, size_t] read_callback\n        read_callback.bind1[voidptr](self.read_callback, <void *> <PyObject *> self)\n\n        with nogil:\n            self.thisptr = new c_LZXDecompressor(\n                read_callback, window_bits, reset_interval)\n\n    def __dealloc__(self):\n        with nogil:\n            del self.thisptr\n\n    def decompress_next_frame(self):\n        \"\"\"\n        Decodes the next frame of the stream\n\n        @returns\n            b\"\" on EOF,\n            a bytes object between 1 and LZX_FRAME_SIZE bytes in size\n            otherwise.\n        \"\"\"\n        if self.invalid:\n            raise Exception(\"LZXDecompressor has been invalidated by a \"\n                            \"previous error.\")\n\n        # alloc the 32-KiB frame buffer\n        cdef bytes result = PyBytes_FromStringAndSize(NULL, LZX_FRAME_SIZE)\n        cdef unsigned char *result_buf = result\n\n        # call the C method to fill the frame buffer\n        cdef unsigned int frame_size\n        try:\n            with nogil:\n                frame_size = self.thisptr.decompress_next_frame(result_buf)\n        except:\n            # set self.invalid to make sure we'll never call the decompressor\n            # again (as that would be undefined behavior now).\n            self.invalid = True\n            raise\n\n        # performance optimization:\n        # all but the last frame have this size.\n        if frame_size == LZX_FRAME_SIZE:\n            return result\n\n        # the last frame will have some non-zero size.\n        # EOF is indicated by a zero return value (so we'll return b\"\").\n        return result[:frame_size]\n"
  },
  {
    "path": "openage/cabextract/lzxdstream.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nWraps the LZXDecompressor in a file-like, read-only stream object.\n\"\"\"\n\nimport os\nfrom io import UnsupportedOperation\nfrom typing import NoReturn\n\nfrom ..util.filelike.readonly import ReadOnlyFileLikeObject\nfrom ..util.bytequeue import ByteQueue\nfrom ..util.math import INF\n\nfrom .lzxd import LZXDecompressor\n\n\nclass LZXDStream(ReadOnlyFileLikeObject):\n    \"\"\"\n    Read-only stream object that wraps LZXDecompressor.\n\n    Constructor arguments:\n\n    @param compressed_file\n        The compressed file-like object; must implement only read().\n        If seek(0) works on it, reset() works on this object.\n\n    @param window_bits\n        Provided as metadata in MSCAB files; see LZXDecompressor.\n\n        Defaults to 21.\n\n    @param reset_interval\n        Zero for MSCAB files; see LZXDecompressor.\n\n        Theoretically, if reset_interval > 0, efficient seek() could\n        be implemented. However, it isn't.\n\n        Defaults to 0.\n    \"\"\"\n\n    def __init__(self, sourcestream, window_bits=21, reset_interval=0):\n        super().__init__()\n\n        self.sourcestream = sourcestream\n        self.window_bits = window_bits\n        self.reset_interval = reset_interval\n\n        # position in the decompressed output stream.\n        self.pos = None\n        self.buf = None\n\n        self.reset()\n\n    def reset(self) -> None:\n        \"\"\"\n        Resets the decompressor back to the start of the file.\n        \"\"\"\n        self.sourcestream.seek(0)\n\n        self.decompressor = LZXDecompressor(self.sourcestream.read,\n                                            self.window_bits,\n                                            self.reset_interval)\n\n        self.pos = 0\n        self.buf = ByteQueue()\n\n    def read(self, size: int = -1) -> bytes:\n        if size < 0:\n            size = INF\n\n        while len(self.buf) < size:\n            data = self.decompressor.decompress_next_frame()\n            if not data:\n                # EOF; return all we have\n                return self.buf.popleft(len(self.buf))\n\n            self.buf.append(data)\n\n        return self.buf.popleft(size)\n\n    def get_size(self) -> int:\n        del self  # unused\n        # size is unknown in advance\n        return -1\n\n    def seek(self, offset: int, whence=os.SEEK_SET) -> NoReturn:\n        del offset, whence  # unused\n        raise UnsupportedOperation(\"Cannot seek in LZXDStream.\")\n\n    def seekable(self) -> bool:\n        return False\n\n    def tell(self) -> int:\n        return self.pos\n\n    def close(self) -> None:\n        self.closed = True\n        del self.decompressor\n        del self.sourcestream\n"
  },
  {
    "path": "openage/cabextract/test.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\"\"\"\nDownloads the SFT test cab archive and uses it to test the cabextract code.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport os\nfrom tempfile import gettempdir\nfrom hashlib import md5\nfrom urllib.request import urlopen\n\nfrom .cab import CABFile\n\nif typing.TYPE_CHECKING:\n    from io import BufferedReader\n\n# the test archive file has been generated using ./gen_test_arc.sh\n\n# filesize of the test archive file\nTEST_ARCHIVE_SIZE = 1057766\n\n# URL of the test archive file\nTEST_ARCHIVE_URL = \"http://pub.sft.mx/openage/openage_testarc.cab\"\n\n# list of all files in the test archive, with (md5sum, size).\nTEST_FILES = {\n    \"testfilea\": (\"68fe7d874c7887bdd3f6aaca46abd306\", 524288),\n    \"testfileb\": (\"7f614da9329cd3aebf59b91aadc30bf0\", 67108864),\n    \"testfilec\": (\"d41d8cd98f00b204e9800998ecf8427e\", 0),\n    \"testdir/testfiled\": (\"6f75302850b485421030f034fe67380b\", 10),\n    \"testdir/testfilee\": (\"65116b303f205837d280d3c08b1e0419\", 1033720)\n}\n\n# local cache filename\nTEST_ARCHIVE_FILENAME = os.path.join(gettempdir(), \"openage_testarc.cab\")\n\n\ndef open_cached_test_archive():\n    \"\"\"\n    Opens the cached test archive file.\n    \"\"\"\n    if os.path.getsize(TEST_ARCHIVE_FILENAME) != TEST_ARCHIVE_SIZE:\n        raise OSError(\"test archive has wrong size\")\n\n    return open(TEST_ARCHIVE_FILENAME, 'rb')\n\n\ndef open_test_archive() -> BufferedReader:\n    \"\"\"\n    Opens the cached test archive file, or downloads it if necessary.\n    \"\"\"\n    try:\n        return open_cached_test_archive()\n    except OSError:\n        pass\n\n    with open(TEST_ARCHIVE_FILENAME, 'wb') as test_arc:\n        with urlopen(TEST_ARCHIVE_URL) as url:\n            test_arc.write(url.read())\n\n    return open_cached_test_archive()\n\n\ndef test():\n    \"\"\"\n    The actual test function; registered in openage.testing.testlist.\n    \"\"\"\n    from ..testing.testing import assert_value, assert_raises, result\n\n    # acquire the actual test archive file and create the CABFile Path object\n    cab = CABFile(open_test_archive()).root\n\n    testdir = cab[\"..////./../testdir\"]\n    nonexistingdir = cab[\"nonexistingdir\"]\n    nonexistingfile = cab[\"nonexistingfile\"]\n    testfilea = cab[\"testfilea\"]\n\n    # list dir\n    assert_value(\n        list(cab.list()),\n        [b\"testdir\", b\"testfilea\", b\"testfileb\", b\"testfilec\"]\n    )\n\n    # list subdir\n    assert_value(\n        list(testdir.list()),\n        [b\"testfiled\", b\"testfilee\"]\n    )\n\n    # list nonexisting dir\n    with assert_raises(FileNotFoundError):\n        result(tuple(nonexistingdir.iterdir()))\n\n    # filesize for nonexisting file\n    with assert_raises(FileNotFoundError):\n        result(nonexistingfile.filesize)\n\n    # filesize for dir\n    with assert_raises(IsADirectoryError):\n        result(testdir.filesize)\n\n    # mtime for a non-existing file\n    with assert_raises(FileNotFoundError):\n        result(nonexistingfile.mtime)\n\n    # attempt getting mtime for a directory\n    with assert_raises(IsADirectoryError):\n        result(testdir.mtime)\n\n    # attempt getting mtime for a file\n    assert_value(testfilea.mtime, 1430844692)\n\n    # open nonexisting file\n    with assert_raises(FileNotFoundError):\n        result(nonexistingfile.open('rb'))\n\n    # open existing file\n    testfiled = cab.joinpath('///testdir//.//testfiled').open('rb')\n    assert_value(testfiled, validator=bool)\n\n    # seek around and read a bit\n    testfiled.seek(3)\n    assert_value(testfiled.read(4), b\"tfil\")\n    testfiled.seek(-1, os.SEEK_CUR)\n    assert_value(testfiled.tell(), 6)\n\n    with assert_raises(ValueError):\n        result(testfiled.seek(-8, os.SEEK_CUR))\n\n    testfiled.seek(-4, os.SEEK_END)\n    assert_value(testfiled.tell(), 6)\n    assert_value(testfiled.read(5), b\"led\\n\")\n\n    # test filesize and md5sum for all files\n    for filename, (md5sum, size) in TEST_FILES.items():\n        path = cab[filename]\n\n        assert_value(md5(path.open('rb').read()).hexdigest(), md5sum)\n        assert_value(path.filesize, size)\n"
  },
  {
    "path": "openage/codegen/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tcodegen.py\n\tcoord.py\n\tcpp_testlist.py\n\tlisting.py\n\tmain.py\n)\n"
  },
  {
    "path": "openage/codegen/__init__.py",
    "content": "# Copyright 2013-2015 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPackage for C++ code generation, as part of the openage build process.\n\"\"\"\n"
  },
  {
    "path": "openage/codegen/codegen.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nUtility and driver module for C++ code generation.\n\"\"\"\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom io import UnsupportedOperation\nfrom itertools import chain\nimport os\nfrom sys import modules\nimport sys\nfrom typing import Generator\n\nfrom ..log import err\nfrom ..util.filelike.fifo import FIFO\nfrom ..util.fslike.directory import Directory\nfrom ..util.fslike.wrapper import Wrapper\nfrom .listing import generate_all\n\n\nclass CodegenMode(Enum):\n    \"\"\"\n    Modus operandi\n    \"\"\"\n\n    # pylint doesn't understand that this Enum doesn't require member methods.\n    # pylint: disable=too-few-public-methods\n\n    # source files are created regularily\n    CODEGEN = \"codegen\"\n\n    # caches are updated, but no source files are created\n    DRYRUN = \"dryrun\"\n\n    # files are deleted\n    CLEAN = \"clean\"\n\n\nclass WriteCatcher(FIFO):\n    \"\"\"\n    Behaves like FIFO, but close() is converted to seteof(),\n    and read() fails if eof is not set.\n    \"\"\"\n\n    def close(self) -> None:\n        self.eof = True\n\n    def read(self, size: int = -1) -> bytes:\n        if not self.eof:\n            raise UnsupportedOperation(\n                \"can not read from WriteCatcher while not closed for writing\")\n        return super().read(size)\n\n\nclass CodegenDirWrapper(Wrapper):\n    \"\"\"\n    Only allows pure-read and pure-write operations;\n\n    Intercepts all writes for later inspection, and logs all reads.\n\n    The constructor takes the to-be-wrapped fslike object.\n    \"\"\"\n\n    def __init__(self, obj):\n        super().__init__(obj)\n\n        # stores tuples (parts, intercept_obj), where intercept_obj is a FIFO.\n        self.writes = []\n\n        # stores a list of parts.\n        self.reads = []\n\n    def open_r(self, parts):\n        self.reads.append(parts)\n        return super().open_r(parts)\n\n    def open_w(self, parts):\n        intercept_obj = WriteCatcher()\n        self.writes.append((parts, intercept_obj))\n        return intercept_obj\n\n    def get_reads(self) -> None:\n        \"\"\"\n        Returns an iterable of all path component tuples for files that have\n        been read.\n        \"\"\"\n        for parts in self.reads:\n            yield parts\n\n        self.reads.clear()\n\n    def get_writes(self) -> None:\n        \"\"\"\n        Returns an iterable of all (path components, data_written) tuples for\n        files that have been written.\n        \"\"\"\n        for parts, intercept_obj in self.writes:\n            yield parts, intercept_obj.read()\n\n        self.writes.clear()\n\n    def __repr__(self):\n        return f\"CodegenDirWrapper({repr(self.obj)})\"\n\n\ndef codegen(mode: CodegenMode, input_dir: str, output_dir: str) -> tuple[list[str], list[str]]:\n    \"\"\"\n    Calls .listing.generate_all(), and post-processes the generated\n    data, checking them and adding a header.\n    Reads the input templates relative to input_dir.\n    Writes them to output_dir according to mode. output_dir is a path or str.\n\n    Returns ({generated}, {depends}), where\n    generated is a list of (absolute) filenames of generated files, and\n    depends is a list of (absolute) filenames of dependency files.\n    \"\"\"\n    input_dir = Directory(input_dir).root\n    output_dir = Directory(output_dir).root\n\n    # this wrapper intercepts all writes and logs all reads.\n    wrapper = CodegenDirWrapper(input_dir)\n    generate_all(wrapper.root)\n\n    # set of all generated filenames\n    generated = set()\n\n    for parts, data in wrapper.get_writes():\n        # TODO: this assumes output_dir is a fslike.Directory!\n        generated.add(output_dir.fsobj.resolve(parts))\n\n        # now, actually perform the generation.\n        # first, assemble the path for the current file\n        wpath = output_dir[parts]\n\n        data = postprocess_write(parts, data)\n\n        if mode == CodegenMode.CODEGEN:\n            # skip writing if the file already has that exact content\n            try:\n                with wpath.open('rb') as outfile:\n                    if outfile.read() == data:\n                        continue\n            except FileNotFoundError:\n                pass\n\n            # write new content to file\n            wpath.parent.mkdirs()\n            with wpath.open('wb') as outfile:\n                print(f\"\\x1b[36mcodegen: {b'/'.join(parts).decode(errors='replace')}\\x1b[0m\")\n                outfile.write(data)\n\n        elif mode == CodegenMode.DRYRUN:\n            # no-op\n            pass\n\n        elif mode == CodegenMode.CLEAN:\n            if wpath.is_file():\n                print(b'/'.join(parts).decode(errors='replace'))\n                wpath.unlink()\n        else:\n            err(\"unknown codegen mode: %s\", mode)\n            sys.exit(1)\n\n    generated = {os.path.realpath(path).decode() for path in generated}\n    depends = {os.path.realpath(path) for path in get_codegen_depends(wrapper)}\n\n    return generated, depends\n\n\ndef depend_module_blacklist():\n    \"\"\"\n    Yields all modules whose source files shall explicitly not appear in the\n    dependency list, even if they have been imported.\n    \"\"\"\n    # openage.config is created only after the first run of cmake,\n    # thus, the depends list will change at the second run of codegen,\n    # re-triggering cmake.\n    try:\n        import openage.config\n        yield openage.config\n    except ImportError:\n        pass\n\n    # devmode is imported by config, so the same reason as above applies.\n    try:\n        import openage.devmode\n        yield openage.devmode\n    except ImportError:\n        pass\n\n\ndef get_codegen_depends(outputwrapper: CodegenDirWrapper) -> Generator[str, None, None]:\n    \"\"\"\n    Yields all codegen dependencies.\n\n    outputwrapper is the CodegenDirWrapper that was passed to generate_all;\n    it's used to determine the files that have been read.\n\n    In addition, all imported python modules are yielded.\n    \"\"\"\n    # add all files that have been read as depends\n    for parts in outputwrapper.get_reads():\n        # TODO: this assumes that the wrap.obj.fsobj is a fslike.Directory\n        # this just resolves paths to the output directory\n        yield outputwrapper.obj.fsobj.resolve(parts).decode()\n\n    module_blacklist = set(depend_module_blacklist())\n\n    # add all source files that have been loaded as depends\n    for module in modules.values():\n        if module in module_blacklist:\n            continue\n\n        try:\n            filename = module.__file__\n        except AttributeError:\n            # built-in modules don't have __file__, we don't want those as\n            # depends.\n            continue\n\n        if filename is None:\n            # some modules have __file__ == None, we don't want those either.\n            continue\n\n        if module.__package__ == '':\n            continue\n\n        if not filename.endswith('.py'):\n            # This usually means that some .so file is imported as module.\n            # This is not a problem as long as it's not \"our\" .so file.\n            # => just handle non-openage non-.py files normally\n\n            if 'openage' in module.__name__:\n                print(\"codegeneration depends on non-.py module \" + filename)\n                sys.exit(1)\n\n        yield filename\n\n\ndef get_header_lines() -> Generator[str, None, None]:\n    \"\"\"\n    Yields the lines for the automatically-added file header.\n    \"\"\"\n\n    yield (\n        f\"Copyright 2013-{datetime.now().year} the openage authors. \"\n        \"See copying.md for legal info.\"\n    )\n\n    yield \"\"\n    yield \"Warning: this file was auto-generated; manual changes are futile.\"\n    yield \"For details, see buildsystem/codegen.cmake and openage/codegen.\"\n    yield \"\"\n\n\ndef postprocess_write(parts, data: str) -> str:\n    \"\"\"\n    Post-processes a single write operation, as intercepted during codegen.\n    \"\"\"\n    # test whether filename starts with 'libopenage/'\n    if parts[0] != b\"libopenage\":\n        raise ValueError(\"Not in libopenage source directory\")\n\n    # test whether filename matches the pattern *.gen.*\n    name, extension = os.path.splitext(parts[-1].decode())\n    if not name.endswith('.gen'):\n        raise ValueError(\"Doesn't match required filename format .gen.SUFFIX\")\n\n    # check file extension, and use the appropriate comment prefix\n    if extension in {'.h', '.cpp'}:\n        comment_prefix = '//'\n    else:\n        raise ValueError(\"Extension not in {.h, .cpp}\")\n\n    datalines = data.decode('ascii').split('\\n')\n    if 'Copyright' in datalines[0]:\n        datalines = datalines[1:]\n\n    headerlines = []\n    for line in get_header_lines():\n        if line:\n            headerlines.append(comment_prefix + \" \" + line)\n        else:\n            headerlines.append(\"\")\n\n    return '\\n'.join(chain(headerlines, datalines)).encode('ascii')\n"
  },
  {
    "path": "openage/codegen/coord.py",
    "content": "# Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nGenerates libopenage/coord/coord_{xy, xyz, ne_se, ne_se_up}.{h, cpp}\n\"\"\"\n\nfrom mako.template import Template\n\n\ndef generate_coord_basetypes(projectdir):\n    \"\"\"\n    Generates the test/demo method symbol lookup file from tests_cpp.\n\n    projectdir is a util.fslike.path.Path.\n    \"\"\"\n    # pylint: disable=cell-var-from-loop\n    # this list contains all required member lists.\n    member_lists = [\n        [\"x\", \"y\"],\n        [\"x\", \"y\", \"z\"],\n        [\"ne\", \"se\"],\n        [\"ne\", \"se\", \"up\"]\n    ]\n\n    # this list maps template file name to output file name.\n    # the output filename is a mako template itself.\n    template_files_spec = [\n        (\"libopenage/coord/coord.h.template\",\n         \"libopenage/coord/coord_${''.join(members)}.gen.h\"),\n        (\"libopenage/coord/coord.cpp.template\",\n         \"libopenage/coord/coord_${''.join(members)}.gen.cpp\")\n    ]\n\n    templates = []\n    for template_filename, output_filename in template_files_spec:\n        with projectdir.joinpath(template_filename).open() as template_file:\n            templates.append((\n                Template(template_file.read()),\n                Template(output_filename)\n            ))\n\n    for member_list in member_lists:\n        def format_members(formatstring, join_with=\", \"):\n            \"\"\"\n            For being called by the template engine.\n\n            >>> format_members(\"{0} = {0}\")\n            \"x = x, y = y\"\n            \"\"\"\n            return join_with.join(formatstring.format(m) for m in member_list)\n\n        template_dict = {\n            \"members\": member_list,\n            \"formatted_members\": format_members,\n            \"camelcase\": \"\".join(member.title() for member in member_list),\n        }\n\n        for template, output_filename_template in templates:\n            output_filename = output_filename_template.render(**template_dict)\n            with projectdir.joinpath(output_filename).open(\"w\") as output_file:\n                output = template.render(**template_dict)\n                output_file.write(output)\n                if not output.endswith('\\n'):\n                    output_file.write('\\n')\n"
  },
  {
    "path": "openage/codegen/cpp_testlist.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nGenerates code for C++ testing, mostly the table to look up symbols from test\nnames.\n\"\"\"\n\nimport collections\n\n\nclass Namespace:\n    \"\"\"\n    Represents a C++ namespace, which contains other namespaces and functions.\n\n    gen_prototypes() generates the code for the namespace.\n    \"\"\"\n\n    def __init__(self):\n        self.namespaces = collections.defaultdict(self.__class__)\n        self.functions = []\n\n    def add_functionname(self, path):\n        \"\"\"\n        Adds a function to the namespace.\n\n        Path is the qualified function \"path\" (e.g., openage::test::foo)\n        has the path [\"openage\", \"test\", \"foo\"].\n\n        Descends recursively, creating subnamespaces as required.\n        \"\"\"\n        if len(path) == 1:\n            self.functions.append(path[0])\n        else:\n            subnamespace = self.namespaces[path[0]]\n            subnamespace.add_functionname(path[1:])\n\n    def gen_prototypes(self):\n        \"\"\"\n        Generates the actual C++ code for this namespace,\n        including all sub-namespaces and function prototypes.\n        \"\"\"\n        for name in self.functions:\n            yield f\"void {name}();\\n\"\n\n        for namespacename, namespace in sorted(self.namespaces.items()):\n            yield f\"namespace {namespacename} {{\\n\"\n            for line in namespace.gen_prototypes():\n                yield line\n            yield f\"}} // {namespacename}\\n\\n\"\n\n    def get_functionnames(self):\n        \"\"\"\n        Yields all function names in this namespace,\n        as well as all subnamespaces.\n        \"\"\"\n        for name in self.functions:\n            yield name\n\n        for namespacename, namespace in sorted(self.namespaces.items()):\n            for name in namespace.get_functionnames():\n                yield namespacename + \"::\" + name\n\n\ndef generate_testlist(projectdir):\n    \"\"\"\n    Generates the test/demo method symbol lookup file from tests_cpp.\n\n    projectdir is a util.fslike.path.Path.\n    \"\"\"\n    root_namespace = Namespace()\n\n    from ..testing.list_processor import list_targets_cpp\n\n    for testname, _, _, _ in list_targets_cpp():\n        root_namespace.add_functionname(testname.split('::'))\n\n    func_prototypes = list(root_namespace.gen_prototypes())\n\n    method_mappings = [\n        f\"{{\\\"{functionname}\\\", ::{functionname}}}\"\n        for functionname in root_namespace.get_functionnames()\n    ]\n\n    tmpl_path = projectdir.joinpath(\"libopenage/testing/testlist.cpp.template\")\n    with tmpl_path.open() as tmpl:\n        content = tmpl.read()\n\n    content = content.replace('FUNCTION_PROTOTYPES', \"\".join(func_prototypes))\n    content = content.replace('METHOD_MAPPINGS', \",\\n\\t\".join(method_mappings))\n\n    gen_path = projectdir.joinpath(\"libopenage/testing/testlist.gen.cpp\")\n    with gen_path.open(\"w\") as gen:\n        gen.write(content)\n"
  },
  {
    "path": "openage/codegen/listing.py",
    "content": "# Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains the listing of all code generator invocations.\n\"\"\"\n\n\ndef generate_all(projectdir):\n    \"\"\"\n    Generates all source files in targetdir.\n    \"\"\"\n    from .cpp_testlist import generate_testlist\n    generate_testlist(projectdir)\n\n    from .coord import generate_coord_basetypes\n    generate_coord_basetypes(projectdir)\n"
  },
  {
    "path": "openage/codegen/main.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCodegen interface to the build system.\n\nSee buildsystem/codegen.cmake.\n\nGenerates c++ code within the source tree.\nDesigned to be used by [buildsystem/codegen.cmake].\n\nAll file and directory names should be absolute;\notherwise, assumptions made by this script or the cmake script might\nnot be fulfilled.\n\nInvocation synopsis:\n\npython3 -m openage codegen\n\n    (mandatory)\n\n    --generated-list-file\n    --depend-list-file\n    --project-dir\n    --mode in {clean, dryrun, codegen}\n\n    (optional)\n\n    --touch-file-on-cache-change (CMakeLists.txt)\n    --force-rerun-on-generated-list-change (bool)\n\nall file and directory names SHOULD be absolute paths.\nthis is not enforced, but relative paths may violate assumptions made by\ncodegen.cmake.\n\nfor each invocation, all code generation is performed, and the generated\nsourcefiles are stored in an internal dict.\n\nin addition, text data is written to the specified cache files:\n\n- a list of all generated files (targets) to target-cache\n- a list of all loaded python module files to depend-cache\n\ndepending on the specified invocation commands,\n\n- generated sources are written to the source dir\n- generated sources are cleaned\n- cmake re-builds are triggered if a cache has changed\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nimport os\nimport sys\n\nfrom .codegen import CodegenMode, codegen\n\nif typing.TYPE_CHECKING:\n    from argparse import ArgumentParser\n\n\ndef init_subparser(cli: ArgumentParser):\n    \"\"\" Codegen-specific CLI. \"\"\"\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\n        \"--input-dir\", required=True,\n        help=(\"the directory to read inputs from.\"\n              \"this is the usually the repository root.\"))\n\n    cli.add_argument(\n        \"--output-dir\", required=True,\n        help=(\"the directory to produce outputs in.\"\n              \"this is the directory corresponding to the repository root \"\n              \"which is inferred using current working directory.\"))\n\n    cli.add_argument(\n        \"--generated-list-file\", required=True,\n        help=(\"filename for target cache. a list of all generated sources \"\n              \"is written there during each invocation. \"\n              \"if the list changes, --touch-file-on-cache-change and \"\n              \"--force-rerun-on-generated-list-change trigger cmake re-runs.\"))\n\n    cli.add_argument(\n        \"--depend-list-file\", required=True,\n        help=(\"filename for dependency cache. a list of all python files and \"\n              \"other resources that were used during source generation is \"\n              \"stored here. \"\n              \"if the list changes, --touch-file-on-cache-change will trigger \"\n              \"cmake re-runs\"))\n\n    cli.add_argument(\n        \"--mode\", required=True,\n        help=(\"operating mode; must be one of {'codegen', 'dryrun', 'clean'}. \"\n              \"in dry run mode, only the caches are updated. \"\n              \"in codegen mode, all source files are created. \"\n              \"in clean mode, the source files are deleted instead.\"))\n\n    cli.add_argument(\n        \"--touch-file-on-cache-change\", dest=\"file_to_touch\",\n        help=(\"the file passed here is touched if one of the caches changes. \"\n              \"designed for use with a CMakeLists.txt file, to trigger cmake \"\n              \"re-runs.\"))\n\n    cli.add_argument(\n        \"--force-rerun-on-generated-list-change\",\n        action=\"store_true\",\n        help=(\"more drastic than --touch-file-on-cache-change, this causes \"\n              \"codegen to abort with an error message if the target cache has \"\n              \"changed.\"))\n\n\ndef main(args, error):\n    \"\"\" Codegen CLI entry point. \"\"\"\n\n    if args.file_to_touch and not os.path.isfile(args.file_to_touch):\n        error(\"file doesn't exist: \" + args.file_to_touch)\n\n    def read_cache_file_lines(filename):\n        \"\"\"\n        Yields all lines from the file.\n        If the file doesn't exist, a warning is printed.\n        \"\"\"\n        try:\n            with open(filename, encoding='utf8') as fileobj:\n                for line in fileobj:\n                    line = line.strip()\n                    if line:\n                        yield line\n        except FileNotFoundError:\n            if args.file_to_touch or args.force_rerun_on_generated_list_change:\n                print(\"warning: cache actions were requested, \" +\n                      \"but the target cache could not be read!\")\n\n    old_generated = set(read_cache_file_lines(args.generated_list_file))\n    old_depends = set(read_cache_file_lines(args.depend_list_file))\n\n    try:\n        mode = CodegenMode(args.mode)\n    except ValueError as exc:\n        error(exc.args[0])\n\n    # arguments are OK.\n\n    # generate sources\n    generated, depends = codegen(mode, args.input_dir, args.output_dir)\n\n    def print_set_differences(old, new, name):\n        \"\"\" Prints the difference between old and new. \"\"\"\n        if old - new:\n            print(\"codegen: removed from \" + name + \":\\n\"\n                  \"\\t\" + \"\\n\\t\".join(old - new) + \"\\n\")\n\n        if new - old and old:\n            print(\"codegen: new \" + name + \":\\n\"\n                  \"\\t\" + \"\\n\\t\".join(new - old) + \"\\n\")\n\n    print_set_differences(old_generated, generated, \"generated files\")\n    print_set_differences(old_depends, depends, \"dependencies\")\n\n    with open(args.generated_list_file, 'w', encoding='utf8') as fileobj:\n        fileobj.write('\\n'.join(generated))\n\n    with open(args.depend_list_file, 'w', encoding='utf8') as fileobj:\n        fileobj.write('\\n'.join(depends))\n\n    if args.file_to_touch:\n        if old_generated != generated or old_depends != depends:\n            os.utime(args.file_to_touch)\n\n    if args.force_rerun_on_generated_list_change:\n        if old_generated != generated:\n            print(\"\\n\\n\\n\\n\"\n                  \"The list of generated source files has changed.\\n\"\n                  \"A build update has been triggered; you need to build \"\n                  \"again.\\n\"\n                  \"\\n\\n\\n\")\n\n            # fail\n            sys.exit(1)\n"
  },
  {
    "path": "openage/config.py.in",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n# ${AUTOGEN_WARNING}\n\n# autogen produces lines of config-dependent length (e.g. COMPILERFLAGS).\n# pylint: disable=line-too-long\n\n\"\"\"\nProject configuration, written by the build system.\n\"\"\"\n\nimport sys\nimport importlib\nimport cython\nimport mako\nimport numpy\nimport PIL\nimport pygments\n\nGLOBAL_CONFIG_DIR = \"${GLOBAL_CONFIG_DIR}\"\nGLOBAL_ASSET_DIR = \"${GLOBAL_ASSET_DIR}\"\nBUILD_SRC_DIR = \"${CMAKE_SOURCE_DIR}\"\nBUILD_BIN_DIR = \"${CMAKE_BINARY_DIR}\"\n\n# version information\nVERSION = \"${VERSION_FULL_STRING}\"\nCONFIG_OPTIONS = \"${CONFIG_OPTION_STRING}\"\n\n# toolchain\nCOMPILER = \"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\"\nCOMPILERFLAGS = \"${CMAKE_CXX_FLAGS}\"\nCICFGVERSION = \"${CI_CFG_VERSION}\"\nPYTHONINTERPRETER = sys.version\nPYTHONCAPI = sys.api_version\nCYTHONVERSION = cython.__version__\nMAKOVERSION = mako.__version__\nNUMPYVERSION = numpy.__version__\nPILVERSION = PIL.__version__\nPYGMENTSVERSION = pygments.__version__\n\n# features\nWITH_NCURSES = (\"${WITH_NCURSES}\" == \"true\")\n\n\ntry:\n    importlib.import_module(\"openage.devmode\")\n    DEVMODE = True\nexcept ImportError:\n    DEVMODE = False\n"
  },
  {
    "path": "openage/convert/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tmain.py\n)\n\nadd_subdirectory(entity_object)\nadd_subdirectory(processor)\nadd_subdirectory(service)\nadd_subdirectory(tool)\nadd_subdirectory(value_object)\n"
  },
  {
    "path": "openage/convert/__init__.py",
    "content": "# Copyright 2013-2015 the openage authors. See copying.md for legal info.\n\n\"\"\"\nAsset conversion\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n)\n\nadd_subdirectory(conversion)\nadd_subdirectory(export)\n"
  },
  {
    "path": "openage/convert/entity_object/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nEntity objects used by the converter\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tcombined_sprite.py\n\tcombined_sound.py\n\tcombined_terrain.py\n\tconverter_object.py\n\tmodpack.py\n\tstringresource.py\n)\n\nadd_subdirectory(aoc)\nadd_subdirectory(ror)\nadd_subdirectory(swgbcc)\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/__init__.py",
    "content": "# Copyright 2013-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nObjects for storing conversion data.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tgenie_civ.py\n\tgenie_connection.py\n\tgenie_effect.py\n\tgenie_graphic.py\n\tgenie_object_container.py\n\tgenie_sound.py\n\tgenie_tech.py\n\tgenie_terrain.py\n\tgenie_unit.py\n)\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/__init__.py",
    "content": "# Copyright 2019-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_civ.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for civilization from AoC.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject, ConverterObjectGroup\nfrom .genie_tech import CivTeamBonus, CivTechTree\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_tech import CivBonus, GenieTechObject\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieCivilizationObject(ConverterObject):\n    \"\"\"\n    Civilization in AoE2.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        civ_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie civilization object.\n\n        :param civ_id: The index of the civilization in the .dat file's civilization\n                       block. (the index is referenced as civilization_id by techs)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(civ_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieCivilizationObject<{self.get_id()}>\"\n\n\nclass GenieCivilizationGroup(ConverterObjectGroup):\n    \"\"\"\n    All necessary civiization data.\n\n    This will become a Civilization API object.\n    \"\"\"\n\n    __slots__ = ('data', 'civ', 'team_bonus', 'tech_tree', 'civ_boni',\n                 'unique_entities', 'unique_techs')\n\n    def __init__(\n        self,\n        civ_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new Genie civ group line.\n\n        :param civ_id: The index of the civilization in the .dat file's civilization\n                       block. (the index is referenced as civ_id by techs)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(civ_id)\n\n        # Reference to everything else in the gamedata\n        self.data = full_data_set\n\n        self.civ: dict[int, GenieCivilizationObject] = self.data.genie_civs[civ_id]\n\n        self.team_bonus: CivTeamBonus = None\n        if self.civ.has_member(\"team_bonus_id\"):\n            team_bonus_id = self.civ[\"team_bonus_id\"].value\n            if team_bonus_id == -1:\n                # Gaia civ has no team bonus\n                self.team_bonus = None\n            else:\n                # Create an object for the team bonus. We use the effect ID + 10000 to avoid\n                # conflicts with techs or effects\n                self.team_bonus = CivTeamBonus(10000 + team_bonus_id, civ_id,\n                                               team_bonus_id, full_data_set)\n\n        # Create an object for the tech tree bonus. We use the effect ID + 10000 to avoid\n        # conflicts with techs or effects\n        tech_tree_id: int  = self.civ[\"tech_tree_id\"].value\n        self.tech_tree = CivTechTree(10000 + tech_tree_id, civ_id,\n                                     tech_tree_id, full_data_set)\n\n        # Civ boni (without team bonus)\n        self.civ_boni: dict[int, CivBonus] = {}\n\n        # Unique units/buildings\n        self.unique_entities: dict[int, GenieGameEntityGroup] = {}\n\n        # Unique techs\n        self.unique_techs: dict[int, GenieTechObject] = {}\n\n    def add_civ_bonus(self, civ_bonus: CivBonus):\n        \"\"\"\n        Adds a civ bonus tech to the civilization.\n        \"\"\"\n        self.civ_boni.update({civ_bonus.get_id(): civ_bonus})\n\n    def add_unique_entity(self, entity_group: GenieGameEntityGroup):\n        \"\"\"\n        Adds a unique unit to the civilization.\n        \"\"\"\n        self.unique_entities.update({entity_group.get_head_unit_id(): entity_group})\n\n    def add_unique_tech(self, tech_group: GenieTechObject):\n        \"\"\"\n        Adds a unique tech to the civilization.\n        \"\"\"\n        self.unique_techs.update({tech_group.get_id(): tech_group})\n\n    def get_team_bonus_effects(self) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the effects of the team bonus.\n        \"\"\"\n        if self.team_bonus:\n            return self.team_bonus.get_effects()\n\n        return []\n\n    def get_tech_tree_effects(self) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the tech tree effects.\n        \"\"\"\n        if self.tech_tree:\n            return self.tech_tree.get_effects()\n\n        return []\n\n    def __repr__(self):\n        return f\"GenieCivilizationGroup<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_connection.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for connections from AoC.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nfrom ..converter_object import ConverterObject\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieAgeConnection(ConverterObject):\n    \"\"\"\n    A relation between an Age and buildings/techs/units in AoE.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        age_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie age connection.\n\n        :param age_id: The index of the Age. (First Age = 0)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(age_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieAgeConnection<{self.get_id()}>\"\n\n\nclass GenieBuildingConnection(ConverterObject):\n    \"\"\"\n    A relation between a building and other buildings/techs/units in AoE.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        building_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie building connection.\n\n        :param building_id: The id of the building from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(building_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieBuildingConnection<{self.get_id()}>\"\n\n\nclass GenieTechConnection(ConverterObject):\n    \"\"\"\n    A relation between a tech and other buildings/techs/units in AoE.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie tech connection.\n\n        :param tech_id: The id of the tech from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(tech_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieTechConnection<{self.get_id()}>\"\n\n\nclass GenieUnitConnection(ConverterObject):\n    \"\"\"\n    A relation between a unit and other buildings/techs/units in AoE.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        unit_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie unit connection.\n\n        :param unit_id: The id of the unit from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(unit_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieUnitConnection<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_effect.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for effects from AoC.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieEffectObject(ConverterObject):\n    \"\"\"\n    Single effect contained in GenieEffectBundle.\n    \"\"\"\n\n    __slots__ = ('bundle_id', 'data')\n\n    def __init__(\n        self,\n        effect_id: int,\n        bundle_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie effect object.\n\n        :param effect_id: The index of the effect in the .dat file's effect\n        :param bundle_id: The index of the effect bundle that the effect belongs to.\n                          (the index is referenced as tech_effect_id by techs)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(effect_id, members=members)\n\n        self.bundle_id = bundle_id\n        self.data = full_data_set\n\n    def get_type(self) -> int:\n        \"\"\"\n        Returns the effect's type.\n        \"\"\"\n        return self[\"type_id\"].value\n\n    def __repr__(self):\n        return f\"GenieEffectObject<{self.get_id()}>\"\n\n\nclass GenieEffectBundle(ConverterObject):\n    \"\"\"\n    A set of effects of a tech.\n    \"\"\"\n\n    __slots__ = ('effects', 'sanitized', 'data')\n\n    def __init__(\n        self,\n        bundle_id: int,\n        effects: list[GenieEffectObject],\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie effect bundle.\n\n        :param bundle_id: The index of the effect in the .dat file's effect\n                          block. (the index is referenced as tech_effect_id by techs)\n        :param effects: Effects of the bundle as list of GenieEffectObject.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(bundle_id, members=members)\n\n        self.effects = effects\n\n        # Sanitized bundles should not contain 'garbage' effects, e.g.\n        #     - effects that do nothing\n        #     - effects without a type        #\n        # Processors should set this to True, once the bundle is sanitized.\n        self.sanitized: bool = False\n\n        self.data = full_data_set\n\n    def get_effects(self, effect_type: int = None) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the effects in the bundle, optionally only effects with a specific\n        type.\n\n        :param effect_type: Type that the effects should have.\n        :type effect_type: int\n        :returns: List of matching effects.\n        :rtype: list\n        \"\"\"\n        if effect_type:\n            matching_effects = []\n            for effect in self.effects.values():\n                if effect.get_type() == effect_type:\n                    matching_effects.append(effect)\n\n            return matching_effects\n\n        return list(self.effects.values())\n\n    def is_sanitized(self) -> bool:\n        \"\"\"\n        Returns whether the effect bundle has been sanitized.\n        \"\"\"\n        return self.sanitized\n\n    def __repr__(self):\n        return f\"GenieEffectBundle<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_graphic.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for graphics from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieGraphic(ConverterObject):\n    \"\"\"\n    Graphic definition from a .dat file.\n    \"\"\"\n\n    __slots__ = ('exists', 'subgraphics', '_refs', 'data')\n\n    def __init__(\n        self,\n        graphic_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie graphic object.\n\n        :param graphic_id: The graphic id from the .dat file.\n        :type graphic_id: int\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.converter_object.ConverterObjectContainer\n        :param members: Members belonging to the graphic.\n        :type members: dict, optional\n        \"\"\"\n\n        super().__init__(graphic_id, members=members)\n\n        self.data = full_data_set\n\n        # Should be set to False if no graphics file exists for it\n        self.exists = True\n\n        # Direct subgraphics (deltas) of this graphic\n        self.subgraphics: list[GenieGraphic] = []\n\n        # Other graphics that have this graphic as a subgraphic (delta)\n        self._refs: list[GenieGraphic] = []\n\n    def add_reference(self, referer: GenieGraphic) -> None:\n        \"\"\"\n        Add another graphic that is referencing this sprite.\n        \"\"\"\n        self._refs.append(referer)\n\n    def detect_subgraphics(self) -> None:\n        \"\"\"\n        Add references for the direct subgraphics to this object.\n        \"\"\"\n        graphic_deltas = self[\"graphic_deltas\"].value\n\n        for subgraphic in graphic_deltas:\n            graphic_id = subgraphic[\"graphic_id\"].value\n\n            # Ignore invalid IDs\n            if graphic_id not in self.data.genie_graphics.keys():\n                continue\n\n            graphic = self.data.genie_graphics[graphic_id]\n\n            self.subgraphics.append(graphic)\n            graphic.add_reference(self)\n\n    def get_animation_length(self) -> float:\n        \"\"\"\n        Returns the time taken to display all frames in this graphic.\n        \"\"\"\n        head_graphic = self.data.genie_graphics[self.get_id()]\n        return head_graphic[\"frame_rate\"].value * head_graphic[\"frame_count\"].value\n\n    def get_subgraphics(self) -> list[GenieGraphic]:\n        \"\"\"\n        Return the subgraphics of this graphic\n        \"\"\"\n        return self.subgraphics\n\n    def get_frame_rate(self) -> float:\n        \"\"\"\n        Returns the time taken to display a single frame in this graphic.\n        \"\"\"\n        head_graphic = self.data.genie_graphics[self.get_id()]\n        return head_graphic[\"frame_rate\"].value\n\n    def is_shared(self) -> bool:\n        \"\"\"\n        Return True if the number of references to this graphic is >1.\n        \"\"\"\n        return len(self._refs) > 1\n\n    def __repr__(self):\n        return f\"GenieGraphic<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_object_container.py",
    "content": "# Copyright 2019-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-instance-attributes,too-few-public-methods\n\n\"\"\"\nObject for comparing and passing around data from a dataset.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ..converter_object import ConverterObjectContainer\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import RawAPIObject\n    from openage.convert.entity_object.conversion.combined_sound import CombinedSound\n    from openage.convert.entity_object.conversion.combined_sprite import CombinedSprite\n    from openage.convert.entity_object.conversion.combined_terrain import CombinedTerrain\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationObject, \\\n        GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_connection import GenieAgeConnection, \\\n        GenieBuildingConnection, GenieTechConnection, GenieUnitConnection\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectBundle\n    from openage.convert.entity_object.conversion.aoc.genie_graphic import GenieGraphic\n    from openage.convert.entity_object.conversion.aoc.genie_sound import GenieSound\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechObject, \\\n        AgeUpgrade, BuildingLineUpgrade, BuildingUnlock, CivBonus, GenieTechEffectBundleGroup, \\\n        InitiatedTech, StatUpgrade, UnitLineUpgrade, UnitUnlock\n    from openage.convert.entity_object.conversion.aoc.genie_terrain import GenieTerrainObject, \\\n        GenieTerrainGroup, GenieTerrainRestriction\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitObject, \\\n        GenieAmbientGroup, GenieBuildingLineGroup, GenieMonkGroup, GenieUnitLineGroup, \\\n        GenieUnitTaskGroup, GenieUnitTransformGroup, GenieVariantGroup, GenieVillagerGroup, \\\n        GenieGameEntityGroup\n    from openage.convert.entity_object.export.media_export_request import MediaExportRequest\n    from openage.convert.entity_object.export.metadata_export import MetadataExport\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.nyan.nyan_structs import NyanObject\n\n\nclass GenieObjectContainer(ConverterObjectContainer):\n    \"\"\"\n    Contains everything from the dat file, sorted into several\n    categories.\n    \"\"\"\n\n    def __init__(self):\n\n        # Game version\n        self.game_version: GameVersion = None\n\n        # API reference\n        self.nyan_api_objects: dict[str, NyanObject] = None\n\n        # Things that don't exist in the game, e.g. Attributes\n        # saved as RawAPIObjects\n        self.pregen_nyan_objects: dict[str, RawAPIObject] = {}\n\n        # Auxiliary\n        self.strings: StringResource = None\n        self.existing_graphics: set[str] = None\n\n        # Phase 1: Genie-like objects\n        # ConverterObject types (the data from the game)\n        # key: obj_id; value: ConverterObject instance\n        self.genie_units: dict[int, GenieUnitObject] = {}\n        self.genie_techs: dict[int, GenieTechObject]  = {}\n        self.genie_effect_bundles: dict[int, GenieEffectBundle]  = {}\n        self.genie_civs: dict[int, GenieCivilizationObject]  = {}\n        self.age_connections: dict[int, GenieAgeConnection]  = {}\n        self.building_connections: dict[int, GenieBuildingConnection]  = {}\n        self.tech_connections: dict[int, GenieTechConnection]  = {}\n        self.unit_connections: dict[int, GenieUnitConnection]  = {}\n        self.genie_graphics: dict[int, GenieGraphic]  = {}\n        self.genie_sounds: dict[int, GenieSound]  = {}\n        self.genie_terrains: dict[int, GenieTerrainObject]  = {}\n        self.genie_terrain_restrictions: dict[int, GenieTerrainRestriction]  = {}\n\n        # Phase 2: API-like objects\n        # ConverterObjectGroup types (things that will become\n        # nyan objects)\n        # key: group_id; value: ConverterObjectGroup instance\n\n        # Keys are the ID of the first unit in line\n        self.unit_lines: dict[int, GenieUnitLineGroup] = {}\n        # Keys are the line ID of the unit connection\n        self.unit_lines_vertical_ref: dict[int, GenieUnitLineGroup] = {}\n        self.building_lines: dict[int, GenieBuildingLineGroup] = {}\n        self.task_groups: dict[int, GenieUnitTaskGroup] = {}\n        self.transform_groups: dict[int, GenieUnitTransformGroup] = {}\n        self.villager_groups: dict[int, GenieVillagerGroup] = {}\n        self.monk_groups: dict[int, GenieMonkGroup] = {}\n        self.ambient_groups: dict[int, GenieAmbientGroup] = {}\n        self.variant_groups: dict[int, GenieVariantGroup] = {}\n\n        self.civ_groups: dict[int, GenieCivilizationGroup]  = {}\n\n        self.tech_groups: dict[int, GenieTechEffectBundleGroup]  = {}\n        self.age_upgrades: dict[int, AgeUpgrade]  = {}\n        self.unit_upgrades: dict[int, UnitLineUpgrade]  = {}\n        self.building_upgrades: dict[int, BuildingLineUpgrade] = {}\n        self.stat_upgrades: dict[int, StatUpgrade] = {}\n        self.unit_unlocks: dict[int, UnitUnlock] = {}\n        self.building_unlocks: dict[int, BuildingUnlock] = {}\n        self.civ_boni: dict[int, CivBonus] = {}\n        self.initiated_techs: dict[int, InitiatedTech] = {}\n\n        self.terrain_groups: dict[int, GenieTerrainGroup] = {}\n\n        # Stores which line a unit is part of\n        self.unit_ref: dict[int, GenieGameEntityGroup] = {}\n\n        # Phase 3: sprites, sounds\n        # Animation or Terrain graphics\n        self.combined_sprites: dict[int, CombinedSprite] = {}\n        self.combined_sounds: dict[int, CombinedSound] = {}\n        self.combined_terrains: dict[int, CombinedTerrain] = {}\n\n        self.graphics_exports: dict[int, MediaExportRequest] = {}\n        self.blend_exports: dict[int, MediaExportRequest] = {}\n        self.sound_exports: dict[int, MediaExportRequest] = {}\n        self.metadata_exports: list[MetadataExport] = []\n\n    def __repr__(self):\n        return \"GenieObjectContainer\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_sound.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for sounds from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieSound(ConverterObject):\n    \"\"\"\n    Sound definition from a .dat file.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        sound_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie sound object.\n\n        :param sound_id: The sound id from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(sound_id, members=members)\n\n        self.data = full_data_set\n\n    def get_sounds(self, civ_id: int = -1) -> list[int]:\n        \"\"\"\n        Return sound resource ids for the associated DRS file.\n\n        :param civ_id: If specified, only return sounds that belong to this civ.\n        :type civ_id: int\n        \"\"\"\n        sound_ids = []\n        sound_items = self[\"sound_items\"].value\n        for item in sound_items:\n            item_civ_id = item[\"civilization_id\"].value\n            if not item_civ_id == civ_id:\n                continue\n\n            sound_id = item[\"resource_id\"].value\n            sound_ids.append(sound_id)\n\n        return sound_ids\n\n    def __repr__(self):\n        return f\"GenieSound<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_tech.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for techs from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject, ConverterObjectGroup\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject, \\\n        GenieEffectBundle\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n        GenieBuildingLineGroup\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieTechObject(ConverterObject):\n    \"\"\"\n    Technology in AoE2.\n\n    Techs are not limited to researchable technologies. They also\n    unlock the unique units of civs and contain the civ bonuses\n    (excluding team boni).\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie tech object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(tech_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieTechObject<{self.get_id()}>\"\n\n\nclass GenieTechEffectBundleGroup(ConverterObjectGroup):\n    \"\"\"\n    A tech and the collection of its effects.\n    \"\"\"\n\n    __slots__ = ('data', 'tech', 'effects')\n\n    def __init__(\n        self,\n        tech_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id)\n\n        self.data = full_data_set\n\n        # The tech that belongs to the tech id\n        self.tech = self.data.genie_techs[tech_id]\n\n        # Effects of the tech\n        effect_bundle_id: int = self.tech[\"tech_effect_id\"].value\n\n        self.effects: GenieEffectBundle = None\n        if effect_bundle_id > -1:\n            self.effects = self.data.genie_effect_bundles[effect_bundle_id]\n\n    def is_researchable(self) -> bool:\n        \"\"\"\n        Techs are researchable if they are associated with an ingame tech.\n        This is the case if the research time is greater than 0 and the research\n        location is a valid unit ID.\n\n        :returns: True if the research time is greater than zero\n                  and research location greater than -1.\n        \"\"\"\n        research_time = self.tech[\"research_time\"].value\n        research_location_id = self.tech[\"research_location_id\"].value\n\n        return research_time > 0 and research_location_id > -1\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Techs are unique if they belong to a specific civ.\n\n        :returns: True if the civilization id is greater than zero.\n        \"\"\"\n        civilization_id = self.tech[\"civilization_id\"].value\n\n        # -1 = no train location\n        return civilization_id > -1\n\n    def get_civilization(self) -> typing.Union[int, None]:\n        \"\"\"\n        Returns the civilization id if the tech is unique, otherwise return None.\n        \"\"\"\n        if self.is_unique():\n            return self.tech[\"civilization_id\"].value\n\n        return None\n\n    def get_effects(self, effect_type: int = None) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the associated effects.\n        \"\"\"\n        if self.effects:\n            return self.effects.get_effects(effect_type=effect_type)\n\n        return []\n\n    def get_required_techs(self) -> list[GenieTechObject]:\n        \"\"\"\n        Returns the techs that are required for this tech.\n        \"\"\"\n        required_tech_ids = self.tech[\"required_techs\"].value\n\n        required_techs = []\n\n        for tech_id_member in required_tech_ids:\n            tech_id = tech_id_member.value\n            if tech_id == -1:\n                break\n\n            required_techs.append(self.data.genie_techs[tech_id])\n\n        return required_techs\n\n    def get_required_tech_count(self) -> int:\n        \"\"\"\n        Returns the number of required techs necessary to unlock this  tech.\n        \"\"\"\n        return self.tech[\"required_tech_count\"].value\n\n    def get_research_location_id(self) -> int:\n        \"\"\"\n        Returns the group_id for a building line if the tech is\n        researchable, otherwise return None.\n        \"\"\"\n        if self.is_researchable():\n            return self.tech[\"research_location_id\"].value\n\n        return None\n\n    def has_effect(self) -> bool:\n        \"\"\"\n        Returns True if the techology's effects do anything.\n        \"\"\"\n        if self.effects:\n            return len(self.effects.get_effects()) > 0\n\n        return False\n\n    def __repr__(self):\n        return f\"GenieTechEffectBundleGroup<{self.get_id()}>\"\n\n\nclass StatUpgrade(GenieTechEffectBundleGroup):\n    \"\"\"\n    Upgrades attributes of units/buildings or other stats in the game.\n    \"\"\"\n\n    def __repr__(self):\n        return f\"StatUpgrade<{self.get_id()}>\"\n\n\nclass AgeUpgrade(GenieTechEffectBundleGroup):\n    \"\"\"\n    Researches a new Age.\n\n    openage actually does not care about Ages, so this will\n    not be different from any other Tech API object. However,\n    we will use this object to push all Age-related upgrades\n    here and create a Tech from it.\n    \"\"\"\n\n    __slots__ = ('age_id',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        age_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param age_id: The index of the Age. (First Age = 0)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(tech_id, full_data_set)\n\n        self.age_id = age_id\n\n    def __repr__(self):\n        return f\"AgeUpgrade<{self.get_id()}>\"\n\n\nclass UnitLineUpgrade(GenieTechEffectBundleGroup):\n    \"\"\"\n    Upgrades a unit in a line.\n\n    This will become a Tech API object targeted at the line's game entity.\n    \"\"\"\n\n    __slots__ = ('unit_line_id', 'upgrade_target_id')\n\n    def __init__(\n        self,\n        tech_id: int,\n        unit_line_id: int,\n        upgrade_target_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie line upgrade object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param unit_line_id: The unit line that is upgraded.\n        :param upgrade_target_id: The unit that is the result of the upgrade.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(tech_id, full_data_set)\n\n        self.unit_line_id = unit_line_id\n        self.upgrade_target_id = upgrade_target_id\n\n    def get_line_id(self) -> int:\n        \"\"\"\n        Returns the line id of the upgraded line.\n        \"\"\"\n        return self.unit_line_id\n\n    def get_upgraded_line(self) -> GenieUnitLineGroup:\n        \"\"\"\n        Returns the line that is upgraded.\n        \"\"\"\n        return self.data.unit_lines[self.unit_line_id]\n\n    def get_upgrade_target_id(self) -> int:\n        \"\"\"\n        Returns the target unit that is upgraded to.\n        \"\"\"\n        return self.upgrade_target_id\n\n    def __repr__(self):\n        return f\"UnitLineUpgrade<{self.get_id()}>\"\n\n\nclass BuildingLineUpgrade(GenieTechEffectBundleGroup):\n    \"\"\"\n    Upgrades a building in a line.\n\n    This will become a Tech API object targeted at the line's game entity.\n    \"\"\"\n\n    __slots__ = ('building_line_id', 'upgrade_target_id')\n\n    def __init__(\n        self,\n        tech_id: int,\n        building_line_id: int,\n        upgrade_target_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie line upgrade object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param building_line_id: The building line that is upgraded.\n        :param upgrade_target_id: The unit that is the result of the upgrade.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(tech_id, full_data_set)\n\n        self.building_line_id = building_line_id\n        self.upgrade_target_id = upgrade_target_id\n\n    def get_line_id(self) -> int:\n        \"\"\"\n        Returns the line id of the upgraded line.\n        \"\"\"\n        return self.building_line_id\n\n    def get_upgraded_line(self) -> GenieBuildingLineGroup:\n        \"\"\"\n        Returns the line that is upgraded.\n        \"\"\"\n        return self.data.building_lines[self.building_line_id]\n\n    def get_upgrade_target_id(self) -> int:\n        \"\"\"\n        Returns the target unit that is upgraded to.\n        \"\"\"\n        return self.upgrade_target_id\n\n    def __repr__(self):\n        return f\"BuildingLineUpgrade<{self.get_id()}>\"\n\n\nclass UnitUnlock(GenieTechEffectBundleGroup):\n    \"\"\"\n    Unlocks units, sometimes with additional requirements like (266 - Castle built).\n\n    This will become one or more patches for an AgeUpgrade Tech. If the unlock\n    is civ-specific, two patches (one for the age, one for the civ)\n    will be created.\n    \"\"\"\n\n    __slots__ = ('line_id',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        line_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param line_id: The id of the unlocked line.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, full_data_set)\n\n        self.line_id = line_id\n\n    def get_line_id(self) -> int:\n        \"\"\"\n        Returns the ID of the line that is unlocked by this tech.\n        \"\"\"\n        return self.line_id\n\n    def get_unlocked_line(self) -> GenieUnitLineGroup:\n        \"\"\"\n        Returns the line that is unlocked by this tech.\n        \"\"\"\n        return self.data.unit_lines[self.line_id]\n\n    def __repr__(self):\n        return f\"UnitUnlock<{self.get_id()}>\"\n\n\nclass BuildingUnlock(GenieTechEffectBundleGroup):\n    \"\"\"\n    Unlocks buildings, sometimes with additional requirements like (266 - Castle built).\n\n    This will become one or more patches for an AgeUpgrade Tech. If the unlock\n    is civ-specific, two patches (one for the age, one for the civ)\n    will be created.\n    \"\"\"\n\n    __slots__ = ('head_unit_id',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        head_unit_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param head_unit_id: The id of the unlocked line.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, full_data_set)\n\n        self.head_unit_id = head_unit_id\n\n    def get_line_id(self) -> int:\n        \"\"\"\n        Returns the ID of the line that is unlocked by this tech.\n        \"\"\"\n        return self.head_unit_id\n\n    def get_unlocked_line(self) -> GenieBuildingLineGroup:\n        \"\"\"\n        Returns the line that is unlocked by this tech.\n        \"\"\"\n        return self.data.building_lines[self.head_unit_id]\n\n    def __repr__(self):\n        return f\"BuildingUnlock<{self.get_id()}>\"\n\n\nclass InitiatedTech(GenieTechEffectBundleGroup):\n    \"\"\"\n    Techs initiated by buildings when they have finished constructing.\n\n    This will used to determine requirements for the creatables.\n    \"\"\"\n\n    __slots__ = ('building_id',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        building_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param building_id: The id of the genie building initiatig this tech.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, full_data_set)\n\n        self.building_id = building_id\n\n    def get_building_id(self) -> int:\n        \"\"\"\n        Returns the ID of the building intiating this tech.\n        \"\"\"\n        return self.building_id\n\n    def __repr__(self):\n        return f\"InitiatedTech<{self.get_id()}>\"\n\n\nclass CivBonus(GenieTechEffectBundleGroup):\n    \"\"\"\n    Gives one specific civilization a bonus. Not the team bonus or tech tree.\n\n    This will become patches in the Civilization API object.\n    \"\"\"\n\n    __slots__ = ('civ_id',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        civ_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param civ_id: The index of the civ.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, full_data_set)\n\n        self.civ_id = civ_id\n\n    def get_civilization_id(self) -> int:\n        \"\"\"\n        Get the ID of the civilization that receives the bonus.\n        \"\"\"\n        return self.civ_id\n\n    def replaces_researchable_tech(self) -> typing.Union[GenieTechEffectBundleGroup, None]:\n        \"\"\"\n        Checks if this bonus replaces a researchable Tech and returns the tech group\n        if thats the case. Otherwise None is returned.\n        \"\"\"\n        for tech_group in self.data.tech_groups.values():\n            if tech_group.is_researchable():\n                bonus_effect_id = self.tech[\"tech_effect_id\"].value\n                tech_group_effect_id = tech_group.tech[\"tech_effect_id\"].value\n\n                if bonus_effect_id == tech_group_effect_id:\n                    return tech_group\n\n        return None\n\n    def __repr__(self):\n        return f\"CivBonus<{self.get_id()}>\"\n\n\nclass CivTeamBonus(ConverterObjectGroup):\n    \"\"\"\n    Gives a civilization and all allies a bonus.\n\n    This will become patches in the Civilization API object.\n    \"\"\"\n\n    __slots__ = ('tech_id', 'data', 'civ_id', 'effects')\n\n    def __init__(\n        self,\n        tech_id: int,\n        civ_id: int,\n        effect_bundle_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param civ_id: The index of the civ.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(tech_id)\n\n        self.tech_id = tech_id\n        self.data = full_data_set\n        self.civ_id = civ_id\n        self.effects = self.data.genie_effect_bundles[effect_bundle_id]\n\n    def get_civilization_id(self) -> int:\n        \"\"\"\n        Returns ID of the civilization that has this bonus.\n        \"\"\"\n        return self.civ_id\n\n    def get_effects(self) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the associated effects.\n        \"\"\"\n        return self.effects.get_effects()\n\n    def __repr__(self):\n        return f\"CivTeamBonus<{self.get_id()}>\"\n\n\nclass CivTechTree(ConverterObjectGroup):\n    \"\"\"\n    Tech tree of a civilization.\n\n    This will become patches in the Civilization API object.\n    \"\"\"\n\n    __slots__ = ('tech_id', 'data', 'civ_id', 'effects')\n\n    def __init__(\n        self,\n        tech_id: int,\n        civ_id: int,\n        effect_bundle_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param civ_id: The index of the civ.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(tech_id)\n\n        self.tech_id = tech_id\n        self.data = full_data_set\n        self.civ_id = civ_id\n\n        self.effects: GenieEffectBundle = None\n        if effect_bundle_id > -1:\n            self.effects = self.data.genie_effect_bundles[effect_bundle_id]\n\n    def get_civilization_id(self):\n        \"\"\"\n        Returns ID of the civilization that has this tech tree.\n        \"\"\"\n        return self.civ_id\n\n    def get_effects(self) -> list[GenieEffectObject]:\n        \"\"\"\n        Returns the associated effects.\n        \"\"\"\n        if self.effects:\n            return self.effects.get_effects()\n\n        return []\n\n    def __repr__(self):\n        return f\"CivTechTree<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_terrain.py",
    "content": "# Copyright 2019-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for terrain from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..converter_object import ConverterObject, ConverterObjectGroup\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieTerrainObject(ConverterObject):\n    \"\"\"\n    Terrain definition from a .dat file.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        terrain_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie terrain object.\n\n        :param terrain_id: The index of the terrain in the .dat file's terrain\n                           block. (the index is referenced by other terrains)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(terrain_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieTerrainObject<{self.get_id()}>\"\n\n\nclass GenieTerrainGroup(ConverterObjectGroup):\n    \"\"\"\n    A terrain from AoE that will become an openage Terrain object.\n    \"\"\"\n\n    __slots__ = ('data', 'terrain')\n\n    def __init__(\n        self,\n        terrain_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie tech group object.\n\n        :param terrain_id: The index of the terrain in the .dat file's terrain table.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(terrain_id)\n\n        self.data = full_data_set\n\n        # The terrain that belongs to the index\n        self.terrain = self.data.genie_terrains[terrain_id]\n\n    def has_subterrain(self) -> bool:\n        \"\"\"\n        Checks if this terrain uses a subterrain for its graphics.\n        \"\"\"\n        return self.terrain[\"terrain_replacement_id\"].value > -1\n\n    def get_subterrain(self) -> GenieTerrainObject:\n        \"\"\"\n        Return the subterrain used for the graphics.\n        \"\"\"\n        return self.data.genie_terrains[self.terrain[\"terrain_replacement_id\"].value]\n\n    def get_terrain(self) -> GenieTerrainObject:\n        \"\"\"\n        Return the subterrain used for the graphics.\n        \"\"\"\n        return self.terrain\n\n    def __repr__(self):\n        return f\"GenieTerrainGroup<{self.get_id()}>\"\n\n\nclass GenieTerrainRestriction(ConverterObject):\n    \"\"\"\n    Terrain restriction definition from a .dat file.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        restriction_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie terrain restriction object.\n\n        :param restriction_id: The index of the terrain restriction in the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(restriction_id, members=members)\n\n        self.data = full_data_set\n\n    def is_accessible(self, terrain_index: int) -> bool:\n        \"\"\"\n        Checks if a terrain is accessible by this restriction.\n\n        :param terrain_index: Index of the terrain.\n        \"\"\"\n        multiplier = self.members[\"accessible_dmgmultiplier\"][terrain_index].value\n\n        return multiplier > 0\n\n    def __repr__(self):\n        return f\"GenieTerrainRestriction<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/aoc/genie_unit.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-public-methods,too-many-instance-attributes,consider-iterating-dictionary\n\n\"\"\"\nContains structures and API-like objects for game entities from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom enum import Enum\n\nfrom ..converter_object import ConverterObject, ConverterObjectGroup\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass GenieUnitObject(ConverterObject):\n    \"\"\"\n    Ingame object in AoE2.\n    \"\"\"\n\n    __slots__ = ('data',)\n\n    def __init__(\n        self,\n        unit_id: int,\n        full_data_set: GenieObjectContainer,\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new Genie unit object.\n\n        :param unit_id: The internal unit_id of the unit.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :param members: An already existing member dict.\n        \"\"\"\n\n        super().__init__(unit_id, members=members)\n\n        self.data = full_data_set\n\n    def __repr__(self):\n        return f\"GenieUnitObject<{self.get_id()}>\"\n\n\nclass GenieGameEntityGroup(ConverterObjectGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that form an \"upgrade line\"\n    in Age of Empires. This acts as the common super class for\n    units and buildings.\n\n    The first unit in the line will become the GameEntity, the rest will\n    be patches to that GameEntity applied by Techs.\n    \"\"\"\n\n    __slots__ = (\n        'data',\n        'line',\n        'line_positions',\n        'creates',\n        'researches',\n        'garrison_entities',\n        'garrison_locations',\n        'repairable'\n    )\n\n    def __init__(\n        self,\n        line_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie game entity line.\n\n        :param line_id: Internal line obj_id in the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(line_id)\n\n        # Reference to everything else in the gamedata\n        self.data = full_data_set\n\n        # The line is stored as an ordered list of GenieUnitObjects.\n        self.line: list[GenieUnitObject] = []\n\n        # This dict stores unit ids and their repective position in the\n        # unit line for quick referencing and searching\n        self.line_positions: dict[int, int] = {}\n\n        # List of units/buildings that the line can create\n        self.creates: list[GenieGameEntityGroup] = []\n\n        # List of GenieTechEffectBundleGroup objects\n        self.researches: list[GenieTechEffectBundleGroup] = []\n\n        # List of units that the line can garrison\n        self.garrison_entities: list[GenieGameEntityGroup] = []\n\n        # List of units/buildings where the line can garrison\n        self.garrison_locations: list[GenieGameEntityGroup] = []\n\n        # Can be repaired by villagers\n        self.repairable = False\n\n    def add_creatable(self, line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Adds another line to the list of creatables.\n\n        :param line: The GenieBuildingLineGroup the villager produces.\n        \"\"\"\n        if not self.contains_creatable(line.get_head_unit_id()):\n            self.creates.append(line)\n\n    def add_researchable(self, tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Adds a tech group to the list of researchables.\n\n        :param tech_group: The GenieTechLineGroup the building researches.\n        \"\"\"\n        if not self.contains_researchable(tech_group.get_id()):\n            self.researches.append(tech_group)\n\n    def add_unit(\n        self,\n        genie_unit: GenieUnitObject,\n        position: int = -1,\n        after: GenieUnitObject = None\n    ) -> None:\n        \"\"\"\n        Adds a unit/building to the line.\n\n        :param genie_unit: A GenieUnit object that is part of this\n                           line.\n        :param position: Puts the unit at an specific position in the line.\n                         If this is -1, the unit is placed at the end.\n        :param after: ID of a unit after which the new unit is\n                      placed in the line. If a unit with this obj_id\n                      is not present, the unit is appended at the end\n                      of the line.\n        \"\"\"\n        unit_id = genie_unit[\"id0\"].value\n\n        # Only add unit if it is not already in the list\n        if not self.contains_entity(unit_id):\n            insert_index = len(self.line)\n\n            if position > -1:\n                insert_index = position\n\n                for unit, line_pos in self.line_positions.items():\n                    if line_pos >= insert_index:\n                        self.line_positions[unit] += 1\n\n            elif after:\n                if self.contains_entity(after):\n                    insert_index = self.line_positions[after] + 1\n\n                    for unit, line_pos in self.line_positions.items():\n                        if line_pos >= insert_index:\n                            self.line_positions[unit] += 1\n\n            self.line_positions.update({genie_unit.get_id(): insert_index})\n            self.line.insert(insert_index, genie_unit)\n\n    def contains_creatable(self, line_id: int) -> bool:\n        \"\"\"\n        Returns True if a line with line_id is a creatable of\n        this unit.\n        \"\"\"\n        if isinstance(self, GenieUnitLineGroup):\n            line = self.data.building_lines[line_id]\n\n        elif isinstance(self, GenieBuildingLineGroup):\n            line = self.data.unit_lines[line_id]\n\n        else:\n            raise ValueError(f\"Unknown creatable line ID {line_id} for {repr(self)}\")\n\n        return line in self.creates\n\n    def contains_entity(self, unit_id: int) -> bool:\n        \"\"\"\n        Returns True if a entity with unit_id is part of the line.\n        \"\"\"\n        return unit_id in self.line_positions.keys()\n\n    def contains_researchable(self, line_id: int) -> bool:\n        \"\"\"\n        Returns True if a tech line with line_id is researchable\n        in this building.\n        \"\"\"\n        tech_line = self.data.tech_groups[line_id]\n\n        return tech_line in self.researches\n\n    def has_armor(self, armor_class: int) -> bool:\n        \"\"\"\n        Checks if units in the line have a specific armor class.\n\n        :param armor_class: The type of armor class searched for.\n        :type armor_class: int\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        armors = head_unit[\"armors\"].value\n        for armor in armors.values():\n            type_id = armor[\"type_id\"].value\n\n            if type_id == armor_class:\n                return True\n\n        return False\n\n    def has_attack(self, armor_class: int) -> bool:\n        \"\"\"\n        Checks if units in the line can execute a specific command.\n\n        :param armor_class: The type of attack class searched for.\n        :type armor_class: int\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        attacks = head_unit[\"attacks\"].value\n        for attack in attacks.values():\n            type_id = attack[\"type_id\"].value\n\n            if type_id == armor_class:\n                return True\n\n        return False\n\n    def has_command(self, command_id: int, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks if units in the line can execute a specific command.\n\n        :param command_id: The type of command searched for.\n        :type command_id: int\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        commands = head_unit[\"unit_commands\"].value\n        for command in commands:\n            type_id = command[\"type\"].value\n\n            if type_id == command_id:\n                return True\n\n        return False\n\n    def has_projectile(self, projectile_id: int, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks if units shoot a projectile with this ID.\n\n        :param projectile_id: The ID of the projectile unit.\n        :type projectile_id: int\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        projectile_id_0 = head_unit[\"projectile_id0\"].value\n        projectile_id_1 = -2\n        if head_unit.has_member(\"projectile_id1\"):\n            projectile_id_1 = head_unit[\"projectile_id1\"].value\n\n        return projectile_id in (projectile_id_0, projectile_id_1)\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Units/Buildings are creatable if they have a valid train location.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        # Get the train location obj_id for the first unit in the line\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        train_location_id = head_unit[\"train_location_id\"].value\n\n        # -1 = no train location\n        return train_location_id > -1\n\n    def is_harvestable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group holds any of the 4 main resources Food,\n        Wood, Gold and Stone.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group contains at least one resource storage.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        for resource_storage in head_unit[\"resource_storage\"].value:\n            type_id = resource_storage[\"type\"].value\n\n            if type_id in (0, 1, 2, 3, 15, 16, 17):\n                return True\n\n        return False\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group can garrison other entities. This covers\n        all of these garrisons:\n\n        - Natural garrisons (castle, town center, tower)\n        - Production garrisons (barracks, archery range, stable, etc.)\n        - Unit garrisons (ram, siege tower)\n        - Transport ships\n\n        This does not include kidnapping units!\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group falls into the above categories.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        trait = head_unit[\"trait\"].value\n\n        # Transport ship/ram\n        if trait & 0x01:\n            return True\n\n        # Production garrison\n        type_id = head_unit[\"unit_type\"].value\n        if len(self.creates) > 0 and type_id == 80:\n            return True\n\n        # Natural garrison\n        if head_unit.has_member(\"garrison_type\"):\n            garrison_type = head_unit[\"garrison_type\"].value\n\n            if garrison_type > 0:\n                return True\n\n        return False\n\n    def is_gatherer(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has any gather abilities.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group contains at least one resource storage.\n        \"\"\"\n        return self.has_command(5, civ_id=civ_id)\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group has obstruction type 0.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        return head_unit[\"obstruction_type\"].value == 0\n\n    def is_projectile_shooter(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Units/Buildings are projectile shooters if they have assigned a projectile ID.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if one of the projectile IDs is greater than zero.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        if not head_unit.has_member(\"projectile_id0\"):\n            return False\n\n        # Get the projectiles' obj_id for the first unit in the line. AoE's\n        # units stay ranged with upgrades, so this should be fine.\n        projectile_id_0 = head_unit[\"projectile_id0\"].value\n\n        projectile_id_1 = -1\n        if head_unit.has_member(\"projectile_id1\"):\n            projectile_id_1 = head_unit[\"projectile_id1\"].value\n\n        # -1 -> no projectile\n        return projectile_id_0 > -1 or projectile_id_1 > -1\n\n    def is_ranged(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Groups are ranged if their maximum range is greater than 0.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group's max range is greater than 0.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        return head_unit[\"weapon_range_max\"].value > 0\n\n    def is_melee(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Groups are melee if they have a Combat ability and are not ranged units.\n\n        :param civ_id: Test if the property is true for unit lines\n                       of the civ with this ID:\n        :type civ_id: int\n        :returns: True if the group is not ranged and has a combat ability.\n        \"\"\"\n        return self.has_command(7, civ_id=civ_id)\n\n    def is_repairable(self) -> bool:\n        \"\"\"\n        Only certain lines and classes are repairable.\n\n        :returns: True if the group is repairable.\n        \"\"\"\n        return self.repairable\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Groups are unique if they belong to a specific civ.\n\n        :returns: True if the group is tied to one specific civ.\n        \"\"\"\n        # Get the enabling research obj_id for the first unit in the line\n        head_unit = self.get_head_unit()\n        head_unit_id = head_unit[\"id0\"].value\n\n        if isinstance(self, GenieUnitLineGroup):\n            if head_unit_id in self.data.unit_connections.keys():\n                head_unit_connection = self.data.unit_connections[head_unit_id]\n\n            else:\n                # Animals or AoE1\n                return False\n\n        elif isinstance(self, GenieBuildingLineGroup):\n            if head_unit_id in self.data.building_connections.keys():\n                head_unit_connection = self.data.building_connections[head_unit_id]\n\n            else:\n                # AoE1\n                return False\n        else:\n            raise ValueError(f\"Unknown group type for {repr(self)}\")\n\n        enabling_research_id = head_unit_connection[\"enabling_research\"].value\n\n        # does not need to be enabled -> not unique\n        if enabling_research_id == -1:\n            return False\n\n        # Get enabling civ\n        enabling_research = self.data.genie_techs[enabling_research_id]\n        enabling_civ_id = enabling_research[\"civilization_id\"].value\n\n        # Enabling tech has no specific civ -> not unique\n        return enabling_civ_id > -1\n\n    def get_class_id(self) -> int:\n        \"\"\"\n        Return the class ID for units in the group.\n        \"\"\"\n        return self.get_head_unit()[\"unit_class\"].value\n\n    def get_garrison_mode(self, civ_id: int = -1) -> GenieGarrisonMode:\n        \"\"\"\n        Returns the mode the garrison operates in. This is used by the\n        converter to determine which storage abilities the line will get.\n\n        :param civ_id: Get the garrison mode for unit lines of the civ\n                       with this ID.\n        :type civ_id: int\n        :returns: The garrison mode of the line.\n        :rtype: GenieGarrisonMode\n        \"\"\"\n        head_unit = self.get_head_unit()\n        if civ_id != -1:\n            head_unit = self.data.civ_groups[civ_id][\"units\"][self.get_head_unit_id()]\n\n        trait = head_unit[\"trait\"].value\n\n        # Ram\n        if trait == 1:\n            return GenieGarrisonMode.UNIT_GARRISON\n\n        # Transport ship\n        if trait == 3:\n            return GenieGarrisonMode.TRANSPORT\n\n        # Natural garrison\n        if head_unit.has_member(\"garrison_type\"):\n            garrison_type = head_unit[\"garrison_type\"].value\n\n            if garrison_type > 0:\n                return GenieGarrisonMode.NATURAL\n\n        # Production garrison\n        type_id = head_unit[\"unit_type\"].value\n        if len(self.creates) > 0 and type_id == 80:\n            return GenieGarrisonMode.SELF_PRODUCED\n\n        return None\n\n    def get_head_unit_id(self) -> int:\n        \"\"\"\n        Return the obj_id of the first unit in the line.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"id0\"].value\n\n    def get_head_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Return the first unit in the line.\n        \"\"\"\n        return self.line[0]\n\n    def get_unit_position(self, unit_id: int) -> int:\n        \"\"\"\n        Return the position of a unit in the line.\n        \"\"\"\n        return self.line_positions[unit_id]\n\n    def get_train_location_id(self) -> typing.Union[int, None]:\n        \"\"\"\n        Returns the group_id for building line if the unit is\n        creatable, otherwise return None.\n        \"\"\"\n        if self.is_creatable():\n            head_unit = self.get_head_unit()\n            return head_unit[\"train_location_id\"].value\n\n        return None\n\n    def __repr__(self):\n        return f\"GenieGameEntityGroup<{self.get_id()}>\"\n\n\nclass GenieUnitLineGroup(GenieGameEntityGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that form an \"upgrade line\"\n    in Age of Empires.\n\n    Example: Spearman->Pikeman->Helbardier\n\n    The first unit in the line will become the GameEntity, the rest will\n    be patches to that GameEntity applied by Techs.\n    \"\"\"\n\n    def contains_unit(self, unit_id: int) -> bool:\n        \"\"\"\n        Returns True if a unit with unit_id is part of the line.\n        \"\"\"\n        return self.contains_entity(unit_id)\n\n    def get_civ_id(self) -> typing.Union[int, None]:\n        \"\"\"\n        Returns the enabling civ obj_id if the unit is unique,\n        otherwise return None.\n        \"\"\"\n        if self.is_unique():\n            head_unit = self.get_head_unit()\n            head_unit_id = head_unit[\"id0\"].value\n            head_unit_connection = self.data.unit_connections[head_unit_id]\n            enabling_research_id = head_unit_connection[\"enabling_research\"].value\n\n            enabling_research = self.data.genie_techs[enabling_research_id]\n            return enabling_research[\"civilization_id\"].value\n\n        return None\n\n    def get_enabling_research_id(self) -> int:\n        \"\"\"\n        Returns the enabling tech id of the unit\n\n        TODO: Find enabling research ID in pre-processor and save it in the group\n              like we do for RoR. Not all creatable units must be present in the\n              unit connections.\n        TODO: Move function into GeneGameEntityGroup after doing the above.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        head_unit_id = head_unit[\"id0\"].value\n\n        if head_unit_id not in self.data.unit_connections.keys():\n            # TODO: Remove this check, see TODOs above\n            return -1\n\n        head_unit_connection = self.data.unit_connections[head_unit_id]\n        enabling_research_id = head_unit_connection[\"enabling_research\"].value\n\n        return enabling_research_id\n\n    def __repr__(self):\n        return f\"GenieUnitLineGroup<{self.get_id()}>\"\n\n\nclass GenieBuildingLineGroup(GenieGameEntityGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that represent a building\n    in Age of Empires. Buildings actually have no line obj_id, so we take\n    the obj_id of the first occurence of the building's obj_id as the line obj_id.\n\n    Example1: Blacksmith(feudal)->Blacksmith(castle)->Blacksmith(imp)\n\n    Example2: WatchTower->GuardTower->Keep\n\n    Buildings in AoE2 also create units and research techs, so\n    this is handled in here.\n\n    The 'head unit' of a building line becomes the GameEntity, the rest will\n    be patches to that GameEntity applied by Techs.\n    \"\"\"\n\n    __slots__ = ('gatherer_ids', 'trades_with')\n\n    def __init__(\n        self,\n        line_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        super().__init__(line_id, full_data_set)\n\n        # IDs of gatherers that drop off resources here\n        self.gatherer_ids: set[int] = set()\n\n        # Unit lines that this building trades with\n        self.trades_with: set[GenieGameEntityGroup] = []\n\n    def add_gatherer_id(self, unit_id: int) -> None:\n        \"\"\"\n        Adds Id of gatherers that drop off resources at this building.\n\n        :param unit_id: ID of the gatherer.\n        \"\"\"\n        self.gatherer_ids.add(unit_id)\n\n    def add_trading_line(self, unit_line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Adds a reference to a line that trades with this building.\n\n        :param unit_line: Line that trades with this building.\n        \"\"\"\n        self.trades_with.append(unit_line)\n\n    def contains_unit(self, building_id: int) -> bool:\n        \"\"\"\n        Returns True if a building with building_id is part of the line.\n        \"\"\"\n        return self.contains_entity(building_id)\n\n    def has_foundation(self) -> bool:\n        \"\"\"\n        Returns True if the building has a foundation terrain.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"foundation_terrain_id\"].value > -1\n\n    def is_dropsite(self) -> bool:\n        \"\"\"\n        Returns True if the building accepts resources.\n        \"\"\"\n        return len(self.gatherer_ids) > 0\n\n    def is_repairable(self) -> bool:\n        return True\n\n    def is_trade_post(self) -> bool:\n        \"\"\"\n        Returns True if the building is traded with.\n        \"\"\"\n        return len(self.trades_with) > 0\n\n    def get_gatherer_ids(self) -> set[int]:\n        \"\"\"\n        Returns gatherer unit IDs that drop off resources at this building.\n        \"\"\"\n        return self.gatherer_ids\n\n    def get_enabling_research_id(self) -> int:\n        \"\"\"\n        Returns the enabling tech id of the unit\n        \"\"\"\n        head_unit = self.get_head_unit()\n        head_unit_id = head_unit[\"id0\"].value\n        if head_unit_id in self.data.building_connections.keys():\n            head_unit_connection = self.data.building_connections[head_unit_id]\n            enabling_research_id = head_unit_connection[\"enabling_research\"].value\n\n        else:\n            # Assume it is avialable from the start\n            enabling_research_id = -1\n\n        return enabling_research_id\n\n    def __repr__(self):\n        return f\"GenieBuildingLineGroup<{self.get_id()}>\"\n\n\nclass GenieStackBuildingGroup(GenieBuildingLineGroup):\n    \"\"\"\n    Buildings that stack with other units and have annexes. These buildings\n    are replaced by their stack unit once built.\n\n    Examples: Gate, Town Center\n\n    The 'stack unit' becomes the GameEntity, the 'head unit' will be a state\n    during construction.\n    \"\"\"\n\n    __slots__ = ('head', 'stack')\n\n    def __init__(\n        self,\n        stack_unit_id: int,\n        head_building_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie building line.\n\n        :param stack_unit_id: \"Actual\" building that appears when constructed.\n        :param head_building_id: The building used during construction.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(stack_unit_id, full_data_set)\n\n        self.head = self.data.genie_units[head_building_id]\n        self.stack = self.data.genie_units[stack_unit_id]\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Stack buildings are created through their head building. We have to\n        lookup its values.\n\n        :returns: True if the train location obj_id is greater than zero.\n        \"\"\"\n        train_location_id = self.head[\"train_location_id\"].value\n\n        # -1 = no train location\n        if train_location_id == -1:\n            return False\n\n        return True\n\n    def is_gate(self) -> bool:\n        \"\"\"\n        Checks if the building has gate properties.\n\n        :returns: True if the building has obstruction class 4.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"obstruction_class\"].value == 4\n\n    def get_head_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Return the first unit in the line.\n        \"\"\"\n        return self.head\n\n    def get_stack_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Returns the unit that is stacked on this building after construction.\n        \"\"\"\n        return self.stack\n\n    def get_head_unit_id(self) -> int:\n        \"\"\"\n        Returns the stack unit ID because that is the unit that is referenced by other entities.\n        \"\"\"\n        return self.get_stack_unit_id()\n\n    def get_stack_unit_id(self) -> int:\n        \"\"\"\n        Returns the stack unit ID.\n        \"\"\"\n        return self.stack[\"id0\"].value\n\n    def get_train_location_id(self) -> int:\n        \"\"\"\n        Stack buildings are creatable when their head building is creatable.\n\n        Returns the group_id for a villager group if the head building is\n        creatable, otherwise return None.\n        \"\"\"\n        if self.is_creatable():\n            return self.head[\"train_location_id\"].value\n\n        return None\n\n    def __repr__(self):\n        return f\"GenieStackBuildingGroup<{self.get_id()}>\"\n\n\nclass GenieUnitTransformGroup(GenieUnitLineGroup):\n    \"\"\"\n    Collection of genie units that reference each other with their\n    transform_id.\n\n    Example: Trebuchet\n    \"\"\"\n\n    __slots__ = ('head_unit', 'transform_unit')\n\n    def __init__(\n        self,\n        line_id: int,\n        head_unit_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie transform group.\n\n        :param head_unit_id: Internal unit obj_id of the unit that should be\n                             the initial state.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(line_id, full_data_set)\n\n        self.head_unit = self.data.genie_units[head_unit_id]\n\n        transform_id = self.head_unit[\"transform_unit_id\"].value\n        self.transform_unit = self.data.genie_units[transform_id]\n\n    def is_projectile_shooter(self, civ_id: int = -1) -> int:\n        \"\"\"\n        Transform groups are projectile shooters if their head or transform units\n        have assigned a projectile ID.\n\n        :returns: True if one of the projectile IDs is greater than zero.\n        \"\"\"\n        projectile_id_0 = self.head_unit[\"projectile_id0\"].value\n        projectile_id_1 = self.head_unit[\"projectile_id1\"].value\n        projectile_id_2 = self.transform_unit[\"projectile_id0\"].value\n        projectile_id_3 = self.transform_unit[\"projectile_id1\"].value\n\n        # -1 -> no projectile\n        return (projectile_id_0 > -1 or projectile_id_1 > -1\n                or projectile_id_2 > -1 or projectile_id_3 > -1)\n\n    def get_head_unit_id(self) -> int:\n        \"\"\"\n        Returns the ID of the head unit.\n        \"\"\"\n        return self.head_unit[\"id0\"].value\n\n    def get_head_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Returns the head unit.\n        \"\"\"\n        return self.head_unit\n\n    def get_transform_unit_id(self) -> int:\n        \"\"\"\n        Returns the ID of the transform unit.\n        \"\"\"\n        return self.transform_unit[\"id0\"].value\n\n    def get_transform_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Returns the transform unit.\n        \"\"\"\n        return self.transform_unit\n\n    def __repr__(self):\n        return f\"GenieUnitTransformGroup<{self.get_id()}>\"\n\n\nclass GenieMonkGroup(GenieUnitLineGroup):\n    \"\"\"\n    Collection of monk and monk with relic. The switch\n    is hardcoded in AoE2. (Missionaries are handled as normal lines\n    because they cannot pick up relics).\n\n    The 'head unit' will become the GameEntity, the 'switch unit'\n    will become a Container ability with CarryProgress.\n    \"\"\"\n\n    __slots__ = ('head_unit', 'switch_unit')\n\n    def __init__(\n        self,\n        line_id: int,\n        head_unit_id: int,\n        switch_unit_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie monk group.\n\n        :param head_unit_id: The unit with this task will become the actual\n                             GameEntity.\n        :param switch_unit_id: This unit will be used to determine the\n                               CarryProgress objects.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, full_data_set)\n\n        self.head_unit = self.data.genie_units[head_unit_id]\n        self.switch_unit = self.data.genie_units[switch_unit_id]\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        return True\n\n    def get_garrison_mode(self, civ_id=-1) -> GenieGarrisonMode:\n        return GenieGarrisonMode.MONK\n\n    def get_switch_unit(self) -> GenieUnitObject:\n        \"\"\"\n        Returns the unit that is switched to when picking up something.\n        \"\"\"\n        return self.switch_unit\n\n    def __repr__(self):\n        return f\"GenieMonkGroup<{self.get_id()}>\"\n\n\nclass GenieAmbientGroup(GenieGameEntityGroup):\n    \"\"\"\n    One Genie unit that is an ambient scenery object.\n    Mostly for resources, specifically trees. For these objects\n    every frame in their graphics file is a variant.\n\n    Example: Trees, Gold mines, Sign\n    \"\"\"\n\n    def contains_unit(self, ambient_id: int) -> bool:\n        \"\"\"\n        Returns True if the ambient object with ambient_id is in this group.\n        \"\"\"\n        return self.contains_entity(ambient_id)\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_gatherer(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_melee(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_ranged(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_projectile_shooter(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"GenieAmbientGroup<{self.get_id()}>\"\n\n\nclass GenieVariantGroup(GenieGameEntityGroup):\n    \"\"\"\n    Collection of multiple Genie units that are variants of the same game entity.\n    Mostly for cliffs and ambient terrain objects.\n\n    Example: Cliffs, flowers, mountains\n    \"\"\"\n\n    def contains_unit(self, object_id: int) -> bool:\n        \"\"\"\n        Returns True if a unit with unit_id is part of the group.\n        \"\"\"\n        return self.contains_entity(object_id)\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_gatherer(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_melee(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_ranged(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"GenieVariantGroup<{self.get_id()}>\"\n\n\nclass GenieUnitTaskGroup(GenieUnitLineGroup):\n    \"\"\"\n    Collection of genie units that have the same task group.\n\n    Example: Male Villager, Female Villager\n\n    The 'head unit' of a task group becomes the GameEntity, all\n    the other are used to create more abilities with AnimationOverride.\n    \"\"\"\n\n    __slots__ = ('task_group_id',)\n\n    # From unit connection\n    male_line_id = 83   # male villager (with combat task)\n\n    # Female villagers have no line obj_id, so we use the combat unit\n    female_line_id = 293  # female villager (with combat task)\n\n    def __init__(\n        self,\n        line_id: int,\n        task_group_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie task group.\n\n        :param line_id: Internal task group obj_id in the .dat file.\n        :param task_group_id: ID of the task group.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(line_id, full_data_set)\n\n        self.task_group_id = task_group_id\n\n    def add_unit(\n        self,\n        genie_unit: GenieUnitObject,\n        position: int = -1,\n        after: GenieUnitObject = None\n    ) -> None:\n        # Force the idle/combat units at the beginning of the line\n        if genie_unit[\"id0\"].value in (GenieUnitTaskGroup.male_line_id,\n                                       GenieUnitTaskGroup.female_line_id):\n            super().add_unit(genie_unit, 0, after)\n\n        else:\n            super().add_unit(genie_unit, position, after)\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Task groups are creatable if any unit in the group is creatable.\n\n        :returns: True if any train location obj_id is greater than zero.\n        \"\"\"\n        for unit in self.line:\n            train_location_id = unit[\"train_location_id\"].value\n            # -1 = no train location\n            if train_location_id > -1:\n                return True\n\n        return False\n\n    def get_train_location_id(self) -> int:\n        \"\"\"\n        Returns the group_id for building line if the task group is\n        creatable, otherwise return None.\n        \"\"\"\n        for unit in self.line:\n            train_location_id = unit[\"train_location_id\"].value\n            # -1 = no train location\n            if train_location_id > -1:\n                return train_location_id\n\n        return None\n\n    def __repr__(self):\n        return f\"GenieUnitTaskGroup<{self.get_id()}>\"\n\n\nclass GenieVillagerGroup(GenieUnitLineGroup):\n    \"\"\"\n    Special collection of task groups for villagers.\n\n    Villagers come in two task groups (male/female) and will form\n    variants of the common villager game entity.\n    \"\"\"\n\n    __slots__ = ('variants', )\n\n    valid_switch_tasks_lookup = {\n        5: \"GATHER\",    # Gather from resource spots\n        7: \"COMBAT\",    # Attack\n        101: \"BUILD\",   # Build buildings\n        106: \"REPAIR\",  # Repair buildings, ships, rams\n        110: \"HUNT\",    # Kill first, then gather\n    }\n\n    def __init__(\n        self,\n        group_id: int,\n        task_group_ids: list[int],\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie villager group.\n\n        :param group_id: Unit obj_id for the villager unit that is referenced by buildings\n                         (in AoE2: 118 = male builder).\n        :param task_group_ids: Internal task group ids in the .dat file.\n                               (as a list of integers)\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(group_id, full_data_set)\n\n        self.data = full_data_set\n\n        # Reference to the variant task groups\n        self.variants: list[GenieUnitTaskGroup] = []\n        for task_group_id in task_group_ids:\n            task_group = self.data.task_groups[task_group_id]\n            self.variants.append(task_group)\n\n        # List of buildings that units can create\n        self.creates: list[GenieGameEntityGroup] = []\n\n    def contains_entity(self, unit_id: int) -> bool:\n        for task_group in self.variants:\n            if task_group.contains_entity(unit_id):\n                return True\n\n        return False\n\n    def has_command(self, command_id: int, civ_id: int = -1) -> bool:\n        for variant in self.variants:\n            for genie_unit in variant.line:\n                commands = genie_unit[\"unit_commands\"].value\n\n                for command in commands:\n                    type_id = command[\"type\"].value\n\n                    if type_id == command_id:\n                        return True\n\n        return False\n\n    def is_creatable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Villagers are creatable if any of their variant task groups are creatable.\n\n        :returns: True if any train location obj_id is greater than zero.\n        \"\"\"\n        for variant in self.variants:\n            if variant.is_creatable():\n                return True\n\n        return False\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_gatherer(self, civ_id: int = -1) -> bool:\n        return True\n\n    @classmethod\n    def is_hunter(cls) -> bool:\n        \"\"\"\n        Returns True if the unit hunts animals.\n        \"\"\"\n        return True\n\n    def is_unique(self) -> bool:\n        return False\n\n    def is_projectile_shooter(self, civ_id: int = -1) -> bool:\n        return False\n\n    def get_garrison_mode(self, civ_id: int = -1) -> bool:\n        return None\n\n    def get_head_unit_id(self) -> int:\n        \"\"\"\n        For villagers, this returns the group obj_id.\n        \"\"\"\n        return self.get_id()\n\n    def get_head_unit(self) -> GenieUnitObject:\n        \"\"\"\n        For villagers, this returns the group obj_id.\n        \"\"\"\n        return self.variants[0].line[0]\n\n    def get_units_with_command(self, command_id: int) -> GenieUnitObject:\n        \"\"\"\n        Returns all genie units which have the specified command.\n        \"\"\"\n        matching_units = []\n\n        for variant in self.variants:\n            for genie_unit in variant.line:\n                commands = genie_unit[\"unit_commands\"].value\n\n                for command in commands:\n                    type_id = command[\"type\"].value\n\n                    if type_id == command_id:\n                        matching_units.append(genie_unit)\n\n        return matching_units\n\n    def get_train_location_id(self) -> int:\n        \"\"\"\n        Returns the group_id for building line if the task group is\n        creatable, otherwise return None.\n        \"\"\"\n        for variant in self.variants:\n            if variant.is_creatable():\n                return variant.get_train_location_id()\n\n        return None\n\n    def __repr__(self):\n        return f\"GenieVillagerGroup<{self.get_id()}>\"\n\n\nclass GenieGarrisonMode(Enum):\n    \"\"\"\n    Garrison mode of a genie group. This should not be confused with\n    the \"garrison_type\" from the .dat file. These garrison modes reflect\n    how the garrison will be handled in the openage API.\n    \"\"\"\n    # pylint: disable=line-too-long\n\n    # Keys = all possible creatable types; may be specified further by other factors\n    # The negative integers at the start of the tupe prevent Python from creating\n    # aliases for the enums.\n    NATURAL       = (-1, 1, 2, 3, 5, 6)  # enter/exit/remove; rally point\n    # enter/exit/remove; no cavalry/monks; speedboost for infantry; no rally point\n    UNIT_GARRISON = (-2, 1, 2, 5)\n    TRANSPORT     = (-3, 1, 2, 3, 5, 6)  # enter/exit/remove; no rally point\n    # enter only with OwnStorage; exit/remove; only produced units; rally point\n    SELF_PRODUCED = (-4, 1, 2, 3, 5, 6)\n    MONK          = (-5, 4,)             # remove/collect/transfer; only relics; no rally point\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/combined_sound.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nReferences a sound in the game that has to be converted.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectContainer\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject\n\n\nclass CombinedSound:\n    \"\"\"\n    Collection of sound information for openage files.\n    \"\"\"\n\n    __slots__ = ('head_sound_id', 'file_id', 'filename', 'data', 'genie_sound', '_refs')\n\n    def __init__(\n        self,\n        head_sound_id: int,\n        file_id: int,\n        filename: str,\n        full_data_set: ConverterObjectContainer\n    ):\n        \"\"\"\n        Creates a new CombinedSound instance.\n\n        :param head_sound_id: The id of the GenieSound object of this sound.\n        :type head_sound_id: int\n        :param file_id: The id of the file resource in the GenieSound.\n        :type file_id: int\n        :param filename: Name of the sound file.\n        :type filename: str\n        :param full_data_set: ConverterObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.converter_object.ConverterObjectContainer\n        \"\"\"\n\n        self.head_sound_id = head_sound_id\n        self.file_id = file_id\n        self.filename = filename\n        self.data = full_data_set\n\n        self.genie_sound = self.data.genie_sounds[self.head_sound_id]\n\n        # Depending on the amounts of references:\n        # 0 = do not convert;\n        # 1 = store with GameEntity;\n        # >1 = store in 'shared' resources;\n        self._refs = []\n\n    def add_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Add an object that is referencing this sound.\n        \"\"\"\n        self._refs.append(referer)\n\n    def get_filename(self) -> str:\n        \"\"\"\n        Returns the desired filename of the sprite.\n        \"\"\"\n        return self.filename\n\n    def get_file_id(self) -> int:\n        \"\"\"\n        Returns the ID of the sound file in the game folder.\n        \"\"\"\n        return self.file_id\n\n    def get_id(self) -> int:\n        \"\"\"\n        Returns the ID of the sound object in the .dat.\n        \"\"\"\n        return self.head_sound_id\n\n    def get_relative_file_location(self) -> str:\n        \"\"\"\n        Return the sound file location relative to where the file\n        is expected to be in the modpack.\n        \"\"\"\n        if len(self._refs) > 1:\n            return f\"../shared/sounds/{self.filename}.opus\"\n\n        if len(self._refs) == 1:\n            return f\"./sounds/{self.filename}.opus\"\n\n        return None\n\n    def resolve_sound_location(self) -> str:\n        \"\"\"\n        Returns the planned location of the sound file in the modpack.\n        \"\"\"\n        if len(self._refs) > 1:\n            return \"data/game_entity/shared/sounds/\"\n\n        if len(self._refs) == 1:\n            return f\"{self._refs[0].get_file_location()[0]}{'sounds/'}\"\n\n        return None\n\n    def remove_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Remove an object that is referencing this sound.\n        \"\"\"\n        self._refs.remove(referer)\n\n    def __repr__(self):\n        return f\"CombinedSound<{self.head_sound_id}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/combined_sprite.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nReferences a graphic in the game that has to be converted.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectContainer\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject\n    from openage.convert.entity_object.conversion.aoc.genie_graphic import GenieGraphic\n\n\nclass CombinedSprite:\n    \"\"\"\n    Collection of sprite information for openage files.\n\n    This will become a spritesheet texture with a sprite file.\n    \"\"\"\n\n    __slots__ = ('head_sprite_id', 'filename', 'data', 'metadata', '_refs')\n\n    def __init__(\n        self,\n        head_sprite_id: int,\n        filename: str,\n        full_data_set: ConverterObjectContainer\n    ):\n        \"\"\"\n        Creates a new CombinedSprite instance.\n\n        :param head_sprite_id: The id of the top level graphic of this sprite.\n        :type head_sprite_id: int\n        :param filename: Name of the sprite and definition file.\n        :type filename: str\n        :param full_data_set: ConverterObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.converter_object.ConverterObjectContainer\n        \"\"\"\n\n        self.head_sprite_id = head_sprite_id\n        self.filename = filename\n        self.data = full_data_set\n\n        self.metadata = None\n\n        # Depending on the amounts of references:\n        # 0 = do not convert;\n        # 1 = store with GameEntity;\n        # >1 = store in 'shared' resources;\n        self._refs = []\n\n    def add_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Add an object that is referencing this sprite.\n        \"\"\"\n        self._refs.append(referer)\n\n    def get_filename(self) -> str:\n        \"\"\"\n        Returns the desired filename of the sprite.\n        \"\"\"\n        return self.filename\n\n    def get_graphics(self) -> list[GenieGraphic]:\n        \"\"\"\n        Return all graphics referenced by this sprite.\n        \"\"\"\n        graphics = [self.data.genie_graphics[self.head_sprite_id]]\n        graphics.extend(self.data.genie_graphics[self.head_sprite_id].get_subgraphics())\n\n        # Only consider existing graphics\n        existing_graphics = []\n        for graphic in graphics:\n            if graphic.exists:\n                existing_graphics.append(graphic)\n\n        return existing_graphics\n\n    def get_id(self) -> int:\n        \"\"\"\n        Returns the head sprite ID of the sprite.\n        \"\"\"\n        return self.head_sprite_id\n\n    def get_relative_sprite_location(self) -> str:\n        \"\"\"\n        Return the sprite file location relative to where the file\n        is expected to be in the modpack.\n        \"\"\"\n        if len(self._refs) > 1:\n            return f\"../shared/graphics/{self.filename}.sprite\"\n\n        if len(self._refs) == 1:\n            return f\"./graphics/{self.filename}.sprite\"\n\n        return None\n\n    def remove_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Remove an object that is referencing this sprite.\n        \"\"\"\n        self._refs.remove(referer)\n\n    def resolve_graphics_location(self) -> str:\n        \"\"\"\n        Returns the planned location in the modpack of all image files\n        referenced by the sprite.\n        \"\"\"\n        location_dict = {}\n\n        for graphic in self.get_graphics():\n            if graphic.is_shared():\n                location_dict.update({graphic.get_id(): \"data/game_entity/shared/graphics/\"})\n\n            else:\n                location_dict.update({graphic.get_id(): self.resolve_sprite_location()})\n\n        return location_dict\n\n    def resolve_sprite_location(self) -> str:\n        \"\"\"\n        Returns the planned location of the definition file in the modpack.\n        \"\"\"\n        if len(self._refs) > 1:\n            return \"data/game_entity/shared/graphics/\"\n\n        if len(self._refs) == 1:\n            return f\"{self._refs[0].get_file_location()[0]}{'graphics/'}\"\n\n        return None\n\n    def __repr__(self):\n        return f\"CombinedSprite<{self.head_sprite_id}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/combined_terrain.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nReferences a graphic in the game that has to be converted.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectContainer\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject\n    from openage.convert.entity_object.conversion.aoc.genie_terrain import GenieTerrainObject\n\n\nclass CombinedTerrain:\n    \"\"\"\n    Collection of terrain information for openage files.\n\n    This will become a spritesheet texture with a terrain file.\n    \"\"\"\n\n    __slots__ = ('terrain_id', 'filename', 'data', 'metadata', '_refs')\n\n    def __init__(\n        self,\n        terrain_id: int,\n        filename: str,\n        full_data_set: ConverterObjectContainer\n    ):\n        \"\"\"\n        Creates a new CombinedTerrain instance.\n\n        :param terrain_id: The index of the terrain that references the sprite.\n        :type terrain_id: int\n        :param filename: Name of the terrain and definition file.\n        :type filename: str\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.converter_object.ConverterObjectContainer\n        \"\"\"\n\n        self.terrain_id = terrain_id\n        self.filename = filename\n        self.data = full_data_set\n\n        self.metadata = None\n\n        # Depending on the amounts of references:\n        # 0 = do not convert;\n        # >=1 = store with first occuring Terrain;\n        self._refs = []\n\n    def add_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Add an object that is referencing this terrain.\n        \"\"\"\n        self._refs.append(referer)\n\n    def get_filename(self) -> str:\n        \"\"\"\n        Returns the destination filename of the terrain.\n        \"\"\"\n        return self.filename\n\n    def get_terrain(self) -> GenieTerrainObject:\n        \"\"\"\n        Returns the terrain referenced by this terrain sprite.\n        \"\"\"\n        return self.data.genie_terrains[self.terrain_id]\n\n    def get_id(self) -> int:\n        \"\"\"\n        Returns the terrain id of the terrain.\n        \"\"\"\n        return self.terrain_id\n\n    def get_relative_terrain_location(self) -> str:\n        \"\"\"\n        Return the terrain file location relative to where the file\n        is expected to be in the modpack.\n        \"\"\"\n        if len(self._refs) >= 1:\n            return f\"./graphics/{self.filename}.terrain\"\n\n        return None\n\n    def remove_reference(self, referer: ConverterObject) -> None:\n        \"\"\"\n        Remove an object that is referencing this sprite.\n        \"\"\"\n        self._refs.remove(referer)\n\n    def resolve_graphics_location(self) -> str:\n        \"\"\"\n        Returns the planned location in the modpack of the image file\n        referenced by the terrain file.\n        \"\"\"\n        return self.resolve_terrain_location()\n\n    def resolve_terrain_location(self) -> str:\n        \"\"\"\n        Returns the planned location of the definition file in the modpack.\n        \"\"\"\n        if len(self._refs) >= 1:\n            return f\"{self._refs[0].get_file_location()[0]}{'graphics/'}\"\n\n        return None\n\n    def __repr__(self):\n        return f\"CombinedTerrain<{self.terrain_id}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/converter_object.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n# pylint: disable=too-many-instance-attributes,too-many-branches,too-few-public-methods\n\n\"\"\"\nObjects that represent data structures in the original game.\n\nThese are simple containers that can be processed by the converter.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\n\nfrom openage.convert.value_object.read.dynamic_loader import DynamicLoader\n\nfrom ....nyan.nyan_structs import NyanObject, NyanPatch, NyanPatchMember, MemberOperator\nfrom ...value_object.conversion.forward_ref import ForwardRef\nfrom ...value_object.read.value_members import NoDiffMember, ValueMember\nfrom .combined_sound import CombinedSound\nfrom .combined_sprite import CombinedSprite\nfrom .combined_terrain import CombinedTerrain\n\n\nclass ConverterObject:\n    \"\"\"\n    Storage object for data objects in the to-be-converted games.\n    \"\"\"\n\n    __slots__ = ('obj_id', 'members')\n\n    def __init__(\n        self,\n        obj_id: typing.Union[str, int],\n        members: dict[str, ValueMember] = None\n    ):\n        \"\"\"\n        Creates a new ConverterObject.\n\n        :param obj_id: An identifier for the object (as a string or int)\n        :type obj_id: str|int\n        :param members: An already existing member dict.\n        :type members: dict[str, ValueMember]\n        \"\"\"\n        self.obj_id = obj_id\n\n        self.members = {}\n\n        if members:\n            if isinstance(members, DynamicLoader):\n                self.members = members\n\n            elif all(isinstance(member, ValueMember) for member in members.values()):\n                self.members.update(members)\n\n            else:\n                raise TypeError(\"members must be an instance of ValueMember\")\n\n    def get_id(self) -> typing.Union[str, int]:\n        \"\"\"\n        Returns the object's ID.\n        \"\"\"\n        return self.obj_id\n\n    def add_member(self, member: ValueMember) -> None:\n        \"\"\"\n        Adds a member to the object.\n        \"\"\"\n        self.members.update({member.name: member})\n\n    def add_members(self, members: dict[str, ValueMember]) -> None:\n        \"\"\"\n        Adds multiple members to the object.\n        \"\"\"\n        self.members.update(members)\n\n    def get_member(self, member_id: str) -> ValueMember:\n        \"\"\"\n        Returns a member of the object.\n        \"\"\"\n        try:\n            return self.members[member_id]\n\n        except KeyError as err:\n            raise KeyError(f\"{self} has no attribute: {member_id}\") from err\n\n    def has_member(self, member_id: str) -> bool:\n        \"\"\"\n        Returns True if the object has a member with the specified name.\n        \"\"\"\n        return member_id in self.members\n\n    def remove_member(self, member_id: str) -> None:\n        \"\"\"\n        Removes a member from the object.\n        \"\"\"\n        self.members.pop(member_id, None)\n\n    def short_diff(self, other: ConverterObject) -> ConverterObject:\n        \"\"\"\n        Returns the obj_diff between two objects as another ConverterObject.\n\n        The object created by short_diff() only contains members\n        that are different. It does not contain NoDiffMembers.\n        \"\"\"\n        if type(self) is not type(other):\n            raise TypeError(f\"type {type(self)} cannot be diffed with type {type(other)}\")\n\n        obj_diff = {}\n\n        for member_id, member in self.members.items():\n            member_diff = member.diff(other.get_member(member_id))\n\n            if not isinstance(member_diff, NoDiffMember):\n                obj_diff.update({member_id: member_diff})\n\n        return ConverterObject(f\"{self.obj_id}-{other.get_id()}-sdiff\", members=obj_diff)\n\n    def diff(self, other: ConverterObject) -> ConverterObject:\n        \"\"\"\n        Returns the obj_diff between two objects as another ConverterObject.\n        \"\"\"\n        if type(self) is not type(other):\n            raise TypeError(f\"type {type(self)} cannot be diffed with type {type(other)}\")\n\n        obj_diff = {}\n\n        for member_id, member in self.members.items():\n            obj_diff.update({member_id: member.diff(other.get_member(member_id))})\n\n        return ConverterObject(f\"{self.obj_id}-{other.get_id()}-diff\", members=obj_diff)\n\n    def __getitem__(self, key):\n        \"\"\"\n        Short command for getting a member of the object.\n        \"\"\"\n        return self.get_member(key)\n\n    def __repr__(self):\n        raise NotImplementedError(\n            f\"return short description of the object {type(self)}\")\n\n\nclass ConverterObjectGroup:\n    \"\"\"\n    A group of objects that are connected together in some way\n    and need each other for conversion. ConverterObjectGroup\n    instances are converted to the nyan API.\n    \"\"\"\n\n    __slots__ = ('group_id', 'raw_api_objects', 'raw_member_pushs')\n\n    def __init__(\n        self,\n        group_id: typing.Union[str, int],\n        raw_api_objects: list[RawAPIObject] = None\n    ):\n        \"\"\"\n        Creates a new ConverterObjectGroup.\n\n        :paran group_id:  An identifier for the object group (as a string or int)\n        :param raw_api_objects: A list of raw API objects. These will become\n                                proper API objects during conversion.\n        \"\"\"\n        self.group_id = group_id\n\n        # Stores the objects that will later be converted to nyan objects\n        # This uses the RawAPIObject's ids as keys.\n        self.raw_api_objects = {}\n\n        # Stores push values to members of other converter object groups\n        self.raw_member_pushs = []\n\n        if raw_api_objects:\n            self._create_raw_api_object_dict(raw_api_objects)\n\n    def get_id(self) -> typing.Union[str, int]:\n        \"\"\"\n        Returns the object group's ID.\n        \"\"\"\n        return self.group_id\n\n    def add_raw_api_object(self, subobject: RawAPIObject) -> None:\n        \"\"\"\n        Adds a subobject to the object.\n        \"\"\"\n        key = subobject.get_id()\n        self.raw_api_objects.update({key: subobject})\n\n    def add_raw_api_objects(self, subobjects: list[RawAPIObject]) -> None:\n        \"\"\"\n        Adds several subobject to the object.\n        \"\"\"\n        for subobject in subobjects:\n            self.add_raw_api_object(subobject)\n\n    def add_raw_member_push(self, push_object: RawMemberPush) -> None:\n        \"\"\"\n        Adds a RawPushMember to the object.\n        \"\"\"\n        self.raw_member_pushs.append(push_object)\n\n    def create_nyan_objects(self) -> None:\n        \"\"\"\n        Creates nyan objects from the existing raw API objects.\n        \"\"\"\n        patch_objects = []\n        for raw_api_object in self.raw_api_objects.values():\n            raw_api_object.create_nyan_object()\n\n            if raw_api_object.is_patch():\n                patch_objects.append(raw_api_object)\n\n        for patch_object in patch_objects:\n            patch_object.link_patch_target()\n\n    def create_nyan_members(self) -> None:\n        \"\"\"\n        Fill nyan members of all raw API objects.\n        \"\"\"\n        for raw_api_object in self.raw_api_objects.values():\n            raw_api_object.create_nyan_members()\n\n    def check_readiness(self) -> None:\n        \"\"\"\n        check if all nyan objects in the group are ready for export.\n        \"\"\"\n        for raw_api_object in self.raw_api_objects.values():\n            if not raw_api_object.is_ready():\n                if not raw_api_object.nyan_object:\n                    raise ValueError(f\"{raw_api_object}: object is not ready for export: \"\n                                     \"Nyan object not initialized.\")\n\n                uninit_members = raw_api_object.get_nyan_object().get_uninitialized_members()\n                concat_names = \", \".join(f\"'{member.get_name()}'\" for member in uninit_members)\n                raise ValueError(f\"{raw_api_object}: object is not ready for export: \"\n                                 f\"Member(s) {concat_names} not initialized.\")\n\n    def execute_raw_member_pushs(self) -> None:\n        \"\"\"\n        Extend raw members of referenced raw API objects.\n        \"\"\"\n        for push_object in self.raw_member_pushs:\n            forward_ref = push_object.get_object_target()\n            raw_api_object = forward_ref.resolve_raw()\n            raw_api_object.extend_raw_member(push_object.get_member_name(),\n                                             push_object.get_push_value(),\n                                             push_object.get_member_origin())\n\n    def get_raw_api_object(self, obj_id: str) -> RawAPIObject:\n        \"\"\"\n        Returns a subobject of the object.\n        \"\"\"\n        try:\n            return self.raw_api_objects[obj_id]\n\n        except KeyError as missing_raw_api_obj:\n            raise KeyError(f\"{repr(self)}: Could not find raw API object \"\n                           f\"with obj_id {obj_id}\") from missing_raw_api_obj\n\n    def get_raw_api_objects(self) -> dict[str, RawAPIObject]:\n        \"\"\"\n        Returns all raw API objects.\n        \"\"\"\n        return self.raw_api_objects\n\n    def has_raw_api_object(self, obj_id: typing.Union[str, int]) -> bool:\n        \"\"\"\n        Returns True if the object has a subobject with the specified ID.\n        \"\"\"\n        return obj_id in self.raw_api_objects\n\n    def remove_raw_api_object(self, obj_id: typing.Union[str, int]) -> None:\n        \"\"\"\n        Removes a subobject from the object.\n        \"\"\"\n        self.raw_api_objects.pop(obj_id)\n\n    def _create_raw_api_object_dict(self, subobject_list: list[RawAPIObject]) -> None:\n        \"\"\"\n        Creates the dict from the subobject list passed to __init__.\n        \"\"\"\n        for subobject in subobject_list:\n            self.add_raw_api_object(subobject)\n\n    def __repr__(self):\n        return f\"ConverterObjectGroup<{self.group_id}>\"\n\n\nclass RawAPIObject:\n    \"\"\"\n    An object that contains all the necessary information to create\n    a nyan API object. Members are stored as (membername, value) pairs.\n    Values refer either to primitive values (int, float, str),\n    forward references to objects or expected media files.\n    The 'expected' values two have to be resolved in an additional step.\n    \"\"\"\n\n    __slots__ = ('obj_id', 'name', 'api_ref', 'raw_members', 'raw_parents',\n                 '_location', '_filename', 'nyan_object', '_patch_target',\n                 'raw_patch_parents')\n\n    def __init__(\n        self,\n        obj_id: typing.Union[str, int],\n        name: str,\n        api_ref: dict[str, NyanObject],\n        location: typing.Union[str, ForwardRef] = \"\"\n    ):\n        \"\"\"\n        Creates a raw API object.\n\n        :param obj_id: Unique identifier for the raw API object.\n        :type obj_id: str\n        :param name: Name of the nyan object created from the raw API object.\n        :type name: str\n        :param api_ref: The openage API objects used as reference for creating the nyan object.\n        :type api_ref: dict\n        :param location: Relative path of the nyan file in the modpack or another raw API object.\n        :type location: str, .forward_ref.ForwardRef\n        \"\"\"\n\n        self.obj_id = obj_id\n        self.name = name\n\n        self.api_ref = api_ref\n\n        self.raw_members = []\n        self.raw_parents = []\n        self.raw_patch_parents = []\n\n        self._location = location\n        self._filename = None\n\n        self.nyan_object = None\n        self._patch_target = None\n\n    def add_raw_member(\n        self,\n        name: str,\n        value: typing.Union[int, float, bool, str, list, dict, ForwardRef],\n        origin: str\n    ) -> None:\n        \"\"\"\n        Adds a raw member to the object.\n\n        :param name: Name of the member (has to be a valid inherited member name).\n        :type name: str\n        :param value: Value of the member.\n        :type value: int, float, bool, str, list, dict, ForwardRef\n        :param origin: fqon of the object from which the member was inherited.\n        :type origin: str\n        \"\"\"\n        self.raw_members.append((name, value, origin))\n\n    def add_raw_patch_member(\n        self,\n        name: str,\n        value: typing.Union[int, float, bool, str, list, dict, ForwardRef],\n        origin: str,\n        operator: MemberOperator\n    ) -> None:\n        \"\"\"\n        Adds a raw patch member to the object.\n\n        :param name: Name of the member (has to be a valid target member name).\n        :type name: str\n        :param value: Value of the member.\n        :type value: int, float, bool, str, list, dict, ForwardRef\n        :param origin: fqon of the object from which the member was inherited.\n        :type origin: str\n        :param operator: the operator for the patched member\n        :type operator: MemberOperator\n        \"\"\"\n        self.raw_members.append((name, value, origin, operator))\n\n    def add_raw_parent(self, parent_id: str) -> None:\n        \"\"\"\n        Adds a raw parent to the object.\n\n        :param parent_id: fqon of the parent in the API object dictionary\n        :type parent_id: str\n        \"\"\"\n        self.raw_parents.append(parent_id)\n\n    def add_raw_patch_parent(self, parent_id: str) -> None:\n        \"\"\"\n        Adds a raw patch parent to the object.\n\n        :param parent_id: fqon of the parent in the API object dictionary\n        :type parent_id: str\n        \"\"\"\n        self.raw_patch_parents.append(parent_id)\n\n    def extend_raw_member(\n        self,\n        name: str,\n        push_value: list,\n        origin: str\n    ) -> None:\n        \"\"\"\n        Extends a raw member value if the value is a list.\n\n        :param name: Name of the member (has to be a valid inherited member name).\n        :type name: str\n        :param push_value: Extended value of the member.\n        :type push_value: list\n        :param origin: fqon of the object from which the member was inherited.\n        :type origin: str\n        \"\"\"\n        for raw_member in self.raw_members:\n            member_name = raw_member[0]\n            member_value = raw_member[1]\n            member_origin = raw_member[2]\n\n            if name == member_name and member_origin == origin:\n                member_value.extend(push_value)\n                break\n\n        else:\n            raise ValueError(f\"{repr(self)}: Cannot extend raw member {name} \"\n                             f\"with origin {origin}: member not found\")\n\n    def create_nyan_object(self) -> None:\n        \"\"\"\n        Create the nyan object for this raw API object. Members have to be created separately.\n        \"\"\"\n        parents = []\n        for raw_parent in self.raw_parents:\n            parents.append(self.api_ref[raw_parent])\n\n        if self.is_patch():\n            self.nyan_object = NyanPatch(self.name, parents)\n\n        else:\n            self.nyan_object = NyanObject(self.name, parents)\n\n    def create_nyan_members(self) -> None:\n        \"\"\"\n        Fills the nyan object members with values from the raw members.\n        References to nyan objects or media files with be resolved.\n        The nyan object has to be created before this function can be called.\n        \"\"\"\n        if self.nyan_object is None:\n            raise RuntimeError(f\"{repr(self)}: nyan object needs to be created before \"\n                               \"member values can be assigned\")\n\n        for raw_member in self.raw_members:\n            member_name = raw_member[0]\n            member_value = raw_member[1]\n            member_origin = self.api_ref[raw_member[2]]\n            member_operator = None\n            if self.is_patch():\n                member_operator = raw_member[3]\n\n            # Resolve forward references to objects/assets and trim floats\n            member_value = self._resolve_raw_values(member_value)\n\n            if self.is_patch():\n                nyan_member = NyanPatchMember(member_name, self.nyan_object.get_target(),\n                                              member_origin, member_value, member_operator)\n                self.nyan_object.add_member(nyan_member)\n\n            else:\n                nyan_member = self.nyan_object.get_member_by_name(member_name, member_origin)\n                nyan_member.set_value(member_value, MemberOperator.ASSIGN)\n\n    def link_patch_target(self) -> None:\n        \"\"\"\n        Set the target NyanObject for a patch.\n        \"\"\"\n        if not self.is_patch():\n            raise TypeError(f\"Cannot link patch target: {self} is not a patch\")\n\n        if isinstance(self._patch_target, ForwardRef):\n            target = self._patch_target.resolve()\n\n        else:\n            target = self._patch_target\n\n        self.nyan_object.set_target(target)\n\n    def get_filename(self) -> str:\n        \"\"\"\n        Returns the filename of the raw API object.\n        \"\"\"\n        return self._filename\n\n    def get_file_location(self) -> str:\n        \"\"\"\n        Returns a tuple with\n            1. the relative path to the directory\n            2. the filename\n        where the nyan object will be stored.\n\n        This method can be called instead of get_location() when\n        you are unsure whether the nyan object will be nested.\n        \"\"\"\n        if isinstance(self._location, ForwardRef):\n            # Work upwards until we find the root object\n            nesting_raw_api_object = self._location.resolve_raw()\n            nesting_location = nesting_raw_api_object.get_location()\n\n            while isinstance(nesting_location, ForwardRef):\n                nesting_raw_api_object = nesting_location.resolve_raw()\n                nesting_location = nesting_raw_api_object.get_location()\n\n            return (nesting_location, nesting_raw_api_object.get_filename())\n\n        return (self._location, self._filename)\n\n    def get_id(self) -> typing.Union[str, int]:\n        \"\"\"\n        Returns the ID of the raw API object.\n        \"\"\"\n        return self.obj_id\n\n    def get_location(self) -> typing.Union[str, ForwardRef]:\n        \"\"\"\n        Returns the relative path to a directory or an ForwardRef\n        to another RawAPIObject.\n        \"\"\"\n        return self._location\n\n    def get_nyan_object(self) -> NyanObject:\n        \"\"\"\n        Returns the nyan API object for the raw API object.\n        \"\"\"\n        if self.nyan_object:\n            return self.nyan_object\n\n        raise RuntimeError(f\"nyan object for {self} has not been created yet\")\n\n    def is_ready(self) -> bool:\n        \"\"\"\n        Returns whether the object is ready to be exported.\n        \"\"\"\n        return self.nyan_object is not None and not self.nyan_object.is_abstract()\n\n    def is_patch(self) -> bool:\n        \"\"\"\n        Returns True if the object is a patch.\n        \"\"\"\n        return self._patch_target is not None\n\n    def set_filename(self, filename: str, suffix: str = \"nyan\") -> None:\n        \"\"\"\n        Set the filename of the resulting nyan file.\n\n        :param filename: File name prefix (without extension).\n        :type filename: str\n        :param suffix: File extension (defaults to \"nyan\")\n        :type suffix: str\n        \"\"\"\n        self._filename = f\"{filename}.{suffix}\"\n\n    def set_location(self, location: typing.Union[str, ForwardRef]) -> None:\n        \"\"\"\n        Set the relative location of the object in a modpack. This must\n        be a path to a nyan file or an ForwardRef to a nyan object.\n\n        :param location: Relative path of the nyan file in the modpack or\n                         a forward reference to another raw API object.\n        :type location: str, ForwardRef\n        \"\"\"\n        self._location = location\n\n    def set_patch_target(self, target: typing.Union[ForwardRef, NyanObject]):\n        \"\"\"\n        Set an ForwardRef as a target for this object. If this\n        is done, the RawAPIObject will be converted to a patch.\n\n        :param target: A forward reference to another raw API object or a nyan object.\n        :type target: ForwardRef, NyanObject\n        \"\"\"\n        self._patch_target = target\n\n    @staticmethod\n    def _resolve_raw_value(value) -> typing.Union[NyanObject, str, float]:\n        \"\"\"\n        Check if a raw member value contains a reference to a resource (nyan\n        objects or asset files), resolve the reference to a nyan-compatible value\n        and return it.\n\n        If the value contains no resource reference, it is returned as-is.\n\n        :param value: Raw member value.\n        :return: Value usable by a nyan object or nyan member.\n        \"\"\"\n        if isinstance(value, ForwardRef):\n            # Object references\n            return value.resolve()\n\n        if isinstance(value, CombinedSprite):\n            return value.get_relative_sprite_location()\n\n        if isinstance(value, CombinedTerrain):\n            return value.get_relative_terrain_location()\n\n        if isinstance(value, CombinedSound):\n            return value.get_relative_file_location()\n\n        if isinstance(value, float):\n            # Round floats to 6 decimal places for increased readability\n            # should have no effect on balance, hopefully\n            return round(value, ndigits=6)\n\n        return value\n\n    def _resolve_raw_values(self, values):\n        \"\"\"\n        Convert a raw member values to nyan-compatible values by resolving\n        contained references to resources.\n\n        :param values: Raw member values.\n        :return: Value usable by a nyan object or nyan member.\n        \"\"\"\n        if isinstance(values, list):\n            # Sets or orderedsets\n            temp_values = []\n            for temp_value in values:\n                temp_values.append(self._resolve_raw_value(temp_value))\n\n            return temp_values\n\n        if isinstance(values, dict):\n            # Dicts\n            temp_values = {}\n            for key, val in values.items():\n                temp_values.update({\n                    self._resolve_raw_value(key): self._resolve_raw_value(val)\n                })\n\n            return temp_values\n\n        return self._resolve_raw_value(values)\n\n    def __repr__(self):\n        return f\"RawAPIObject<{self.obj_id}>\"\n\n\nclass RawMemberPush:\n    \"\"\"\n    An object that contains additional values for complex members\n    in raw API objects (lists or sets). Pushing these values to the\n    raw API object will extennd the list or set. The values should be\n    pushed to the raw API objects before their nyan members are created.\n    \"\"\"\n\n    __slots__ = ('forward_ref', 'member_name', 'member_origin', 'push_value')\n\n    def __init__(\n        self,\n        forward_ref: ForwardRef,\n        member_name: str,\n        member_origin: str,\n        push_value: list\n    ):\n        \"\"\"\n        Creates a new member push.\n\n        :param forward_ref: forward reference of the RawAPIObject.\n        :type forward_ref: ForwardRef\n        :param member_name: Name of the member that is extended.\n        :type member_name: str\n        :param member_origin: Fqon of the object the member was inherited from.\n        :type member_origin: str\n        :param push_value: Value that extends the existing member value.\n        :type push_value: list\n        \"\"\"\n        self.forward_ref = forward_ref\n        self.member_name = member_name\n        self.member_origin = member_origin\n        self.push_value = push_value\n\n    def get_object_target(self) -> ForwardRef:\n        \"\"\"\n        Returns the forward reference for the push target.\n        \"\"\"\n        return self.forward_ref\n\n    def get_member_name(self) -> str:\n        \"\"\"\n        Returns the name of the member that is extended.\n        \"\"\"\n        return self.member_name\n\n    def get_member_origin(self) -> str:\n        \"\"\"\n        Returns the fqon of the member's origin.\n        \"\"\"\n        return self.member_origin\n\n    def get_push_value(self) -> list:\n        \"\"\"\n        Returns the value that extends the member's existing value.\n        \"\"\"\n        return self.push_value\n\n\nclass ConverterObjectContainer:\n    \"\"\"\n    A conainer for all ConverterObject instances in a converter process.\n\n    It is recommended to create one ConverterObjectContainer for everything\n    and pass the reference around.\n    \"\"\"\n\n    def __repr__(self):\n        return \"ConverterObjectContainer\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/modpack.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDefines a modpack that can be exported.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom ..export.data_definition import DataDefinition\nfrom ..export.formats.modpack_info import ModpackInfo\nfrom ..export.formats.modpack_manifest import ManifestFile\nfrom ..export.media_export_request import MediaExportRequest\nfrom ..export.metadata_export import MetadataExport\n\n\nclass Modpack:\n    \"\"\"\n    A collection of data and media files.\n    \"\"\"\n\n    def __init__(self, name: str):\n\n        self.name = name\n\n        # Definition file\n        self.info = ModpackInfo(\"\", \"modpack.toml\")\n\n        # Manifest file\n        self.manifest = ManifestFile(\"\", \"manifest.toml\")\n\n        # Data/media export\n        self.data_export_files: list[DataDefinition] = []\n        self.media_export_files: list[MediaExportRequest] = {}\n        self.metadata_files: list[MetadataExport] = []\n\n    def add_data_export(self, export_file: DataDefinition) -> None:\n        \"\"\"\n        Add a data file to the modpack for exporting.\n        \"\"\"\n        if not isinstance(export_file, DataDefinition):\n            raise TypeError(f\"{repr(self)}: export file must be of type DataDefinition \"\n                            f\"not {type(export_file)}\")\n\n        self.data_export_files.append(export_file)\n\n    def add_media_export(self, export_request: MediaExportRequest) -> None:\n        \"\"\"\n        Add a media export request to the modpack.\n        \"\"\"\n        if not isinstance(export_request, MediaExportRequest):\n            raise TypeError(f\"{repr(self)}: export file must be of type MediaExportRequest \"\n                            f\"not {type(export_request)}\")\n\n        if export_request.get_type() in self.media_export_files:\n            self.media_export_files[export_request.get_type()].append(export_request)\n\n        else:\n            self.media_export_files[export_request.get_type()] = [export_request]\n\n    def add_metadata_export(self, export_file: MetadataExport) -> None:\n        \"\"\"\n        Add a metadata file to the modpack for exporting.\n        \"\"\"\n        if not isinstance(export_file, MetadataExport):\n            raise TypeError(f\"{repr(self)}: export file must be of type MetadataExport \"\n                            f\"not {type(export_file)}\")\n\n        self.metadata_files.append(export_file)\n\n    def get_info(self) -> ModpackInfo:\n        \"\"\"\n        Return the modpack definition file.\n        \"\"\"\n        return self.info\n\n    def get_data_files(self) -> list[DataDefinition]:\n        \"\"\"\n        Returns the data files for exporting.\n        \"\"\"\n        return self.data_export_files\n\n    def get_media_files(self) -> list[MediaExportRequest]:\n        \"\"\"\n        Returns the media requests for exporting.\n        \"\"\"\n        return self.media_export_files\n\n    def get_metadata_files(self) -> list[MetadataExport]:\n        \"\"\"\n        Returns the metadata exports.\n        \"\"\"\n        return self.metadata_files\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/ror/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tgenie_sound.py\n\tgenie_tech.py\n\tgenie_unit.py\n)\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/ror/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires I.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/ror/genie_sound.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for sounds from RoR.\n\nBased on the classes from the AoC converter.\n\"\"\"\nfrom __future__ import annotations\n\n\nfrom ..aoc.genie_sound import GenieSound\n\n\nclass RoRSound(GenieSound):\n    \"\"\"\n    Sound definition from a .dat file. Some methods are reimplemented as RoR does not\n    have all features from AoE2.\n    \"\"\"\n\n    def get_sounds(self, civ_id: int = -1) -> list[int]:\n        \"\"\"\n        Does not search for the civ id because RoR does not have\n        a .dat entry for that.\n        \"\"\"\n        sound_ids = []\n        sound_items = self[\"sound_items\"].value\n        for item in sound_items:\n            sound_id = item[\"resource_id\"].value\n            sound_ids.append(sound_id)\n\n        return sound_ids\n\n    def __repr__(self):\n        return f\"RoRSouns<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/ror/genie_tech.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for techs from RoR.\n\nBased on the classes from the AoC converter.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..aoc.genie_tech import StatUpgrade, AgeUpgrade, UnitLineUpgrade, \\\n    BuildingLineUpgrade, UnitUnlock, BuildingUnlock\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.ror.genie_unit import RoRUnitLineGroup\n\n\nclass RoRStatUpgrade(StatUpgrade):\n    \"\"\"\n    Upgrades attributes of units/buildings or other stats in the game.\n    \"\"\"\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"RoRStatUpgrade<{self.get_id()}>\"\n\n\nclass RoRAgeUpgrade(AgeUpgrade):\n    \"\"\"\n    Researches a new Age.\n    \"\"\"\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"RoRAgeUpgrade<{self.get_id()}>\"\n\n\nclass RoRUnitLineUpgrade(UnitLineUpgrade):\n    \"\"\"\n    Upgrades a unit in a line.\n    \"\"\"\n\n    def get_upgraded_line(self) -> RoRUnitLineGroup:\n        return self.data.unit_lines[self.unit_line_id]\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"RoRUnitLineUpgrade<{self.get_id()}>\"\n\n\nclass RoRBuildingLineUpgrade(BuildingLineUpgrade):\n    \"\"\"\n    Upgrades a building in a line.\n    \"\"\"\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"RoRBuildingLineUpgrade<{self.get_id()}>\"\n\n\nclass RoRUnitUnlock(UnitUnlock):\n    \"\"\"\n    Unlocks units.\n    \"\"\"\n\n    def is_unique(self) -> bool:\n        return False\n\n    def get_unlocked_line(self) -> RoRUnitLineGroup:\n        \"\"\"\n        Returns the line that is unlocked by this tech.\n        \"\"\"\n        return self.data.unit_lines[self.line_id]\n\n    def __repr__(self):\n        return f\"RoRUnitUnlock<{self.get_id()}>\"\n\n\nclass RoRBuildingUnlock(BuildingUnlock):\n    \"\"\"\n    Unlocks buildings.\n    \"\"\"\n\n    def is_unique(self) -> bool:\n        return False\n\n    def __repr__(self):\n        return f\"RoRBuildingUnlock<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/ror/genie_unit.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains structures and API-like objects for game entities from RoR.\n\nBased on the classes from the AoC converter.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..aoc.genie_unit import GenieUnitLineGroup, GenieBuildingLineGroup, \\\n    GenieAmbientGroup, GenieVariantGroup, GenieUnitTaskGroup, \\\n    GenieVillagerGroup, GenieGarrisonMode\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass RoRUnitLineGroup(GenieUnitLineGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that form an \"upgrade line\"\n    in Age of Empires I. Some methods are reimplemented as RoR does not\n    have all features from AoE2.\n\n    Example: Clubman-> Axeman\n    \"\"\"\n\n    __slots__ = ('enabling_research_id',)\n\n    def __init__(\n        self,\n        line_id: int,\n        enabling_research_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new RoR game entity line.\n\n        :param line_id: Internal line obj_id in the .dat file.\n        :param enabling_research_id: ID of the tech that enables this unit.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, full_data_set)\n\n        # Saved for RoR because there's no easy way to detect it with a connection\n        self.enabling_research_id = enabling_research_id\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Only transport ships can garrison in RoR.\n\n        :returns: True if the unit has the unload command (ID: 12).\n        \"\"\"\n        return self.has_command(12)\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :returns: True if the unit class is 10.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"unit_class\"].value == 10\n\n    def get_garrison_mode(self, civ_id: int = -1) -> typing.Union[GenieGarrisonMode, None]:\n        \"\"\"\n        Checks only for transport boat commands.\n\n        :returns: The garrison mode of the line.\n        :rtype: GenieGarrisonMode\n        \"\"\"\n        if self.has_command(12):\n            return GenieGarrisonMode.TRANSPORT\n\n        return None\n\n    def get_enabling_research_id(self) -> int:\n        return self.enabling_research_id\n\n    def __repr__(self):\n        return f\"RoRUnitLineGroup<{self.get_id()}>\"\n\n\nclass RoRBuildingLineGroup(GenieBuildingLineGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that represent a building\n    in Age of Empires 1. Some methods are reimplemented as RoR does not\n    have all features from AoE2.\n\n    Example2: WatchTower->SentryTower->GuardTower->BallistaTower\n    \"\"\"\n\n    __slots__ = ('enabling_research_id',)\n\n    def __init__(\n        self,\n        line_id: int,\n        enabling_research_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new RoR game entity line.\n\n        :param line_id: Internal line obj_id in the .dat file.\n        :param enabling_research_id: ID of the tech that enables this unit.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, full_data_set)\n\n        # Saved for RoR because there's no easy way to detect it with a connection\n        self.enabling_research_id = enabling_research_id\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :returns: True if the unit class is 10.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"unit_class\"].value == 10\n\n    def get_garrison_mode(self, civ_id: int = -1) -> None:\n        return None\n\n    def get_enabling_research_id(self) -> int:\n        return self.enabling_research_id\n\n    def __repr__(self):\n        return f\"RoRBuildingLineGroup<{self.get_id()}>\"\n\n\nclass RoRAmbientGroup(GenieAmbientGroup):\n    \"\"\"\n    One Genie unit that is an ambient scenery object.\n    Mostly for resources, specifically trees. For these objects\n    every frame in their graphics file is a variant.\n\n    Example: Trees, Gold mines, Sign\n    \"\"\"\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :returns: True if the unit class is 10.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"unit_class\"].value == 10\n\n    def get_garrison_mode(self, civ_id: int = -1) -> None:\n        return None\n\n    def __repr__(self):\n        return f\"RoRAmbientGroup<{self.get_id()}>\"\n\n\nclass RoRVariantGroup(GenieVariantGroup):\n    \"\"\"\n    Collection of multiple Genie units that are variants of the same game entity.\n    Mostly for cliffs and ambient terrain objects.\n\n    Example: Cliffs, flowers, mountains\n    \"\"\"\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        return False\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :returns: True if the unit class is 10.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"unit_class\"].value == 10\n\n    def get_garrison_mode(self, civ_id: int = -1) -> None:\n        return None\n\n    def __repr__(self):\n        return f\"RoRVariantGroup<{self.get_id()}>\"\n\n\nclass RoRUnitTaskGroup(GenieUnitTaskGroup):\n    \"\"\"\n    Collection of genie units that have the same task group. This is only\n    the villager unit in RoR.\n\n    Example: Villager\n    \"\"\"\n\n    # Female villagers do not exist in RoR\n    female_line_id = -1\n\n    __slots__ = ('enabling_research_id',)\n\n    def __init__(\n        self,\n        line_id: int,\n        task_group_id: int,\n        enabling_research_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new RoR task group.\n\n        :param line_id: Internal task group obj_id in the .dat file.\n        :param task_group_id: ID of the task group.\n        :param enabling_research_id: ID of the tech that enables this unit.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, task_group_id, full_data_set)\n\n        # Saved for RoR because there's no easy way to detect it with a connection\n        self.enabling_research_id = enabling_research_id\n\n    def is_garrison(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Only transport ships can garrison in RoR.\n\n        :returns: True if the unit has the unload command (ID: 12).\n        \"\"\"\n        return self.has_command(12)\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        \"\"\"\n        Checks whether the group has a passable hitbox.\n\n        :returns: True if the unit class is 10.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"unit_class\"].value == 10\n\n    def get_garrison_mode(self, civ_id: int = -1) -> typing.Union[GenieGarrisonMode, None]:\n        \"\"\"\n        Checks only for transport boat commands.\n\n        :returns: The garrison mode of the line.\n        :rtype: GenieGarrisonMode\n        \"\"\"\n        if self.has_command(12):\n            return GenieGarrisonMode.TRANSPORT\n\n        return None\n\n    def get_enabling_research_id(self) -> int:\n        return self.enabling_research_id\n\n    def __repr__(self):\n        return f\"RoRUnitTaskGroup<{self.get_id()}>\"\n\n\nclass RoRVillagerGroup(GenieVillagerGroup):\n    \"\"\"\n    Special collection of task groups for villagers with some special\n    configurations for RoR.\n    \"\"\"\n\n    def is_passable(self, civ_id: int = -1) -> bool:\n        return False\n\n    def get_enabling_research_id(self) -> int:\n        return -1\n\n    def __repr__(self):\n        return f\"RoRVillagerGroup<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/stringresource.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,too-many-function-args\n\nfrom __future__ import annotations\nimport typing\n\nfrom collections import defaultdict\n\nfrom ...value_object.read.genie_structure import GenieStructure\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass StringResource(GenieStructure):\n\n    def __init__(self):\n        super().__init__()\n        self.strings = defaultdict(lambda: {})\n\n    def fill_from(self, stringtable: dict[str, dict[str, str]]) -> None:\n        \"\"\"\n        stringtable is a dict {langcode: {id: string}}\n        \"\"\"\n        for lang, langstrings in stringtable.items():\n            self.strings[lang].update(langstrings)\n\n    def get_tables(self) -> dict[str, dict[str, str]]:\n        \"\"\"\n        Returns the stringtable.\n        \"\"\"\n        return self.strings\n\n    @classmethod\n    def get_data_format_members(cls, game_version: GameVersion) -> tuple:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = (\n            (True, \"id\", None,   \"int32_t\"),\n            (True, \"lang\", None, \"char[16]\"),\n            (True, \"text\", None, \"std::string\"),\n        )\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/swgbcc/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tgenie_tech.py\n\tgenie_unit.py\n)\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/swgbcc/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Star Wars: Galactic Battlegrounds (Clone Campaigns).\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/swgbcc/genie_tech.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSWGB tech objects. These extend the normal Genie techs to reflect\nthat SWGB techs can have unique variants for every civilization.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..aoc.genie_tech import UnitUnlock, UnitLineUpgrade\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass SWGBUnitLineUpgrade(UnitLineUpgrade):\n    \"\"\"\n    Upgrades attributes of units/buildings or other stats in the game.\n    \"\"\"\n\n    __slots__ = ('civ_unlocks',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        unit_line_id: int,\n        upgrade_target_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new SWGB unit upgrade object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param unit_line_id: The unit line that is upgraded.\n        :param upgrade_target_id: The unit that is the result of the upgrade.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, unit_line_id, upgrade_target_id, full_data_set)\n\n        # Unlocks for other civs\n        self.civ_unlocks: dict[int, SWGBUnitUnlock] = {}\n\n    def add_civ_upgrade(self, other_unlock: SWGBUnitUnlock) -> None:\n        \"\"\"\n        Adds a reference to an alternative unlock tech for another civ\n        to this tech group.\n        \"\"\"\n        other_civ_id = other_unlock.tech[\"civilization_id\"].value\n        self.civ_unlocks[other_civ_id] = other_unlock\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Techs are unique if they belong to a specific civ.\n\n        :returns: True if the civilization id is greater than zero.\n        \"\"\"\n        return len(self.civ_unlocks) == 0\n\n\nclass SWGBUnitUnlock(UnitUnlock):\n    \"\"\"\n    Upgrades attributes of units/buildings or other stats in the game.\n    \"\"\"\n\n    __slots__ = ('civ_unlocks',)\n\n    def __init__(\n        self,\n        tech_id: int,\n        line_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new SWGB unit unlock object.\n\n        :param tech_id: The internal tech_id from the .dat file.\n        :param line_id: The id of the unlocked line.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n\n        super().__init__(tech_id, line_id, full_data_set)\n\n        # Unlocks for other civs\n        self.civ_unlocks: dict[int, SWGBUnitUnlock] = {}\n\n    def add_civ_unlock(self, other_unlock: SWGBUnitUnlock) -> None:\n        \"\"\"\n        Adds a reference to an alternative unlock tech for another civ\n        to this tech group.\n        \"\"\"\n        other_civ_id = other_unlock.tech[\"civilization_id\"].value\n        self.civ_unlocks[other_civ_id] = other_unlock\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Techs are unique if they belong to a specific civ.\n\n        :returns: True if the civilization id is greater than zero.\n        \"\"\"\n        return len(self.civ_unlocks) == 0\n"
  },
  {
    "path": "openage/convert/entity_object/conversion/swgbcc/genie_unit.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConverter objects for SWGB. Reimplements the ConverterObjectGroup\ninstances from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..aoc.genie_unit import GenieUnitLineGroup, GenieUnitTransformGroup, \\\n    GenieMonkGroup, GenieStackBuildingGroup\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass SWGBUnitLineGroup(GenieUnitLineGroup):\n    \"\"\"\n    A collection of GenieUnitObject types that form an \"upgrade line\"\n    in SWGB. In comparison to AoE, there is one almost identical line\n    for every civ (civ line).\n\n    Example: Trooper Recruit->Trooper->Heavy Trooper->Repeater Trooper\n\n    Only the civ lines will get converted to a game entity. All others\n    with have their differences patched in by the civ.\n    \"\"\"\n\n    __slots__ = ('civ_lines',)\n\n    def __init__(\n        self,\n        line_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new SWGBUnitLineGroup.\n\n        :param line_id: Internal line obj_id in the .dat file.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, full_data_set)\n\n        # References to alternative lines from other civs\n        self.civ_lines: dict[int, SWGBUnitLineGroup] = {}\n\n    def add_civ_line(self, other_line: SWGBUnitLineGroup) -> None:\n        \"\"\"\n        Adds a reference to an alternative line from another civ\n        to this line.\n        \"\"\"\n        other_civ_id = other_line.get_civ_id()\n        self.civ_lines[other_civ_id] = other_line\n\n    def get_civ_id(self) -> int:\n        \"\"\"\n        Returns the ID of the civ that the line belongs to.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"civilization_id\"].value\n\n    def is_civ_unique(self) -> bool:\n        \"\"\"\n        Groups are civ unique if there are alternative lines for this unit line..\n\n        :returns: True if alternative lines for this unit line exist.\n        \"\"\"\n        return len(self.civ_lines) > 0\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Groups are unique if they belong to a specific civ.\n\n        :returns: True if the civ id is not Gaia's and no alternative lines\n                  for this unit line exist.\n        \"\"\"\n        return (self.get_civ_id() != 0 and\n                len(self.civ_lines) == 0 and\n                self.get_enabling_research_id() > -1)\n\n    def __repr__(self):\n        return f\"SWGBUnitLineGroup<{self.get_id()}>\"\n\n\nclass SWGBStackBuildingGroup(GenieStackBuildingGroup):\n    \"\"\"\n    Buildings that stack with other units and have annexes. These buildings\n    are replaced by their stack unit once built.\n\n    Examples: Gate, Command Center\n    \"\"\"\n\n    def get_enabling_research_id(self) -> int:\n        \"\"\"\n        Returns the enabling tech id of the unit\n        \"\"\"\n        stack_unit = self.get_stack_unit()\n        stack_unit_id = stack_unit[\"id0\"].value\n        stack_unit_connection = self.data.building_connections[stack_unit_id]\n        enabling_research_id = stack_unit_connection[\"enabling_research\"].value\n\n        return enabling_research_id\n\n    def __repr__(self):\n        return f\"SWGBStackBuildingGroup<{self.get_id()}>\"\n\n\nclass SWGBUnitTransformGroup(GenieUnitTransformGroup):\n    \"\"\"\n    Collection of genie units that reference each other with their\n    transform_id.\n\n    Example: Cannon\n\n    Only the civ lines will get converted to a game entity. All others\n    with have their differences patched in by the civ.\n    \"\"\"\n\n    __slots__ = ('civ_lines',)\n\n    def __init__(\n        self,\n        line_id: int,\n        head_unit_id: int,\n        full_data_set: GenieObjectContainer\n    ):\n        \"\"\"\n        Creates a new SWGB transform group.\n\n        :param head_unit_id: Internal unit obj_id of the unit that should be\n                             the initial state.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, head_unit_id, full_data_set)\n\n        # References to alternative lines from other civs\n        self.civ_lines: dict[int, SWGBUnitTransformGroup] = {}\n\n    def add_civ_line(self, other_line: SWGBUnitLineGroup) -> None:\n        \"\"\"\n        Adds a reference to an alternative line from another civ\n        to this line.\n        \"\"\"\n        other_civ_id = other_line.get_civ_id()\n        self.civ_lines[other_civ_id] = other_line\n\n    def get_civ_id(self) -> int:\n        \"\"\"\n        Returns the ID of the civ that the line belongs to.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"civilization_id\"].value\n\n    def is_civ_unique(self) -> bool:\n        \"\"\"\n        Groups are civ unique if there are alternative lines for this unit line..\n\n        :returns: True if alternative lines for this unit line exist.\n        \"\"\"\n        return len(self.civ_lines) > 0\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Groups are unique if they belong to a specific civ.\n\n        :returns: True if the civ id is not Gaia's and no alternative lines\n                  for this unit line exist.\n        \"\"\"\n        return False\n\n    def get_enabling_research_id(self) -> int:\n        \"\"\"\n        Returns the enabling tech id of the unit\n        \"\"\"\n        head_unit_connection = self.data.unit_connections[self.get_transform_unit_id()]\n        enabling_research_id = head_unit_connection[\"enabling_research\"].value\n\n        return enabling_research_id\n\n    def __repr__(self):\n        return f\"SWGBUnitTransformGroup<{self.get_id()}>\"\n\n\nclass SWGBMonkGroup(GenieMonkGroup):\n    \"\"\"\n    Collection of jedi/sith units and jedi/sith with holocron. The switch\n    between these is hardcoded like in AoE2.\n\n    Only the civ lines will get converted to a game entity. All others\n    with have their differences patched in by the civ.\n    \"\"\"\n\n    __slots__ = ('civ_lines',)\n\n    def __init__(\n        self,\n        line_id: int,\n        head_unit_id: int,\n        switch_unit_id: int,\n        full_data_set: GenieObjectContainer,\n    ):\n        \"\"\"\n        Creates a new Genie monk group.\n\n        :param head_unit_id: The unit with this task will become the actual\n                             GameEntity.\n        :param switch_unit_id: This unit will be used to determine the\n                               CarryProgress objects.\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        \"\"\"\n        super().__init__(line_id, head_unit_id, switch_unit_id, full_data_set)\n\n        # References to alternative lines from other civs\n        self.civ_lines: dict[int, SWGBMonkGroup] = {}\n\n    def add_civ_line(self, other_line: SWGBMonkGroup) -> None:\n        \"\"\"\n        Adds a reference to an alternative line from another civ\n        to this line.\n        \"\"\"\n        other_civ_id = other_line.get_civ_id()\n        self.civ_lines[other_civ_id] = other_line\n\n    def get_civ_id(self) -> int:\n        \"\"\"\n        Returns the ID of the civ that the line belongs to.\n        \"\"\"\n        head_unit = self.get_head_unit()\n        return head_unit[\"civilization_id\"].value\n\n    def is_civ_unique(self) -> bool:\n        \"\"\"\n        Groups are civ unique if there are alternative lines for this unit line..\n\n        :returns: True if alternative lines for this unit line exist.\n        \"\"\"\n        return len(self.civ_lines) > 0\n\n    def is_unique(self) -> bool:\n        \"\"\"\n        Groups are unique if they belong to a specific civ.\n\n        :returns: True if the civ id is not Gaia's and no alternative lines\n                  for this unit line exist.\n        \"\"\"\n        return False\n\n    def __repr__(self):\n        return f\"SWGBMonkGroup<{self.get_id()}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/export/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tdata_definition.py\n\tmedia_export_request.py\n\tmetadata_export.py\n\ttexture.py\n)\n\nadd_subdirectory(formats)\n"
  },
  {
    "path": "openage/convert/entity_object/export/__init__.py",
    "content": "# Copyright 2019-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nEntity objects for exporting.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/export/data_definition.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nOutput format specification for data to write.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nclass DataDefinition:\n    \"\"\"\n    Contains a data definition that can then be\n    formatted to an arbitrary output file.\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        \"\"\"\n        Creates a new data definition.\n\n        :param targetdir: Relative path to the export directory.\n        :type targetdir: str\n        :param filename: Filename of the resulting file.\n        :type filename: str\n        \"\"\"\n        self.targetdir = targetdir\n        self.filename = filename\n\n    def dump(self) -> typing.NoReturn:\n        \"\"\"\n        Creates a human-readable string that can be written to a file.\n        \"\"\"\n        raise NotImplementedError(f\"{type(self)} has not implemented dump() method\")\n\n    def set_filename(self, filename: str) -> None:\n        \"\"\"\n        Sets the filename for the file.\n\n        :param filename: Filename of the resuilting file.\n        :type filename: str\n        \"\"\"\n        if not isinstance(filename, str):\n            raise ValueError(f\"str expected as filename, not {type(filename)}\")\n\n        self.filename = filename\n\n    def set_targetdir(self, targetdir: str) -> None:\n        \"\"\"\n        Sets the target directory for the file.\n\n        :param targetdir: Relative path to the export directory.\n        :type targetdir: str\n        \"\"\"\n        if not isinstance(targetdir, str):\n            raise ValueError(\"str expected as targetdir\")\n\n        self.targetdir = targetdir\n\n    def __repr__(self):\n        return f\"DataDefinition<{type(self)}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/CMakeLists.txt",
    "content": "add_py_modules(\n\tblmask_metadata.py\n\tbltable_metadata.py\n\t__init__.py\n\tmedia_cache.py\n\tmodpack_info.py\n\tmodpack_manifest.py\n\tnyan_file.py\n\tpalette_metadata.py\n\tsprite_metadata.py\n\tterrain_metadata.py\n\ttexture_metadata.py\n)\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nExport formats used by the engine.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/blmask_metadata.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nBlendmask definition file.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '1'\n\n\nclass BlendmaskMetadata(DataDefinition):\n    \"\"\"\n    Collects blendmask metadata and can format it\n    as a .blmask custom format\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.image_files: dict[int, dict[str, typing.Any]] = {}\n        self.scalefactor = 1.0\n        self.masks: dict[int, dict[str, int]] = {}\n\n    def add_image(self, img_id: int, filename: str) -> None:\n        \"\"\"\n        Add an image and the relative file name.\n\n        :param img_id: Image identifier.\n        :type img_id: int\n        :param filename: Path to the image file.\n        :type filename: str\n        \"\"\"\n        self.image_files[img_id] = {\n            \"image_id\": img_id,\n            \"filename\": filename,\n        }\n\n    def add_mask(\n        self,\n        directions: int,\n        img_id: int,\n        xpos: int,\n        ypos: int,\n        xsize: int,\n        ysize: int\n    ) -> None:\n        \"\"\"\n        Add a mask for directions.\n\n        :param directions: Directions bitfield value.\n        :type directions: int\n        :param img_id: ID of the image used by this mask.\n        :type img_id: int\n        :param xpos: X position of the mask on the image canvas.\n        :type xpos: int\n        :param ypos: Y position of the mask on the image canvas.\n        :type ypos: int\n        :param xsize: Width of the mask.\n        :type xsize: int\n        :param ysize: Height of the mask.\n        :type ysize: int\n        \"\"\"\n        self.masks[directions] = {\n            \"directions\": directions,\n            \"img_id\": img_id,\n            \"xpos\": xpos,\n            \"ypos\": ypos,\n            \"xsize\": xsize,\n            \"ysize\": ysize,\n        }\n\n    def set_scalefactor(self, factor: typing.Union[int, float]) -> None:\n        \"\"\"\n        Set the scale factor of the animation.\n\n        :param factor: Factor by which sprite images are scaled down at default zoom level.\n        :type factor: float\n        \"\"\"\n        self.scalefactor = float(factor)\n\n    def dump(self) -> str:\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage blendmask definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # image files\n        for image in self.image_files.values():\n            output_str += f\"imagefile {image['image_id']} {image['filename']}\\n\"\n\n        output_str += \"\\n\"\n\n        # scale factor\n        output_str += f\"scalefactor {self.scalefactor}\\n\\n\"\n\n        # mask definitions\n        for mask in self.masks.values():\n            output_str += f'mask {\" \".join(str(param) for param in mask.values())}\\n'\n\n        return output_str\n\n    def __repr__(self):\n        return f'BlendmaskMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/bltable_metadata.py",
    "content": "# Copyright 2021-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nBlendtable definition file.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '1'\n\n\nclass BlendtableMetadata(DataDefinition):\n    \"\"\"\n    Collects blentable metadata and can format it\n    as a .bltable custom format\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.blendtable: tuple = None\n        self.patterns: dict[int, dict[str, typing.Any]] = {}\n\n    def add_pattern(self, pattern_id: int, filename: str) -> None:\n        \"\"\"\n        Define a pattern in the table.\n\n        :param pattern_id: Pattern identifier.\n        :type pattern_id: int\n        :param filename: Path to the pattern file.\n        :type filename: str\n        \"\"\"\n        self.patterns[pattern_id] = {\n            \"pattern_id\": pattern_id,\n            \"filename\": filename\n        }\n\n    def set_blendtabe(self, table: tuple) -> None:\n        \"\"\"\n        Set the blendtable. This expects a tuple of integers with nxn entries.\n\n        :param table: Blending lookup table.\n        :type table: tuple\n        \"\"\"\n        self.blendtable = table\n\n        self._check_table()\n\n    def dump(self) -> str:\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage blendtable definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # blendtable\n        output_str += \"blendtable [\\n\"\n\n        # table entries\n        table_width = self._get_table_width()\n        for idx in range(table_width):\n            row_entries = self.blendtable[idx * table_width:(idx + 1) * table_width]\n            output_str += f'{\" \".join(row_entries)}\\n'\n\n        output_str += \"]\\n\\n\"\n\n        # pattern definitions\n        for pattern in self.patterns.values():\n            output_str += f\"pattern {pattern['pattern_id']} {pattern['filename']}\\n\"\n\n        output_str += \"\\n\"\n\n        return output_str\n\n    def _get_table_width(self) -> int:\n        \"\"\"\n        Get the width of the blending table.\n        \"\"\"\n        table_size = len(self.blendtable)\n\n        # Newton's method\n        left = table_size\n        right = (left + 1) // 2\n        while right < left:\n            left = right\n            right = (left + table_size // left) // 2\n\n        return left\n\n    def _check_table(self) -> typing.Union[None, typing.NoReturn]:\n        \"\"\"\n        Check if the blending table is a nxn matrix.\n        \"\"\"\n        table_width = self._get_table_width()\n\n        if table_width * table_width != len(self.blendtable):\n            raise ValueError(f\"blendtable entries malformed: \"\n                             f\"{len(self.blendtable)} is not an integer square\")\n\n    def __repr__(self):\n        return f'BlendtableMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/media_cache.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nCreate a media cache file for a game version.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport toml\n\nfrom ..data_definition import DataDefinition\n\nFILE_VERSION = \"1.0\"\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media_types import MediaType\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass MediaCacheFile(DataDefinition):\n    \"\"\"\n    Used for creating a media cache file.\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str, game_version: GameVersion):\n        super().__init__(targetdir, filename)\n\n        self.game_version = game_version\n        self.hash_func: str = None\n        self.cache = {}\n\n    def dump(self) -> str:\n        \"\"\"\n        Returns the media cache file content in TOML format.\n        \"\"\"\n        output_dict = {}\n\n        output_dict[\"file_version\"] = FILE_VERSION\n        output_dict[\"hash_algo\"] = self.hash_func\n\n        for media_type, cachedata in self.cache.items():\n            output_dict.update({media_type.value: {}})\n\n            for idx, cache in enumerate(cachedata):\n                cache_table = output_dict[media_type.value]\n                cache_table[f\"file{idx}\"] = {\n                    \"filepath\": cache[0],\n                    \"hash\": cache[1],\n                    \"compression_settings\": cache[2],\n                    \"packer_settings\": cache[3]\n                }\n\n        output_str = \"# openage media cache file\\n\\n\"\n        output_str += toml.dumps(output_dict)\n\n        return output_str\n\n    def add_cache_data(\n        self,\n        media_type: MediaType,\n        filepath: str,\n        filehash: str,\n        compr_settings: tuple,\n        packer_settings: tuple\n    ) -> None:\n        \"\"\"\n        Add cache data for a file.\n\n        :param media_type: Media type of the file (should be a graphics format)\n        :type media_type: MediaType\n        :param filepath: Path of the source file in the sourcedir\n                         mounted by the openage converter.\n        :type filepath: str\n        :param filehash: Hash value of the source file.\n        :type filehash: str\n        :param compr_settings: Settings for the PNG compression.\n        :type compr_settings: tuple\n        :param packer_settings: Settings for the packing algorithm.\n        :type packer_settings: tuple\n        \"\"\"\n        if media_type not in self.cache:\n            self.cache[media_type] = []\n\n        self.cache[media_type].append(\n            (filepath, filehash, compr_settings, packer_settings)\n        )\n\n    def set_hash_func(self, hash_func: str) -> None:\n        \"\"\"\n        Set the hash function used for generating\n        hash values for the graphic files.\n\n        :param hash_func: Hash algorithm\n        :type hash_func: str\n        \"\"\"\n        self.hash_func = hash_func\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/modpack_info.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-instance-attributes,too-many-arguments\n\n\"\"\"\nModpack definition file.\n\"\"\"\nimport toml\n\nfrom ..data_definition import DataDefinition\n\n\nFILE_VERSION = \"2\"\n\n\nclass ModpackInfo(DataDefinition):\n    \"\"\"\n    Represents the header file of the modpack. Contains info for loading data\n    and about the creators of the modpack.\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        # Info\n        self.packagename: str = None\n        self.version: str = None\n        self.versionstr: str = None\n        self.extra_info: dict[str, str] = {}\n\n        # Assets\n        self.includes: list[str] = []\n        self.excludes: list[str] = []\n\n        # Dependency\n        self.requires: list[str] = []\n\n        # Conflict\n        self.conflicts: list[str] = []\n\n        # Authors\n        self.authors: dict[str, str] = {}\n\n        # Author groups\n        self.author_groups: dict[str, str] = {}\n\n    def add_author(\n        self,\n        name: str,\n        fullname: str = None,\n        since: str = None,\n        until: str = None,\n        roles: str = None,\n        contact: str = None\n    ) -> None:\n        \"\"\"\n        Adds an author with optional contact info.\n\n        :param name: Nickname of the author. Must be unique for the modpack.\n        :type name: str\n        :param fullname: Full name of the author.\n        :type fullname: str\n        :param since: Version number of the release where the author started to contribute.\n        :type since: str\n        :param until: Version number of the release where the author stopped to contribute.\n        :type until: str\n        :param roles: List of roles of the author during the creation of the modpack.\n        :type roles: list\n        :param contact: Dictionary with contact info. See the spec\n                        for available parameters.\n        :type contact: dict\n        \"\"\"\n        author = {}\n        author[\"name\"] = name\n        if fullname:\n            author[\"fullname\"] = fullname\n\n        if since:\n            author[\"since\"] = since\n\n        if until:\n            author[\"until\"] = until\n\n        if roles:\n            author[\"roles\"] = roles\n\n        if contact:\n            author[\"contact\"] = contact\n\n        self.authors[name] = author\n\n    def add_author_group(\n        self,\n        name: str,\n        authors: list[str],\n        description: str = None\n    ) -> None:\n        \"\"\"\n        Adds an author with optional contact info.\n\n        :param name: Group or team name.\n        :type name: str\n        :param authors: List of author identifiers. These must match up\n                        with subtable keys in the self.authors.\n        :type authors: list\n        :param description: Path to a file with a description of the team.\n        :type description: str\n        \"\"\"\n        author_group = {}\n        author_group[\"name\"] = name\n        author_group[\"authors\"] = authors\n        if description:\n            author_group[\"description\"] = description\n\n        self.author_groups[name] = author_group\n\n    def add_include(self, path: str) -> None:\n        \"\"\"\n        Add a path to an asset that is loaded by the modpack.\n\n        :param path: Path to assets that should be mounted on load time.\n        :type path: str\n        \"\"\"\n        self.includes.append(path)\n\n    def add_exclude(self, path: str) -> None:\n        \"\"\"\n        Add a path to an asset that excluded from loading.\n\n        :param path: Path to assets.\n        :type path: str\n        \"\"\"\n        self.excludes.append(path)\n\n    def add_conflict(self, modpack_id: str) -> None:\n        \"\"\"\n        Add an identifier of another modpack that has a conflict with this modpack.\n\n        :param modpack_id: Modpack alias or identifier.\n        :type modpack_id: str\n        \"\"\"\n        self.conflicts.append(modpack_id)\n\n    def add_dependency(self, modpack_id: str) -> None:\n        \"\"\"\n        Add an identifier of another modpack that is a dependency of this modpack.\n\n        :param modpack_id: Modpack alias or identifier.\n        :type modpack_id: str\n        \"\"\"\n        self.requires.append(modpack_id)\n\n    def set_info(\n        self,\n        packagename: str,\n        modpack_version: str,\n        versionstr: str = None,\n        repo: str = None,\n        alias: str = None,\n        title: str = None,\n        description: str = None,\n        long_description: str = None,\n        url: str = None,\n        licenses: str = None\n    ) -> None:\n        \"\"\"\n        Set the general information about the modpack.\n\n        :param packagename: Name of the modpack.\n        :type packagename: str\n        :param modpack_version: Internal version number. Must have semver format.\n        :type modpack_version: str\n        :param versionstr: Human-readable version number. Can be anything.\n        :type versionstr: str\n        :param repo: Name of the repo where the package is hosted.\n        :type repo: str\n        :param alias: Alias of the modpack.\n        :type alias: str\n        :param title: Title used in UI.\n        :type title: str\n        :param description: Path to a file with a short description (max 500 chars).\n        :type description: str\n        :param long_description: Path to a file with a detailed description.\n        :type long_description: str\n        :param url: Link to the modpack's website.\n        :type url: str\n        :param licenses: License(s) of the modpack.\n        :type licenses: list\n        \"\"\"\n        self.packagename = packagename\n        self.version = modpack_version\n\n        if versionstr:\n            self.extra_info[\"versionstr\"] = versionstr\n\n        if repo:\n            self.extra_info[\"repo\"] = repo\n\n        if alias:\n            self.extra_info[\"alias\"] = alias\n\n        if title:\n            self.extra_info[\"title\"] = title\n\n        if description:\n            self.extra_info[\"description\"] = description\n\n        if long_description:\n            self.extra_info[\"long_description\"] = long_description\n\n        if url:\n            self.extra_info[\"url\"] = url\n\n        if licenses:\n            self.extra_info[\"licenses\"] = licenses\n\n    def dump(self) -> str:\n        \"\"\"\n        Outputs the modpack info to the TOML output format.\n        \"\"\"\n        output_str = \"# openage modpack definition file\\n\\n\"\n        output_dict = {}\n\n        # File version\n        output_dict.update({\"file_version\": FILE_VERSION})\n\n        # info table\n        if not self.packagename:\n            raise RuntimeError(f\"{self}: packagename needs to be defined before dumping.\")\n\n        if not self.version:\n            raise RuntimeError(f\"{self}: version needs to be defined before dumping.\")\n\n        info_table = {\"info\": {}}\n        info_table[\"info\"].update(\n            {\n                \"name\": self.packagename,\n                \"version\": self.version\n            }\n        )\n        info_table[\"info\"].update(self.extra_info)\n\n        output_dict.update(info_table)\n\n        # assets table\n        assets_table = {\"assets\": {}}\n        assets_table[\"assets\"].update(\n            {\n                \"include\": self.includes,\n                \"exclude\": self.excludes\n            }\n        )\n\n        output_dict.update(assets_table)\n\n        # dependency table\n        dependency_table = {\"dependency\": {}}\n        dependency_table[\"dependency\"].update({\"modpacks\": self.requires})\n\n        output_dict.update(dependency_table)\n\n        # conflicts table\n        conflicts_table = {\"conflict\": {}}\n        conflicts_table[\"conflict\"].update({\"modpacks\": self.conflicts})\n\n        output_dict.update(conflicts_table)\n\n        # authors table\n        authors_table = {\"authors\": {}}\n        authors_table[\"authors\"].update(self.authors)\n\n        output_dict.update(authors_table)\n\n        # authorgroups table\n        authorgroups_table = {\"authorgroups\": {}}\n        authorgroups_table[\"authorgroups\"].update(self.author_groups)\n\n        output_dict.update(authorgroups_table)\n\n        output_str += toml.dumps(output_dict)\n\n        return output_str\n\n    def __repr__(self):\n        return f\"ModpackInfo<{self.packagename}>\"\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/modpack_manifest.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCreate a manifest file for a modpack\n\"\"\"\n\nimport toml\n\nfrom ..data_definition import DataDefinition\n\n\nclass ManifestFile(DataDefinition):\n    \"\"\"\n    Used for creating a manifest file for a modpack.\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.hash_values: list[tuple[str, str]] = []\n        self.hashing_func: str = None\n\n    def dump(self) -> str:\n        \"\"\"\n        Returns the manifest file content in TOML format.\n        \"\"\"\n        output_dict = {}\n\n        info_table = {\"info\": {}}\n        # write the hashing algorithm used\n        info_table[\"info\"].update({\"hash\": self.hashing_func})\n\n        output_dict.update(info_table)\n\n        # write the hash values and the relative paths\n        # of the items in the exported directory\n        hash_values_table = {'hash-values': {}}\n        for hash_val, item_path in self.hash_values:\n            hash_values_table['hash-values'].update({hash_val: item_path})\n\n        output_dict.update(hash_values_table)\n\n        output_str = \"# openage autogenerated modpack integrity check\\n\\n\"\n        output_str += toml.dumps(output_dict)\n\n        return output_str\n\n    def add_hash_value(self, hash_val: str, item_path: str) -> None:\n        \"\"\"\n        Add the item path and its hash value to the instances\n        hash_values list.\n\n        :param hash_val: the hash value of the item\n        :type hash_val: str\n        :param item_path: relative path of item to the exported path\n        :type item_path: str\n        \"\"\"\n        self.hash_values.append((hash_val, item_path,))\n\n    def set_hashing_func(self, hashing_func: str) -> None:\n        \"\"\"\n        Add the hashing function used for generating\n        hash values for modpack files\n\n        :param hashing_func: Hashing algorithm\n        :type hashing_func: str\n        \"\"\"\n        self.hashing_func = hashing_func\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/nyan_file.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nNyan file struct that stores a bunch of objects and\nmanages imports.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import NyanObject\nfrom .....util.ordered_set import OrderedSet\nfrom ..data_definition import DataDefinition\n\nif typing.TYPE_CHECKING:\n    from openage.nyan.import_tree import ImportTree\n\n\nFILE_VERSION = \"0.2.0\"\n\n\nclass NyanFile(DataDefinition):\n    \"\"\"\n    Groups nyan objects into files. Contains methods for creating imports\n    and dumping all objects into a human-readable .nyan file.\n    \"\"\"\n\n    def __init__(\n        self,\n        targetdir: str,\n        filename: str,\n        modpack_name: str,\n        nyan_objects: typing.Collection = None\n    ):\n        super().__init__(targetdir, filename)\n\n        self.modpack_name = modpack_name\n\n        self.nyan_objects = OrderedSet()\n        if nyan_objects:\n            for nyan_object in nyan_objects:\n                self.add_nyan_object(nyan_object)\n\n        self.import_tree = None\n\n        if len(targetdir) == 0 or targetdir == \"/\":\n            self.fqon = (self.modpack_name, self.filename.split(\".\")[0])\n\n        else:\n            self.fqon = (self.modpack_name,\n                         *self.targetdir.replace(\"/\", \".\")[:-1].split(\".\"),\n                         self.filename.split(\".\")[0])\n\n    def add_nyan_object(self, new_object: NyanObject) -> None:\n        \"\"\"\n        Adds a nyan object to the file.\n        \"\"\"\n        if not isinstance(new_object, NyanObject):\n            raise TypeError(f\"nyan file cannot contain non-nyan object {new_object}\")\n\n        self.nyan_objects.add(new_object)\n\n        new_fqon = (*self.fqon, new_object.get_name())\n        new_object.set_fqon(new_fqon)\n\n    def dump(self) -> str:\n        \"\"\"\n        Returns the string that represents the nyan file.\n        \"\"\"\n        fileinfo_str = \"# NYAN FILE\\n\"\n        fileinfo_str += f\"!version {FILE_VERSION}\\n\\n\"\n\n        import_str = \"\"\n        objects_str = \"\"\n\n        for nyan_object in self.nyan_objects:\n            objects_str += nyan_object.dump(import_tree=self.import_tree)\n\n        # Removes one empty newline at the end of the objects definition\n        objects_str = objects_str[:-1]\n\n        import_aliases = self.import_tree.get_alias_dict()\n        import_files = self.import_tree.get_import_list()\n        self.import_tree.clear_marks()\n\n        if len(import_files) > 0:\n            for fqon in import_files:\n                import_str += \"import \"\n\n                import_str += \".\".join(fqon)\n\n                import_str += \"\\n\"\n\n            import_str += \"\\n\"\n\n        if len(import_aliases) > 0:\n            for alias, fqon in import_aliases.items():\n                import_str += \"import \"\n\n                import_str += \".\".join(fqon)\n\n                if len(alias) > 0:\n                    import_str += f\" as {alias}\"\n\n                import_str += \"\\n\"\n\n            import_str += \"\\n\"\n\n        output_str = fileinfo_str + import_str + objects_str\n\n        return output_str\n\n    def get_fqon(self) -> str:\n        \"\"\"\n        Return the fqon of the nyan file\n        \"\"\"\n        return self.fqon\n\n    def get_relative_file_path(self) -> str:\n        \"\"\"\n        Relative path of the nyan file in the modpack.\n        \"\"\"\n        return f\"{self.modpack_name}/{self.targetdir}{self.filename}\"\n\n    def set_import_tree(self, import_tree: ImportTree) -> None:\n        \"\"\"\n        Sets the import tree of the file.\n        \"\"\"\n        self.import_tree = import_tree\n\n    def set_filename(self, filename: str):\n        super().set_filename(filename)\n        self._reset_fqons()\n\n    def set_modpack_name(self, modpack_name: str) -> None:\n        \"\"\"\n        Set the name of the modpack, the file is contained in.\n        \"\"\"\n        self.modpack_name = modpack_name\n\n    def set_targetdir(self, targetdir: str) -> None:\n        super().set_targetdir(targetdir)\n        self._reset_fqons()\n\n    def _reset_fqons(self) -> None:\n        \"\"\"\n        Resets fqons, depending on the modpack name,\n        target directory and filename.\n        \"\"\"\n        for nyan_object in self.nyan_objects:\n            new_fqon = (*self.fqon, nyan_object.get_name())\n\n            nyan_object.set_fqon(new_fqon)\n\n        self.fqon = (self.modpack_name,\n                     *self.targetdir.replace(\"/\", \".\")[:-1].split(\".\"),\n                     self.filename.split(\".\")[0])\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/palette_metadata.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPalette definition file.\n\"\"\"\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '1'\n\n\nclass PaletteMetadata(DataDefinition):\n    \"\"\"\n    Collects palette metadata and can format it\n    as a .opal custom format\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.colours: list[tuple] = []\n\n    def add_colour(self, colour: tuple) -> None:\n        \"\"\"\n        Add a RGBA colour to the end of the palette.\n\n        :param colour: RGBA colour tuple.\n        :type colour: tuple\n        \"\"\"\n        self.colours.append(colour)\n\n    def add_colours(self, colours: list[tuple]) -> None:\n        \"\"\"\n        Add a collection of RGBA colours to the end of the palette.\n\n        :param colours: Collection of RGBA coulour tuples.\n        :type colours: tuple, list\n        \"\"\"\n        self.colours.extend(colours)\n\n    def dump(self) -> str:\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage palette definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # entries\n        output_str += f\"entries {len(self.colours)}\\n\\n\"\n\n        # palette\n        output_str += \"colours [\\n\"\n\n        # frame definitions\n        for colour in self.colours:\n            output_str += f'{\" \".join(str(param) for param in colour)}\\n'\n\n        output_str += \"]\\n\"\n\n        return output_str\n\n    def __repr__(self):\n        return f'PaletteMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/sprite_metadata.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nSprite definition file.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom enum import Enum\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '2'\n\n\nclass LayerMode(Enum):\n    \"\"\"\n    Possible values for the mode of a layer.\n    \"\"\"\n    OFF = 'off'     # layer is not animated\n    ONCE = 'once'   # animation plays once\n    LOOP = 'loop'   # animation loops indefinitely\n\n\nclass SpriteMetadata(DataDefinition):\n    \"\"\"\n    Collects sprite metadata and can format it\n    as a .sprite custom format\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.texture_files: dict[int, dict[str, typing.Any]] = {}\n        self.scalefactor = 1.0\n        self.layers: dict[int, dict[str, typing.Any]] = {}\n        self.angles: dict[int, dict[str, int]] = {}\n        self.frames: list[dict[str, int]] = []\n\n    def add_texture(self, texture_id: int, filename: str) -> None:\n        \"\"\"\n        Add a texture and the relative file name.\n\n        :param texture_id: Texture identifier.\n        :type texture_id: int\n        :param filename: Path to the image file.\n        :type filename: str\n        \"\"\"\n        self.texture_files[texture_id] = {\n            \"texture_id\": texture_id,\n            \"filename\": filename,\n        }\n\n    def add_layer(\n        self,\n        layer_id: int,\n        mode: LayerMode = None,\n        position: int = None,\n        time_per_frame: float = None,\n        replay_delay: float = None\n    ) -> None:\n        \"\"\"\n        Define a layer for the rendered sprite.\n\n        :param layer_id: Layer identifier.\n        :type layer_id: int\n        :param mode: Animation mode (off, once, loop).\n        :type mode: LayerMode\n        :param position: Layer position.\n        :type position: int\n        :param time_per_frame: Time spent on each frame.\n        :type time_per_frame: float\n        :param replay_delay: Time delay before replaying the animation.\n        :type replay_delay: float\n        \"\"\"\n        self.layers[layer_id] = {\n            \"layer_id\": layer_id,\n            \"mode\": mode,\n            \"position\": position,\n            \"time_per_frame\": time_per_frame,\n            \"replay_delay\": replay_delay,\n        }\n\n    def add_angle(self, degree: int, mirror_from: int = None) -> None:\n        \"\"\"\n        Specifies an angle that frames can get assigned to.\n\n        :param degree: Angle identifier expressed in degrees.\n        :type degree: int\n        :param mirror_from: Other angle to copy frames from, if any.\n        :type mirror_from: int\n        \"\"\"\n        self.angles[degree] = {\n            \"degree\": degree,\n            \"mirror_from\": mirror_from,\n        }\n\n    def add_frame(\n        self,\n        frame_idx: int,\n        angle: int,\n        layer_id: int,\n        texture_id: int,\n        subtex_id: int\n    ) -> None:\n        \"\"\"\n        Add frame with all its spacial information.\n\n        :param frame_idx: Index of the frame in the animation for the specified angle.\n        :type frame_idx: int\n        :param angle: Angle to which the frame belongs, in degrees.\n        :type angle: int\n        :param layer_id: ID of the layer to which the frame belongs.\n        :type layer_id: int\n        :param texture_id: ID of the texture used by this frame.\n        :type texture_id: int\n        :param subtex_id: ID of the subtexture from the texture used by this frame.\n        :type subtex_id: int\n        \"\"\"\n        self.frames.append(\n            {\n                \"frame_idx\": frame_idx,\n                \"angle\": angle,\n                \"layer_id\": layer_id,\n                \"texture_id\": texture_id,\n                \"subtex_id\": subtex_id,\n            }\n        )\n\n    def set_scalefactor(self, factor: typing.Union[int, float]) -> None:\n        \"\"\"\n        Set the scale factor of the animation.\n\n        :param factor: Factor by which sprite images are scaled down at default zoom level.\n        :type factor: float\n        \"\"\"\n        self.scalefactor = float(factor)\n\n    def dump(self) -> str:\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage sprite definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # texture files\n        for texture in self.texture_files.values():\n            output_str += f\"texture {texture['texture_id']} \\\"{texture['filename']}\\\"\\n\"\n\n        output_str += \"\\n\"\n\n        # scale factor\n        output_str += f\"scalefactor {self.scalefactor}\\n\\n\"\n\n        # layer definitions\n        for layer in self.layers.values():\n            output_str += f\"layer {layer['layer_id']}\"\n\n            if layer[\"mode\"]:\n                output_str += f\" mode={layer['mode'].value}\"\n\n            if layer[\"position\"]:\n                output_str += f\" position={layer['position']}\"\n\n            if layer[\"time_per_frame\"]:\n                output_str += f\" time_per_frame={layer['time_per_frame']}\"\n\n            if layer[\"replay_delay\"]:\n                output_str += f\" replay_delay={layer['replay_delay']}\"\n\n            output_str += \"\\n\"\n\n        output_str += \"\\n\"\n\n        # angle mirroring declarations\n        for angle in self.angles.values():\n            output_str += f\"angle {angle['degree']}\"\n\n            if angle[\"mirror_from\"]:\n                output_str += f\" mirror_from={angle['mirror_from']}\"\n\n            output_str += '\\n'\n\n        output_str += '\\n'\n\n        # frame definitions\n        for frame in self.frames:\n            output_str += f'frame {\" \".join(str(param) for param in frame.values())}\\n'\n\n        return output_str\n\n    def __repr__(self):\n        return f'SpriteMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/terrain_metadata.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nTerrain definition file.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom enum import Enum\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '2'\n\n\nclass LayerMode(Enum):\n    \"\"\"\n    Possible values for the mode of a layer.\n    \"\"\"\n    OFF = 'off'     # layer is not animated\n    LOOP = 'loop'   # animation loops indefinitely\n\n\nclass TerrainMetadata(DataDefinition):\n    \"\"\"\n    Collects terrain metadata and can format it\n    as a .terrain custom format\n    \"\"\"\n\n    def __init__(self, targetdir: str, filename: str):\n        super().__init__(targetdir, filename)\n\n        self.texture_files: dict[int, dict[str, typing.Any]] = {}\n        self.scalefactor = 1.0\n        self.blendtable: dict[str, typing.Any] = None\n        self.layers: dict[int, dict[str, typing.Any]] = {}\n        self.frames: list[dict[str, int]] = []\n\n    def add_texture(self, texture_id: int, filename: str) -> None:\n        \"\"\"\n        Add a texture and the relative file name.\n\n        :param texture_id: Texture identifier.\n        :type texture_id: int\n        :param filename: Path to the image file.\n        :type filename: str\n        \"\"\"\n        self.texture_files[texture_id] = {\n            \"texture_id\": texture_id,\n            \"filename\": filename,\n        }\n\n    def add_layer(\n        self,\n        layer_id: int,\n        mode: LayerMode = None,\n        position: int = None,\n        time_per_frame: float = None,\n        replay_delay: float = None\n    ) -> None:\n        \"\"\"\n        Define a layer for the rendered texture.\n\n        :param layer_id: Layer identifier.\n        :type layer_id: int\n        :param mode: Animation mode (off, loop).\n        :type mode: LayerMode\n        :param position: Layer position.\n        :type position: int\n        :param time_per_frame: Time spent on each frame.\n        :type time_per_frame: float\n        :param replay_delay: Time delay before replaying the animation.\n        :type replay_delay: float\n        \"\"\"\n        self.layers[layer_id] = {\n            \"layer_id\": layer_id,\n            \"mode\": mode,\n            \"position\": position,\n            \"time_per_frame\": time_per_frame,\n            \"replay_delay\": replay_delay,\n        }\n\n    def add_frame(\n        self,\n        frame_idx: int,\n        layer_id: int,\n        texture_id: int,\n        subtex_id: int,\n        priority=None,\n        blend_mode=None\n    ) -> None:\n        \"\"\"\n        Add frame with all its spacial information.\n\n        :param frame_idx: Index of the frame in the animation.\n        :type frame_idx: int\n        :param layer_id: ID of the layer to which the frame belongs.\n        :type layer_id: int\n        :param texture_id: ID of the texture used by this frame.\n        :type texture_id: int\n        :param subtex_id: ID of the subtexture from the texture used by this frame.\n        :type subtex_id: int\n        :param priority: Priority for blending.\n        :type priority: int\n        :param blend_mode: Used for looking up the blending pattern index in the blending table.\n        :type blend_mode: int\n        \"\"\"\n        self.frames.append(\n            {\n                \"frame_idx\": frame_idx,\n                \"layer_id\": layer_id,\n                \"texture_id\": texture_id,\n                \"subtex_id\": subtex_id,\n                \"priority\": priority,\n                \"blend_mode\": blend_mode,\n            }\n        )\n\n    def set_blendtable(self, table_id: int, filename: str) -> None:\n        \"\"\"\n        Set the blendtable and the relative filename.\n\n        :param img_id: Table identifier.\n        :type img_id: int\n        :param filename: Path to the blendtable file.\n        :type filename: str\n        \"\"\"\n        self.blendtable = {\n            \"table_id\": table_id,\n            \"filename\": filename,\n        }\n\n    def set_scalefactor(self, factor: typing.Union[int, float]) -> None:\n        \"\"\"\n        Set the scale factor of the texture.\n\n        :param factor: Factor by which sprite images are scaled down at default zoom level.\n        :type factor: float\n        \"\"\"\n        self.scalefactor = float(factor)\n\n    def dump(self) -> str:\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage terrain definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # texture files\n        for texture in self.texture_files.values():\n            output_str += f\"texture {texture['texture_id']} \\\"{texture['filename']}\\\"\\n\"\n\n        output_str += \"\\n\"\n\n        # blendtable reference\n        if self.blendtable:\n            output_str += (f\"blendtable {self.blendtable['table_id']} \"\n                           \"{self.blendtable['filename']}\\n\\n\")\n\n        # scale factor\n        output_str += f\"scalefactor {self.scalefactor}\\n\\n\"\n\n        # layer definitions\n        for layer in self.layers.values():\n            output_str += f\"layer {layer['layer_id']}\"\n\n            if layer[\"mode\"]:\n                output_str += f\" mode={layer['mode'].value}\"\n\n            if layer[\"position\"]:\n                output_str += f\" position={layer['position']}\"\n\n            if layer[\"time_per_frame\"]:\n                output_str += f\" time_per_frame={layer['time_per_frame']}\"\n\n            if layer[\"replay_delay\"]:\n                output_str += f\" replay_delay={layer['replay_delay']}\"\n\n            output_str += \"\\n\"\n\n        output_str += \"\\n\"\n\n        # frame definitions\n        for frame in self.frames:\n            frame_attributes = list(frame.values())\n            output_str += f'frame {\" \".join(str(param) for param in frame_attributes[:4])}'\n\n            if frame[\"priority\"]:\n                output_str += f\" priority={frame['priority']}\"\n\n            if frame[\"blend_mode\"]:\n                output_str += f\" blend_mode={frame['blend_mode']}\"\n\n            output_str += \"\\n\"\n\n        return output_str\n\n    def __repr__(self):\n        return f'TerrainMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/formats/texture_metadata.py",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nTexture definition file.\n\"\"\"\n\nfrom enum import Enum\n\nfrom ..data_definition import DataDefinition\n\nFORMAT_VERSION = '1'\n\n\nclass LayerMode(Enum):\n    \"\"\"\n    Possible values for the mode of a layer.\n    \"\"\"\n    OFF = 'off'     # layer is not animated\n    ONCE = 'once'   # animation plays once\n    LOOP = 'loop'   # animation loops indefinitely\n\n\nclass TextureMetadata(DataDefinition):\n    \"\"\"\n    Collects texture metadata and can format it\n    as a .texture custom format\n    \"\"\"\n\n    def __init__(self, targetdir, filename):\n        super().__init__(targetdir, filename)\n\n        self.image_file = None\n        self.size = {}\n        self.pxformat = {}\n        self.subtexs = []\n\n    def add_subtex(self, xpos, ypos, xsize, ysize, xhotspot, yhotspot):\n        \"\"\"\n        Add a subtex with all its spacial information.\n\n        :param xpos: X position of the subtex on the image canvas.\n        :type xpos: int\n        :param ypos: Y position of the subtex on the image canvas.\n        :type ypos: int\n        :param xsize: Width of the subtex.\n        :type xsize: int\n        :param ysize: Height of the subtex.\n        :type ysize: int\n        :param xhotspot: X position of the hotspot of the subtex.\n        :type xhotspot: int\n        :param yhotspot: Y position of the hotspot of the subtex.\n        :type yhotspot: int\n        \"\"\"\n        self.subtexs.append(\n            {\n                \"xpos\": xpos,\n                \"ypos\": ypos,\n                \"xsize\": xsize,\n                \"ysize\": ysize,\n                \"xhotspot\": xhotspot,\n                \"yhotspot\": yhotspot,\n            }\n        )\n\n    def set_imagefile(self, filename):\n        \"\"\"\n        Set the relative filename of the texture.\n\n        :param filename: Path to the image file.\n        :type filename: str\n        \"\"\"\n        self.image_file = filename\n\n    def set_size(self, width, height):\n        \"\"\"\n        Define the size of the PNG file.\n\n        :param width: Width of the exported PNG in pixels.\n        :type width: int\n        :param height: Height of the exported PNG in pixels.\n        :type height: int\n        \"\"\"\n        self.size = {\n            \"width\": width,\n            \"height\": height\n        }\n\n    def set_pxformat(self, pxformat=\"rgba8\", cbits=True):\n        \"\"\"\n        Specify the pixel format of the texture.\n\n        :param pxformat: Identifier for the pixel format of each pixel.\n        :type pxformat: str\n        :param cbits: True if the pixels use a command bit.\n        :type cbits: bool\n        \"\"\"\n        self.pxformat = {\n            \"format\": pxformat,\n            \"cbits\": cbits,\n        }\n\n    def dump(self):\n        output_str = \"\"\n\n        # header\n        output_str += \"# openage texture definition file\\n\\n\"\n\n        # version\n        output_str += f\"version {FORMAT_VERSION}\\n\\n\"\n\n        # image file\n        output_str += f\"imagefile \\\"{self.image_file}\\\"\\n\"\n\n        output_str += \"\\n\"\n\n        # size\n        output_str += f\"size {self.size['width']} {self.size['height']}\\n\"\n\n        output_str += \"\\n\"\n\n        # pixel format\n        output_str += f\"pxformat {self.pxformat['format']}\"\n\n        if self.pxformat[\"cbits\"]:\n            output_str += f\" cbits={self.pxformat['cbits']}\"\n\n        output_str += \"\\n\\n\"\n\n        # subtex definitions\n        for subtex in self.subtexs:\n            output_str += f'subtex {\" \".join(str(param) for param in subtex.values())}\\n'\n\n        return output_str\n\n    def __repr__(self):\n        return f'TextureMetadata<{self.filename}>'\n"
  },
  {
    "path": "openage/convert/entity_object/export/media_export_request.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,arguments-differ\n\"\"\"\nSpecifies a request for a media resource that should be\nconverted and exported into a modpack.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....util.observer import Observable\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media_types import MediaType\n\n\nclass MediaExportRequest(Observable):\n    \"\"\"\n    Generic superclass for export requests.\n    \"\"\"\n\n    __slots__ = (\"media_type\", \"targetdir\", \"source_filename\", \"target_filename\")\n\n    def __init__(\n        self,\n        media_type: MediaType,\n        targetdir: str,\n        source_filename: str,\n        target_filename: str\n    ):\n        \"\"\"\n        Create a request for a media file.\n\n        :param media_type: Media type of the requested  source file.\n        :type media_type: MediaType\n        :param targetdir: Relative path to the export directory.\n        :type targetdir: str\n        :param source_filename: Filename of the source file.\n        :type source_filename: str\n        :param target_filename: Filename of the resulting file.\n        :type target_filename: str\n        \"\"\"\n        super().__init__()\n\n        self.media_type = media_type\n        self.targetdir = targetdir\n        self.source_filename = source_filename\n        self.target_filename = target_filename\n\n    def get_type(self) -> MediaType:\n        \"\"\"\n        Return the media type.\n        \"\"\"\n        return self.media_type\n\n    def set_source_filename(self, filename: str) -> None:\n        \"\"\"\n        Sets the filename for the source file.\n\n        :param filename: Filename of the source file.\n        :type filename: str\n        \"\"\"\n        if not isinstance(filename, str):\n            raise ValueError(f\"str expected as source filename, not {type(filename)}\")\n\n        self.source_filename = filename\n\n    def set_target_filename(self, filename: str) -> None:\n        \"\"\"\n        Sets the filename for the target file.\n\n        :param filename: Filename of the resulting file.\n        :type filename: str\n        \"\"\"\n        if not isinstance(filename, str):\n            raise ValueError(f\"str expected as target filename, not {type(filename)}\")\n\n        self.target_filename = filename\n\n    def set_targetdir(self, targetdir: str) -> None:\n        \"\"\"\n        Sets the target directory for the file.\n\n        :param targetdir: Relative path to the export directory.\n        :type targetdir: str\n        \"\"\"\n        if not isinstance(targetdir, str):\n            raise ValueError(f\"str expected as targetdir, not {type(targetdir)}\")\n\n        self.targetdir = targetdir\n"
  },
  {
    "path": "openage/convert/entity_object/export/metadata_export.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments,too-many-locals\n\"\"\"\nExport requests for media metadata.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....util.observer import Observer\nfrom .formats.sprite_metadata import SpriteMetadata\nfrom .formats.texture_metadata import TextureMetadata\nfrom .formats.terrain_metadata import TerrainMetadata\n\nif typing.TYPE_CHECKING:\n    from openage.util.observer import Observable\n    from openage.convert.entity_object.export.formats.sprite_metadata import LayerMode\\\n        as SpriteLayerMode\n    from openage.convert.entity_object.export.formats.terrain_metadata import LayerMode\\\n        as TerrainLayerMode\n\n\nclass MetadataExport(Observer):\n    \"\"\"\n    A class for exporting metadata from another format. MetadataExports are\n    observers so they can receive data from media conversion.\n    \"\"\"\n\n    def __init__(self, targetdir: str, target_filename: str):\n\n        self.targetdir = targetdir\n        self.filename = target_filename\n\n    def update(self, observable: Observable, message=None):\n        return NotImplementedError(\"Interface does not implement update()\")\n\n    def __repr__(self):\n        return f\"MetadataExport<{type(self)}>\"\n\n\nclass SpriteMetadataExport(MetadataExport):\n    \"\"\"\n    Export requests for sprite definition files.\n    \"\"\"\n\n    def __init__(self, targetdir, target_filename):\n        super().__init__(targetdir, target_filename)\n\n        self.graphics_metadata: dict[int, tuple] = {}\n        self.subtex_count: dict[str, int] = {}\n\n    def add_graphics_metadata(\n        self,\n        img_filename: str,\n        tex_filename: str,\n        layer_mode: SpriteLayerMode,\n        layer_pos: int,\n        frame_rate: float,\n        replay_delay: float,\n        frame_count: int,\n        angle_count: int,\n        mirror_mode: int,\n        start_angle: int = 0,\n    ):\n        \"\"\"\n        Add metadata from the GenieGraphic object.\n\n        :param img_filename: Filename of the exported PNG file.\n        :param tex_filename: Filename of the .texture file.\n        :param layer_mode: Animation mode (off, once, loop).\n        :param layer_pos: Layer position.\n        :param frame_rate: Time spent on each frame.\n        :param replay_delay: Time delay before replaying the animation.\n        :param frame_count: Number of frames per angle in the animation.\n        :param angle_count: Number of angles in the animation.\n        :param mirror_mode: Mirroring mode (0, 1). If 1, angles above 180 degrees are mirrored.\n        :param start_angle: Angle used for the first frame in the .texture file.\n        \"\"\"\n        self.graphics_metadata[img_filename] = (\n            tex_filename,\n            layer_mode,\n            layer_pos,\n            frame_rate,\n            replay_delay,\n            frame_count,\n            angle_count,\n            mirror_mode,\n            start_angle\n        )\n\n    def dump(self) -> str:\n        \"\"\"\n        Creates a human-readable string that can be written to a file.\n        \"\"\"\n        sprite_file = SpriteMetadata(self.targetdir, self.filename)\n\n        tex_index = 0\n\n        # if len(self.graphics_metadata) == 0:\n        #     raise ValueError(\"No graphics metadata in sprite file.\")\n\n        for img_filename, metadata in self.graphics_metadata.items():\n            tex_filename = metadata[0]\n            sprite_file.add_texture(tex_index, tex_filename)\n            sprite_file.add_layer(tex_index, *metadata[1:5])\n\n            frame_count = metadata[5]\n            angle_count = metadata[6]\n            mirror_mode = metadata[7]\n            start_angle = metadata[8]\n\n            if angle_count == 0:\n                angle_count = 1\n\n            degree = 0\n            if start_angle and angle_count > 1:\n                # set the angle of the first frame in the sprite list\n                # in openage, angles are defined clockwise, with 0 being the\n                # front-facing angle (i.e. the unit sprite faces the camera)\n                degree = start_angle % 360\n\n            degree_step = 360 / angle_count\n            for angle_index in range(angle_count):\n                mirror_from = None\n                if mirror_mode:\n                    if degree > 180:\n                        mirrored_angle = (angle_index - angle_count) * (-1)\n                        mirror_from = (start_angle + int(mirrored_angle * degree_step)) % 360\n\n                sprite_file.add_angle(int(degree), mirror_from)\n\n                if not mirror_from:\n                    for frame_idx in range(frame_count):\n                        subtex_index = frame_idx + angle_index * frame_count\n                        if subtex_index >= self.subtex_count[img_filename]:\n                            # TODO: Can happen for some death and projectile animations. Why?\n                            break\n\n                        sprite_file.add_frame(\n                            frame_idx,\n                            int(degree),\n                            tex_index,\n                            tex_index,\n                            subtex_index\n                        )\n\n                degree = (degree + degree_step) % 360\n\n            tex_index += 1\n\n        return sprite_file.dump()\n\n    def update(self, observable, message=None):\n        \"\"\"\n        Receive metdata from the graphics file export.\n\n        :param message: A dict with frame metadata from the exported PNG file.\n        :type message: dict\n        \"\"\"\n        if message:\n            for tex_filename, metadata in message.items():\n                self.subtex_count[tex_filename] = len(metadata[\"subtex_metadata\"])\n\n\nclass TextureMetadataExport(MetadataExport):\n    \"\"\"\n    Export requests for texture definition files.\n    \"\"\"\n\n    def __init__(self, targetdir, target_filename):\n        super().__init__(targetdir, target_filename)\n\n        self.imagefile = None\n        self.size = None\n        self.pxformat = \"rgba8\"\n        self.cbits = True\n        self.subtex_metadata = []\n\n    def add_imagefile(self, img_filename):\n        \"\"\"\n        Add metadata from the GenieGraphic object.\n\n        :param img_filename: Filename of the exported PNG file.\n        \"\"\"\n        self.imagefile = img_filename\n\n    def dump(self):\n        \"\"\"\n        Creates a human-readable string that can be written to a file.\n        \"\"\"\n        texture_file = TextureMetadata(self.targetdir, self.filename)\n\n        texture_file.set_imagefile(self.imagefile)\n        texture_file.set_size(self.size[0], self.size[1])\n        texture_file.set_pxformat(self.pxformat, self.cbits)\n\n        for subtex_metadata in self.subtex_metadata:\n            texture_file.add_subtex(*subtex_metadata.values())\n\n        return texture_file.dump()\n\n    def update(self, observable: Observable, message: dict = None):\n        \"\"\"\n        Receive metdata from the graphics file export.\n\n        :param message: A dict with texture metadata from the exported PNG file.\n        :type message: dict\n        \"\"\"\n        if message:\n            texture_metadata = message[self.imagefile]\n            self.size = texture_metadata[\"size\"]\n            self.subtex_metadata = texture_metadata[\"subtex_metadata\"]\n\n\nclass TerrainMetadataExport(MetadataExport):\n    \"\"\"\n    Export requests for texture definition files.\n    \"\"\"\n\n    def __init__(self, targetdir, target_filename):\n        super().__init__(targetdir, target_filename)\n\n        self.graphics_metadata: dict[int, tuple] = {}\n        self.subtex_count: dict[str, int] = {}\n\n    def add_graphics_metadata(\n        self,\n        img_filename: str,\n        tex_filename: str,\n        layer_mode: TerrainLayerMode,\n        layer_pos: int,\n        frame_rate: float,\n        replay_delay: float,\n        frame_count: int,\n    ):\n        \"\"\"\n        Add metadata from the GenieGraphic object.\n\n        :param img_filename: Filename of the exported PNG file.\n        :param tex_filename: Filename of the .texture file.\n        :param layer_mode: Animation mode (off, loop).\n        :param layer_pos: Layer position.\n        :param frame_rate: Time spent on each frame.\n        :param replay_delay: Time delay before replaying the animation.\n        :param frame_count: Number of frames in the animation.\n        \"\"\"\n        self.graphics_metadata[img_filename] = (\n            tex_filename,\n            layer_mode,\n            layer_pos,\n            frame_rate,\n            replay_delay,\n            frame_count\n        )\n\n    def dump(self) -> str:\n        \"\"\"\n        Creates a human-readable string that can be written to a file.\n        \"\"\"\n        terrain_file = TerrainMetadata(self.targetdir, self.filename)\n\n        tex_index = 0\n        for _, metadata in self.graphics_metadata.items():\n            tex_filename = metadata[0]\n            terrain_file.add_texture(tex_index, tex_filename)\n            terrain_file.add_layer(tex_index, *metadata[1:5])\n\n            frame_count = metadata[5]\n\n            for frame_idx in range(frame_count):\n                subtex_index = frame_idx\n                terrain_file.add_frame(\n                    frame_idx,\n                    tex_index,\n                    tex_index,\n                    subtex_index\n                )\n\n            tex_index += 1\n\n        return terrain_file.dump()\n"
  },
  {
    "path": "openage/convert/entity_object/export/texture.py",
    "content": "# Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n\"\"\" Routines for texture generation etc \"\"\"\n\n# TODO pylint: disable=C,R\n\nfrom __future__ import annotations\nimport typing\n\nfrom PIL import Image\n\nimport numpy\n\nfrom ....log import spam\nfrom ...value_object.read.media.blendomatic import BlendingMode\nfrom ...value_object.read.media.hardcoded.terrain_tile_size import TILE_HALFSIZE\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media.colortable import ColorTable\n    from openage.convert.service.export.interface.cutter import InterfaceCutter\n    from openage.convert.value_object.read.media.slp import SLP, SLPFrame\n    from openage.convert.value_object.read.media.smp import SMP, SMPLayer\n    from openage.convert.value_object.read.media.smx import SMX, SMXLayer\n    from openage.convert.value_object.read.media.sld import SLD, SLDLayer\n\n\nclass TextureImage:\n    \"\"\"\n    represents a image created from a (r,g,b,a) matrix.\n    \"\"\"\n\n    def __init__(\n        self,\n        picture_data: typing.Union[Image.Image, numpy.ndarray],\n        hotspot: tuple[int, int] = None\n    ):\n\n        if isinstance(picture_data, Image.Image):\n            if picture_data.mode != 'RGBA':\n                picture_data = picture_data.convert('RGBA')\n\n            picture_data = numpy.array(picture_data)\n\n        if not isinstance(picture_data, numpy.ndarray):\n            raise ValueError(\"Texture image must be created from PIL Image \"\n                             \"or numpy array, not '%s'\" % type(picture_data))\n\n        self.width: int = picture_data.shape[1]\n        self.height: int = picture_data.shape[0]\n\n        spam(\"creating TextureImage with size %d x %d\", self.width, self.height)\n\n        if hotspot is None:\n            self.hotspot = (0, 0)\n        else:\n            self.hotspot = hotspot\n\n        self.data = picture_data\n\n    def get_pil_image(self) -> Image.Image:\n        return Image.fromarray(self.data)\n\n    def get_data(self) -> numpy.ndarray:\n        return self.data\n\n\nclass Texture:\n    \"\"\"\n    one sprite, as part of a texture atlas.\n\n    stores information about positions and sizes\n    of sprites included in the 'big texture'.\n    \"\"\"\n\n    def __init__(\n        self,\n        input_data: typing.Union[SLP, SMP, SMX, SLD, BlendingMode],\n        palettes: dict[int, ColorTable] = None,\n        custom_cutter: InterfaceCutter = None,\n        layer: int = 0\n    ):\n        super().__init__()\n\n        # Compression setting values for libpng\n        self.best_compr: tuple = None\n\n        # Best packer hints (positions of sprites in texture)\n        self.best_packer_hints: tuple = None\n\n        self.image_data: TextureImage = None\n        self.image_metadata: list[dict[str, int]] = {}\n\n        spam(\"creating Texture from %s\", repr(input_data))\n\n        from ...value_object.read.media.slp import SLP\n        from ...value_object.read.media.smp import SMP\n        from ...value_object.read.media.smx import SMX\n        from ...value_object.read.media.sld import SLD\n\n        self.frames = []\n        if isinstance(input_data, (SLP, SMP, SMX)):\n            input_frames = input_data.get_frames(layer)\n            for frame in input_frames:\n                # Palette can be different for every frame\n                palette_number = frame.get_palette_number()\n\n                if palette_number is None:\n                    main_palette = None\n\n                else:\n                    main_palette = palettes[palette_number].array\n\n                for subtex in self._to_subtextures(frame,\n                                                   main_palette,\n                                                   custom_cutter):\n                    self.frames.append(subtex)\n\n        elif isinstance(input_data, SLD):\n            input_frames = input_data.get_frames(layer)\n            if layer == 0 and len(input_frames) == 0:\n                # Use shadows if no main graphics are inside\n                input_frames = input_data.get_frames(layer=1)\n\n            for frame in input_frames:\n                subtex = TextureImage(\n                    frame.get_picture_data(),\n                    hotspot=frame.get_hotspot()\n                )\n                self.frames.append(subtex)\n\n        elif isinstance(input_data, BlendingMode):\n            self.frames = [\n                # the hotspot is in the west corner of a tile.\n                TextureImage(\n                    tile.get_picture_data(),\n                    hotspot=(0, TILE_HALFSIZE[\"y\"])\n                )\n                for tile in input_data.alphamasks\n            ]\n        else:\n            raise TypeError(\"cannot create Texture \"\n                            \"from unknown source type: %s\" % (type(input_data)))\n\n    def _to_subtextures(\n        self,\n        frame: typing.Union[SLPFrame, SMPLayer, SMXLayer],\n        main_palette: ColorTable,\n        custom_cutter: InterfaceCutter = None\n    ):\n        \"\"\"\n        convert slp to subtexture or subtextures, using a palette.\n        \"\"\"\n        subtex = TextureImage(\n            frame.get_picture_data(main_palette),\n            hotspot=frame.get_hotspot()\n        )\n\n        if custom_cutter:\n            # this may cut the texture into some parts\n            return custom_cutter.cut(subtex)\n\n        else:\n            return [subtex]\n\n    def get_metadata(self) -> list[dict[str, int]]:\n        \"\"\"\n        Get the image metadata information.\n        \"\"\"\n        return self.image_metadata\n\n    def get_cache_params(self) -> tuple[tuple, tuple]:\n        \"\"\"\n        Get the parameters used for packing and saving the texture.\n            - Packing hints (sprite index, (xpos, ypos) in the final texture)\n            - PNG compression parameters (compression level + deflate params)\n        \"\"\"\n        return self.best_packer_hints, self.best_compr\n"
  },
  {
    "path": "openage/convert/main.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-branches\n\"\"\"\nEntry point for all of the asset conversion.\n\"\"\"\nfrom __future__ import annotations\n\nfrom datetime import datetime\nimport typing\n\nfrom ..log import info, warn\n\nfrom ..util.fslike.directory import CaseIgnoringDirectory\nfrom ..util.fslike.wrapper import (DirectoryCreator,\n                                   Synchronizer as AccessSynchronizer)\nfrom .service.debug_info import debug_cli_args, debug_game_version, debug_mounts\nfrom .service.init.changelog import check_updates\nfrom .service.init.modpack_search import enumerate_modpacks\nfrom .service.init.mount_asset_dirs import mount_asset_dirs\nfrom .service.init.version_detect import create_version_objects\nfrom .tool.interactive import interactive_browser\nfrom .tool.subtool.acquire_sourcedir import acquire_conversion_source_dir, wanna_convert, \\\n    wanna_check_updates\nfrom .tool.subtool.version_select import get_game_version\n\nif typing.TYPE_CHECKING:\n    from argparse import ArgumentParser, Namespace\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.union import UnionPath\n    from openage.util.fslike.path import Path\n\n\ndef convert_assets(\n    assets: UnionPath,\n    args: Namespace,\n    srcdir: Directory = None\n) -> None:\n    \"\"\"\n    Perform asset conversion.\n\n    Requires original assets and stores them in usable and free formats.\n\n    assets must be a filesystem-like object pointing at the game's asset dir.\n    srcdir must be None, or point at some source directory.\n\n    This method prepares srcdir and targetdir to allow a pleasant, unified\n    conversion experience, then passes them to .driver.convert().\n    \"\"\"\n    converted_path = assets / \"converted\"\n    converted_path.mkdirs()\n    targetdir = DirectoryCreator(converted_path).root\n\n    # Set compression level for media output if it was not set\n    if \"compression_level\" not in vars(args):\n        args.compression_level = 1\n\n    # Set worker count for multi-threading if it was not set\n    if \"jobs\" not in vars(args):\n        args.jobs = None\n\n    # Set verbosity for debug output\n    if \"debug_info\" not in vars(args) or not args.debug_info:\n        if args.devmode:\n            args.debug_info = 3\n\n        else:\n            args.debug_info = 0\n\n    # add a dir for debug info\n    debug_log_path = converted_path / \"debug\" / datetime.now().strftime(\"%Y-%m-%d-%H-%M-%S\")\n    debugdir = DirectoryCreator(debug_log_path).root\n    args.debugdir = debugdir\n\n    # Create CLI args info\n    debug_cli_args(args.debugdir, args.debug_info, args)\n\n    # Initialize game versions data\n    auxiliary_files_dir = args.cfg_dir / \"converter\" / \"games\"\n    args.avail_game_eds, args.avail_game_exps = create_version_objects(auxiliary_files_dir)\n\n    # try to get previously used source dir\n    asset_locations_path = assets / \"converted\" / \"asset_locations.cache\"\n    prev_srcdirs = get_prev_srcdir_paths(asset_locations_path)\n\n    # acquire conversion source directory\n    if srcdir is None:\n        srcdir = acquire_conversion_source_dir(args.avail_game_eds, prev_srcdirs)\n\n    # Acquire game version info\n    args.game_version = get_game_version(srcdir, args.avail_game_eds, args.avail_game_exps)\n    debug_game_version(args.debugdir, args.debug_info, args)\n\n    if not args.game_version.edition:\n        return None\n\n    # Mount assets into conversion folder\n    data_dir = mount_asset_dirs(srcdir, args.game_version)\n    if not data_dir:\n        return None\n\n    args.srcdir = AccessSynchronizer(data_dir).root\n    args.targetdir = targetdir\n\n    # Create mountpoint info\n    debug_mounts(args.debugdir, args.debug_info, args)\n\n    def flag(name):\n        \"\"\"\n        Convenience function for accessing boolean flags in args.\n        Flags default to False if they don't exist.\n        \"\"\"\n        return getattr(args, name, False)\n\n    args.flag = flag\n\n    # import here so codegen.py doesn't depend on it.\n    from .tool.driver import convert\n\n    # Run the conversion process\n    convert(args)\n\n    # clean args\n    del args.srcdir\n    del args.targetdir\n\n    # Remember the asset location if it is not already in the cache\n    if prev_srcdirs is None:\n        asset_locations_path.touch()\n        prev_srcdirs = set()\n\n    used_asset_path = data_dir.resolve_native_path().decode('utf-8')\n    if used_asset_path not in prev_srcdirs:\n        try:\n            with asset_locations_path.open(\"a\") as file_obj:\n                if len(prev_srcdirs) > 0:\n                    file_obj.write(\"\\n\")\n\n                file_obj.write(used_asset_path)\n\n        except IOError:\n            # cache file cannot be accessed, skip writing\n            warn(f\"Cannot access asset location cache file {asset_locations_path}\")\n            info(\"Skipped saving asset location\")\n\n\ndef get_prev_srcdir_paths(asset_location_path: Path) -> set[str] | None:\n    \"\"\"\n    Get previously used source directories from a cache file.\n\n    :param asset_location_path: Path to the cache file.\n    :type asset_location_path: Path\n    :return: Previously used source directories.\n    :rtype: set[str] | None\n    \"\"\"\n    prev_source_dirs: set[str] = set()\n    try:\n        with asset_location_path.open(\"r\") as file_obj:\n            prev_source_dirs.update(file_obj.read().split(\"\\n\"))\n\n    except FileNotFoundError:\n        prev_source_dirs = None\n\n    return prev_source_dirs\n\n\ndef init_subparser(cli: ArgumentParser):\n    \"\"\" Initializes the parser for convert-specific args. \"\"\"\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\n        \"--source-dir\", default=None,\n        help=\"source data directory\")\n\n    cli.add_argument(\n        \"--output-dir\", default=None,\n        help=\"destination data output directory\")\n\n    cli.add_argument(\n        \"--force\", action='store_true',\n        help=\"force conversion, even if up-to-date assets already exist.\")\n\n    cli.add_argument(\n        \"--gen-extra-files\", action='store_true',\n        help=\"generate some extra files, useful for debugging the converter.\")\n\n    cli.add_argument(\n        \"--no-media\", action='store_true',\n        help=\"do not convert any media files (slp, wav, ...)\")\n\n    cli.add_argument(\n        \"--no-metadata\", action='store_true',\n        help=(\"do not store any metadata \"\n              \"(except for those associated with media files)\"))\n\n    cli.add_argument(\n        \"--no-sounds\", action='store_true',\n        help=\"do not convert any sound files\")\n\n    cli.add_argument(\n        \"--no-graphics\", action='store_true',\n        help=\"do not convert game graphics\")\n\n    cli.add_argument(\n        \"--no-interface\", action='store_true',\n        help=\"do not convert interface graphics\")\n\n    cli.add_argument(\n        \"--no-scripts\", action='store_true',\n        help=\"do not convert scripts (AI and Random Maps)\")\n\n    cli.add_argument(\n        \"--no-pickle-cache\", action='store_true',\n        help=\"don't use a pickle file to skip the dat file reading.\")\n\n    cli.add_argument(\n        \"--jobs\", \"-j\", type=int, default=None)\n\n    cli.add_argument(\n        \"--interactive\", \"-i\", action='store_true',\n        help=\"browse the files interactively\")\n\n    cli.add_argument(\n        \"--id\", type=int, default=None,\n        help=\"only convert files with this id (used for debugging..)\")\n\n    cli.add_argument(\n        \"--compression-level\", type=int, default=2, choices=[0, 1, 2, 3, 4],\n        help=\"set PNG compression level\")\n\n    cli.add_argument(\n        \"--debug-info\", type=int, choices=[0, 1, 2, 3, 4, 5, 6],\n        help=\"create debug output for the converter run; verbosity levels 0-6\")\n\n    cli.add_argument(\n        \"--low-memory\", action='store_true',\n        help=\"Activate low memory mode\")\n\n    cli.add_argument(\n        \"--export-api\", action='store_true',\n        help=\"Export the openage nyan API definition as a modpack\")\n\n    cli.add_argument(\n        \"--check-updates\", action='store_true',\n        help=\"Check if the assets are up to date\"\n    )\n\n    cli.add_argument(\n        \"--no-prompts\", action='store_false', dest='show_prompts',\n        help=\"Disable user prompts\"\n    )\n\n\ndef main(args, error):\n    \"\"\" CLI entry point \"\"\"\n    del error  # unused\n\n    # initialize libopenage\n    from ..cppinterface.setup import setup\n    setup(args)\n\n    # conversion source\n    if args.source_dir is not None:\n        srcdir = CaseIgnoringDirectory(args.source_dir).root\n    else:\n        srcdir = None\n\n    # mount the config folder at \"cfg/\"\n    from ..cvar.location import get_config_path\n    from ..util.fslike.union import Union\n    root = Union().root\n    root[\"cfg\"].mount(get_config_path())\n    args.cfg_dir = root[\"cfg\"]\n\n    if args.interactive:\n        interactive_browser(root[\"cfg\"], srcdir)\n        return 0\n\n    # conversion target\n    from ..assets import get_asset_path\n    outdir = get_asset_path(args.output_dir)\n\n    if args.force or (args.show_prompts and wanna_convert()):\n        convert_assets(outdir, args, srcdir)\n\n    if args.check_updates or (args.show_prompts and wanna_check_updates()):\n        # check if the assets are up to date\n        modpack_dir = outdir / \"converted\"\n        available_modpacks = enumerate_modpacks(modpack_dir, exclude={\"engine\"})\n\n        game_info_dir = args.cfg_dir / \"converter\" / \"games\"\n\n        check_updates(available_modpacks, game_info_dir)\n\n    return 0\n"
  },
  {
    "path": "openage/convert/processor/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n)\n\nadd_subdirectory(conversion)\nadd_subdirectory(export)\n"
  },
  {
    "path": "openage/convert/processor/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProcessors for the conversion.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n)\n\nadd_subdirectory(aoc)\nadd_subdirectory(aoc_demo)\nadd_subdirectory(de1)\nadd_subdirectory(de2)\nadd_subdirectory(hd)\nadd_subdirectory(ror)\nadd_subdirectory(swgbcc)\n"
  },
  {
    "path": "openage/convert/processor/conversion/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProcessors used for conversion.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tability_subprocessor.py\n\tauxiliary_subprocessor.py\n\tciv_subprocessor.py\n\teffect_subprocessor.py\n\tmedia_subprocessor.py\n\tmodifier_subprocessor.py\n\tmodpack_subprocessor.py\n\tnyan_subprocessor.py\n\tpregen_processor.py\n\tprocessor.py\n\ttech_subprocessor.py\n\tupgrade_ability_subprocessor.py\n\tupgrade_attribute_subprocessor.py\n\tupgrade_effect_subprocessor.py\n\tupgrade_resource_subprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/__init__.py",
    "content": "# Copyright 2019-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for AoE2: The Conquerors 1.0c.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/ability_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-public-methods,too-many-lines,too-many-locals\n# pylint: disable=too-many-branches,too-many-statements,too-many-arguments\n# pylint: disable=invalid-name\n#\n# TODO:\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nDerives and adds abilities to lines. Subroutine of the\nnyan subprocessor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom math import degrees\n\n\nfrom .....nyan.nyan_structs import MemberSpecialValue, MemberOperator\nfrom .....util.ordered_set import OrderedSet\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup, \\\n    GenieAmbientGroup, GenieGarrisonMode, GenieStackBuildingGroup, \\\n    GenieUnitLineGroup, GenieMonkGroup, GenieVillagerGroup\nfrom ....entity_object.conversion.combined_sound import CombinedSound\nfrom ....entity_object.conversion.combined_sprite import CombinedSprite\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....entity_object.conversion.converter_object import RawMemberPush\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom .effect_subprocessor import AoCEffectSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n\n\nFLOAT32_MAX = 3.4028234663852886e+38\n\n\nclass AoCAbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for abilities in AoC.\n    \"\"\"\n\n    @staticmethod\n    def active_transform_to_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ActiveTransformTo ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        # TODO: Implement\n        return None\n\n    @staticmethod\n    def apply_continuous_effect_ability(\n        line: GenieGameEntityGroup,\n        command_id: int,\n        ranged: bool = False\n    ) -> ForwardRef:\n        \"\"\"\n        Adds the ApplyContinuousEffect ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.get_units_with_command(command_id)[0]\n\n        else:\n            current_unit = line.get_head_unit()\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        if ranged:\n            ability_parent = \"engine.ability.type.RangedContinuousEffect\"\n\n        else:\n            ability_parent = \"engine.ability.type.ApplyContinuousEffect\"\n\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(ability_parent)\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Get animation from commands proceed sprite\n        unit_commands = current_unit[\"unit_commands\"].value\n        for command in unit_commands:\n            type_id = command[\"type\"].value\n\n            if type_id != command_id:\n                continue\n\n            ability_animation_id = command[\"proceed_sprite_id\"].value\n            break\n\n        else:\n            ability_animation_id = -1\n\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"attack_sprite_id\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}{ability_name}\"\n                    filename_prefix = (f\"{command_lookup_dict[command_id][1]}_\"\n                                       f\"{gset_lookup_dict[graphics_set_id][2]}_\")\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        # Command Sound\n        ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    ability_name,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\", sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        if ranged:\n            # Min range\n            min_range = current_unit[\"weapon_range_min\"].value\n            ability_raw_api_object.add_raw_member(\"min_range\",\n                                                  min_range,\n                                                  \"engine.ability.type.RangedContinuousEffect\")\n\n            # Max range\n            if command_id == 105:\n                # Heal\n                max_range = 4\n\n            else:\n                max_range = current_unit[\"weapon_range_max\"].value\n\n            ability_raw_api_object.add_raw_member(\"max_range\",\n                                                  max_range,\n                                                  \"engine.ability.type.RangedContinuousEffect\")\n\n        # Effects\n        if command_id == 101:\n            # Construct\n            effects = AoCEffectSubprocessor.get_construct_effects(line, ability_ref)\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n                )\n            ]\n\n        elif command_id == 105:\n            # Heal\n            effects = AoCEffectSubprocessor.get_heal_effects(line, ability_ref)\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n            ]\n\n        elif command_id == 106:\n            # Repair\n            effects = AoCEffectSubprocessor.get_repair_effects(line, ability_ref)\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n                )\n            ]\n\n        ability_raw_api_object.add_raw_member(\"effects\",\n                                              effects,\n                                              \"engine.ability.type.ApplyContinuousEffect\")\n\n        # Application delay\n        apply_graphic = dataset.genie_graphics[ability_animation_id]\n        frame_rate = apply_graphic.get_frame_rate()\n        frame_delay = current_unit[\"frame_delay\"].value\n        application_delay = frame_rate * frame_delay\n        ability_raw_api_object.add_raw_member(\"application_delay\",\n                                              application_delay,\n                                              \"engine.ability.type.ApplyContinuousEffect\")\n\n        # Allowed types\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ApplyContinuousEffect\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.ApplyContinuousEffect\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def activity_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Activity ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Activity\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Activity\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Activity\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # activity graph\n        if isinstance(line, GenieUnitLineGroup):\n            activity = dataset.pregen_nyan_objects[\"util.activity.types.Unit\"].get_nyan_object()\n\n        else:\n            activity = dataset.pregen_nyan_objects[\"util.activity.types.Default\"].get_nyan_object()\n\n        ability_raw_api_object.add_raw_member(\"graph\", activity, \"engine.ability.type.Activity\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def apply_discrete_effect_ability(\n        line: GenieGameEntityGroup,\n        command_id: int,\n        ranged: bool = False,\n        projectile: int = -1\n    ) -> ForwardRef:\n        \"\"\"\n        Adds the ApplyDiscreteEffect ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.get_units_with_command(command_id)[0]\n            current_unit_id = current_unit[\"id0\"].value\n\n        else:\n            current_unit = line.get_head_unit()\n            current_unit_id = line.get_head_unit_id()\n\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        if ranged:\n            ability_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        else:\n            ability_parent = \"engine.ability.type.ApplyDiscreteEffect\"\n\n        if projectile == -1:\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(ability_ref,\n                                                  ability_name,\n                                                  dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            ability_animation_id = current_unit[\"attack_sprite_id\"].value\n\n        else:\n            ability_ref = (f\"{game_entity_name}.ShootProjectile.Projectile{projectile}.\"\n                           f\"{ability_name}\")\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(\n                line,\n                f\"{game_entity_name}.ShootProjectile.Projectile{projectile}\"\n            )\n            ability_raw_api_object.set_location(ability_location)\n\n            ability_animation_id = -1\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animated\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"attack_sprite_id\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}{ability_name}\"\n                    filename_prefix = (f\"{command_lookup_dict[command_id][1]}_\"\n                                       f\"{gset_lookup_dict[graphics_set_id][2]}_\")\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        # Command Sound\n        if projectile == -1:\n            ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n\n        else:\n            ability_comm_sound_id = -1\n\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n\n            if projectile == -1:\n                sound_obj_prefix = ability_name\n\n            else:\n                sound_obj_prefix = \"ProjectileAttack\"\n\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    sound_obj_prefix,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\", sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        if ranged:\n            # Min range\n            min_range = current_unit[\"weapon_range_min\"].value\n            ability_raw_api_object.add_raw_member(\"min_range\",\n                                                  min_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n            # Max range\n            max_range = current_unit[\"weapon_range_max\"].value\n            ability_raw_api_object.add_raw_member(\"max_range\",\n                                                  max_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n        # Effects\n        batch_ref = f\"{ability_ref}.Batch\"\n        batch_raw_api_object = RawAPIObject(batch_ref, \"Batch\", dataset.nyan_api_objects)\n        batch_raw_api_object.add_raw_parent(\"engine.util.effect_batch.type.UnorderedBatch\")\n        batch_location = ForwardRef(line, ability_ref)\n        batch_raw_api_object.set_location(batch_location)\n\n        line.add_raw_api_object(batch_raw_api_object)\n\n        if command_id == 7:\n            # Attack\n            if projectile != 1:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref)\n\n            else:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref, projectile=1)\n\n        elif command_id == 104:\n            # Convert\n            effects = AoCEffectSubprocessor.get_convert_effects(line, batch_ref)\n\n        batch_raw_api_object.add_raw_member(\"effects\",\n                                            effects,\n                                            \"engine.util.effect_batch.EffectBatch\")\n\n        batch_forward_ref = ForwardRef(line, batch_ref)\n        ability_raw_api_object.add_raw_member(\"batches\",\n                                              [batch_forward_ref],\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Reload time\n        if projectile == -1:\n            reload_time = current_unit[\"attack_speed\"].value\n\n        else:\n            reload_time = 0\n\n        ability_raw_api_object.add_raw_member(\"reload_time\",\n                                              reload_time,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Application delay\n        if projectile == -1:\n            attack_graphic_id = current_unit[\"attack_sprite_id\"].value\n            attack_graphic = dataset.genie_graphics[attack_graphic_id]\n            frame_rate = attack_graphic.get_frame_rate()\n            frame_delay = current_unit[\"frame_delay\"].value\n            application_delay = frame_rate * frame_delay\n\n        else:\n            application_delay = 0\n\n        ability_raw_api_object.add_raw_member(\"application_delay\",\n                                              application_delay,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Allowed types (all buildings/units)\n        if command_id == 104:\n            # Convert\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n            ]\n\n        else:\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n                )\n            ]\n\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        if command_id == 104:\n            # Convert\n            blacklisted_entities = []\n            for unit_line in dataset.unit_lines.values():\n                if unit_line.has_command(104):\n                    # Blacklist other monks\n                    blacklisted_name = name_lookup_dict[unit_line.get_head_unit_id()][0]\n                    blacklisted_entities.append(ForwardRef(unit_line, blacklisted_name))\n\n                elif unit_line.get_class_id() in (13, 55):\n                    # Blacklist siege\n                    blacklisted_name = name_lookup_dict[unit_line.get_head_unit_id()][0]\n                    blacklisted_entities.append(ForwardRef(unit_line, blacklisted_name))\n\n        else:\n            blacklisted_entities = []\n\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              blacklisted_entities,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def attribute_change_tracker_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the AttributeChangeTracker ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.AttributeChangeTracker\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"AttributeChangeTracker\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.AttributeChangeTracker\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Attribute\n        attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n        ability_raw_api_object.add_raw_member(\"attribute\",\n                                              attribute,\n                                              \"engine.ability.type.AttributeChangeTracker\")\n\n        # Change progress\n        damage_graphics = current_unit[\"damage_graphics\"].value\n        progress_forward_refs = []\n\n        # Damage graphics are ordered ascending, so we start from 0\n        interval_left_bound = 0\n        for damage_graphic_member in damage_graphics:\n            interval_right_bound = damage_graphic_member[\"damage_percent\"].value\n            progress_ref = f\"{ability_ref}.ChangeProgress{interval_right_bound}\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   f\"ChangeProgress{interval_right_bound}\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.AttributeChange\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   interval_left_bound,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   interval_right_bound,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # AnimationOverlay property\n            # =====================================================================================\n            progress_animation_id = damage_graphic_member[\"graphic_id\"].value\n            if progress_animation_id > -1:\n                property_ref = f\"{progress_ref}.AnimationOverlay\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"AnimationOverlay\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.AnimationOverlay\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                    line,\n                    progress_animation_id,\n                    property_ref,\n                    \"Idle\",\n                    f\"idle_damage_override_{interval_right_bound}_\"\n                )\n                animations_set.append(animation_forward_ref)\n                property_raw_api_object.add_raw_member(\"overlays\",\n                                                       animations_set,\n                                                       \"engine.util.progress.property.type.AnimationOverlay\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.AnimationOverlay\"]: property_forward_ref\n                })\n\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            interval_left_bound = interval_right_bound\n\n        ability_raw_api_object.add_raw_member(\"change_progress\",\n                                              progress_forward_refs,\n                                              \"engine.ability.type.AttributeChangeTracker\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def collect_storage_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the CollectStorage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.CollectStorage\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"CollectStorage\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.CollectStorage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Container\n        container_ref = f\"{game_entity_name}.Storage.{game_entity_name}Container\"\n        container_forward_ref = ForwardRef(line, container_ref)\n        ability_raw_api_object.add_raw_member(\"container\",\n                                              container_forward_ref,\n                                              \"engine.ability.type.CollectStorage\")\n\n        # Storage elements\n        elements = []\n        entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        for entity in line.garrison_entities:\n            entity_ref = entity_lookups[entity.get_head_unit_id()][0]\n            entity_forward_ref = ForwardRef(entity, entity_ref)\n            elements.append(entity_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"storage_elements\",\n                                              elements,\n                                              \"engine.ability.type.CollectStorage\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def collision_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Collision ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Collision\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Collision\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Collision\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Hitbox object\n        hitbox_name = f\"{game_entity_name}.Collision.{game_entity_name}Hitbox\"\n        hitbox_raw_api_object = RawAPIObject(hitbox_name,\n                                             f\"{game_entity_name}Hitbox\",\n                                             dataset.nyan_api_objects)\n        hitbox_raw_api_object.add_raw_parent(\"engine.util.hitbox.Hitbox\")\n        hitbox_location = ForwardRef(line, ability_ref)\n        hitbox_raw_api_object.set_location(hitbox_location)\n\n        radius_x = current_unit[\"radius_x\"].value\n        radius_y = current_unit[\"radius_y\"].value\n        radius_z = current_unit[\"radius_z\"].value\n\n        hitbox_raw_api_object.add_raw_member(\"radius_x\",\n                                             radius_x,\n                                             \"engine.util.hitbox.Hitbox\")\n        hitbox_raw_api_object.add_raw_member(\"radius_y\",\n                                             radius_y,\n                                             \"engine.util.hitbox.Hitbox\")\n        hitbox_raw_api_object.add_raw_member(\"radius_z\",\n                                             radius_z,\n                                             \"engine.util.hitbox.Hitbox\")\n\n        hitbox_forward_ref = ForwardRef(line, hitbox_name)\n        ability_raw_api_object.add_raw_member(\"hitbox\",\n                                              hitbox_forward_ref,\n                                              \"engine.ability.type.Collision\")\n\n        line.add_raw_api_object(hitbox_raw_api_object)\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def constructable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Constructable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Constructable\"\n        ability_raw_api_object = RawAPIObject(\n            ability_ref, \"Constructable\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Constructable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Starting progress (always 0)\n        ability_raw_api_object.add_raw_member(\"starting_progress\",\n                                              0,\n                                              \"engine.ability.type.Constructable\")\n\n        construction_animation_id = current_unit[\"construction_graphic_id\"].value\n\n        # Construction progress\n        progress_forward_refs = []\n        if line.get_class_id() == 49:\n            # Farms\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress0\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress0\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 0.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction1\"\n            terrain_group = dataset.terrain_groups[29]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            # =====================================================================================\n            init_state_name = f\"{ability_ref}.InitState\"\n            init_state_raw_api_object = RawAPIObject(init_state_name,\n                                                     \"InitState\",\n                                                     dataset.nyan_api_objects)\n            init_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n            init_state_location = ForwardRef(line, property_ref)\n            init_state_raw_api_object.set_location(init_state_location)\n\n            line.add_raw_api_object(init_state_raw_api_object)\n\n            # Priority\n            init_state_raw_api_object.add_raw_member(\"priority\",\n                                                     1,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Enabled abilities\n            enabled_forward_refs = [\n                ForwardRef(line,\n                           f\"{game_entity_name}.VisibilityConstruct0\")\n            ]\n            init_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                     enabled_forward_refs,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Disabled abilities\n            disabled_forward_refs = [\n                ForwardRef(line,\n                           f\"{game_entity_name}.AttributeChangeTracker\"),\n                ForwardRef(line,\n                           f\"{game_entity_name}.LineOfSight\"),\n                ForwardRef(line,\n                           f\"{game_entity_name}.Visibility\")\n            ]\n            if len(line.creates) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Create\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.ProductionQueue\"))\n\n            if len(line.researches) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Research\"))\n\n            if line.is_projectile_shooter():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Attack\"))\n\n            if line.is_garrison():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Storage\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.RemoveStorage\"))\n\n                garrison_mode = line.get_garrison_mode()\n\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.SendBackToTask\"))\n\n                if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.RallyPoint\"))\n\n            if line.is_harvestable():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Harvestable\"))\n\n            if line.is_dropsite():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.DropSite\"))\n\n            if line.is_trade_post():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.TradePost\"))\n\n            init_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                     disabled_forward_refs,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Enabled modifiers\n            init_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                     [],\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Disabled modifiers\n            init_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                     [],\n                                                     \"engine.util.state_machine.StateChanger\")\n            # =====================================================================================\n            init_state_forward_ref = ForwardRef(line, init_state_name)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   init_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress33\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress33\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 33.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction1\"\n            terrain_group = dataset.terrain_groups[29]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            # =====================================================================================\n            construct_state_name = f\"{ability_ref}.ConstructState\"\n            construct_state_raw_api_object = RawAPIObject(construct_state_name,\n                                                          \"ConstructState\",\n                                                          dataset.nyan_api_objects)\n            construct_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n            construct_state_location = ForwardRef(line, ability_ref)\n            construct_state_raw_api_object.set_location(construct_state_location)\n\n            line.add_raw_api_object(construct_state_raw_api_object)\n\n            # Priority\n            construct_state_raw_api_object.add_raw_member(\"priority\",\n                                                          1,\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Enabled abilities\n            construct_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Disabled abilities\n            disabled_forward_refs = [ForwardRef(line,\n                                                f\"{game_entity_name}.AttributeChangeTracker\")]\n            if len(line.creates) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Create\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.ProductionQueue\"))\n            if len(line.researches) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Research\"))\n\n            if line.is_projectile_shooter():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Attack\"))\n\n            if line.is_garrison():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Storage\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.RemoveStorage\"))\n\n                garrison_mode = line.get_garrison_mode()\n\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.SendBackToTask\"))\n\n                if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.RallyPoint\"))\n\n            if line.is_harvestable():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Harvestable\"))\n\n            if line.is_dropsite():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.DropSite\"))\n\n            if line.is_trade_post():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.TradePost\"))\n\n            construct_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                          disabled_forward_refs,\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Enabled modifiers\n            construct_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Disabled modifiers\n            construct_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # =====================================================================================\n            construct_state_forward_ref = ForwardRef(line, construct_state_name)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress66\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress66\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (33.0, 66.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction2\"\n            terrain_group = dataset.terrain_groups[30]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress100\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress100\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (66.0, 100.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   100.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction3\"\n            terrain_group = dataset.terrain_groups[31]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n\n        else:\n            progress_ref = f\"{ability_ref}.ConstructionProgress0\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress0\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 0.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =================================================================================\n            # Idle override\n            # =================================================================================\n            if construction_animation_id > -1:\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                overrides = []\n                override_ref = f\"{property_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                construction_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_construct0_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            # =====================================================================================\n            init_state_name = f\"{ability_ref}.InitState\"\n            init_state_raw_api_object = RawAPIObject(init_state_name,\n                                                     \"InitState\",\n                                                     dataset.nyan_api_objects)\n            init_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n            init_state_location = ForwardRef(line, property_ref)\n            init_state_raw_api_object.set_location(init_state_location)\n\n            line.add_raw_api_object(init_state_raw_api_object)\n\n            # Priority\n            init_state_raw_api_object.add_raw_member(\"priority\",\n                                                     1,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Enabled abilities\n            enabled_forward_refs = [\n                ForwardRef(line,\n                           f\"{game_entity_name}.VisibilityConstruct0\")\n            ]\n            init_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                     enabled_forward_refs,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Disabled abilities\n            disabled_forward_refs = [\n                ForwardRef(line,\n                           f\"{game_entity_name}.AttributeChangeTracker\"),\n                ForwardRef(line,\n                           f\"{game_entity_name}.LineOfSight\"),\n                ForwardRef(line,\n                           f\"{game_entity_name}.Visibility\")\n            ]\n            if len(line.creates) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Create\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.ProductionQueue\"))\n            if len(line.researches) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Research\"))\n\n            if line.is_projectile_shooter():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Attack\"))\n\n            if line.is_garrison():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Storage\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.RemoveStorage\"))\n\n                garrison_mode = line.get_garrison_mode()\n\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.SendBackToTask\"))\n\n                if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.RallyPoint\"))\n\n            if line.is_harvestable():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Harvestable\"))\n\n            if line.is_dropsite():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.DropSite\"))\n\n            if line.is_trade_post():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.TradePost\"))\n\n            init_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                     disabled_forward_refs,\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Enabled modifiers\n            init_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                     [],\n                                                     \"engine.util.state_machine.StateChanger\")\n\n            # Disabled modifiers\n            init_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                     [],\n                                                     \"engine.util.state_machine.StateChanger\")\n            # =====================================================================================\n            init_state_forward_ref = ForwardRef(line, init_state_name)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   init_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress25\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress25\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 25.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   25.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =================================================================================\n            # Idle override\n            # =================================================================================\n            if construction_animation_id > -1:\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                overrides = []\n                override_ref = f\"{progress_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                construction_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_construct25_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            # =====================================================================================\n            construct_state_name = f\"{ability_ref}.ConstructState\"\n            construct_state_raw_api_object = RawAPIObject(construct_state_name,\n                                                          \"ConstructState\",\n                                                          dataset.nyan_api_objects)\n            construct_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n            construct_state_location = ForwardRef(line, property_ref)\n            construct_state_raw_api_object.set_location(construct_state_location)\n\n            line.add_raw_api_object(construct_state_raw_api_object)\n\n            # Priority\n            construct_state_raw_api_object.add_raw_member(\"priority\",\n                                                          1,\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Enabled abilities\n            construct_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Disabled abilities\n            disabled_forward_refs = [ForwardRef(line,\n                                                f\"{game_entity_name}.AttributeChangeTracker\")]\n            if len(line.creates) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Create\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.ProductionQueue\"))\n            if len(line.researches) > 0:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Research\"))\n\n            if line.is_projectile_shooter():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Attack\"))\n\n            if line.is_garrison():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Storage\"))\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.RemoveStorage\"))\n\n                garrison_mode = line.get_garrison_mode()\n\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.SendBackToTask\"))\n\n                if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                    disabled_forward_refs.append(ForwardRef(line,\n                                                            f\"{game_entity_name}.RallyPoint\"))\n\n            if line.is_harvestable():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Harvestable\"))\n\n            if line.is_dropsite():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.DropSite\"))\n\n            if line.is_trade_post():\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.TradePost\"))\n\n            construct_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                          disabled_forward_refs,\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Enabled modifiers\n            construct_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n\n            # Disabled modifiers\n            construct_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                          [],\n                                                          \"engine.util.state_machine.StateChanger\")\n            # =====================================================================================\n            construct_state_forward_ref = ForwardRef(line, construct_state_name)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress50\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress50\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (25.0, 50.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   25.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   50.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =================================================================================\n            # Idle override\n            # =================================================================================\n            if construction_animation_id > -1:\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                overrides = []\n                override_ref = f\"{progress_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                construction_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_construct50_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress75\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress75\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (50.0, 75.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   50.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   75.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =================================================================================\n            # Idle override\n            # =================================================================================\n            if construction_animation_id > -1:\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                overrides = []\n                override_ref = f\"{progress_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                construction_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_construct75_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.ConstructionProgress100\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"ConstructionProgress100\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Construct\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (75.0, 100.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   75.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   100.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =================================================================================\n            # Idle override\n            # =================================================================================\n            if construction_animation_id > -1:\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                overrides = []\n                override_ref = f\"{progress_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, progress_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                construction_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_construct100_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n        # =====================================================================================\n        ability_raw_api_object.add_raw_member(\"construction_progress\",\n                                              progress_forward_refs,\n                                              \"engine.ability.type.Constructable\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def create_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Create ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.Create\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Create\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Create\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Creatables\n        creatables_set = []\n        for creatable in line.creates:\n            if creatable.is_unique():\n                # Skip this because unique units are handled by civs\n                continue\n\n            # CreatableGameEntity objects are created for each unit/building\n            # line individually to avoid duplicates. We just point to the\n            # raw API objects here.\n            creatable_id = creatable.get_head_unit_id()\n            creatable_name = name_lookup_dict[creatable_id][0]\n\n            raw_api_object_ref = f\"{creatable_name}.CreatableGameEntity\"\n            creatable_forward_ref = ForwardRef(creatable,\n                                               raw_api_object_ref)\n            creatables_set.append(creatable_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"creatables\", creatables_set,\n                                              \"engine.ability.type.Create\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def death_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds a PassiveTransformTo ability to a line that is used to make entities die.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Death\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Death\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.PassiveTransformTo\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"dying_graphic\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            ability_animation_id,\n                                                                            ability_ref,\n                                                                            \"Death\",\n                                                                            \"death_\")\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"dying_graphic\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}Death\"\n                    filename_prefix = f\"death_{gset_lookup_dict[graphics_set_id][2]}_\"\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Death condition\n        death_condition = [\n            dataset.pregen_nyan_objects[\"util.logic.literal.death.StandardHealthDeathLiteral\"].get_nyan_object(\n            )\n        ]\n        ability_raw_api_object.add_raw_member(\"condition\",\n                                              death_condition,\n                                              \"engine.ability.type.PassiveTransformTo\")\n\n        # Transform time\n        # Use the time of the dying graphics\n        if ability_animation_id > -1:\n            dying_animation = dataset.genie_graphics[ability_animation_id]\n            death_time = dying_animation.get_animation_length()\n\n        else:\n            death_time = 0.0\n\n        ability_raw_api_object.add_raw_member(\"transform_time\",\n                                              death_time,\n                                              \"engine.ability.type.PassiveTransformTo\")\n\n        # Target state\n        # =====================================================================================\n        target_state_name = f\"{game_entity_name}.Death.DeadState\"\n        target_state_raw_api_object = RawAPIObject(target_state_name,\n                                                   \"DeadState\",\n                                                   dataset.nyan_api_objects)\n        target_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n        target_state_location = ForwardRef(line, ability_ref)\n        target_state_raw_api_object.set_location(target_state_location)\n\n        # Priority\n        target_state_raw_api_object.add_raw_member(\"priority\",\n                                                   1000,\n                                                   \"engine.util.state_machine.StateChanger\")\n\n        # Enabled abilities\n        target_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                   [],\n                                                   \"engine.util.state_machine.StateChanger\")\n\n        # Disabled abilities\n        disabled_forward_refs = []\n        if isinstance(line, (GenieUnitLineGroup, GenieBuildingLineGroup)):\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.LineOfSight\"))\n\n        if isinstance(line, GenieBuildingLineGroup):\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.AttributeChangeTracker\"))\n\n        if len(line.creates) > 0:\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.Create\"))\n\n            if isinstance(line, GenieBuildingLineGroup):\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.ProductionQueue\"))\n        if len(line.researches) > 0:\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.Research\"))\n\n        if line.is_projectile_shooter():\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.Attack\"))\n\n        if line.is_garrison():\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.Storage\"))\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.RemoveStorage\"))\n\n            garrison_mode = line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.NATURAL:\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.SendBackToTask\"))\n\n            if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.RallyPoint\"))\n\n        if line.is_harvestable():\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.Harvestable\"))\n\n        if isinstance(line, GenieBuildingLineGroup) and line.is_dropsite():\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.DropSite\"))\n\n        if isinstance(line, GenieBuildingLineGroup) and line.is_trade_post():\n            disabled_forward_refs.append(ForwardRef(line,\n                                                    f\"{game_entity_name}.TradePost\"))\n\n        target_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                   disabled_forward_refs,\n                                                   \"engine.util.state_machine.StateChanger\")\n\n        # Enabled modifiers\n        target_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                   [],\n                                                   \"engine.util.state_machine.StateChanger\")\n\n        # Disabled modifiers\n        target_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                   [],\n                                                   \"engine.util.state_machine.StateChanger\")\n\n        line.add_raw_api_object(target_state_raw_api_object)\n        # =====================================================================================\n        target_state_forward_ref = ForwardRef(line, target_state_name)\n        ability_raw_api_object.add_raw_member(\"target_state\",\n                                              target_state_forward_ref,\n                                              \"engine.ability.type.PassiveTransformTo\")\n\n        # Transform progress\n        # =====================================================================================\n        progress_ref = f\"{ability_ref}.DeathProgress\"\n        progress_raw_api_object = RawAPIObject(progress_ref,\n                                               \"DeathProgress\",\n                                               dataset.nyan_api_objects)\n        progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n        progress_location = ForwardRef(line, ability_ref)\n        progress_raw_api_object.set_location(progress_location)\n\n        line.add_raw_api_object(progress_raw_api_object)\n\n        # Type\n        progress_raw_api_object.add_raw_member(\"type\",\n                                               api_objects[\"engine.util.progress_type.type.AttributeChange\"],\n                                               \"engine.util.progress.Progress\")\n\n        # Interval = (0.0, 100.0)\n        progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                               0.0,\n                                               \"engine.util.progress.Progress\")\n        progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                               100.0,\n                                               \"engine.util.progress.Progress\")\n\n        # Progress properties\n        properties = {}\n        # =====================================================================================\n        # State change property\n        # =====================================================================================\n        property_ref = f\"{progress_ref}.StateChange\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"StateChange\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n        property_location = ForwardRef(line, progress_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        # State change = target state\n        property_raw_api_object.add_raw_member(\"state_change\",\n                                               target_state_forward_ref,\n                                               \"engine.util.progress.property.type.StateChange\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n        })\n        # =====================================================================================\n        progress_raw_api_object.add_raw_member(\"properties\",\n                                               properties,\n                                               \"engine.util.progress.Progress\")\n        # =====================================================================================\n        progress_forward_ref = ForwardRef(line, progress_ref)\n        ability_raw_api_object.add_raw_member(\"transform_progress\",\n                                              [progress_forward_ref],\n                                              \"engine.ability.type.PassiveTransformTo\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def delete_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds a PassiveTransformTo ability to a line that is used to make entities die.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Delete\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Delete\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ActiveTransformTo\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"dying_graphic\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Use the animation from Death ability\n            animations_set = []\n            animation_ref = f\"{game_entity_name}.Death.DeathAnimation\"\n            animation_forward_ref = ForwardRef(line, animation_ref)\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Transform time\n        # Use the time of the dying graphics\n        if ability_animation_id > -1:\n            dying_animation = dataset.genie_graphics[ability_animation_id]\n            death_time = dying_animation.get_animation_length()\n\n        else:\n            death_time = 0.0\n\n        ability_raw_api_object.add_raw_member(\"transform_time\",\n                                              death_time,\n                                              \"engine.ability.type.ActiveTransformTo\")\n\n        # Target state (reuse from Death)\n        target_state_ref = f\"{game_entity_name}.Death.DeadState\"\n        target_state_forward_ref = ForwardRef(line, target_state_ref)\n        ability_raw_api_object.add_raw_member(\"target_state\",\n                                              target_state_forward_ref,\n                                              \"engine.ability.type.ActiveTransformTo\")\n\n        # Transform progress (reuse from Death)\n        progress_ref = f\"{game_entity_name}.Death.DeathProgress\"\n        progress_forward_ref = ForwardRef(line, progress_ref)\n        ability_raw_api_object.add_raw_member(\"transform_progress\",\n                                              [progress_forward_ref],\n                                              \"engine.ability.type.ActiveTransformTo\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def despawn_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Despawn ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        # Animation and time come from dead unit\n        death_animation_id = current_unit[\"dying_graphic\"].value\n        dead_unit_id = current_unit[\"dead_unit_id\"].value\n        dead_unit = None\n        if dead_unit_id > -1:\n            dead_unit = dataset.genie_units[dead_unit_id]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Despawn\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Despawn\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Despawn\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = -1\n        if dead_unit:\n            ability_animation_id = dead_unit[\"idle_graphic0\"].value\n\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            ability_animation_id,\n                                                                            property_ref,\n                                                                            \"Despawn\",\n                                                                            \"despawn_\")\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_unit = civ[\"units\"][current_unit_id]\n                civ_dead_unit_id = civ_unit[\"dead_unit_id\"].value\n                civ_dead_unit = None\n                if civ_dead_unit_id > -1:\n                    civ_dead_unit = dataset.genie_units[civ_dead_unit_id]\n\n                civ_animation_id = civ_dead_unit[\"idle_graphic0\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}Despawn\"\n                    filename_prefix = f\"despawn_{gset_lookup_dict[graphics_set_id][2]}_\"\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Activation condition\n        # Uses the death condition of the units\n        activation_condition = [\n            dataset.pregen_nyan_objects[\"util.logic.literal.death.StandardHealthDeathLiteral\"].get_nyan_object(\n            )\n        ]\n        ability_raw_api_object.add_raw_member(\"activation_condition\",\n                                              activation_condition,\n                                              \"engine.ability.type.Despawn\")\n\n        # Despawn condition\n        ability_raw_api_object.add_raw_member(\"despawn_condition\",\n                                              [],\n                                              \"engine.ability.type.Despawn\")\n\n        # Despawn time = corpse decay time (dead unit) or Death animation time (if no dead unit exist)\n        despawn_time = 0\n        if dead_unit:\n            resource_storage = dead_unit[\"resource_storage\"].value\n            for storage in resource_storage:\n                resource_id = storage[\"type\"].value\n\n                if resource_id == 12:\n                    despawn_time = storage[\"amount\"].value\n\n        elif death_animation_id > -1:\n            despawn_time = dataset.genie_graphics[death_animation_id].get_animation_length()\n\n        ability_raw_api_object.add_raw_member(\"despawn_time\",\n                                              despawn_time,\n                                              \"engine.ability.type.Despawn\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def drop_resources_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the DropResources ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            gatherers = line.variants[0].line\n\n        else:\n            gatherers = [line.line[0]]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.DropResources\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"DropResources\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.DropResources\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Resource containers\n        containers = []\n        for gatherer in gatherers:\n            unit_commands = gatherer[\"unit_commands\"].value\n\n            for command in unit_commands:\n                # Find a gather ability. It doesn't matter which one because\n                # they should all produce the same resource for one genie unit.\n                type_id = command[\"type\"].value\n\n                if type_id in (5, 110):\n                    break\n\n            gatherer_unit_id = gatherer.get_id()\n            if gatherer_unit_id not in gather_lookup_dict:\n                # Skips hunting wolves\n                continue\n\n            container_ref = (f\"{game_entity_name}.ResourceStorage.\"\n                             f\"{gather_lookup_dict[gatherer_unit_id][0]}Container\")\n            container_forward_ref = ForwardRef(line, container_ref)\n            containers.append(container_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"containers\",\n                                              containers,\n                                              \"engine.ability.type.DropResources\")\n\n        # Search range\n        ability_raw_api_object.add_raw_member(\"search_range\",\n                                              MemberSpecialValue.NYAN_INF,\n                                              \"engine.ability.type.DropResources\")\n\n        # Allowed types\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.DropSite\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.DropResources\")\n        # Blacklisted enties\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.DropResources\")\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def drop_site_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the DropSite ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.DropSite\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"DropSite\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.DropSite\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Resource containers\n        gatherer_ids = line.get_gatherer_ids()\n\n        containers = []\n        for gatherer_id in gatherer_ids:\n            if gatherer_id not in gather_lookup_dict:\n                # Skips hunting wolves\n                continue\n\n            gatherer_line = dataset.unit_ref[gatherer_id]\n            gatherer_head_unit_id = gatherer_line.get_head_unit_id()\n            gatherer_name = name_lookup_dict[gatherer_head_unit_id][0]\n\n            container_ref = (f\"{gatherer_name}.ResourceStorage.\"\n                             f\"{gather_lookup_dict[gatherer_id][0]}Container\")\n            container_forward_ref = ForwardRef(gatherer_line, container_ref)\n            containers.append(container_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"accepts_from\",\n                                              containers,\n                                              \"engine.ability.type.DropSite\")\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def enter_container_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the EnterContainer ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability. None if no valid containers were found.\n        :rtype: ...dataformat.forward_ref.ForwardRef, None\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.EnterContainer\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"EnterContainer\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.EnterContainer\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Containers\n        containers = []\n        entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        for garrison in line.garrison_locations:\n            garrison_mode = garrison.get_garrison_mode()\n\n            # Cannot enter production buildings or monk inventories\n            if garrison_mode in (GenieGarrisonMode.SELF_PRODUCED, GenieGarrisonMode.MONK):\n                continue\n\n            garrison_name = entity_lookups[garrison.get_head_unit_id()][0]\n\n            container_ref = f\"{garrison_name}.Storage.{garrison_name}Container\"\n            container_forward_ref = ForwardRef(garrison, container_ref)\n            containers.append(container_forward_ref)\n\n        if not containers:\n            return None\n\n        ability_raw_api_object.add_raw_member(\"allowed_containers\",\n                                              containers,\n                                              \"engine.ability.type.EnterContainer\")\n\n        # Allowed types (all buildings/units)\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()\n        ]\n\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.EnterContainer\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.EnterContainer\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def exchange_resources_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ExchangeResources ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        resource_names = [\"Food\", \"Wood\", \"Stone\"]\n\n        abilities = []\n        for resource_name in resource_names:\n            ability_name = f\"MarketExchange{resource_name}\"\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(\"engine.ability.type.ExchangeResources\")\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            line.add_raw_api_object(ability_raw_api_object)\n\n            # Resource that is exchanged (resource A)\n            resource_a = dataset.pregen_nyan_objects[f\"util.resource.types.{resource_name}\"].get_nyan_object(\n            )\n            ability_raw_api_object.add_raw_member(\"resource_a\",\n                                                  resource_a,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Resource that is exchanged for (resource B)\n            resource_b = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n            ability_raw_api_object.add_raw_member(\"resource_b\",\n                                                  resource_b,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Exchange rate\n            exchange_rate_ref = f\"util.resource.market_trading.Market{resource_name}ExchangeRate\"\n            exchange_rate = dataset.pregen_nyan_objects[exchange_rate_ref].get_nyan_object()\n            ability_raw_api_object.add_raw_member(\"exchange_rate\",\n                                                  exchange_rate,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Exchange modes\n            buy_exchange_ref = \"util.resource.market_trading.MarketBuyExchangeMode\"\n            sell_exchange_ref = \"util.resource.market_trading.MarketSellExchangeMode\"\n            exchange_modes = [\n                dataset.pregen_nyan_objects[buy_exchange_ref].get_nyan_object(),\n                dataset.pregen_nyan_objects[sell_exchange_ref].get_nyan_object(),\n            ]\n            ability_raw_api_object.add_raw_member(\"exchange_modes\",\n                                                  exchange_modes,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n            abilities.append(ability_forward_ref)\n\n        return abilities\n\n    @ staticmethod\n    def exit_container_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ExitContainer ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability. None if no valid containers were found.\n        :rtype: ...dataformat.forward_ref.ForwardRef, None\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ExitContainer\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"ExitContainer\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ExitContainer\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Containers\n        containers = []\n        entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        for garrison in line.garrison_locations:\n            garrison_mode = garrison.get_garrison_mode()\n\n            # Cannot enter production buildings or monk inventories\n            if garrison_mode == GenieGarrisonMode.MONK:\n                continue\n\n            garrison_name = entity_lookups[garrison.get_head_unit_id()][0]\n\n            container_ref = f\"{garrison_name}.Storage.{garrison_name}Container\"\n            container_forward_ref = ForwardRef(garrison, container_ref)\n            containers.append(container_forward_ref)\n\n        if not containers:\n            return None\n\n        ability_raw_api_object.add_raw_member(\"allowed_containers\",\n                                              containers,\n                                              \"engine.ability.type.ExitContainer\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def game_entity_stance_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the GameEntityStance ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.GameEntityStance\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"GameEntityStance\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.GameEntityStance\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Stances\n        search_range = current_unit[\"search_radius\"].value\n        stance_names = [\"Aggressive\", \"Defensive\", \"StandGround\", \"Passive\"]\n\n        # Attacking is prefered\n        ability_preferences = []\n        if line.is_projectile_shooter():\n            ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Attack\"))\n\n        elif line.is_melee() or line.is_ranged():\n            if line.has_command(7):\n                ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Attack\"))\n\n            if line.has_command(105):\n                ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Heal\"))\n\n        # Units are prefered before buildings\n        type_preferences = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n        ]\n\n        stances = []\n        for stance_name in stance_names:\n            stance_api_ref = f\"engine.util.game_entity_stance.type.{stance_name}\"\n\n            stance_ref = f\"{game_entity_name}.GameEntityStance.{stance_name}\"\n            stance_raw_api_object = RawAPIObject(stance_ref, stance_name, dataset.nyan_api_objects)\n            stance_raw_api_object.add_raw_parent(stance_api_ref)\n            stance_location = ForwardRef(line, ability_ref)\n            stance_raw_api_object.set_location(stance_location)\n\n            # Search range\n            stance_raw_api_object.add_raw_member(\"search_range\",\n                                                 search_range,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            # Ability preferences\n            stance_raw_api_object.add_raw_member(\"ability_preference\",\n                                                 ability_preferences,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            # Type preferences\n            stance_raw_api_object.add_raw_member(\"type_preference\",\n                                                 type_preferences,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            line.add_raw_api_object(stance_raw_api_object)\n            stance_forward_ref = ForwardRef(line, stance_ref)\n            stances.append(stance_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"stances\",\n                                              stances,\n                                              \"engine.ability.type.GameEntityStance\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def formation_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Formation ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Formation\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Formation\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Formation\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Formation definitions\n        if line.get_class_id() in (6,):\n            subformation = dataset.pregen_nyan_objects[\"util.formation.subformation.types.Infantry\"].get_nyan_object(\n            )\n\n        elif line.get_class_id() in (12, 47):\n            subformation = dataset.pregen_nyan_objects[\"util.formation.subformation.types.Cavalry\"].get_nyan_object(\n            )\n\n        elif line.get_class_id() in (0, 23, 36, 44, 55):\n            subformation = dataset.pregen_nyan_objects[\"util.formation.subformation.types.Ranged\"].get_nyan_object(\n            )\n\n        elif line.get_class_id() in (2, 13, 18, 20, 35, 43, 51, 59):\n            subformation = dataset.pregen_nyan_objects[\"util.formation.subformation.types.Siege\"].get_nyan_object(\n            )\n\n        else:\n            subformation = dataset.pregen_nyan_objects[\"util.formation.subformation.types.Support\"].get_nyan_object(\n            )\n\n        formation_names = [\"Line\", \"Staggered\", \"Box\", \"Flank\"]\n\n        formation_defs = []\n        for formation_name in formation_names:\n            ge_formation_ref = f\"{game_entity_name}.Formation.{formation_name}\"\n            ge_formation_raw_api_object = RawAPIObject(ge_formation_ref,\n                                                       formation_name,\n                                                       dataset.nyan_api_objects)\n            ge_formation_raw_api_object.add_raw_parent(\n                \"engine.util.game_entity_formation.GameEntityFormation\")\n            ge_formation_location = ForwardRef(line, ability_ref)\n            ge_formation_raw_api_object.set_location(ge_formation_location)\n\n            # Formation\n            formation_ref = f\"util.formation.types.{formation_name}\"\n            formation = dataset.pregen_nyan_objects[formation_ref].get_nyan_object()\n            ge_formation_raw_api_object.add_raw_member(\"formation\",\n                                                       formation,\n                                                       \"engine.util.game_entity_formation.GameEntityFormation\")\n\n            # Subformation\n            ge_formation_raw_api_object.add_raw_member(\"subformation\",\n                                                       subformation,\n                                                       \"engine.util.game_entity_formation.GameEntityFormation\")\n\n            line.add_raw_api_object(ge_formation_raw_api_object)\n            ge_formation_forward_ref = ForwardRef(line, ge_formation_ref)\n            formation_defs.append(ge_formation_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"formations\",\n                                              formation_defs,\n                                              \"engine.ability.type.Formation\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def foundation_ability(line: GenieGameEntityGroup, terrain_id: int = -1) -> ForwardRef:\n        \"\"\"\n        Adds the Foundation abilities to a line. Optionally chooses the specified\n        terrain ID.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param terrain_id: Force this terrain ID as foundation\n        :type terrain_id: int\n        :returns: The forward references for the abilities.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_lookup_dict = internal_name_lookups.get_terrain_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Foundation\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Foundation\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Foundation\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Terrain\n        if terrain_id == -1:\n            terrain_id = current_unit[\"foundation_terrain_id\"].value\n\n        terrain = dataset.terrain_groups[terrain_id]\n        terrain_forward_ref = ForwardRef(terrain, terrain_lookup_dict[terrain_id][1])\n        ability_raw_api_object.add_raw_member(\"foundation_terrain\",\n                                              terrain_forward_ref,\n                                              \"engine.ability.type.Foundation\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def gather_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Gather abilities to a line. Unlike the other methods, this\n        creates multiple abilities.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the abilities.\n        :rtype: list\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            gatherers = line.variants[0].line\n\n        else:\n            gatherers = [line.line[0]]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        abilities = []\n        for gatherer in gatherers:\n            unit_commands = gatherer[\"unit_commands\"].value\n            resource = None\n            ability_animation_id = -1\n            harvestable_class_ids = OrderedSet()\n            harvestable_unit_ids = OrderedSet()\n\n            for command in unit_commands:\n                # Find a gather ability. It doesn't matter which one because\n                # they should all produce the same resource for one genie unit.\n                type_id = command[\"type\"].value\n\n                if type_id not in (5, 110):\n                    continue\n\n                target_class_id = command[\"class_id\"].value\n                if target_class_id > -1:\n                    harvestable_class_ids.add(target_class_id)\n\n                target_unit_id = command[\"unit_id\"].value\n                if target_unit_id > -1:\n                    harvestable_unit_ids.add(target_unit_id)\n\n                resource_id = command[\"resource_out\"].value\n\n                # If resource_out is not specified, the gatherer harvests resource_in\n                if resource_id == -1:\n                    resource_id = command[\"resource_in\"].value\n\n                if resource_id == 0:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object(\n                    )\n\n                elif resource_id == 1:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object(\n                    )\n\n                elif resource_id == 2:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                    )\n\n                elif resource_id == 3:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object(\n                    )\n\n                else:\n                    continue\n\n                if type_id == 110:\n                    ability_animation_id = command[\"work_sprite_id\"].value\n\n                else:\n                    ability_animation_id = command[\"proceed_sprite_id\"].value\n\n            # Look for the harvestable groups that match the class IDs and unit IDs\n            check_groups = []\n            check_groups.extend(dataset.unit_lines.values())\n            check_groups.extend(dataset.building_lines.values())\n            check_groups.extend(dataset.ambient_groups.values())\n\n            harvestable_groups = []\n            for group in check_groups:\n                if not group.is_harvestable():\n                    continue\n\n                if group.get_class_id() in harvestable_class_ids:\n                    harvestable_groups.append(group)\n                    continue\n\n                for unit_id in harvestable_unit_ids:\n                    if group.contains_entity(unit_id):\n                        harvestable_groups.append(group)\n\n            if len(harvestable_groups) == 0:\n                # If no matching groups are found, then we don't\n                # need to create an ability.\n                continue\n\n            gatherer_unit_id = gatherer.get_id()\n            if gatherer_unit_id not in gather_lookup_dict:\n                # Skips hunting wolves\n                continue\n\n            ability_name = gather_lookup_dict[gatherer_unit_id][0]\n\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(\"engine.ability.type.Gather\")\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            line.add_raw_api_object(ability_raw_api_object)\n\n            # Ability properties\n            properties = {}\n\n            # Animation\n            if ability_animation_id > -1:\n                property_ref = f\"{ability_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n                property_location = ForwardRef(line, ability_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                    line,\n                    ability_animation_id,\n                    property_ref,\n                    ability_name,\n                    f\"{gather_lookup_dict[gatherer_unit_id][1]}_\"\n                )\n                animations_set.append(animation_forward_ref)\n                property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                       \"engine.ability.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n                })\n\n            # Diplomacy settings\n            property_ref = f\"{ability_ref}.Diplomatic\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Diplomatic\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            diplomatic_stances = [\n                dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n            property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                                   \"engine.ability.property.type.Diplomatic\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n            })\n\n            ability_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.ability.Ability\")\n\n            # Auto resume\n            ability_raw_api_object.add_raw_member(\"auto_resume\",\n                                                  True,\n                                                  \"engine.ability.type.Gather\")\n\n            # search range\n            ability_raw_api_object.add_raw_member(\"resume_search_range\",\n                                                  MemberSpecialValue.NYAN_INF,\n                                                  \"engine.ability.type.Gather\")\n\n            # Gather rate\n            rate_name = f\"{game_entity_name}.{ability_name}.GatherRate\"\n            rate_raw_api_object = RawAPIObject(rate_name, \"GatherRate\", dataset.nyan_api_objects)\n            rate_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceRate\")\n            rate_location = ForwardRef(line, ability_ref)\n            rate_raw_api_object.set_location(rate_location)\n\n            rate_raw_api_object.add_raw_member(\n                \"type\", resource, \"engine.util.resource.ResourceRate\")\n\n            gather_rate = gatherer[\"work_rate\"].value\n            rate_raw_api_object.add_raw_member(\n                \"rate\", gather_rate, \"engine.util.resource.ResourceRate\")\n\n            line.add_raw_api_object(rate_raw_api_object)\n\n            rate_forward_ref = ForwardRef(line, rate_name)\n            ability_raw_api_object.add_raw_member(\"gather_rate\",\n                                                  rate_forward_ref,\n                                                  \"engine.ability.type.Gather\")\n\n            # Resource container\n            container_ref = (f\"{game_entity_name}.ResourceStorage.\"\n                             f\"{gather_lookup_dict[gatherer_unit_id][0]}Container\")\n            container_forward_ref = ForwardRef(line, container_ref)\n            ability_raw_api_object.add_raw_member(\"container\",\n                                                  container_forward_ref,\n                                                  \"engine.ability.type.Gather\")\n\n            # Targets (resource spots)\n            entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n            spot_forward_refs = []\n            for group in harvestable_groups:\n                group_id = group.get_head_unit_id()\n                group_name = entity_lookups[group_id][0]\n\n                spot_forward_ref = ForwardRef(group,\n                                              f\"{group_name}.Harvestable.{group_name}ResourceSpot\")\n                spot_forward_refs.append(spot_forward_ref)\n\n            ability_raw_api_object.add_raw_member(\"targets\",\n                                                  spot_forward_refs,\n                                                  \"engine.ability.type.Gather\")\n\n            ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n            abilities.append(ability_forward_ref)\n\n        return abilities\n\n    @ staticmethod\n    def harvestable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Harvestable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Harvestable\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Harvestable\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Harvestable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Resource spot\n        resource_storage = current_unit[\"resource_storage\"].value\n\n        for storage in resource_storage:\n            resource_id = storage[\"type\"].value\n\n            # IDs 15, 16, 17 are other types of food (meat, berries, fish)\n            if resource_id in (0, 15, 16, 17):\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                )\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n\n            else:\n                continue\n\n            spot_name = f\"{game_entity_name}.Harvestable.{game_entity_name}ResourceSpot\"\n            spot_raw_api_object = RawAPIObject(spot_name,\n                                               f\"{game_entity_name}ResourceSpot\",\n                                               dataset.nyan_api_objects)\n            spot_raw_api_object.add_raw_parent(\"engine.util.resource_spot.ResourceSpot\")\n            spot_location = ForwardRef(line, ability_ref)\n            spot_raw_api_object.set_location(spot_location)\n\n            # Type\n            spot_raw_api_object.add_raw_member(\"resource\",\n                                               resource,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Start amount (equals max amount)\n            if line.get_id() == 50:\n                # Farm food amount (hardcoded in civ)\n                starting_amount = dataset.genie_civs[1][\"resources\"][36].value\n\n            elif line.get_id() == 199:\n                # Fish trap food amount (hardcoded in civ)\n                starting_amount = storage[\"amount\"].value\n                starting_amount += dataset.genie_civs[1][\"resources\"][88].value\n\n            else:\n                starting_amount = storage[\"amount\"].value\n\n            spot_raw_api_object.add_raw_member(\"starting_amount\",\n                                               starting_amount,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Max amount\n            spot_raw_api_object.add_raw_member(\"max_amount\",\n                                               starting_amount,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Decay rate\n            decay_rate = current_unit[\"resource_decay\"].value\n            spot_raw_api_object.add_raw_member(\"decay_rate\",\n                                               decay_rate,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            spot_forward_ref = ForwardRef(line, spot_name)\n            ability_raw_api_object.add_raw_member(\"resources\",\n                                                  spot_forward_ref,\n                                                  \"engine.ability.type.Harvestable\")\n            line.add_raw_api_object(spot_raw_api_object)\n\n            # Only one resource spot per ability\n            break\n\n        # Harvest Progress (we don't use this for Aoe2)\n        ability_raw_api_object.add_raw_member(\"harvest_progress\",\n                                              [],\n                                              \"engine.ability.type.Harvestable\")\n\n        # Restock Progress\n        progress_forward_refs = []\n        if line.get_class_id() == 49:\n            # Farms\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress33\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress33\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 33.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction1\"\n            terrain_group = dataset.terrain_groups[29]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            init_state_ref = f\"{game_entity_name}.Constructable.InitState\"\n            init_state_forward_ref = ForwardRef(line, init_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   init_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress66\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress66\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (33.0, 66.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction2\"\n            terrain_group = dataset.terrain_groups[30]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            construct_state_ref = f\"{game_entity_name}.Constructable.ConstructState\"\n            construct_state_forward_ref = ForwardRef(line, construct_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress100\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress100\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   100.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction3\"\n            terrain_group = dataset.terrain_groups[31]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            construct_state_ref = f\"{game_entity_name}.Constructable.ConstructState\"\n            construct_state_forward_ref = ForwardRef(line, construct_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =======================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n\n        ability_raw_api_object.add_raw_member(\"restock_progress\",\n                                              progress_forward_refs,\n                                              \"engine.ability.type.Harvestable\")\n\n        # Gatherer limit (infinite in AoC except for farms)\n        gatherer_limit = MemberSpecialValue.NYAN_INF\n        if line.get_class_id() == 49:\n            gatherer_limit = 1\n\n        ability_raw_api_object.add_raw_member(\"gatherer_limit\",\n                                              gatherer_limit,\n                                              \"engine.ability.type.Harvestable\")\n\n        # Unit have to die before they are harvestable (except for farms)\n        harvestable_by_default = current_unit[\"hit_points\"].value == 0\n        if line.get_class_id() == 49:\n            harvestable_by_default = True\n\n        ability_raw_api_object.add_raw_member(\"harvestable_by_default\",\n                                              harvestable_by_default,\n                                              \"engine.ability.type.Harvestable\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def herd_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Herd ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Herd\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Herd\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Herd\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Range\n        ability_raw_api_object.add_raw_member(\"range\",\n                                              3.0,\n                                              \"engine.ability.type.Herd\")\n\n        # Strength\n        ability_raw_api_object.add_raw_member(\"strength\",\n                                              0,\n                                              \"engine.ability.type.Herd\")\n\n        # Allowed types\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Herdable\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.Herd\")\n\n        # Blacklisted entities\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.Herd\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def herdable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Herdable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Herdable\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Herdable\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Herdable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Mode\n        mode = dataset.nyan_api_objects[\"engine.util.herdable_mode.type.LongestTimeInRange\"]\n        ability_raw_api_object.add_raw_member(\"mode\", mode, \"engine.ability.type.Herdable\")\n\n        # Discover range\n        ability_raw_api_object.add_raw_member(\"adjacent_discover_range\",\n                                              1.0,\n                                              \"engine.ability.type.Herdable\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def idle_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Idle ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Idle\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Idle\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Idle\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"idle_graphic0\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            ability_animation_id,\n                                                                            property_ref,\n                                                                            \"Idle\",\n                                                                            \"idle_\")\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"idle_graphic0\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    else:\n                        raise RuntimeError(f\"No graphics set found for civ id {civ_id}\")\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}Idle\"\n                    filename_prefix = f\"idle_{gset_lookup_dict[graphics_set_id][2]}_\"\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def live_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Live ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Live\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Live\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Live\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        attributes_set = []\n\n        # Health\n        # =======================================================================================\n        health_ref = f\"{game_entity_name}.Live.Health\"\n        health_raw_api_object = RawAPIObject(health_ref, \"Health\", dataset.nyan_api_objects)\n        health_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeSetting\")\n        health_location = ForwardRef(line, ability_ref)\n        health_raw_api_object.set_location(health_location)\n\n        attribute_value = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object(\n        )\n        health_raw_api_object.add_raw_member(\"attribute\",\n                                             attribute_value,\n                                             \"engine.util.attribute.AttributeSetting\")\n\n        # Lowest HP can go\n        health_raw_api_object.add_raw_member(\"min_value\",\n                                             0,\n                                             \"engine.util.attribute.AttributeSetting\")\n\n        # Max HP and starting HP\n        max_hp_value = current_unit[\"hit_points\"].value\n        health_raw_api_object.add_raw_member(\"max_value\",\n                                             max_hp_value,\n                                             \"engine.util.attribute.AttributeSetting\")\n\n        starting_value = max_hp_value\n        if isinstance(line, GenieBuildingLineGroup):\n            # Buildings spawn with 1 HP\n            starting_value = 1\n\n        health_raw_api_object.add_raw_member(\"starting_value\",\n                                             starting_value,\n                                             \"engine.util.attribute.AttributeSetting\")\n\n        line.add_raw_api_object(health_raw_api_object)\n\n        # =======================================================================================\n        health_forward_ref = ForwardRef(line, health_raw_api_object.get_id())\n        attributes_set.append(health_forward_ref)\n\n        if current_unit_id == 125:\n            # Faith (only monk)\n            faith_ref = f\"{game_entity_name}.Live.Faith\"\n            faith_raw_api_object = RawAPIObject(faith_ref, \"Faith\", dataset.nyan_api_objects)\n            faith_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeSetting\")\n            faith_location = ForwardRef(line, ability_ref)\n            faith_raw_api_object.set_location(faith_location)\n\n            attribute_value = dataset.pregen_nyan_objects[\"util.attribute.types.Faith\"].get_nyan_object(\n            )\n            faith_raw_api_object.add_raw_member(\"attribute\", attribute_value,\n                                                \"engine.util.attribute.AttributeSetting\")\n\n            # Lowest faith can go\n            faith_raw_api_object.add_raw_member(\"min_value\",\n                                                0,\n                                                \"engine.util.attribute.AttributeSetting\")\n\n            # Max faith and starting faith\n            faith_raw_api_object.add_raw_member(\"max_value\",\n                                                100,\n                                                \"engine.util.attribute.AttributeSetting\")\n            faith_raw_api_object.add_raw_member(\"starting_value\",\n                                                100,\n                                                \"engine.util.attribute.AttributeSetting\")\n\n            line.add_raw_api_object(faith_raw_api_object)\n\n            faith_forward_ref = ForwardRef(line, faith_ref)\n            attributes_set.append(faith_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"attributes\", attributes_set,\n                                              \"engine.ability.type.Live\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def los_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the LineOfSight ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.LineOfSight\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"LineOfSight\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.LineOfSight\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Line of sight\n        line_of_sight = current_unit[\"line_of_sight\"].value\n        ability_raw_api_object.add_raw_member(\"range\", line_of_sight,\n                                              \"engine.ability.type.LineOfSight\")\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def move_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Move ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Move\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Move\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Move\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"move_graphics\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n\n            animation_obj_prefix = \"Move\"\n            animation_filename_prefix = \"move_\"\n\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            ability_animation_id,\n                                                                            property_ref,\n                                                                            animation_obj_prefix,\n                                                                            animation_filename_prefix)\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"move_graphics\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}Move\"\n                    filename_prefix = f\"move_{gset_lookup_dict[graphics_set_id][2]}_\"\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        # Command Sound\n        ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n\n            sound_obj_prefix = \"Move\"\n\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    sound_obj_prefix,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\", sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Speed\n        speed = current_unit[\"speed\"].value\n        ability_raw_api_object.add_raw_member(\"speed\", speed, \"engine.ability.type.Move\")\n\n        # Standard move modes\n        move_modes = [\n            dataset.nyan_api_objects[\"engine.util.move_mode.type.AttackMove\"],\n            dataset.nyan_api_objects[\"engine.util.move_mode.type.Normal\"],\n            dataset.nyan_api_objects[\"engine.util.move_mode.type.Patrol\"]\n        ]\n\n        # Follow\n        ability_ref = f\"{game_entity_name}.Move.Follow\"\n        follow_raw_api_object = RawAPIObject(ability_ref, \"Follow\", dataset.nyan_api_objects)\n        follow_raw_api_object.add_raw_parent(\"engine.util.move_mode.type.Follow\")\n        follow_location = ForwardRef(line, f\"{game_entity_name}.Move\")\n        follow_raw_api_object.set_location(follow_location)\n\n        follow_range = current_unit[\"line_of_sight\"].value - 1\n        follow_raw_api_object.add_raw_member(\"range\",\n                                             follow_range,\n                                             \"engine.util.move_mode.type.Follow\")\n\n        line.add_raw_api_object(follow_raw_api_object)\n        follow_forward_ref = ForwardRef(line, follow_raw_api_object.get_id())\n        move_modes.append(follow_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"modes\", move_modes, \"engine.ability.type.Move\")\n\n        # Path type\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Land\"].get_nyan_object()\n        restrictions = current_unit[\"terrain_restriction\"].value\n        if restrictions in (0x00, 0x0C, 0x0E, 0x17):\n            # air units\n            path_type = dataset.pregen_nyan_objects[\"util.path.types.Air\"].get_nyan_object()\n\n        elif restrictions in (0x03, 0x0D, 0x0F):\n            # ships\n            path_type = dataset.pregen_nyan_objects[\"util.path.types.Water\"].get_nyan_object()\n\n        ability_raw_api_object.add_raw_member(\"path_type\", path_type, \"engine.ability.type.Move\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def move_projectile_ability(line: GenieGameEntityGroup, position: int = -1) -> ForwardRef:\n        \"\"\"\n        Adds the Move ability to a projectile of the specified line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        if position == 0:\n            current_unit_id = line.get_head_unit_id()\n            projectile_id = line.get_head_unit()[\"projectile_id0\"].value\n            current_unit = dataset.genie_units[projectile_id]\n\n        elif position == 1:\n            current_unit_id = line.get_head_unit_id()\n            projectile_id = line.get_head_unit()[\"projectile_id1\"].value\n            current_unit = dataset.genie_units[projectile_id]\n\n        else:\n            raise ValueError(f\"Invalid projectile number: {position}\")\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"Projectile{position}.Move\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Move\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Move\")\n        ability_location = ForwardRef(line,\n                                      f\"{game_entity_name}.ShootProjectile.Projectile{position}\")\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"move_graphics\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_obj_prefix = \"ProjectileFly\"\n            animation_filename_prefix = \"projectile_fly_\"\n\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            ability_animation_id,\n                                                                            property_ref,\n                                                                            animation_obj_prefix,\n                                                                            animation_filename_prefix)\n\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Speed\n        speed = current_unit[\"speed\"].value\n        ability_raw_api_object.add_raw_member(\"speed\", speed, \"engine.ability.type.Move\")\n\n        # Move modes\n        move_modes = [\n            dataset.nyan_api_objects[\"engine.util.move_mode.type.Normal\"],\n        ]\n        ability_raw_api_object.add_raw_member(\"modes\", move_modes, \"engine.ability.type.Move\")\n\n        # Path type\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Air\"].get_nyan_object()\n        ability_raw_api_object.add_raw_member(\"path_type\", path_type, \"engine.ability.type.Move\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def named_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Named ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Named\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Named\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Named\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Name\n        name_ref = f\"{game_entity_name}.Named.{game_entity_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{game_entity_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(line, ability_ref)\n        name_raw_api_object.set_location(name_location)\n\n        name_string_id = current_unit[\"language_dll_name\"].value\n        translations = AoCAbilitySubprocessor.create_language_strings(line,\n                                                                      name_string_id,\n                                                                      name_ref,\n                                                                      f\"{game_entity_name}Name\")\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           translations,\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(line, name_ref)\n        ability_raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.ability.type.Named\")\n        line.add_raw_api_object(name_raw_api_object)\n\n        # Description\n        description_ref = f\"{game_entity_name}.Named.{game_entity_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{game_entity_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(line, ability_ref)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(line, description_ref)\n        ability_raw_api_object.add_raw_member(\"description\",\n                                              description_forward_ref,\n                                              \"engine.ability.type.Named\")\n        line.add_raw_api_object(description_raw_api_object)\n\n        # Long description\n        long_description_ref = f\"{game_entity_name}.Named.{game_entity_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{game_entity_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(line, ability_ref)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(line, long_description_ref)\n        ability_raw_api_object.add_raw_member(\"long_description\",\n                                              long_description_forward_ref,\n                                              \"engine.ability.type.Named\")\n        line.add_raw_api_object(long_description_raw_api_object)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def overlay_terrain_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the OverlayTerrain to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the abilities.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_lookup_dict = internal_name_lookups.get_terrain_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.OverlayTerrain\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"OverlayTerrain\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.OverlayTerrain\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Terrain (Use foundation terrain)\n        terrain_id = current_unit[\"foundation_terrain_id\"].value\n        terrain = dataset.terrain_groups[terrain_id]\n        terrain_forward_ref = ForwardRef(terrain, terrain_lookup_dict[terrain_id][1])\n        ability_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                              terrain_forward_ref,\n                                              \"engine.ability.type.OverlayTerrain\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def pathable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Pathable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Pathable\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Pathable\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Pathable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Hitbox\n        hitbox_ref = f\"{game_entity_name}.Collision.{game_entity_name}Hitbox\"\n        hitbox_forward_ref = ForwardRef(line, hitbox_ref)\n        ability_raw_api_object.add_raw_member(\"hitbox\",\n                                              hitbox_forward_ref,\n                                              \"engine.ability.type.Pathable\")\n\n        # Costs\n        path_costs = {\n            dataset.pregen_nyan_objects[\"util.path.types.Land\"].get_nyan_object(): 255,  # impassable\n            dataset.pregen_nyan_objects[\"util.path.types.Water\"].get_nyan_object(): 255,  # impassable\n        }\n        ability_raw_api_object.add_raw_member(\"path_costs\",\n                                              path_costs,\n                                              \"engine.ability.type.Pathable\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def production_queue_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ProductionQueue ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ProductionQueue\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"ProductionQueue\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ProductionQueue\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Size\n        size = 14\n\n        ability_raw_api_object.add_raw_member(\"size\",\n                                              size,\n                                              \"engine.ability.type.ProductionQueue\")\n\n        # Production modes\n        modes = []\n\n        mode_name = f\"{game_entity_name}.ProvideContingent.CreatablesMode\"\n        mode_raw_api_object = RawAPIObject(mode_name, \"CreatablesMode\", dataset.nyan_api_objects)\n        mode_raw_api_object.add_raw_parent(\"engine.util.production_mode.type.Creatables\")\n        mode_location = ForwardRef(line, ability_ref)\n        mode_raw_api_object.set_location(mode_location)\n\n        # AoE2 allows all creatables in production queue\n        mode_raw_api_object.add_raw_member(\"exclude\",\n                                           [],\n                                           \"engine.util.production_mode.type.Creatables\")\n\n        mode_forward_ref = ForwardRef(line, mode_name)\n        modes.append(mode_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"production_modes\",\n                                              modes,\n                                              \"engine.ability.type.ProductionQueue\")\n\n        line.add_raw_api_object(mode_raw_api_object)\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def projectile_ability(line: GenieGameEntityGroup, position: int = 0) -> ForwardRef:\n        \"\"\"\n        Adds a Projectile ability to projectiles in a line. Which projectile should\n        be added is determined by the 'position' argument.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param position: When 0, gives the first projectile its ability. When 1, the second...\n        :type position: int\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        # First projectile is mandatory\n        obj_ref = f\"{game_entity_name}.ShootProjectile.Projectile{str(position)}\"\n        ability_ref = f\"{game_entity_name}.ShootProjectile.Projectile{position}.Projectile\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Projectile\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Projectile\")\n        ability_location = ForwardRef(line, obj_ref)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Arc\n        if position == 0:\n            projectile_id = current_unit[\"projectile_id0\"].value\n\n        elif position == 1:\n            projectile_id = current_unit[\"projectile_id1\"].value\n\n        else:\n            raise ValueError(f\"Invalid projectile position {position}\")\n\n        projectile = dataset.genie_units[projectile_id]\n        arc = degrees(projectile[\"projectile_arc\"].value)\n        ability_raw_api_object.add_raw_member(\"arc\",\n                                              arc,\n                                              \"engine.ability.type.Projectile\")\n\n        # Accuracy\n        accuracy_name = (f\"{game_entity_name}.ShootProjectile.\"\n                         f\"Projectile{position}.Projectile.Accuracy\")\n        accuracy_raw_api_object = RawAPIObject(accuracy_name,\n                                               \"Accuracy\",\n                                               dataset.nyan_api_objects)\n        accuracy_raw_api_object.add_raw_parent(\"engine.util.accuracy.Accuracy\")\n        accuracy_location = ForwardRef(line, ability_ref)\n        accuracy_raw_api_object.set_location(accuracy_location)\n\n        accuracy_value = current_unit[\"accuracy\"].value\n        accuracy_raw_api_object.add_raw_member(\"accuracy\",\n                                               accuracy_value,\n                                               \"engine.util.accuracy.Accuracy\")\n\n        accuracy_dispersion = current_unit[\"accuracy_dispersion\"].value\n        accuracy_raw_api_object.add_raw_member(\"accuracy_dispersion\",\n                                               accuracy_dispersion,\n                                               \"engine.util.accuracy.Accuracy\")\n        dropoff_type = dataset.nyan_api_objects[\"engine.util.dropoff_type.type.InverseLinear\"]\n        accuracy_raw_api_object.add_raw_member(\"dispersion_dropoff\",\n                                               dropoff_type,\n                                               \"engine.util.accuracy.Accuracy\")\n\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n        ]\n        accuracy_raw_api_object.add_raw_member(\"target_types\",\n                                               allowed_types,\n                                               \"engine.util.accuracy.Accuracy\")\n        accuracy_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                               [],\n                                               \"engine.util.accuracy.Accuracy\")\n\n        line.add_raw_api_object(accuracy_raw_api_object)\n        accuracy_forward_ref = ForwardRef(line, accuracy_name)\n        ability_raw_api_object.add_raw_member(\"accuracy\",\n                                              [accuracy_forward_ref],\n                                              \"engine.ability.type.Projectile\")\n\n        # Target mode\n        target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.CurrentPosition\"]\n        ability_raw_api_object.add_raw_member(\"target_mode\",\n                                              target_mode,\n                                              \"engine.ability.type.Projectile\")\n\n        # Ingore types; buildings are ignored unless targeted\n        ignore_forward_refs = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"ignored_types\",\n                                              ignore_forward_refs,\n                                              \"engine.ability.type.Projectile\")\n        ability_raw_api_object.add_raw_member(\"unignored_entities\",\n                                              [],\n                                              \"engine.ability.type.Projectile\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def provide_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ProvideContingent ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        if isinstance(line, GenieStackBuildingGroup):\n            current_unit = line.get_stack_unit()\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ProvideContingent\"\n\n        # Stores the pop space\n        resource_storage = current_unit[\"resource_storage\"].value\n\n        contingents = []\n        for storage in resource_storage:\n            type_id = storage[\"type\"].value\n\n            if type_id == 4:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.PopulationSpace\"].get_nyan_object(\n                )\n                resource_name = \"PopSpace\"\n\n            else:\n                continue\n\n            amount = storage[\"amount\"].value\n\n            contingent_amount_name = f\"{game_entity_name}.ProvideContingent.{resource_name}\"\n            contingent_amount = RawAPIObject(contingent_amount_name, resource_name,\n                                             dataset.nyan_api_objects)\n            contingent_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            ability_forward_ref = ForwardRef(line, ability_ref)\n            contingent_amount.set_location(ability_forward_ref)\n\n            contingent_amount.add_raw_member(\"type\",\n                                             resource,\n                                             \"engine.util.resource.ResourceAmount\")\n            contingent_amount.add_raw_member(\"amount\",\n                                             amount,\n                                             \"engine.util.resource.ResourceAmount\")\n\n            line.add_raw_api_object(contingent_amount)\n            contingent_amount_forward_ref = ForwardRef(line,\n                                                       contingent_amount_name)\n            contingents.append(contingent_amount_forward_ref)\n\n        if not contingents:\n            # Do not create the ability if the unit provides no contingents\n            return None\n\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"ProvideContingent\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ProvideContingent\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_raw_api_object.add_raw_member(\"amount\",\n                                              contingents,\n                                              \"engine.ability.type.ProvideContingent\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def rally_point_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the RallyPoint ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.RallyPoint\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"RallyPoint\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.RallyPoint\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def regenerate_attribute_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the RegenerateAttribute ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the ability.\n        :rtype: list\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        attribute = None\n        attribute_name = \"\"\n        if current_unit_id == 125:\n            # Monk; regenerates Faith\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Faith\"].get_nyan_object()\n            attribute_name = \"Faith\"\n\n        elif current_unit_id == 692:\n            # Berserk: regenerates Health\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            attribute_name = \"Health\"\n\n        else:\n            return []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_name = f\"Regenerate{attribute_name}\"\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.RegenerateAttribute\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Attribute rate\n        # ===============================================================================\n        rate_name = f\"{attribute_name}Rate\"\n        rate_ref = f\"{game_entity_name}.{ability_name}.{rate_name}\"\n        rate_raw_api_object = RawAPIObject(rate_ref, rate_name, dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, ability_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        # Attribute\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        # Rate\n        attribute_rate = 0\n        if current_unit_id == 125:\n            # stored in civ resources\n            attribute_rate = dataset.genie_civs[0][\"resources\"][35].value\n\n        elif current_unit_id == 692:\n            # stored in civ resources, but has to get converted to amount/second\n            heal_timer = dataset.genie_civs[0][\"resources\"][96].value\n            attribute_rate = 1 / heal_timer\n\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           attribute_rate,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # ===============================================================================\n        rate_forward_ref = ForwardRef(line, rate_ref)\n        ability_raw_api_object.add_raw_member(\"rate\",\n                                              rate_forward_ref,\n                                              \"engine.ability.type.RegenerateAttribute\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return [ability_forward_ref]\n\n    @ staticmethod\n    def regenerate_resource_spot_ability(line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Adds the RegenerateResourceSpot ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        # Unused in AoC\n\n    @ staticmethod\n    def remove_storage_ability(line) -> ForwardRef:\n        \"\"\"\n        Adds the RemoveStorage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.RemoveStorage\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"RemoveStorage\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.RemoveStorage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Container\n        container_ref = f\"{game_entity_name}.Storage.{game_entity_name}Container\"\n        container_forward_ref = ForwardRef(line, container_ref)\n        ability_raw_api_object.add_raw_member(\"container\",\n                                              container_forward_ref,\n                                              \"engine.ability.type.RemoveStorage\")\n\n        # Storage elements\n        elements = []\n        entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        for entity in line.garrison_entities:\n            entity_ref = entity_lookups[entity.get_head_unit_id()][0]\n            entity_forward_ref = ForwardRef(entity, entity_ref)\n            elements.append(entity_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"storage_elements\",\n                                              elements,\n                                              \"engine.ability.type.RemoveStorage\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def restock_ability(line: GenieGameEntityGroup, restock_target_id: int) -> ForwardRef:\n        \"\"\"\n        Adds the Restock ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        # get the restock target\n        converter_groups = {}\n        converter_groups.update(dataset.unit_lines)\n        converter_groups.update(dataset.building_lines)\n        converter_groups.update(dataset.ambient_groups)\n\n        restock_target = converter_groups[restock_target_id]\n\n        if not restock_target.is_harvestable():\n            raise RuntimeError(f\"{restock_target} cannot be restocked: is not harvestable\")\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        restock_lookup_dict = internal_name_lookups.get_restock_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.{restock_lookup_dict[restock_target_id][0]}\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              restock_lookup_dict[restock_target_id][0],\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Restock\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Ability properties\n        properties = {}\n\n        ability_animation_id = -1\n        if isinstance(line, GenieVillagerGroup) and restock_target_id == 50:\n            # Search for the build graphic of farms\n            restock_unit = line.get_units_with_command(101)[0]\n            commands = restock_unit[\"unit_commands\"].value\n            for command in commands:\n                type_id = command[\"type\"].value\n\n                if type_id == 101:\n                    ability_animation_id = command[\"work_sprite_id\"].value\n\n        if ability_animation_id > -1:\n            # Make the ability animated\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                restock_lookup_dict[restock_target_id][0],\n                f\"{restock_lookup_dict[restock_target_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Auto restock\n        ability_raw_api_object.add_raw_member(\"auto_restock\",\n                                              True,  # always True since AoC\n                                              \"engine.ability.type.Restock\")\n\n        # Target\n        restock_target_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        restock_target_name = restock_target_lookup_dict[restock_target_id][0]\n        spot_forward_ref = ForwardRef(restock_target,\n                                      (f\"{restock_target_name}.Harvestable.\"\n                                       f\"{restock_target_name}ResourceSpot\"))\n        ability_raw_api_object.add_raw_member(\"target\",\n                                              spot_forward_ref,\n                                              \"engine.ability.type.Restock\")\n\n        # restock time\n        restock_time = restock_target.get_head_unit()[\"creation_time\"].value\n        ability_raw_api_object.add_raw_member(\"restock_time\",\n                                              restock_time,\n                                              \"engine.ability.type.Restock\")\n\n        # Manual/Auto Cost\n        # Link to the same Cost object as Create\n        cost_forward_ref = ForwardRef(restock_target,\n                                      (f\"{restock_target_name}.CreatableGameEntity.\"\n                                       f\"{restock_target_name}Cost\"))\n        ability_raw_api_object.add_raw_member(\"manual_cost\",\n                                              cost_forward_ref,\n                                              \"engine.ability.type.Restock\")\n        ability_raw_api_object.add_raw_member(\"auto_cost\",\n                                              cost_forward_ref,\n                                              \"engine.ability.type.Restock\")\n\n        # Amount\n        restock_amount = restock_target.get_head_unit()[\"resource_capacity\"].value\n        if restock_target_id == 50:\n            # Farm food amount (hardcoded in civ)\n            restock_amount = dataset.genie_civs[1][\"resources\"][36].value\n\n        elif restock_target_id == 199:\n            # Fish trap added food amount (hardcoded in civ)\n            restock_amount += dataset.genie_civs[1][\"resources\"][88].value\n\n        ability_raw_api_object.add_raw_member(\"amount\",\n                                              restock_amount,\n                                              \"engine.ability.type.Restock\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def research_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Research ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.Research\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Research\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Research\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        researchables_set = []\n        for researchable in line.researches:\n            if researchable.is_unique():\n                # Skip this because unique techs are handled by civs\n                continue\n\n            # ResearchableTech objects are created for each unit/building\n            # line individually to avoid duplicates. We just point to the\n            # raw API objects here.\n            researchable_id = researchable.get_id()\n            researchable_name = tech_lookup_dict[researchable_id][0]\n\n            raw_api_object_ref = f\"{researchable_name}.ResearchableTech\"\n            researchable_forward_ref = ForwardRef(researchable,\n                                                  raw_api_object_ref)\n            researchables_set.append(researchable_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"researchables\", researchables_set,\n                                              \"engine.ability.type.Research\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def resistance_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Resistance ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.Resistance\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Resistance\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Resistance\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Resistances\n        resistances = []\n        resistances.extend(AoCEffectSubprocessor.get_attack_resistances(line, ability_ref))\n        if isinstance(line, (GenieUnitLineGroup, GenieBuildingLineGroup)):\n            resistances.extend(AoCEffectSubprocessor.get_convert_resistances(line, ability_ref))\n\n            if isinstance(line, GenieUnitLineGroup) and not line.is_repairable():\n                resistances.extend(AoCEffectSubprocessor.get_heal_resistances(line, ability_ref))\n\n            if isinstance(line, GenieBuildingLineGroup):\n                resistances.extend(\n                    AoCEffectSubprocessor.get_construct_resistances(line, ability_ref))\n\n            if line.is_repairable():\n                resistances.extend(AoCEffectSubprocessor.get_repair_resistances(line, ability_ref))\n\n        ability_raw_api_object.add_raw_member(\"resistances\",\n                                              resistances,\n                                              \"engine.ability.type.Resistance\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ResourceStorage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            gatherers = line.variants[0].line\n\n        else:\n            gatherers = [line.line[0]]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ResourceStorage\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"ResourceStorage\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ResourceStorage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Create containers\n        containers = []\n        for gatherer in gatherers:\n            unit_commands = gatherer[\"unit_commands\"].value\n            resource = None\n\n            used_command = None\n            for command in unit_commands:\n                # Find a gather ability. It doesn't matter which one because\n                # they should all produce the same resource for one genie unit.\n                type_id = command[\"type\"].value\n\n                if type_id not in (5, 110, 111):\n                    continue\n\n                resource_id = command[\"resource_out\"].value\n\n                # If resource_out is not specified, the gatherer harvests resource_in\n                if resource_id == -1:\n                    resource_id = command[\"resource_in\"].value\n\n                if resource_id == 0:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object(\n                    )\n\n                elif resource_id == 1:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object(\n                    )\n\n                elif resource_id == 2:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                    )\n\n                elif resource_id == 3:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object(\n                    )\n\n                elif type_id == 111:\n                    target_id = command[\"unit_id\"].value\n                    if target_id not in dataset.building_lines.keys():\n                        # Skips the trade workshop trading which is never used\n                        continue\n\n                    # Trade goods --> gold\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object(\n                    )\n\n                else:\n                    continue\n\n                used_command = command\n\n            if not used_command:\n                # The unit uses no gathering command or we don't recognize it\n                continue\n\n            if line.is_gatherer():\n                gatherer_unit_id = gatherer.get_id()\n                if gatherer_unit_id not in gather_lookup_dict:\n                    # Skips hunting wolves\n                    continue\n\n                container_name = f\"{gather_lookup_dict[gatherer_unit_id][0]}Container\"\n\n            elif used_command[\"type\"].value == 111:\n                # Trading\n                container_name = \"TradeContainer\"\n\n            container_ref = f\"{ability_ref}.{container_name}\"\n            container_raw_api_object = RawAPIObject(container_ref,\n                                                    container_name,\n                                                    dataset.nyan_api_objects)\n            container_raw_api_object.add_raw_parent(\"engine.util.storage.ResourceContainer\")\n            container_location = ForwardRef(line, ability_ref)\n            container_raw_api_object.set_location(container_location)\n\n            # Resource\n            container_raw_api_object.add_raw_member(\"resource\",\n                                                    resource,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            # Carry capacity\n            if line.is_gatherer():\n                carry_capacity = gatherer[\"resource_capacity\"].value\n\n            elif used_command[\"type\"].value == 111:\n                # No restriction for trading\n                carry_capacity = MemberSpecialValue.NYAN_INF\n\n            container_raw_api_object.add_raw_member(\"max_amount\",\n                                                    carry_capacity,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            # Carry progress\n            carry_progress = []\n            carry_move_animation_id = used_command[\"carry_sprite_id\"].value\n            if carry_move_animation_id > -1:\n                # =================================================================================\n                progress_ref = f\"{ability_ref}.{container_name}CarryProgress\"\n                progress_raw_api_object = RawAPIObject(progress_ref,\n                                                       f\"{container_name}CarryProgress\",\n                                                       dataset.nyan_api_objects)\n                progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n                progress_location = ForwardRef(line, container_ref)\n                progress_raw_api_object.set_location(progress_location)\n\n                line.add_raw_api_object(progress_raw_api_object)\n\n                # Type\n                progress_raw_api_object.add_raw_member(\"type\",\n                                                       api_objects[\"engine.util.progress_type.type.Carry\"],\n                                                       \"engine.util.progress.Progress\")\n\n                # Interval = (20.0, 100.0)\n                progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                       20.0,\n                                                       \"engine.util.progress.Progress\")\n                progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                       100.0,\n                                                       \"engine.util.progress.Progress\")\n\n                # Progress properties\n                properties = {}\n                # =================================================================================\n                # Animated property (animation overrides)\n                # =================================================================================\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n                # =================================================================================\n                overrides = []\n                # =================================================================================\n                # Move override\n                # =================================================================================\n                override_ref = f\"{property_ref}.MoveOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"MoveOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                move_forward_ref = ForwardRef(line, f\"{game_entity_name}.Move\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       move_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                carry_move_animation_id,\n                                                                                override_ref,\n                                                                                \"Move\",\n                                                                                \"move_carry_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                # TODO: Idle override (stops on last used frame of Move override?)\n                # =================================================================================\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n                # =================================================================================\n                progress_raw_api_object.add_raw_member(\"properties\",\n                                                       properties,\n                                                       \"engine.util.progress.Progress\")\n\n                progress_forward_ref = ForwardRef(line, progress_ref)\n                carry_progress.append(progress_forward_ref)\n\n            container_raw_api_object.add_raw_member(\"carry_progress\",\n                                                    carry_progress,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            line.add_raw_api_object(container_raw_api_object)\n\n            container_forward_ref = ForwardRef(line, container_ref)\n            containers.append(container_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"containers\",\n                                              containers,\n                                              \"engine.ability.type.ResourceStorage\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def selectable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds Selectable abilities to a line. Units will get two of these,\n        one Rectangle box for the Self stance and one MatchToSprite box\n        for other stances.\n\n        :param line: Unit/Building line that gets the abilities.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the abilities.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_refs = (f\"{game_entity_name}.Selectable\",)\n        ability_names = (\"Selectable\",)\n\n        if isinstance(line, GenieUnitLineGroup):\n            ability_refs = (f\"{game_entity_name}.SelectableOthers\",\n                            f\"{game_entity_name}.SelectableSelf\")\n            ability_names = (\"SelectableOthers\",\n                             \"SelectableSelf\")\n\n        abilities = []\n\n        # First box (MatchToSprite)\n        ability_ref = ability_refs[0]\n        ability_name = ability_names[0]\n\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Selectable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Selection box\n        box_ref = dataset.nyan_api_objects[\"engine.util.selection_box.type.MatchToSprite\"]\n        ability_raw_api_object.add_raw_member(\"selection_box\",\n                                              box_ref,\n                                              \"engine.ability.type.Selectable\")\n\n        # Ability properties\n        properties = {}\n\n        # Diplomacy setting (for units)\n        if isinstance(line, GenieUnitLineGroup):\n            property_ref = f\"{ability_ref}.Diplomatic\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Diplomatic\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            stances = [\n                dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Enemy\"].get_nyan_object(),\n                dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Neutral\"].get_nyan_object(\n                ),\n                dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Friendly\"].get_nyan_object(\n                )\n            ]\n            property_raw_api_object.add_raw_member(\"stances\",\n                                                   stances,\n                                                   \"engine.ability.property.type.Diplomatic\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n            })\n\n            ability_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.ability.Ability\")\n        else:\n            ability_comm_sound_id = current_unit[\"selection_sound_id\"].value\n            if ability_comm_sound_id > -1:\n                property_ref = f\"{ability_ref}.CommandSound\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"CommandSound\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n                property_location = ForwardRef(line, ability_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                sounds_set = []\n                sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                        ability_comm_sound_id,\n                                                                        property_ref,\n                                                                        ability_name,\n                                                                        \"command_\")\n                sounds_set.append(sound_forward_ref)\n                property_raw_api_object.add_raw_member(\"sounds\",\n                                                       sounds_set,\n                                                       \"engine.ability.property.type.CommandSound\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n                })\n                ability_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.ability.Ability\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        abilities.append(ability_forward_ref)\n\n        if not isinstance(line, GenieUnitLineGroup):\n            return abilities\n\n        # Second box (Rectangle)\n        ability_ref = ability_refs[1]\n        ability_name = ability_names[1]\n\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              ability_name,\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Selectable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Ability properties\n        properties = {}\n\n        # Command Sound\n        ability_comm_sound_id = current_unit[\"selection_sound_id\"].value\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    ability_name,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\",\n                                                   sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Selection box\n        box_name = f\"{game_entity_name}.SelectableSelf.Rectangle\"\n        box_raw_api_object = RawAPIObject(box_name, \"Rectangle\", dataset.nyan_api_objects)\n        box_raw_api_object.add_raw_parent(\"engine.util.selection_box.type.Rectangle\")\n        box_location = ForwardRef(line, ability_ref)\n        box_raw_api_object.set_location(box_location)\n\n        width = current_unit[\"selection_shape_x\"].value\n        box_raw_api_object.add_raw_member(\"width\",\n                                          width,\n                                          \"engine.util.selection_box.type.Rectangle\")\n\n        height = current_unit[\"selection_shape_y\"].value\n        box_raw_api_object.add_raw_member(\"height\",\n                                          height,\n                                          \"engine.util.selection_box.type.Rectangle\")\n\n        line.add_raw_api_object(box_raw_api_object)\n\n        box_forward_ref = ForwardRef(line, box_name)\n        ability_raw_api_object.add_raw_member(\"selection_box\",\n                                              box_forward_ref,\n                                              \"engine.ability.type.Selectable\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        abilities.append(ability_forward_ref)\n\n        return abilities\n\n    @ staticmethod\n    def send_back_to_task_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the SendBackToTask ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.SendBackToTask\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"SendBackToTask\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.SendBackToTask\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Only works on villagers\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Villager\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.SendBackToTask\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.SendBackToTask\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> ForwardRef:\n        \"\"\"\n        Adds the ShootProjectile ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              ability_name,\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ShootProjectile\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"attack_sprite_id\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n        # Command Sound\n        ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    ability_name,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\",\n                                                   sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Projectile\n        projectiles = []\n        projectile_primary = current_unit[\"projectile_id0\"].value\n        if projectile_primary > -1:\n            projectiles.append(ForwardRef(line,\n                                          f\"{game_entity_name}.ShootProjectile.Projectile0\"))\n\n        projectile_secondary = current_unit[\"projectile_id1\"].value\n        if projectile_secondary > -1:\n            projectiles.append(ForwardRef(line,\n                                          f\"{game_entity_name}.ShootProjectile.Projectile1\"))\n\n        ability_raw_api_object.add_raw_member(\"projectiles\",\n                                              projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Projectile count\n        min_projectiles = current_unit[\"projectile_min_count\"].value\n        max_projectiles = current_unit[\"projectile_max_count\"].value\n\n        if projectile_primary == -1:\n            # Special case where only the second projectile is defined (town center)\n            # The min/max projectile count is lowered by 1 in this case\n            min_projectiles -= 1\n            max_projectiles -= 1\n\n        elif min_projectiles == 0 and max_projectiles == 0:\n            # If there's a primary projectile defined, but these values are 0,\n            # the game still fires a projectile on attack.\n            min_projectiles += 1\n            max_projectiles += 1\n\n        if current_unit_id == 236:\n            # Bombard Tower (gets treated like a tower for max projectiles)\n            max_projectiles = 5\n\n        ability_raw_api_object.add_raw_member(\"min_projectiles\",\n                                              min_projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"max_projectiles\",\n                                              max_projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Range\n        min_range = current_unit[\"weapon_range_min\"].value\n        ability_raw_api_object.add_raw_member(\"min_range\",\n                                              min_range,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        max_range = current_unit[\"weapon_range_max\"].value\n        ability_raw_api_object.add_raw_member(\"max_range\",\n                                              max_range,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Reload time and delay\n        reload_time = current_unit[\"attack_speed\"].value\n        ability_raw_api_object.add_raw_member(\"reload_time\",\n                                              reload_time,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        if ability_animation_id > -1:\n            animation = dataset.genie_graphics[ability_animation_id]\n            frame_rate = animation.get_frame_rate()\n\n        else:\n            frame_rate = 0\n\n        spawn_delay_frames = current_unit[\"frame_delay\"].value\n        spawn_delay = frame_rate * spawn_delay_frames\n        ability_raw_api_object.add_raw_member(\"spawn_delay\",\n                                              spawn_delay,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # TODO: Hardcoded?\n        ability_raw_api_object.add_raw_member(\"projectile_delay\",\n                                              0.1,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Turning\n        if isinstance(line, GenieBuildingLineGroup):\n            require_turning = False\n\n        else:\n            require_turning = True\n\n        ability_raw_api_object.add_raw_member(\"require_turning\",\n                                              require_turning,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Manual Aiming (Mangonel + Trebuchet)\n        manual_aiming_allowed = line.get_head_unit_id() in (280, 331)\n\n        ability_raw_api_object.add_raw_member(\"manual_aiming_allowed\",\n                                              manual_aiming_allowed,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Spawning area\n        spawning_area_offset_x = current_unit[\"weapon_offset\"][0].value\n        spawning_area_offset_y = current_unit[\"weapon_offset\"][1].value\n        spawning_area_offset_z = current_unit[\"weapon_offset\"][2].value\n\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_x\",\n                                              spawning_area_offset_x,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_y\",\n                                              spawning_area_offset_y,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_z\",\n                                              spawning_area_offset_z,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        spawning_area_width = current_unit[\"projectile_spawning_area_width\"].value\n        spawning_area_height = current_unit[\"projectile_spawning_area_length\"].value\n        spawning_area_randomness = current_unit[\"projectile_spawning_area_randomness\"].value\n\n        ability_raw_api_object.add_raw_member(\"spawning_area_width\",\n                                              spawning_area_width,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_height\",\n                                              spawning_area_height,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_randomness\",\n                                              spawning_area_randomness,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Restrictions on targets (only units and buildings allowed)\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.ShootProjectile\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def stop_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Stop ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Stop\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Stop\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Stop\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n\n        # Ability properties\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def storage_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Storage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Storage\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Storage\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Storage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Container\n        # ==============================================================================\n        container_name = f\"{game_entity_name}.Storage.{game_entity_name}Container\"\n        container_raw_api_object = RawAPIObject(container_name,\n                                                f\"{game_entity_name}Container\",\n                                                dataset.nyan_api_objects)\n        container_raw_api_object.add_raw_parent(\"engine.util.storage.EntityContainer\")\n        container_location = ForwardRef(line, ability_ref)\n        container_raw_api_object.set_location(container_location)\n\n        garrison_mode = line.get_garrison_mode()\n\n        # Allowed types\n        # TODO: Any should be fine for now, since Enter/Exit abilities limit the stored elements\n        allowed_types = [dataset.nyan_api_objects[\"engine.util.game_entity_type.type.Any\"]]\n\n        container_raw_api_object.add_raw_member(\"allowed_types\",\n                                                allowed_types,\n                                                \"engine.util.storage.EntityContainer\")\n\n        # Blacklisted entities\n        container_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                                [],\n                                                \"engine.util.storage.EntityContainer\")\n\n        # Define storage elements\n        storage_element_defs = []\n        if garrison_mode is GenieGarrisonMode.UNIT_GARRISON:\n            for storage_element in line.garrison_entities:\n                storage_element_name = name_lookup_dict[storage_element.get_head_unit_id()][0]\n                storage_def_ref = (f\"{game_entity_name}.Storage.\"\n                                   f\"{game_entity_name}Container.\"\n                                   f\"{storage_element_name}StorageDef\")\n                storage_def_raw_api_object = RawAPIObject(storage_def_ref,\n                                                          f\"{storage_element_name}StorageDef\",\n                                                          dataset.nyan_api_objects)\n                storage_def_raw_api_object.add_raw_parent(\n                    \"engine.util.storage.StorageElementDefinition\")\n                storage_def_location = ForwardRef(line, container_name)\n                storage_def_raw_api_object.set_location(storage_def_location)\n\n                # Storage element\n                storage_element_forward_ref = ForwardRef(storage_element, storage_element_name)\n                storage_def_raw_api_object.add_raw_member(\"storage_element\",\n                                                          storage_element_forward_ref,\n                                                          \"engine.util.storage.StorageElementDefinition\")\n\n                # Elements per slot\n                storage_def_raw_api_object.add_raw_member(\"elements_per_slot\",\n                                                          1,\n                                                          \"engine.util.storage.StorageElementDefinition\")\n\n                # Conflicts\n                storage_def_raw_api_object.add_raw_member(\"conflicts\",\n                                                          [],\n                                                          \"engine.util.storage.StorageElementDefinition\")\n\n                # TODO: State change (optional) -> speed boost\n\n                storage_def_forward_ref = ForwardRef(line, storage_def_ref)\n                storage_element_defs.append(storage_def_forward_ref)\n                line.add_raw_api_object(storage_def_raw_api_object)\n\n        container_raw_api_object.add_raw_member(\"storage_element_defs\",\n                                                storage_element_defs,\n                                                \"engine.util.storage.EntityContainer\")\n\n        # Container slots\n        slots = current_unit[\"garrison_capacity\"].value\n        if garrison_mode is GenieGarrisonMode.MONK:\n            slots = 1\n\n        container_raw_api_object.add_raw_member(\"slots\",\n                                                slots,\n                                                \"engine.util.storage.EntityContainer\")\n\n        # Carry progress\n        carry_progress = []\n        if garrison_mode is GenieGarrisonMode.MONK and isinstance(line, GenieMonkGroup):\n            switch_unit = line.get_switch_unit()\n            carry_idle_animation_id = switch_unit[\"idle_graphic0\"].value\n            carry_move_animation_id = switch_unit[\"move_graphics\"].value\n\n            progress_ref = f\"{ability_ref}.CarryProgress\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"CarryProgress\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Carry\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 100.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   100.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Animated property (animation overrides)\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.Animated\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n            # =====================================================================================\n            overrides = []\n            # Idle override\n            # =====================================================================================\n            override_ref = f\"{property_ref}.IdleOverride\"\n            override_raw_api_object = RawAPIObject(override_ref,\n                                                   \"IdleOverride\",\n                                                   dataset.nyan_api_objects)\n            override_raw_api_object.add_raw_parent(\n                \"engine.util.animation_override.AnimationOverride\")\n            override_location = ForwardRef(line, property_ref)\n            override_raw_api_object.set_location(override_location)\n\n            idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n            override_raw_api_object.add_raw_member(\"ability\",\n                                                   idle_forward_ref,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            # Animation\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            carry_idle_animation_id,\n                                                                            override_ref,\n                                                                            \"Idle\",\n                                                                            \"idle_carry_override_\")\n\n            animations_set.append(animation_forward_ref)\n            override_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            override_raw_api_object.add_raw_member(\"priority\",\n                                                   1,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            override_forward_ref = ForwardRef(line, override_ref)\n            overrides.append(override_forward_ref)\n            line.add_raw_api_object(override_raw_api_object)\n            # =====================================================================================\n            # Move override\n            # =====================================================================================\n            override_ref = f\"{property_ref}.MoveOverride\"\n            override_raw_api_object = RawAPIObject(override_ref,\n                                                   \"MoveOverride\",\n                                                   dataset.nyan_api_objects)\n            override_raw_api_object.add_raw_parent(\n                \"engine.util.animation_override.AnimationOverride\")\n            override_location = ForwardRef(line, property_ref)\n            override_raw_api_object.set_location(override_location)\n\n            idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Move\")\n            override_raw_api_object.add_raw_member(\"ability\",\n                                                   idle_forward_ref,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            # Animation\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                            carry_move_animation_id,\n                                                                            override_ref,\n                                                                            \"Move\",\n                                                                            \"move_carry_override_\")\n\n            animations_set.append(animation_forward_ref)\n            override_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            override_raw_api_object.add_raw_member(\"priority\",\n                                                   1,\n                                                   \"engine.util.animation_override.AnimationOverride\")\n\n            override_forward_ref = ForwardRef(line, override_ref)\n            overrides.append(override_forward_ref)\n            line.add_raw_api_object(override_raw_api_object)\n            # =====================================================================================\n            property_raw_api_object.add_raw_member(\"overrides\",\n                                                   overrides,\n                                                   \"engine.util.progress.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n            })\n\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n            # =====================================================================================\n            carry_state_name = f\"{property_ref}.CarryRelicState\"\n            carry_state_raw_api_object = RawAPIObject(carry_state_name,\n                                                      \"CarryRelicState\",\n                                                      dataset.nyan_api_objects)\n            carry_state_raw_api_object.add_raw_parent(\"engine.util.state_machine.StateChanger\")\n            carry_state_location = ForwardRef(line, property_ref)\n            carry_state_raw_api_object.set_location(carry_state_location)\n\n            # Priority\n            carry_state_raw_api_object.add_raw_member(\"priority\",\n                                                      1,\n                                                      \"engine.util.state_machine.StateChanger\")\n\n            # Enabled abilities\n            carry_state_raw_api_object.add_raw_member(\"enable_abilities\",\n                                                      [],\n                                                      \"engine.util.state_machine.StateChanger\")\n\n            # Disabled abilities\n            disabled_forward_refs = []\n\n            if line.has_command(104):\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Convert\"))\n\n            if line.has_command(105):\n                disabled_forward_refs.append(ForwardRef(line,\n                                                        f\"{game_entity_name}.Heal\"))\n\n            carry_state_raw_api_object.add_raw_member(\"disable_abilities\",\n                                                      disabled_forward_refs,\n                                                      \"engine.util.state_machine.StateChanger\")\n\n            # Enabled modifiers\n            carry_state_raw_api_object.add_raw_member(\"enable_modifiers\",\n                                                      [],\n                                                      \"engine.util.state_machine.StateChanger\")\n\n            # Disabled modifiers\n            carry_state_raw_api_object.add_raw_member(\"disable_modifiers\",\n                                                      [],\n                                                      \"engine.util.state_machine.StateChanger\")\n\n            line.add_raw_api_object(carry_state_raw_api_object)\n            # =====================================================================================\n            init_state_forward_ref = ForwardRef(line, carry_state_name)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   init_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_ref = ForwardRef(line, progress_ref)\n            carry_progress.append(progress_forward_ref)\n\n        else:\n            # Garrison graphics\n            if current_unit.has_member(\"garrison_graphic\"):\n                garrison_animation_id = current_unit[\"garrison_graphic\"].value\n\n            else:\n                garrison_animation_id = -1\n\n            if garrison_animation_id > -1:\n                progress_ref = f\"{ability_ref}.CarryProgress\"\n                progress_raw_api_object = RawAPIObject(progress_ref,\n                                                       \"CarryProgress\",\n                                                       dataset.nyan_api_objects)\n                progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n                progress_location = ForwardRef(line, ability_ref)\n                progress_raw_api_object.set_location(progress_location)\n\n                # Type\n                progress_raw_api_object.add_raw_member(\"type\",\n                                                       api_objects[\"engine.util.progress_type.type.Carry\"],\n                                                       \"engine.util.progress.Progress\")\n\n                # Interval = (0.0, 100.0)\n                progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                       0.0,\n                                                       \"engine.util.progress.Progress\")\n                progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                       100.0,\n                                                       \"engine.util.progress.Progress\")\n\n                # Progress properties\n                properties = {}\n                # Animated property (animation overrides)\n                # =================================================================================\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n                # =================================================================================\n                override_ref = f\"{property_ref}.IdleOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"IdleOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                idle_forward_ref = ForwardRef(line, f\"{game_entity_name}.Idle\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       idle_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                garrison_animation_id,\n                                                                                override_ref,\n                                                                                \"Idle\",\n                                                                                \"idle_garrison_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                line.add_raw_api_object(override_raw_api_object)\n                # =================================================================================\n                override_forward_ref = ForwardRef(line, override_ref)\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       [override_forward_ref],\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n                # =====================================================================================\n                progress_raw_api_object.add_raw_member(\"properties\",\n                                                       properties,\n                                                       \"engine.util.progress.Progress\")\n\n                progress_forward_ref = ForwardRef(line, progress_ref)\n                carry_progress.append(progress_forward_ref)\n                line.add_raw_api_object(progress_raw_api_object)\n\n        container_raw_api_object.add_raw_member(\"carry_progress\",\n                                                carry_progress,\n                                                \"engine.util.storage.EntityContainer\")\n\n        line.add_raw_api_object(container_raw_api_object)\n        # ==============================================================================\n        container_forward_ref = ForwardRef(line, container_name)\n        ability_raw_api_object.add_raw_member(\"container\",\n                                              container_forward_ref,\n                                              \"engine.ability.type.Storage\")\n\n        # Empty condition\n        if garrison_mode in (GenieGarrisonMode.UNIT_GARRISON, GenieGarrisonMode.MONK):\n            # Empty before death\n            condition = [\n                dataset.pregen_nyan_objects[\"util.logic.literal.death.StandardHealthDeathLiteral\"].get_nyan_object()]\n\n        elif garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n            # Empty when HP < 20%\n            condition = [\n                dataset.pregen_nyan_objects[\"util.logic.literal.garrison.BuildingDamageEmpty\"].get_nyan_object()]\n\n        else:\n            # Never empty automatically (transport ships)\n            condition = []\n\n        ability_raw_api_object.add_raw_member(\"empty_condition\",\n                                              condition,\n                                              \"engine.ability.type.Storage\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def terrain_requirement_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the TerrainRequirement to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the abilities.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_type_lookup_dict = internal_name_lookups.get_terrain_type_lookups(\n            dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.TerrainRequirement\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"TerrainRequirement\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.TerrainRequirement\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Allowed types\n        allowed_types = []\n        terrain_restriction = current_unit[\"terrain_restriction\"].value\n        for terrain_type in terrain_type_lookup_dict.values():\n            # Check if terrain type is covered by terrain restriction\n            if terrain_restriction in terrain_type[1]:\n                type_name = f\"util.terrain_type.types.{terrain_type[2]}\"\n                type_obj = dataset.pregen_nyan_objects[type_name].get_nyan_object()\n                allowed_types.append(type_obj)\n\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.TerrainRequirement\")\n\n        # Blacklisted terrains\n        ability_raw_api_object.add_raw_member(\"blacklisted_terrains\",\n                                              [],\n                                              \"engine.ability.type.TerrainRequirement\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def trade_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Trade ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Trade\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Trade\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Trade\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Trade route (use the trade route to the market)\n        trade_routes = []\n\n        unit_commands = current_unit[\"unit_commands\"].value\n        for command in unit_commands:\n            # Find the trade command and the trade post id\n            type_id = command[\"type\"].value\n\n            if type_id != 111:\n                continue\n\n            trade_post_id = command[\"unit_id\"].value\n            if trade_post_id not in dataset.building_lines.keys():\n                # Skips trade workshop\n                continue\n\n            trade_post_line = dataset.building_lines[trade_post_id]\n            trade_post_name = name_lookup_dict[trade_post_id][0]\n\n            trade_route_ref = f\"{trade_post_name}.TradePost.AoE2{trade_post_name}TradeRoute\"\n            trade_route_forward_ref = ForwardRef(trade_post_line, trade_route_ref)\n            trade_routes.append(trade_route_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"trade_routes\",\n                                              trade_routes,\n                                              \"engine.ability.type.Trade\")\n\n        # container\n        container_forward_ref = ForwardRef(\n            line, f\"{game_entity_name}.ResourceStorage.TradeContainer\")\n        ability_raw_api_object.add_raw_member(\"container\",\n                                              container_forward_ref,\n                                              \"engine.ability.type.Trade\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def trade_post_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the TradePost ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.TradePost\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"TradePost\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.TradePost\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Trade route\n        trade_routes = []\n        # =====================================================================================\n        trade_route_name = f\"AoE2{game_entity_name}TradeRoute\"\n        trade_route_ref = f\"{game_entity_name}.TradePost.{trade_route_name}\"\n        trade_route_raw_api_object = RawAPIObject(trade_route_ref,\n                                                  trade_route_name,\n                                                  dataset.nyan_api_objects)\n        trade_route_raw_api_object.add_raw_parent(\"engine.util.trade_route.type.AoE2TradeRoute\")\n        trade_route_location = ForwardRef(line, ability_ref)\n        trade_route_raw_api_object.set_location(trade_route_location)\n\n        # Trade resource\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n        trade_route_raw_api_object.add_raw_member(\"trade_resource\",\n                                                  resource,\n                                                  \"engine.util.trade_route.TradeRoute\")\n\n        # Start- and endpoints\n        market_forward_ref = ForwardRef(line, game_entity_name)\n        trade_route_raw_api_object.add_raw_member(\"start_trade_post\",\n                                                  market_forward_ref,\n                                                  \"engine.util.trade_route.TradeRoute\")\n        trade_route_raw_api_object.add_raw_member(\"end_trade_post\",\n                                                  market_forward_ref,\n                                                  \"engine.util.trade_route.TradeRoute\")\n\n        trade_route_forward_ref = ForwardRef(line, trade_route_ref)\n        trade_routes.append(trade_route_forward_ref)\n\n        line.add_raw_api_object(trade_route_raw_api_object)\n        # =====================================================================================\n        ability_raw_api_object.add_raw_member(\"trade_routes\",\n                                              trade_routes,\n                                              \"engine.ability.type.TradePost\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def transfer_storage_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the TransferStorage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef, None\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.TransferStorage\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"TransferStorage\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.TransferStorage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # storage element\n        storage_entity = None\n        garrisoned_forward_ref = None\n        for garrisoned in line.garrison_entities:\n            creatable_type = garrisoned.get_head_unit()[\"creatable_type\"].value\n\n            if creatable_type == 4:\n                storage_name = name_lookup_dict[garrisoned.get_id()][0]\n                storage_entity = garrisoned\n                garrisoned_forward_ref = ForwardRef(storage_entity, storage_name)\n\n                break\n\n        else:\n            garrisoned = line.garrison_entities[0]\n            storage_name = name_lookup_dict[garrisoned.get_id()][0]\n            storage_entity = garrisoned\n            garrisoned_forward_ref = ForwardRef(storage_entity, storage_name)\n\n        ability_raw_api_object.add_raw_member(\"storage_element\",\n                                              garrisoned_forward_ref,\n                                              \"engine.ability.type.TransferStorage\")\n\n        # Source container\n        source_ref = f\"{game_entity_name}.Storage.{game_entity_name}Container\"\n        source_forward_ref = ForwardRef(line, source_ref)\n        ability_raw_api_object.add_raw_member(\"source_container\",\n                                              source_forward_ref,\n                                              \"engine.ability.type.TransferStorage\")\n\n        # Target container\n        target = None\n        unit_commands = line.get_switch_unit()[\"unit_commands\"].value\n        for command in unit_commands:\n            type_id = command[\"type\"].value\n\n            # Deposit\n            if type_id == 136:\n                target_id = command[\"unit_id\"].value\n                target = dataset.building_lines[target_id]\n\n        target_name = name_lookup_dict[target.get_id()][0]\n        target_ref = f\"{target_name}.Storage.{target_name}Container\"\n        target_forward_ref = ForwardRef(target, target_ref)\n        ability_raw_api_object.add_raw_member(\"target_container\",\n                                              target_forward_ref,\n                                              \"engine.ability.type.TransferStorage\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def turn_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Turn ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Turn\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Turn\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Turn\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Speed\n        turn_speed_unmodified = current_unit[\"turn_speed\"].value\n\n        # Default case: Instant turning\n        turn_speed = MemberSpecialValue.NYAN_INF\n\n        # Ships/Trebuchets turn slower\n        if turn_speed_unmodified > 0:\n            turn_yaw = current_unit[\"max_yaw_per_sec_moving\"].value\n\n            if not turn_yaw == FLOAT32_MAX:\n                turn_speed = degrees(turn_yaw)\n\n        ability_raw_api_object.add_raw_member(\"turn_speed\",\n                                              turn_speed,\n                                              \"engine.ability.type.Turn\")\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n\n        # Ability properties\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def use_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the UseContingent ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.UseContingent\"\n\n        # Check if contingents are stored in the unit before creating the ability\n\n        # Stores the pop space\n        resource_storage = current_unit[\"resource_storage\"].value\n        contingents = []\n        for storage in resource_storage:\n            type_id = storage[\"type\"].value\n\n            if type_id == 11:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.PopulationSpace\"].get_nyan_object(\n                )\n                resource_name = \"PopSpace\"\n\n            else:\n                continue\n\n            amount = storage[\"amount\"].value\n\n            contingent_amount_name = f\"{game_entity_name}.UseContingent.{resource_name}\"\n            contingent_amount = RawAPIObject(contingent_amount_name, resource_name,\n                                             dataset.nyan_api_objects)\n            contingent_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            ability_forward_ref = ForwardRef(line, ability_ref)\n            contingent_amount.set_location(ability_forward_ref)\n\n            contingent_amount.add_raw_member(\"type\",\n                                             resource,\n                                             \"engine.util.resource.ResourceAmount\")\n            contingent_amount.add_raw_member(\"amount\",\n                                             amount,\n                                             \"engine.util.resource.ResourceAmount\")\n\n            line.add_raw_api_object(contingent_amount)\n            contingent_amount_forward_ref = ForwardRef(line,\n                                                       contingent_amount_name)\n            contingents.append(contingent_amount_forward_ref)\n\n        if not contingents:\n            # Break out of function if no contingents were found\n            return None\n\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"UseContingent\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.UseContingent\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_raw_api_object.add_raw_member(\"amount\",\n                                              contingents,\n                                              \"engine.ability.type.UseContingent\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def visibility_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Visibility ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Visibility\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Visibility\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Visibility\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Units are not visible in fog...\n        visible = False\n\n        # ...Buidings and scenery is though\n        if isinstance(line, (GenieBuildingLineGroup, GenieAmbientGroup)):\n            visible = True\n\n        ability_raw_api_object.add_raw_member(\"visible_in_fog\", visible,\n                                              \"engine.ability.type.Visibility\")\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [\n            dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"],\n            dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Friendly\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Neutral\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Enemy\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Gaia\"].get_nyan_object()\n        ]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n\n        # Ability properties\n        properties = {\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        }\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Add another Visibility ability for buildings with construction progress = 0.0\n        # It is not returned by this method, but referenced by the Constructable ability\n        if isinstance(line, GenieBuildingLineGroup):\n            ability_ref = f\"{game_entity_name}.VisibilityConstruct0\"\n            ability_raw_api_object = RawAPIObject(ability_ref,\n                                                  \"VisibilityConstruct0\",\n                                                  dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(\"engine.ability.type.Visibility\")\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            line.add_raw_api_object(ability_raw_api_object)\n\n            # The construction site is not visible in fog\n            visible = False\n            ability_raw_api_object.add_raw_member(\"visible_in_fog\", visible,\n                                                  \"engine.ability.type.Visibility\")\n\n            # Diplomacy settings\n            property_ref = f\"{ability_ref}.Diplomatic\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Diplomatic\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Only the player and friendly players can see the construction site\n            diplomatic_stances = [\n                dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"],\n                dataset.pregen_nyan_objects[\"util.diplomatic_stance.types.Friendly\"].get_nyan_object(\n                )\n            ]\n            property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                                   \"engine.ability.property.type.Diplomatic\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n\n            # Ability properties\n            properties = {\n                api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n            }\n            ability_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.ability.Ability\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @ staticmethod\n    def create_animation(\n        line: GenieGameEntityGroup,\n        animation_id: int,\n        location_ref: str,\n        obj_name_prefix: str,\n        filename_prefix: str\n    ) -> ForwardRef:\n        \"\"\"\n        Generates an animation for an ability.\n\n        :param line: ConverterObjectGroup that the animation object is added to.\n        :type line: ConverterObjectGroup\n        :param animation_id: ID of the animation in the dataset.\n        :type animation_id: int\n        :param ability_ref: Reference of the object the animation is nested in.\n        :type ability_ref: str\n        :param obj_name_prefix: Name prefix for the animation object.\n        :type obj_name_prefix: str\n        :param filename_prefix: Prefix for the animation PNG and sprite files.\n        :type filename_prefix: str\n        \"\"\"\n        dataset = line.data\n        head_unit_id = line.get_head_unit_id()\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        animation_ref = f\"{location_ref}.{obj_name_prefix}Animation\"\n        animation_obj_name = f\"{obj_name_prefix}Animation\"\n        animation_raw_api_object = RawAPIObject(animation_ref, animation_obj_name,\n                                                dataset.nyan_api_objects)\n        animation_raw_api_object.add_raw_parent(\"engine.util.graphics.Animation\")\n        animation_location = ForwardRef(line, location_ref)\n        animation_raw_api_object.set_location(animation_location)\n\n        if animation_id in dataset.combined_sprites.keys():\n            ability_sprite = dataset.combined_sprites[animation_id]\n\n        else:\n            ability_sprite = CombinedSprite(animation_id,\n                                            (f\"{filename_prefix}\"\n                                             f\"{name_lookup_dict[head_unit_id][1]}\"),\n                                            dataset)\n            dataset.combined_sprites.update({ability_sprite.get_id(): ability_sprite})\n\n        ability_sprite.add_reference(animation_raw_api_object)\n\n        animation_raw_api_object.add_raw_member(\"sprite\", ability_sprite,\n                                                \"engine.util.graphics.Animation\")\n\n        line.add_raw_api_object(animation_raw_api_object)\n\n        animation_forward_ref = ForwardRef(line, animation_ref)\n\n        return animation_forward_ref\n\n    @ staticmethod\n    def create_civ_animation(\n        line: GenieGameEntityGroup,\n        civ_group: GenieCivilizationGroup,\n        animation_id: int,\n        location_ref: str,\n        obj_name_prefix: str,\n        filename_prefix: str,\n        exists: bool = False\n    ) -> None:\n        \"\"\"\n        Generates an animation as a patch for a civ.\n\n        :param line: ConverterObjectGroup that the animation object is added to.\n        :type line: ConverterObjectGroup\n        :param civ_group: ConverterObjectGroup that patches the animation object into the ability.\n        :type civ_group: ConverterObjectGroup\n        :param animation_id: ID of the animation in the dataset.\n        :type animation_id: int\n        :param location_ref: Reference of the object the resulting object is nested in.\n        :type location_ref: str\n        :param obj_name_prefix: Name prefix for the object.\n        :type obj_name_prefix: str\n        :param filename_prefix: Prefix for the animation PNG and sprite files.\n        :type filename_prefix: str\n        :param exists: Tells the method if the animation object has already been created.\n        :type exists: bool\n        \"\"\"\n        dataset = civ_group.data\n        head_unit_id = line.get_head_unit_id()\n        civ_id = civ_group.get_id()\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        patch_target_ref = f\"{location_ref}\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"{game_entity_name}{obj_name_prefix}AnimationWrapper\"\n        wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n        wrapper_raw_api_object.set_location(ForwardRef(civ_group, civ_name))\n\n        # Nyan patch\n        nyan_patch_name = f\"{game_entity_name}{obj_name_prefix}Animation\"\n        nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        if animation_id > -1:\n            # If the animation object already exists, we do not need to create it again\n            if exists:\n                # Point to a previously created animation object\n                animation_ref = f\"{location_ref}.{obj_name_prefix}Animation\"\n                animation_forward_ref = ForwardRef(line, animation_ref)\n\n            else:\n                # Create the animation object\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                animation_id,\n                                                                                location_ref,\n                                                                                obj_name_prefix,\n                                                                                filename_prefix)\n\n            # Patch animation into ability\n            nyan_patch_raw_api_object.add_raw_patch_member(\n                \"animations\",\n                [animation_forward_ref],\n                \"engine.ability.property.type.Animated\",\n                MemberOperator.ASSIGN\n            )\n\n        else:\n            # No animation -> empty the set\n            nyan_patch_raw_api_object.add_raw_patch_member(\n                \"animations\",\n                [],\n                \"engine.ability.property.type.Animated\",\n                MemberOperator.ASSIGN\n            )\n\n        patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        civ_group.add_raw_api_object(wrapper_raw_api_object)\n        civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        # Add patch to game_setup\n        civ_forward_ref = ForwardRef(civ_group, civ_name)\n        wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n        push_object = RawMemberPush(civ_forward_ref,\n                                    \"game_setup\",\n                                    \"engine.util.setup.PlayerSetup\",\n                                    [wrapper_forward_ref])\n        civ_group.add_raw_member_push(push_object)\n\n    @ staticmethod\n    def create_sound(\n        line: GenieGameEntityGroup,\n        sound_id: int,\n        location_ref: str,\n        obj_name_prefix: str,\n        filename_prefix: str\n    ) -> ForwardRef:\n        \"\"\"\n        Generates a sound for an ability.\n\n        :param line: ConverterObjectGroup that the animation object is added to.\n        :type line: ConverterObjectGroup\n        :param sound_id: ID of the sound in the dataset.\n        :type sound_id: int\n        :param location_ref: Reference of the object the sound is nested in.\n        :type location_ref: str\n        :param obj_name_prefix: Name prefix for the sound object.\n        :type obj_name_prefix: str\n        :param filename_prefix: Prefix for the animation PNG and sprite files.\n        :type filename_prefix: str\n        \"\"\"\n        dataset = line.data\n\n        sound_ref = f\"{location_ref}.{obj_name_prefix}Sound\"\n        sound_obj_name = f\"{obj_name_prefix}Sound\"\n        sound_raw_api_object = RawAPIObject(sound_ref, sound_obj_name,\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(line, location_ref)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Search for the sound if it exists\n        sounds_set = []\n\n        genie_sound = dataset.genie_sounds[sound_id]\n        file_ids = genie_sound.get_sounds(civ_id=-1)\n\n        for file_id in file_ids:\n            if file_id in dataset.combined_sounds:\n                sound = dataset.combined_sounds[file_id]\n\n            else:\n                sound = CombinedSound(sound_id,\n                                      file_id,\n                                      f\"{filename_prefix}sound_{str(file_id)}\",\n                                      dataset)\n                dataset.combined_sounds.update({file_id: sound})\n\n            sound.add_reference(sound_raw_api_object)\n            sounds_set.append(sound)\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            sounds_set,\n                                            \"engine.util.sound.Sound\")\n\n        line.add_raw_api_object(sound_raw_api_object)\n\n        sound_forward_ref = ForwardRef(line, sound_ref)\n\n        return sound_forward_ref\n\n    @ staticmethod\n    def create_language_strings(\n        line: GenieGameEntityGroup,\n        string_id: int,\n        location_ref: str,\n        obj_name_prefix: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Generates a language string for an ability.\n\n        :param line: ConverterObjectGroup that the animation object is added to.\n        :type line: ConverterObjectGroup\n        :param string_id: ID of the string in the dataset.\n        :type string_id: int\n        :param location_ref: Reference of the object the string is nested in.\n        :type location_ref: str\n        :param obj_name_prefix: Name prefix for the string object.\n        :type obj_name_prefix: str\n        \"\"\"\n        dataset = line.data\n        string_resources = dataset.strings.get_tables()\n\n        string_objs = []\n        for language, strings in string_resources.items():\n            if string_id in strings.keys():\n                string_name = f\"{obj_name_prefix}String\"\n                string_ref = f\"{location_ref}.{string_name}\"\n                string_raw_api_object = RawAPIObject(string_ref, string_name,\n                                                     dataset.nyan_api_objects)\n                string_raw_api_object.add_raw_parent(\"engine.util.language.LanguageTextPair\")\n                string_location = ForwardRef(line, location_ref)\n                string_raw_api_object.set_location(string_location)\n\n                # Language identifier\n                lang_ref = f\"util.language.{language}\"\n                lang_forward_ref = dataset.pregen_nyan_objects[lang_ref].get_nyan_object()\n                string_raw_api_object.add_raw_member(\"language\",\n                                                     lang_forward_ref,\n                                                     \"engine.util.language.LanguageTextPair\")\n\n                # String\n                string_raw_api_object.add_raw_member(\"string\",\n                                                     strings[string_id],\n                                                     \"engine.util.language.LanguageTextPair\")\n\n                line.add_raw_api_object(string_raw_api_object)\n                string_forward_ref = ForwardRef(line, string_ref)\n                string_objs.append(string_forward_ref)\n\n        return string_objs\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/auxiliary_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long,too-many-locals,too-many-branches,too-many-statements,no-else-return\n\n\"\"\"\nDerives complex auxiliary objects from unit lines, techs\nor other objects.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieBuildingLineGroup, GenieUnitLineGroup\nfrom ....entity_object.conversion.combined_sound import CombinedSound\nfrom ....entity_object.conversion.converter_object import ConverterObjectGroup, RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n\n\nclass AoCAuxiliarySubprocessor:\n    \"\"\"\n    Creates complexer auxiliary raw API objects for abilities in AoC.\n    \"\"\"\n\n    @staticmethod\n    def get_creatable_game_entity(line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Creates the CreatableGameEntity object for a unit/building line.\n\n        :param line: Unit/Building line.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.variants[0].line[0]\n\n        else:\n            current_unit = line.line[0]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        obj_ref = f\"{game_entity_name}.CreatableGameEntity\"\n        obj_name = f\"{game_entity_name}Creatable\"\n        creatable_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n        creatable_raw_api_object.add_raw_parent(\"engine.util.create.CreatableGameEntity\")\n\n        # Get train location of line\n        train_location_id = line.get_train_location_id()\n        if isinstance(line, GenieBuildingLineGroup):\n            train_location = dataset.unit_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        else:\n            train_location = dataset.building_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        # Location of the object depends on whether it'a a unique unit or a normal unit\n        if line.is_unique():\n            # Add object to the Civ object\n            enabling_research_id = line.get_enabling_research_id()\n            enabling_research = dataset.genie_techs[enabling_research_id]\n            enabling_civ_id = enabling_research[\"civilization_id\"].value\n\n            civ = dataset.civ_groups[enabling_civ_id]\n            civ_name = civ_lookup_dict[enabling_civ_id][0]\n\n            creatable_location = ForwardRef(civ, civ_name)\n\n        else:\n            # Add object to the train location's Create ability\n            creatable_location = ForwardRef(train_location,\n                                            f\"{train_location_name}.Create\")\n\n        creatable_raw_api_object.set_location(creatable_location)\n\n        # Game Entity\n        game_entity_forward_ref = ForwardRef(line, game_entity_name)\n        creatable_raw_api_object.add_raw_member(\"game_entity\",\n                                                game_entity_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # TODO: Variants\n        variants_set = []\n\n        creatable_raw_api_object.add_raw_member(\"variants\", variants_set,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Cost (construction)\n        cost_name = f\"{game_entity_name}.CreatableGameEntity.{game_entity_name}Cost\"\n        cost_raw_api_object = RawAPIObject(cost_name,\n                                           f\"{game_entity_name}Cost\",\n                                           dataset.nyan_api_objects)\n        cost_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n        creatable_forward_ref = ForwardRef(line, obj_ref)\n        cost_raw_api_object.set_location(creatable_forward_ref)\n\n        payment_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Advance\"]\n        cost_raw_api_object.add_raw_member(\"payment_mode\",\n                                           payment_mode,\n                                           \"engine.util.cost.Cost\")\n\n        if line.is_repairable():\n            # Cost (repair) for buildings\n            cost_repair_name = (f\"{game_entity_name}.CreatableGameEntity.\"\n                                f\"{game_entity_name}RepairCost\")\n            cost_repair_raw_api_object = RawAPIObject(cost_repair_name,\n                                                      f\"{game_entity_name}RepairCost\",\n                                                      dataset.nyan_api_objects)\n            cost_repair_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n            creatable_forward_ref = ForwardRef(line, obj_ref)\n            cost_repair_raw_api_object.set_location(creatable_forward_ref)\n\n            payment_repair_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Adaptive\"]\n            cost_repair_raw_api_object.add_raw_member(\"payment_mode\",\n                                                      payment_repair_mode,\n                                                      \"engine.util.cost.Cost\")\n            line.add_raw_api_object(cost_repair_raw_api_object)\n\n        cost_amounts = []\n        cost_repair_amounts = []\n        for resource_amount in current_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            resource = None\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n                resource_name = \"Wood\"\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                )\n                resource_name = \"Stone\"\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n                resource_name = \"Gold\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            amount = resource_amount[\"amount\"].value\n\n            cost_amount_name = f\"{cost_name}.{resource_name}Amount\"\n            cost_amount = RawAPIObject(cost_amount_name,\n                                       f\"{resource_name}Amount\",\n                                       dataset.nyan_api_objects)\n            cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            cost_forward_ref = ForwardRef(line, cost_name)\n            cost_amount.set_location(cost_forward_ref)\n\n            cost_amount.add_raw_member(\"type\",\n                                       resource,\n                                       \"engine.util.resource.ResourceAmount\")\n            cost_amount.add_raw_member(\"amount\",\n                                       amount,\n                                       \"engine.util.resource.ResourceAmount\")\n\n            cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n            cost_amounts.append(cost_amount_forward_ref)\n            line.add_raw_api_object(cost_amount)\n\n            if line.is_repairable():\n                # Cost for repairing = half of the construction cost\n                cost_amount_name = f\"{cost_repair_name}.{resource_name}Amount\"\n                cost_amount = RawAPIObject(cost_amount_name,\n                                           f\"{resource_name}Amount\",\n                                           dataset.nyan_api_objects)\n                cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n                cost_forward_ref = ForwardRef(line, cost_repair_name)\n                cost_amount.set_location(cost_forward_ref)\n\n                cost_amount.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n                cost_amount.add_raw_member(\"amount\",\n                                           amount / 2,\n                                           \"engine.util.resource.ResourceAmount\")\n\n                cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n                cost_repair_amounts.append(cost_amount_forward_ref)\n                line.add_raw_api_object(cost_amount)\n\n        cost_raw_api_object.add_raw_member(\"amount\",\n                                           cost_amounts,\n                                           \"engine.util.cost.type.ResourceCost\")\n\n        if line.is_repairable():\n            cost_repair_raw_api_object.add_raw_member(\"amount\",\n                                                      cost_repair_amounts,\n                                                      \"engine.util.cost.type.ResourceCost\")\n\n        cost_forward_ref = ForwardRef(line, cost_name)\n        creatable_raw_api_object.add_raw_member(\"cost\",\n                                                cost_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n        # Creation time\n        if isinstance(line, GenieUnitLineGroup):\n            creation_time = current_unit[\"creation_time\"].value\n\n        else:\n            # Buildings are created immediately\n            creation_time = 0\n\n        creatable_raw_api_object.add_raw_member(\"creation_time\",\n                                                creation_time,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Creation sound\n        creation_sound_id = current_unit[\"train_sound_id\"].value\n\n        # Create sound object\n        obj_name = f\"{game_entity_name}.CreatableGameEntity.Sound\"\n        sound_raw_api_object = RawAPIObject(obj_name, \"CreationSound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(line, obj_ref)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Search for the sound if it exists\n        creation_sounds = []\n        if creation_sound_id > -1:\n            # Creation sound should be civ agnostic\n            genie_sound = dataset.genie_sounds[creation_sound_id]\n            file_id = genie_sound.get_sounds(civ_id=-1)[0]\n\n            if file_id in dataset.combined_sounds:\n                creation_sound = dataset.combined_sounds[file_id]\n                creation_sound.add_reference(sound_raw_api_object)\n\n            else:\n                creation_sound = CombinedSound(creation_sound_id,\n                                               file_id,\n                                               f\"creation_sound_{creation_sound_id}\",\n                                               dataset)\n                dataset.combined_sounds.update({file_id: creation_sound})\n                creation_sound.add_reference(sound_raw_api_object)\n\n            creation_sounds.append(creation_sound)\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            creation_sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(line, obj_name)\n        creatable_raw_api_object.add_raw_member(\"creation_sounds\",\n                                                [sound_forward_ref],\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(sound_raw_api_object)\n\n        # Condition\n        unlock_conditions = []\n        enabling_research_id = line.get_enabling_research_id()\n        if enabling_research_id > -1:\n            unlock_conditions.extend(AoCAuxiliarySubprocessor.get_condition(line,\n                                                                            obj_ref,\n                                                                            enabling_research_id))\n\n        creatable_raw_api_object.add_raw_member(\"condition\",\n                                                unlock_conditions,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Placement modes\n        placement_modes = []\n        if isinstance(line, GenieBuildingLineGroup):\n            # Buildings are placed on the map\n            # Place mode\n            obj_name = f\"{game_entity_name}.CreatableGameEntity.Place\"\n            place_raw_api_object = RawAPIObject(obj_name,\n                                                \"Place\",\n                                                dataset.nyan_api_objects)\n            place_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.Place\")\n            place_location = ForwardRef(line,\n                                        f\"{game_entity_name}.CreatableGameEntity\")\n            place_raw_api_object.set_location(place_location)\n\n            # Tile snap distance (uses 1.0 for grid placement)\n            place_raw_api_object.add_raw_member(\"tile_snap_distance\",\n                                                1.0,\n                                                \"engine.util.placement_mode.type.Place\")\n            # Clearance size\n            clearance_size_x = current_unit[\"clearance_size_x\"].value\n            clearance_size_y = current_unit[\"clearance_size_y\"].value\n            place_raw_api_object.add_raw_member(\"clearance_size_x\",\n                                                clearance_size_x,\n                                                \"engine.util.placement_mode.type.Place\")\n            place_raw_api_object.add_raw_member(\"clearance_size_y\",\n                                                clearance_size_y,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Allow rotation\n            place_raw_api_object.add_raw_member(\"allow_rotation\",\n                                                True,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Max elevation difference\n            elevation_mode = current_unit[\"elevation_mode\"].value\n            if elevation_mode == 2:\n                max_elevation_difference = 0\n\n            elif elevation_mode == 3:\n                max_elevation_difference = 1\n\n            else:\n                max_elevation_difference = MemberSpecialValue.NYAN_INF\n\n            place_raw_api_object.add_raw_member(\"max_elevation_difference\",\n                                                max_elevation_difference,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            line.add_raw_api_object(place_raw_api_object)\n\n            place_forward_ref = ForwardRef(line, obj_name)\n            placement_modes.append(place_forward_ref)\n\n            if line.get_class_id() == 39:\n                # Gates\n                obj_name = f\"{game_entity_name}.CreatableGameEntity.Replace\"\n                replace_raw_api_object = RawAPIObject(obj_name,\n                                                      \"Replace\",\n                                                      dataset.nyan_api_objects)\n                replace_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.Replace\")\n                replace_location = ForwardRef(line,\n                                              f\"{game_entity_name}.CreatableGameEntity\")\n                replace_raw_api_object.set_location(replace_location)\n\n                # Game entities (only stone wall)\n                wall_line_id = 117\n                wall_line = dataset.building_lines[wall_line_id]\n                wall_name = name_lookup_dict[117][0]\n                game_entities = [ForwardRef(wall_line, wall_name)]\n                replace_raw_api_object.add_raw_member(\"game_entities\",\n                                                      game_entities,\n                                                      \"engine.util.placement_mode.type.Replace\")\n\n                line.add_raw_api_object(replace_raw_api_object)\n\n                replace_forward_ref = ForwardRef(line, obj_name)\n                placement_modes.append(replace_forward_ref)\n\n        else:\n            placement_modes.append(\n                dataset.nyan_api_objects[\"engine.util.placement_mode.type.Eject\"])\n\n            # OwnStorage mode\n            obj_name = f\"{game_entity_name}.CreatableGameEntity.OwnStorage\"\n            own_storage_raw_api_object = RawAPIObject(obj_name, \"OwnStorage\",\n                                                      dataset.nyan_api_objects)\n            own_storage_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.OwnStorage\")\n            own_storage_location = ForwardRef(line,\n                                              f\"{game_entity_name}.CreatableGameEntity\")\n            own_storage_raw_api_object.set_location(own_storage_location)\n\n            # Container\n            container_forward_ref = ForwardRef(train_location,\n                                               (f\"{train_location_name}.Storage.\"\n                                                f\"{train_location_name}Container\"))\n            own_storage_raw_api_object.add_raw_member(\"container\",\n                                                      container_forward_ref,\n                                                      \"engine.util.placement_mode.type.OwnStorage\")\n\n            line.add_raw_api_object(own_storage_raw_api_object)\n\n            own_storage_forward_ref = ForwardRef(line, obj_name)\n            placement_modes.append(own_storage_forward_ref)\n\n        creatable_raw_api_object.add_raw_member(\"placement_modes\",\n                                                placement_modes,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(creatable_raw_api_object)\n        line.add_raw_api_object(cost_raw_api_object)\n\n    @staticmethod\n    def get_researchable_tech(tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Creates the ResearchableTech object for a Tech.\n\n        :param tech_group: Tech group that is a technology.\n        :type tech_group: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        dataset = tech_group.data\n        research_location_id = tech_group.get_research_location_id()\n        research_location = dataset.building_lines[research_location_id]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        research_location_name = name_lookup_dict[research_location_id][0]\n        tech_name = tech_lookup_dict[tech_group.get_id()][0]\n\n        obj_ref = f\"{tech_name}.ResearchableTech\"\n        obj_name = f\"{tech_name}Researchable\"\n        researchable_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n        researchable_raw_api_object.add_raw_parent(\"engine.util.research.ResearchableTech\")\n\n        # Location of the object depends on whether it'a a unique tech or a normal tech\n        if tech_group.is_unique():\n            # Add object to the Civ object\n            civ_id = tech_group.get_civilization()\n            civ = dataset.civ_groups[civ_id]\n            civ_name = civ_lookup_dict[civ_id][0]\n\n            researchable_location = ForwardRef(civ, civ_name)\n\n        else:\n            # Add object to the research location's Research ability\n            researchable_location = ForwardRef(research_location,\n                                               f\"{research_location_name}.Research\")\n\n        researchable_raw_api_object.set_location(researchable_location)\n\n        # Tech\n        tech_forward_ref = ForwardRef(tech_group, tech_name)\n        researchable_raw_api_object.add_raw_member(\"tech\",\n                                                   tech_forward_ref,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        # Cost\n        cost_ref = f\"{tech_name}.ResearchableTech.{tech_name}Cost\"\n        cost_raw_api_object = RawAPIObject(cost_ref,\n                                           f\"{tech_name}Cost\",\n                                           dataset.nyan_api_objects)\n        cost_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n        tech_forward_ref = ForwardRef(tech_group, obj_ref)\n        cost_raw_api_object.set_location(tech_forward_ref)\n\n        payment_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Advance\"]\n        cost_raw_api_object.add_raw_member(\"payment_mode\",\n                                           payment_mode,\n                                           \"engine.util.cost.Cost\")\n\n        cost_amounts = []\n        for resource_amount in tech_group.tech[\"research_resource_costs\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n            resource = None\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n                resource_name = \"Wood\"\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                )\n                resource_name = \"Stone\"\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n                resource_name = \"Gold\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            amount = resource_amount[\"amount\"].value\n\n            cost_amount_ref = f\"{cost_ref}.{resource_name}Amount\"\n            cost_amount = RawAPIObject(cost_amount_ref,\n                                       f\"{resource_name}Amount\",\n                                       dataset.nyan_api_objects)\n            cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            cost_forward_ref = ForwardRef(tech_group, cost_ref)\n            cost_amount.set_location(cost_forward_ref)\n\n            cost_amount.add_raw_member(\"type\",\n                                       resource,\n                                       \"engine.util.resource.ResourceAmount\")\n            cost_amount.add_raw_member(\"amount\",\n                                       amount,\n                                       \"engine.util.resource.ResourceAmount\")\n\n            cost_amount_forward_ref = ForwardRef(tech_group, cost_amount_ref)\n            cost_amounts.append(cost_amount_forward_ref)\n            tech_group.add_raw_api_object(cost_amount)\n\n        cost_raw_api_object.add_raw_member(\"amount\",\n                                           cost_amounts,\n                                           \"engine.util.cost.type.ResourceCost\")\n\n        cost_forward_ref = ForwardRef(tech_group, cost_ref)\n        researchable_raw_api_object.add_raw_member(\"cost\",\n                                                   cost_forward_ref,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        research_time = tech_group.tech[\"research_time\"].value\n        researchable_raw_api_object.add_raw_member(\"research_time\",\n                                                   research_time,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        # Create sound object\n        sound_ref = f\"{tech_name}.ResearchableTech.Sound\"\n        sound_raw_api_object = RawAPIObject(sound_ref, \"ResearchSound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(tech_group,\n                                    f\"{tech_name}.ResearchableTech\")\n        sound_raw_api_object.set_location(sound_location)\n\n        # AoE doesn't support sounds here, so this is empty\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            [],\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(tech_group, sound_ref)\n        researchable_raw_api_object.add_raw_member(\"research_sounds\",\n                                                   [sound_forward_ref],\n                                                   \"engine.util.research.ResearchableTech\")\n\n        tech_group.add_raw_api_object(sound_raw_api_object)\n\n        # Condition\n        unlock_conditions = []\n        if tech_group.get_id() > -1:\n            unlock_conditions.extend(AoCAuxiliarySubprocessor.get_condition(tech_group,\n                                                                            obj_ref,\n                                                                            tech_group.get_id(),\n                                                                            top_level=True))\n\n        researchable_raw_api_object.add_raw_member(\"condition\",\n                                                   unlock_conditions,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        tech_group.add_raw_api_object(researchable_raw_api_object)\n        tech_group.add_raw_api_object(cost_raw_api_object)\n\n    @staticmethod\n    def get_condition(\n        converter_obj_group: ConverterObjectGroup,\n        obj_ref: str,\n        tech_id: int,\n        top_level: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the condition for a creatable or researchable from tech\n        by recursively searching the required techs.\n\n        :param converter_object: ConverterObjectGroup that the condition objects should be nested in.\n        :param obj_ref: Reference of converter_object inside the modpack.\n        :param tech_id: tech ID of a tech wth a conditional unlock.\n        :param top_level: True if the condition has subconditions, False otherwise.\n        \"\"\"\n        dataset = converter_obj_group.data\n        tech = dataset.genie_techs[tech_id]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        if not top_level and\\\n            (tech_id in dataset.initiated_techs.keys() or\n             (tech_id in dataset.tech_groups.keys() and\n              dataset.tech_groups[tech_id].is_researchable())):\n            # The tech condition is a building or a researchable tech\n            # and thus a literal.\n            if tech_id in dataset.initiated_techs.keys():\n                initiated_tech = dataset.initiated_techs[tech_id]\n                building_id = initiated_tech.get_building_id()\n                building_name = name_lookup_dict[building_id][0]\n                literal_name = f\"{building_name}Built\"\n                literal_parent = \"engine.util.logic.literal.type.GameEntityProgress\"\n\n            elif dataset.tech_groups[tech_id].is_researchable():\n                tech_name = tech_lookup_dict[tech_id][0]\n                literal_name = f\"{tech_name}Researched\"\n                literal_parent = \"engine.util.logic.literal.type.TechResearched\"\n\n            else:\n                raise ValueError(\"Required tech id {tech_id} is neither intiated nor researchable\")\n\n            literal_ref = f\"{obj_ref}.{literal_name}\"\n            literal_raw_api_object = RawAPIObject(literal_ref,\n                                                  literal_name,\n                                                  dataset.nyan_api_objects)\n            literal_raw_api_object.add_raw_parent(literal_parent)\n            literal_location = ForwardRef(converter_obj_group, obj_ref)\n            literal_raw_api_object.set_location(literal_location)\n\n            if tech_id in dataset.initiated_techs.keys():\n                building_line = dataset.unit_ref[building_id]\n                building_forward_ref = ForwardRef(building_line, building_name)\n\n                # Building\n                literal_raw_api_object.add_raw_member(\"game_entity\",\n                                                      building_forward_ref,\n                                                      literal_parent)\n\n                # Progress\n                # =======================================================================\n                progress_ref = f\"{literal_ref}.ProgressStatus\"\n                progress_raw_api_object = RawAPIObject(progress_ref,\n                                                       \"ProgressStatus\",\n                                                       dataset.nyan_api_objects)\n                progress_raw_api_object.add_raw_parent(\"engine.util.progress_status.ProgressStatus\")\n                progress_location = ForwardRef(converter_obj_group, literal_ref)\n                progress_raw_api_object.set_location(progress_location)\n\n                # Type\n                progress_type = dataset.nyan_api_objects[\"engine.util.progress_type.type.Construct\"]\n                progress_raw_api_object.add_raw_member(\"progress_type\",\n                                                       progress_type,\n                                                       \"engine.util.progress_status.ProgressStatus\")\n\n                # Progress (building must be 100% constructed)\n                progress_raw_api_object.add_raw_member(\"progress\",\n                                                       100,\n                                                       \"engine.util.progress_status.ProgressStatus\")\n\n                converter_obj_group.add_raw_api_object(progress_raw_api_object)\n                # =======================================================================\n                progress_forward_ref = ForwardRef(converter_obj_group, progress_ref)\n                literal_raw_api_object.add_raw_member(\"progress_status\",\n                                                      progress_forward_ref,\n                                                      literal_parent)\n\n            elif dataset.tech_groups[tech_id].is_researchable():\n                tech_group = dataset.tech_groups[tech_id]\n                tech_forward_ref = ForwardRef(tech_group, tech_name)\n                literal_raw_api_object.add_raw_member(\"tech\",\n                                                      tech_forward_ref,\n                                                      literal_parent)\n\n            # LiteralScope\n            # ==========================================================================\n            scope_ref = f\"{literal_ref}.LiteralScope\"\n            scope_raw_api_object = RawAPIObject(scope_ref,\n                                                \"LiteralScope\",\n                                                dataset.nyan_api_objects)\n            scope_raw_api_object.add_raw_parent(\"engine.util.logic.literal_scope.type.Any\")\n            scope_location = ForwardRef(converter_obj_group, literal_ref)\n            scope_raw_api_object.set_location(scope_location)\n\n            scope_diplomatic_stances = [\n                dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]\n            ]\n            scope_raw_api_object.add_raw_member(\"stances\",\n                                                scope_diplomatic_stances,\n                                                \"engine.util.logic.literal_scope.LiteralScope\")\n\n            converter_obj_group.add_raw_api_object(scope_raw_api_object)\n            # ==========================================================================\n            scope_forward_ref = ForwardRef(converter_obj_group, scope_ref)\n            literal_raw_api_object.add_raw_member(\"scope\",\n                                                  scope_forward_ref,\n                                                  \"engine.util.logic.literal.Literal\")\n\n            literal_raw_api_object.add_raw_member(\"only_once\",\n                                                  True,\n                                                  \"engine.util.logic.LogicElement\")\n\n            converter_obj_group.add_raw_api_object(literal_raw_api_object)\n            literal_forward_ref = ForwardRef(converter_obj_group, literal_ref)\n\n            return [literal_forward_ref]\n\n        else:\n            # The tech condition has other requirements that need to be resolved\n\n            # Find required techs for the current tech\n            assoc_tech_id_members = []\n            assoc_tech_id_members.extend(tech[\"required_techs\"].value)\n            required_tech_count = tech[\"required_tech_count\"].value\n\n            # Remove tech ids that are invalid or those we don't use\n            relevant_ids = []\n            for tech_id_member in assoc_tech_id_members:\n                required_tech_id = tech_id_member.value\n                if required_tech_id == -1:\n                    continue\n\n                if required_tech_id == 104:\n                    # Skip Dark Age tech\n                    required_tech_count -= 1\n                    continue\n\n                if required_tech_id in dataset.civ_boni.keys():\n                    continue\n\n                relevant_ids.append(required_tech_id)\n\n            if len(relevant_ids) == 0:\n                return []\n\n            if len(relevant_ids) == 1:\n                # If there's only one required tech we don't need a gate\n                # we can just return the logic element of the only required tech\n                required_tech_id = relevant_ids[0]\n                return AoCAuxiliarySubprocessor.get_condition(converter_obj_group,\n                                                              obj_ref,\n                                                              required_tech_id)\n\n            gate_ref = f\"{obj_ref}.UnlockCondition\"\n            gate_raw_api_object = RawAPIObject(gate_ref,\n                                               \"UnlockCondition\",\n                                               dataset.nyan_api_objects)\n\n            if required_tech_count == len(relevant_ids):\n                gate_raw_api_object.add_raw_parent(\"engine.util.logic.gate.type.AND\")\n                gate_location = ForwardRef(converter_obj_group, obj_ref)\n\n            else:\n                gate_raw_api_object.add_raw_parent(\"engine.util.logic.gate.type.SUBSETMIN\")\n                gate_location = ForwardRef(converter_obj_group, obj_ref)\n\n                gate_raw_api_object.add_raw_member(\"size\",\n                                                   required_tech_count,\n                                                   \"engine.util.logic.gate.type.SUBSETMIN\")\n\n            gate_raw_api_object.set_location(gate_location)\n\n            # Once unlocked, a creatable/researchable is unlocked forever\n            gate_raw_api_object.add_raw_member(\"only_once\",\n                                               True,\n                                               \"engine.util.logic.LogicElement\")\n\n            # Get requirements from subtech recursively\n            inputs = []\n            for required_tech_id in relevant_ids:\n                required = AoCAuxiliarySubprocessor.get_condition(converter_obj_group,\n                                                                  gate_ref,\n                                                                  required_tech_id)\n                inputs.extend(required)\n\n            gate_raw_api_object.add_raw_member(\"inputs\",\n                                               inputs,\n                                               \"engine.util.logic.gate.LogicGate\")\n\n            converter_obj_group.add_raw_api_object(gate_raw_api_object)\n            gate_forward_ref = ForwardRef(converter_obj_group, gate_ref)\n            return [gate_forward_ref]\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/civ_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,too-many-branches\n\n\"\"\"\nCreates patches and modifiers for civs.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup\nfrom ....entity_object.conversion.combined_sprite import CombinedSprite\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom .tech_subprocessor import AoCTechSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass AoCCivSubprocessor:\n    \"\"\"\n    Creates raw API objects for civs in AoC.\n    \"\"\"\n\n    @classmethod\n    def get_civ_setup(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for the civ setup which configures architecture sets\n        unique units, unique techs, team boni and unique stat upgrades.\n        \"\"\"\n        patches = []\n\n        patches.extend(cls.setup_unique_units(civ_group))\n        patches.extend(cls.setup_unique_techs(civ_group))\n        patches.extend(cls.setup_tech_tree(civ_group))\n        patches.extend(cls.setup_civ_bonus(civ_group))\n\n        if len(civ_group.get_team_bonus_effects()) > 0:\n            patches.extend(AoCTechSubprocessor.get_patches(civ_group.team_bonus))\n\n        return patches\n\n    @ classmethod\n    def get_modifiers(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns global modifiers of a civ.\n        \"\"\"\n        modifiers = []\n\n        for civ_bonus in civ_group.civ_boni.values():\n            if civ_bonus.replaces_researchable_tech():\n                # TODO: instant tech research modifier\n                pass\n\n        return modifiers\n\n    @ staticmethod\n    def get_starting_resources(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the starting resources of a civ.\n        \"\"\"\n        resource_amounts = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        # Find starting resource amounts\n        food_amount = civ_group.civ[\"resources\"][91].value\n        wood_amount = civ_group.civ[\"resources\"][92].value\n        gold_amount = civ_group.civ[\"resources\"][93].value\n        stone_amount = civ_group.civ[\"resources\"][94].value\n\n        # Find civ unique starting resources\n        tech_tree = civ_group.get_tech_tree_effects()\n        for effect in tech_tree:\n            type_id = effect.get_type()\n\n            if type_id != 1:\n                continue\n\n            resource_id = effect[\"attr_a\"].value\n            amount = effect[\"attr_d\"].value\n            if resource_id == 91:\n                food_amount += amount\n\n            elif resource_id == 92:\n                wood_amount += amount\n\n            elif resource_id == 93:\n                gold_amount += amount\n\n            elif resource_id == 94:\n                stone_amount += amount\n\n        food_ref = f\"{civ_name}.FoodStartingAmount\"\n        food_raw_api_object = RawAPIObject(food_ref, \"FoodStartingAmount\",\n                                           dataset.nyan_api_objects)\n        food_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        food_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n        food_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_raw_api_object.add_raw_member(\"amount\",\n                                           food_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_forward_ref = ForwardRef(civ_group, food_ref)\n        resource_amounts.append(food_forward_ref)\n\n        wood_ref = f\"{civ_name}.WoodStartingAmount\"\n        wood_raw_api_object = RawAPIObject(wood_ref, \"WoodStartingAmount\",\n                                           dataset.nyan_api_objects)\n        wood_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        wood_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n        wood_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        wood_raw_api_object.add_raw_member(\"amount\",\n                                           wood_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        wood_forward_ref = ForwardRef(civ_group, wood_ref)\n        resource_amounts.append(wood_forward_ref)\n\n        gold_ref = f\"{civ_name}.GoldStartingAmount\"\n        gold_raw_api_object = RawAPIObject(gold_ref, \"GoldStartingAmount\",\n                                           dataset.nyan_api_objects)\n        gold_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        gold_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n        gold_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        gold_raw_api_object.add_raw_member(\"amount\",\n                                           gold_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        gold_forward_ref = ForwardRef(civ_group, gold_ref)\n        resource_amounts.append(gold_forward_ref)\n\n        stone_ref = f\"{civ_name}.StoneStartingAmount\"\n        stone_raw_api_object = RawAPIObject(stone_ref, \"StoneStartingAmount\",\n                                            dataset.nyan_api_objects)\n        stone_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        stone_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object()\n        stone_raw_api_object.add_raw_member(\"type\",\n                                            resource,\n                                            \"engine.util.resource.ResourceAmount\")\n\n        stone_raw_api_object.add_raw_member(\"amount\",\n                                            stone_amount,\n                                            \"engine.util.resource.ResourceAmount\")\n\n        stone_forward_ref = ForwardRef(civ_group, stone_ref)\n        resource_amounts.append(stone_forward_ref)\n\n        civ_group.add_raw_api_object(food_raw_api_object)\n        civ_group.add_raw_api_object(wood_raw_api_object)\n        civ_group.add_raw_api_object(gold_raw_api_object)\n        civ_group.add_raw_api_object(stone_raw_api_object)\n\n        return resource_amounts\n\n    @ classmethod\n    def setup_civ_bonus(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns global modifiers of a civ.\n        \"\"\"\n        patches = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        # key: tech_id; value patched in patches\n        tech_patches = {}\n\n        for civ_bonus in civ_group.civ_boni.values():\n            if not civ_bonus.replaces_researchable_tech():\n                bonus_patches = AoCTechSubprocessor.get_patches(civ_bonus)\n\n                # civ boni might be unlocked by age ups. if so, patch them into the age up\n                # patches are queued here\n                required_tech_count = civ_bonus.tech[\"required_tech_count\"].value\n                if required_tech_count > 0 and len(bonus_patches) > 0:\n                    if required_tech_count == 1:\n                        tech_id = civ_bonus.tech[\"required_techs\"][0].value\n\n                    elif required_tech_count == 2:\n                        tech_id = civ_bonus.tech[\"required_techs\"][1].value\n\n                    if tech_id == 104:\n                        # Skip Dark Age; it is not a tech in openage\n                        patches.extend(bonus_patches)\n\n                    elif tech_id in tech_patches:\n                        tech_patches[tech_id].extend(bonus_patches)\n\n                    else:\n                        tech_patches[tech_id] = bonus_patches\n\n                else:\n                    patches.extend(bonus_patches)\n\n        for tech_id, patches in tech_patches.items():\n            tech_group = dataset.tech_groups[tech_id]\n            tech_name = tech_lookup_dict[tech_id][0]\n\n            patch_target_ref = f\"{tech_name}\"\n            patch_target_forward_ref = ForwardRef(tech_group, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"{tech_name}CivBonusWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"{tech_name}CivBonus\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"updates\",\n                                                           patches,\n                                                           \"engine.util.tech.Tech\",\n                                                           MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @ staticmethod\n    def setup_unique_units(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Patches the unique units into their train location.\n        \"\"\"\n        patches = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        for unique_line in civ_group.unique_entities.values():\n            head_unit_id = unique_line.get_head_unit_id()\n            game_entity_name = name_lookup_dict[head_unit_id][0]\n\n            # Get train location of line\n            train_location_id = unique_line.get_train_location_id()\n            if isinstance(unique_line, GenieBuildingLineGroup):\n                train_location = dataset.unit_lines[train_location_id]\n                train_location_name = name_lookup_dict[train_location_id][0]\n\n            else:\n                train_location = dataset.building_lines[train_location_id]\n                train_location_name = name_lookup_dict[train_location_id][0]\n\n            patch_target_ref = f\"{train_location_name}.Create\"\n            patch_target_forward_ref = ForwardRef(train_location, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Add{game_entity_name}CreatableWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Add{game_entity_name}Creatable\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            # Add creatable\n            creatable_ref = f\"{game_entity_name}.CreatableGameEntity\"\n            creatable_forward_ref = ForwardRef(unique_line, creatable_ref)\n            nyan_patch_raw_api_object.add_raw_patch_member(\"creatables\",\n                                                           [creatable_forward_ref],\n                                                           \"engine.ability.type.Create\",\n                                                           MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @ staticmethod\n    def setup_unique_techs(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Patches the unique techs into their research location.\n        \"\"\"\n        patches = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        for unique_tech in civ_group.unique_techs.values():\n            tech_id = unique_tech.get_id()\n            tech_name = tech_lookup_dict[tech_id][0]\n\n            # Get train location of line\n            research_location_id = unique_tech.get_research_location_id()\n            research_location = dataset.building_lines[research_location_id]\n            research_location_name = name_lookup_dict[research_location_id][0]\n\n            patch_target_ref = f\"{research_location_name}.Research\"\n            patch_target_forward_ref = ForwardRef(research_location, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Add{tech_name}ResearchableWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Add{tech_name}Researchable\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            # Add creatable\n            researchable_ref = f\"{tech_name}.ResearchableTech\"\n            researchable_forward_ref = ForwardRef(unique_tech, researchable_ref)\n            nyan_patch_raw_api_object.add_raw_patch_member(\"researchables\",\n                                                           [researchable_forward_ref],\n                                                           \"engine.ability.type.Research\",\n                                                           MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @ staticmethod\n    def setup_tech_tree(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Patches standard techs and units out of Research and Create.\n        \"\"\"\n        patches = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        disabled_techs = {}\n        disabled_entities = {}\n\n        tech_tree = civ_group.get_tech_tree_effects()\n        for effect in tech_tree:\n            type_id = effect.get_type()\n\n            if type_id == 101:\n                patches.extend(AoCTechSubprocessor.tech_cost_modify_effect(civ_group, effect))\n                continue\n\n            if type_id == 103:\n                patches.extend(AoCTechSubprocessor.tech_time_modify_effect(civ_group, effect))\n                continue\n\n            if type_id != 102:\n                continue\n\n            # Get tech id\n            tech_id = int(effect[\"attr_d\"].value)\n\n            # Check what the purpose of the tech is\n            if tech_id in dataset.unit_unlocks.keys():\n                unlock_tech = dataset.unit_unlocks[tech_id]\n                unlocked_line = unlock_tech.get_unlocked_line()\n                train_location_id = unlocked_line.get_train_location_id()\n\n                if isinstance(unlocked_line, GenieBuildingLineGroup):\n                    train_location = dataset.unit_lines[train_location_id]\n\n                else:\n                    train_location = dataset.building_lines[train_location_id]\n\n                if train_location in disabled_entities:\n                    disabled_entities[train_location].append(unlocked_line)\n\n                else:\n                    disabled_entities[train_location] = [unlocked_line]\n\n            elif tech_id in dataset.civ_boni.keys():\n                # Disables civ boni of other civs\n                continue\n\n            elif tech_id in dataset.tech_groups.keys():\n                tech_group = dataset.tech_groups[tech_id]\n                if tech_group.is_researchable():\n                    research_location_id = tech_group.get_research_location_id()\n                    research_location = dataset.building_lines[research_location_id]\n\n                    if research_location in disabled_techs:\n                        disabled_techs[research_location].append(tech_group)\n\n                    else:\n                        disabled_techs[research_location] = [tech_group]\n\n            else:\n                continue\n\n        for train_location, entities in disabled_entities.items():\n            train_location_id = train_location.get_head_unit_id()\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n            patch_target_ref = f\"{train_location_name}.Create\"\n            patch_target_forward_ref = ForwardRef(train_location, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Disable{train_location_name}CreatablesWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Disable{train_location_name}Creatables\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            entities_forward_refs = []\n            for entity in entities:\n                entity_id = entity.get_head_unit_id()\n                game_entity_name = name_lookup_dict[entity_id][0]\n\n                disabled_ref = f\"{game_entity_name}.CreatableGameEntity\"\n                disabled_forward_ref = ForwardRef(entity, disabled_ref)\n                entities_forward_refs.append(disabled_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"creatables\",\n                                                           entities_forward_refs,\n                                                           \"engine.ability.type.Create\",\n                                                           MemberOperator.SUBTRACT)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        for research_location, techs in disabled_techs.items():\n            research_location_id = research_location.get_head_unit_id()\n            research_location_name = name_lookup_dict[research_location_id][0]\n\n            patch_target_ref = f\"{research_location_name}.Research\"\n            patch_target_forward_ref = ForwardRef(research_location, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Disable{research_location_name}ResearchablesWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Disable{research_location_name}Researchables\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            entities_forward_refs = []\n            for tech_group in techs:\n                tech_id = tech_group.get_id()\n                tech_name = tech_lookup_dict[tech_id][0]\n\n                disabled_ref = f\"{tech_name}.ResearchableTech\"\n                disabled_forward_ref = ForwardRef(tech_group, disabled_ref)\n                entities_forward_refs.append(disabled_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"researchables\",\n                                                           entities_forward_refs,\n                                                           \"engine.ability.type.Research\",\n                                                           MemberOperator.SUBTRACT)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @ staticmethod\n    def create_animation(\n        line: GenieGameEntityGroup,\n        animation_id: int,\n        nyan_patch_ref: str,\n        animation_name: str,\n        filename_prefix: str\n    ) -> ForwardRef:\n        \"\"\"\n        Generates an animation for an ability.\n        \"\"\"\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        animation_ref = f\"{nyan_patch_ref}.{animation_name}Animation\"\n        animation_obj_name = f\"{animation_name}Animation\"\n        animation_raw_api_object = RawAPIObject(animation_ref, animation_obj_name,\n                                                dataset.nyan_api_objects)\n        animation_raw_api_object.add_raw_parent(\"engine.util.graphics.Animation\")\n        animation_location = ForwardRef(line, nyan_patch_ref)\n        animation_raw_api_object.set_location(animation_location)\n\n        if animation_id in dataset.combined_sprites.keys():\n            animation_sprite = dataset.combined_sprites[animation_id]\n\n        else:\n            animation_filename = f\"{filename_prefix}{name_lookup_dict[line.get_head_unit_id()][1]}\"\n            animation_sprite = CombinedSprite(animation_id,\n                                              animation_filename,\n                                              dataset)\n            dataset.combined_sprites.update({animation_sprite.get_id(): animation_sprite})\n\n        animation_sprite.add_reference(animation_raw_api_object)\n\n        animation_raw_api_object.add_raw_member(\"sprite\", animation_sprite,\n                                                \"engine.util.graphics.Animation\")\n\n        line.add_raw_api_object(animation_raw_api_object)\n\n        animation_forward_ref = ForwardRef(line, animation_ref)\n\n        return animation_forward_ref\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/effect_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,invalid-name\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates effects and resistances for the Apply*Effect and Resistance\nabilities.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n    GenieBuildingLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass AoCEffectSubprocessor:\n    \"\"\"\n    Creates raw API objects for attacks/resistances in AoC.\n    \"\"\"\n\n    @staticmethod\n    def get_attack_effects(\n        line: GenieGameEntityGroup,\n        location_ref: str,\n        projectile: int = -1\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates effects that are used for attacking (unit command: 7)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param location_ref: Reference to API object the effects are added to.\n        :type location_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        dataset = line.data\n\n        if projectile != 1:\n            current_unit = line.get_head_unit()\n\n        else:\n            projectile_id = line.get_head_unit()[\"projectile_id1\"].value\n            current_unit = dataset.genie_units[projectile_id]\n\n        effects = []\n\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n\n        # FlatAttributeChangeDecrease\n        effect_parent = \"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"\n        attack_parent = \"engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n\n        attacks = current_unit[\"attacks\"].value\n\n        for attack in attacks.values():\n            armor_class = attack[\"type_id\"].value\n            attack_amount = attack[\"amount\"].value\n            class_name = armor_lookup_dict[armor_class]\n\n            attack_ref = f\"{location_ref}.{class_name}\"\n            attack_raw_api_object = RawAPIObject(attack_ref,\n                                                 class_name,\n                                                 dataset.nyan_api_objects)\n            attack_raw_api_object.add_raw_parent(attack_parent)\n            attack_location = ForwardRef(line, location_ref)\n            attack_raw_api_object.set_location(attack_location)\n\n            # Type\n            type_ref = f\"util.attribute_change_type.types.{class_name}\"\n            change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n            attack_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 effect_parent)\n\n            # Min value (optional)\n            min_value = dataset.pregen_nyan_objects[(\"effect.discrete.flat_attribute_change.\"\n                                                     \"min_damage.AoE2MinChangeAmount\")].get_nyan_object()\n            attack_raw_api_object.add_raw_member(\"min_change_value\",\n                                                 min_value,\n                                                 effect_parent)\n\n            # Max value (optional; not added because there is none in AoE2)\n\n            # Change value\n            # =================================================================================\n            amount_name = f\"{location_ref}.{class_name}.ChangeAmount\"\n            amount_raw_api_object = RawAPIObject(\n                amount_name, \"ChangeAmount\", dataset.nyan_api_objects)\n            amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n            amount_location = ForwardRef(line, attack_ref)\n            amount_raw_api_object.set_location(amount_location)\n\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            amount_raw_api_object.add_raw_member(\"type\",\n                                                 attribute,\n                                                 \"engine.util.attribute.AttributeAmount\")\n            amount_raw_api_object.add_raw_member(\"amount\",\n                                                 attack_amount,\n                                                 \"engine.util.attribute.AttributeAmount\")\n\n            line.add_raw_api_object(amount_raw_api_object)\n            # =================================================================================\n            amount_forward_ref = ForwardRef(line, amount_name)\n            attack_raw_api_object.add_raw_member(\"change_value\",\n                                                 amount_forward_ref,\n                                                 effect_parent)\n\n            # Ignore protection\n            attack_raw_api_object.add_raw_member(\"ignore_protection\",\n                                                 [],\n                                                 effect_parent)\n\n            line.add_raw_api_object(attack_raw_api_object)\n            attack_forward_ref = ForwardRef(line, attack_ref)\n            effects.append(attack_forward_ref)\n\n        # Fallback effect\n        fallback_effect = dataset.pregen_nyan_objects[(\"effect.discrete.flat_attribute_change.\"\n                                                       \"fallback.AoE2AttackFallback\")].get_nyan_object()\n        effects.append(fallback_effect)\n\n        return effects\n\n    @staticmethod\n    def get_convert_effects(\n        line: GenieGameEntityGroup,\n        location_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates effects that are used for conversion (unit command: 104)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param location_ref: Reference to API object the effects are added to.\n        :type location_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        current_unit = line.get_head_unit()\n        dataset = line.data\n\n        effects = []\n\n        effect_parent = \"engine.effect.discrete.convert.Convert\"\n        convert_parent = \"engine.effect.discrete.convert.type.AoE2Convert\"\n\n        unit_commands = current_unit[\"unit_commands\"].value\n        for command in unit_commands:\n            # Find the Heal command.\n            type_id = command[\"type\"].value\n\n            if type_id == 104:\n                skip_guaranteed_rounds = -1 * command[\"work_value1\"].value\n                skip_protected_rounds = -1 * command[\"work_value2\"].value\n                break\n\n        else:\n            # Return the empty set\n            return effects\n\n        # Unit conversion\n        convert_ref = f\"{location_ref}.ConvertUnitEffect\"\n        convert_raw_api_object = RawAPIObject(convert_ref,\n                                              \"ConvertUnitEffect\",\n                                              dataset.nyan_api_objects)\n        convert_raw_api_object.add_raw_parent(convert_parent)\n        convert_location = ForwardRef(line, location_ref)\n        convert_raw_api_object.set_location(convert_location)\n\n        # Type\n        type_ref = \"util.convert_type.types.UnitConvert\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        convert_raw_api_object.add_raw_member(\"type\",\n                                              change_type,\n                                              effect_parent)\n\n        # Min success (optional; not added because there is none in AoE2)\n        # Max success (optional; not added because there is none in AoE2)\n\n        # Chance\n        # hardcoded resource\n        chance_success = dataset.genie_civs[0][\"resources\"][182].value / 100\n        convert_raw_api_object.add_raw_member(\"chance_success\",\n                                              chance_success,\n                                              effect_parent)\n\n        # Fail cost (optional; not added because there is none in AoE2)\n\n        # Guaranteed rounds skip\n        convert_raw_api_object.add_raw_member(\"skip_guaranteed_rounds\",\n                                              skip_guaranteed_rounds,\n                                              convert_parent)\n\n        # Protected rounds skip\n        convert_raw_api_object.add_raw_member(\"skip_protected_rounds\",\n                                              skip_protected_rounds,\n                                              convert_parent)\n\n        line.add_raw_api_object(convert_raw_api_object)\n        attack_forward_ref = ForwardRef(line, convert_ref)\n        effects.append(attack_forward_ref)\n\n        # Building conversion\n        convert_ref = f\"{location_ref}.ConvertBuildingEffect\"\n        convert_raw_api_object = RawAPIObject(convert_ref,\n                                              \"ConvertBuildingUnitEffect\",\n                                              dataset.nyan_api_objects)\n        convert_raw_api_object.add_raw_parent(convert_parent)\n        convert_location = ForwardRef(line, location_ref)\n        convert_raw_api_object.set_location(convert_location)\n\n        # Type\n        type_ref = \"util.convert_type.types.BuildingConvert\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        convert_raw_api_object.add_raw_member(\"type\",\n                                              change_type,\n                                              effect_parent)\n\n        # Min success (optional; not added because there is none in AoE2)\n        # Max success (optional; not added because there is none in AoE2)\n\n        # Chance\n        # hardcoded resource\n        chance_success = dataset.genie_civs[0][\"resources\"][182].value / 100\n        convert_raw_api_object.add_raw_member(\"chance_success\",\n                                              chance_success,\n                                              effect_parent)\n\n        # Fail cost (optional; not added because there is none in AoE2)\n\n        # Guaranteed rounds skip\n        convert_raw_api_object.add_raw_member(\"skip_guaranteed_rounds\",\n                                              0,\n                                              convert_parent)\n\n        # Protected rounds skip\n        convert_raw_api_object.add_raw_member(\"skip_protected_rounds\",\n                                              0,\n                                              convert_parent)\n\n        line.add_raw_api_object(convert_raw_api_object)\n        attack_forward_ref = ForwardRef(line, convert_ref)\n        effects.append(attack_forward_ref)\n\n        return effects\n\n    @staticmethod\n    def get_heal_effects(\n        line: GenieGameEntityGroup,\n        location_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates effects that are used for healing (unit command: 105)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param location_ref: Reference to API object the effects are added to.\n        :type location_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        current_unit = line.get_head_unit()\n        dataset = line.data\n\n        effects = []\n\n        effect_parent = \"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"\n        heal_parent = \"engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n\n        unit_commands = current_unit[\"unit_commands\"].value\n        heal_command = None\n\n        for command in unit_commands:\n            # Find the Heal command.\n            type_id = command[\"type\"].value\n\n            if type_id == 105:\n                heal_command = command\n                break\n\n        else:\n            # Return the empty set\n            return effects\n\n        heal_rate = heal_command[\"work_value1\"].value\n\n        heal_ref = f\"{location_ref}.HealEffect\"\n        heal_raw_api_object = RawAPIObject(heal_ref,\n                                           \"HealEffect\",\n                                           dataset.nyan_api_objects)\n        heal_raw_api_object.add_raw_parent(heal_parent)\n        heal_location = ForwardRef(line, location_ref)\n        heal_raw_api_object.set_location(heal_location)\n\n        # Type\n        type_ref = \"util.attribute_change_type.types.Heal\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        heal_raw_api_object.add_raw_member(\"type\",\n                                           change_type,\n                                           effect_parent)\n\n        # Min value (optional)\n        min_value = dataset.pregen_nyan_objects[(\"effect.discrete.flat_attribute_change.\"\n                                                 \"min_heal.AoE2MinChangeAmount\")].get_nyan_object()\n        heal_raw_api_object.add_raw_member(\"min_change_rate\",\n                                           min_value,\n                                           effect_parent)\n\n        # Max value (optional; not added because there is none in AoE2)\n\n        # Change rate\n        # =================================================================================\n        rate_name = f\"{location_ref}.HealEffect.ChangeRate\"\n        rate_raw_api_object = RawAPIObject(rate_name, \"ChangeRate\", dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, heal_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           heal_rate,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # =================================================================================\n        rate_forward_ref = ForwardRef(line, rate_name)\n        heal_raw_api_object.add_raw_member(\"change_rate\",\n                                           rate_forward_ref,\n                                           effect_parent)\n\n        # Ignore protection\n        heal_raw_api_object.add_raw_member(\"ignore_protection\",\n                                           [],\n                                           effect_parent)\n\n        line.add_raw_api_object(heal_raw_api_object)\n        heal_forward_ref = ForwardRef(line, heal_ref)\n        effects.append(heal_forward_ref)\n\n        return effects\n\n    @staticmethod\n    def get_repair_effects(\n        line: GenieGameEntityGroup,\n        location_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates effects that are used for repairing (unit command: 106)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param location_ref: Reference to API object the effects are added to.\n        :type location_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        effects = []\n\n        effect_parent = \"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"\n        repair_parent = \"engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n\n        repairable_lines = []\n        repairable_lines.extend(dataset.building_lines.values())\n        for unit_line in dataset.unit_lines.values():\n            if unit_line.is_repairable():\n                repairable_lines.append(unit_line)\n\n        for repairable_line in repairable_lines:\n            game_entity_name = name_lookup_dict[repairable_line.get_head_unit_id()][0]\n\n            repair_name = f\"{game_entity_name}RepairEffect\"\n            repair_ref = f\"{location_ref}.{repair_name}\"\n            repair_raw_api_object = RawAPIObject(repair_ref,\n                                                 repair_name,\n                                                 dataset.nyan_api_objects)\n            repair_raw_api_object.add_raw_parent(repair_parent)\n            repair_location = ForwardRef(line, location_ref)\n            repair_raw_api_object.set_location(repair_location)\n\n            line.add_raw_api_object(repair_raw_api_object)\n\n            # Type\n            type_ref = f\"util.attribute_change_type.types.{game_entity_name}Repair\"\n            change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n            repair_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 effect_parent)\n\n            # Min value (optional; not added because buildings don't block repairing)\n\n            # Max value (optional; not added because there is none in AoE2)\n\n            # Change rate\n            # =================================================================================\n            rate_name = f\"{location_ref}.{repair_name}.ChangeRate\"\n            rate_raw_api_object = RawAPIObject(rate_name, \"ChangeRate\", dataset.nyan_api_objects)\n            rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n            rate_location = ForwardRef(line, repair_ref)\n            rate_raw_api_object.set_location(rate_location)\n\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            rate_raw_api_object.add_raw_member(\"type\",\n                                               attribute,\n                                               \"engine.util.attribute.AttributeRate\")\n\n            # Hardcoded repair rate:\n            # - Buildings: 750 HP/min = 12.5 HP/s\n            # - Ships/Siege: 187.5 HP/min = 3.125 HP/s\n            if isinstance(repairable_line, GenieBuildingLineGroup):\n                repair_rate = 12.5\n\n            else:\n                repair_rate = 3.125\n\n            rate_raw_api_object.add_raw_member(\"rate\",\n                                               repair_rate,\n                                               \"engine.util.attribute.AttributeRate\")\n\n            line.add_raw_api_object(rate_raw_api_object)\n            # =================================================================================\n            rate_forward_ref = ForwardRef(line, rate_name)\n            repair_raw_api_object.add_raw_member(\"change_rate\",\n                                                 rate_forward_ref,\n                                                 effect_parent)\n\n            # Ignore protection\n            repair_raw_api_object.add_raw_member(\"ignore_protection\",\n                                                 [],\n                                                 effect_parent)\n\n            # Repair cost\n            property_ref = f\"{repair_ref}.Cost\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Cost\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.effect.property.type.Cost\")\n            property_location = ForwardRef(line, repair_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            cost_ref = f\"{game_entity_name}.CreatableGameEntity.{game_entity_name}RepairCost\"\n            cost_forward_ref = ForwardRef(repairable_line, cost_ref)\n            property_raw_api_object.add_raw_member(\"cost\",\n                                                   cost_forward_ref,\n                                                   \"engine.effect.property.type.Cost\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties = {\n                api_objects[\"engine.effect.property.type.Cost\"]: property_forward_ref\n            }\n\n            repair_raw_api_object.add_raw_member(\"properties\",\n                                                 properties,\n                                                 \"engine.effect.Effect\")\n\n            repair_forward_ref = ForwardRef(line, repair_ref)\n            effects.append(repair_forward_ref)\n\n        return effects\n\n    @staticmethod\n    def get_construct_effects(\n        line: GenieGameEntityGroup,\n        location_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates effects that are used for construction (unit command: 101)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param location_ref: Reference to API object the effects are added to.\n        :type location_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        effects = []\n\n        progress_effect_parent = \"engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\"\n        progress_construct_parent = \"engine.effect.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\"\n        attr_effect_parent = \"engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\"\n        attr_construct_parent = \"engine.effect.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\"\n\n        constructable_lines = []\n        constructable_lines.extend(dataset.building_lines.values())\n\n        for constructable_line in constructable_lines:\n            game_entity_name = name_lookup_dict[constructable_line.get_head_unit_id()][0]\n\n            # Construction progress\n            contruct_progress_name = f\"{game_entity_name}ConstructProgressEffect\"\n            contruct_progress_ref = f\"{location_ref}.{contruct_progress_name}\"\n            contruct_progress_raw_api_object = RawAPIObject(contruct_progress_ref,\n                                                            contruct_progress_name,\n                                                            dataset.nyan_api_objects)\n            contruct_progress_raw_api_object.add_raw_parent(progress_construct_parent)\n            contruct_progress_location = ForwardRef(line, location_ref)\n            contruct_progress_raw_api_object.set_location(contruct_progress_location)\n\n            # Type\n            type_ref = f\"util.construct_type.types.{game_entity_name}Construct\"\n            change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n            contruct_progress_raw_api_object.add_raw_member(\"type\",\n                                                            change_type,\n                                                            progress_effect_parent)\n\n            # Total change time\n            change_time = constructable_line.get_head_unit()[\"creation_time\"].value\n            contruct_progress_raw_api_object.add_raw_member(\"total_change_time\",\n                                                            change_time,\n                                                            progress_effect_parent)\n\n            line.add_raw_api_object(contruct_progress_raw_api_object)\n            contruct_progress_forward_ref = ForwardRef(line, contruct_progress_ref)\n            effects.append(contruct_progress_forward_ref)\n\n            # HP increase during construction\n            contruct_hp_name = f\"{game_entity_name}ConstructHPEffect\"\n            contruct_hp_ref = f\"{location_ref}.{contruct_hp_name}\"\n            contruct_hp_raw_api_object = RawAPIObject(contruct_hp_ref,\n                                                      contruct_hp_name,\n                                                      dataset.nyan_api_objects)\n            contruct_hp_raw_api_object.add_raw_parent(attr_construct_parent)\n            contruct_hp_location = ForwardRef(line, location_ref)\n            contruct_hp_raw_api_object.set_location(contruct_hp_location)\n\n            # Type\n            type_ref = f\"util.attribute_change_type.types.{game_entity_name}Construct\"\n            change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n            contruct_hp_raw_api_object.add_raw_member(\"type\",\n                                                      change_type,\n                                                      attr_effect_parent)\n\n            # Total change time\n            change_time = constructable_line.get_head_unit()[\"creation_time\"].value\n            contruct_hp_raw_api_object.add_raw_member(\"total_change_time\",\n                                                      change_time,\n                                                      attr_effect_parent)\n\n            # Ignore protection\n            contruct_hp_raw_api_object.add_raw_member(\"ignore_protection\",\n                                                      [],\n                                                      attr_effect_parent)\n\n            line.add_raw_api_object(contruct_hp_raw_api_object)\n            contruct_hp_forward_ref = ForwardRef(line, contruct_hp_ref)\n            effects.append(contruct_hp_forward_ref)\n\n        return effects\n\n    @staticmethod\n    def get_attack_resistances(\n        line: GenieGameEntityGroup,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates resistances that are used for attacking (unit command: 7)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        current_unit = line.get_head_unit()\n        dataset = line.data\n\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n\n        resistances = []\n\n        # FlatAttributeChangeDecrease\n        resistance_parent = \"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"\n        armor_parent = \"engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n\n        if current_unit.has_member(\"armors\"):\n            armors = current_unit[\"armors\"].value\n\n        else:\n            # TODO: Trees and blast defense\n            armors = {}\n\n        for armor in armors.values():\n            armor_class = armor[\"type_id\"].value\n            armor_amount = armor[\"amount\"].value\n            class_name = armor_lookup_dict[armor_class]\n\n            armor_ref = f\"{ability_ref}.{class_name}\"\n            armor_raw_api_object = RawAPIObject(armor_ref, class_name, dataset.nyan_api_objects)\n            armor_raw_api_object.add_raw_parent(armor_parent)\n            armor_location = ForwardRef(line, ability_ref)\n            armor_raw_api_object.set_location(armor_location)\n\n            # Type\n            type_ref = f\"util.attribute_change_type.types.{class_name}\"\n            change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n            armor_raw_api_object.add_raw_member(\"type\",\n                                                change_type,\n                                                resistance_parent)\n\n            # Block value\n            # =================================================================================\n            amount_name = f\"{ability_ref}.{class_name}.BlockAmount\"\n            amount_raw_api_object = RawAPIObject(\n                amount_name, \"BlockAmount\", dataset.nyan_api_objects)\n            amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n            amount_location = ForwardRef(line, armor_ref)\n            amount_raw_api_object.set_location(amount_location)\n\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            amount_raw_api_object.add_raw_member(\"type\",\n                                                 attribute,\n                                                 \"engine.util.attribute.AttributeAmount\")\n            amount_raw_api_object.add_raw_member(\"amount\",\n                                                 armor_amount,\n                                                 \"engine.util.attribute.AttributeAmount\")\n\n            line.add_raw_api_object(amount_raw_api_object)\n            # =================================================================================\n            amount_forward_ref = ForwardRef(line, amount_name)\n            armor_raw_api_object.add_raw_member(\"block_value\",\n                                                amount_forward_ref,\n                                                resistance_parent)\n\n            line.add_raw_api_object(armor_raw_api_object)\n            armor_forward_ref = ForwardRef(line, armor_ref)\n            resistances.append(armor_forward_ref)\n\n        # Fallback effect\n        fallback_effect = dataset.pregen_nyan_objects[(\"resistance.discrete.flat_attribute_change.\"\n                                                       \"fallback.AoE2AttackFallback\")].get_nyan_object()\n        resistances.append(fallback_effect)\n\n        return resistances\n\n    @staticmethod\n    def get_convert_resistances(\n        line: GenieGameEntityGroup,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates resistances that are used for conversion (unit command: 104)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        dataset = line.data\n\n        resistances = []\n\n        # AoE2Convert\n        resistance_parent = \"engine.resistance.discrete.convert.Convert\"\n        convert_parent = \"engine.resistance.discrete.convert.type.AoE2Convert\"\n\n        resistance_ref = f\"{ability_ref}.Convert\"\n        resistance_raw_api_object = RawAPIObject(\n            resistance_ref, \"Convert\", dataset.nyan_api_objects)\n        resistance_raw_api_object.add_raw_parent(convert_parent)\n        resistance_location = ForwardRef(line, ability_ref)\n        resistance_raw_api_object.set_location(resistance_location)\n\n        # Type\n        if isinstance(line, GenieUnitLineGroup):\n            type_ref = \"util.convert_type.types.UnitConvert\"\n\n        else:\n            type_ref = \"util.convert_type.types.BuildingConvert\"\n\n        convert_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        resistance_raw_api_object.add_raw_member(\"type\",\n                                                 convert_type,\n                                                 resistance_parent)\n\n        # Chance resist\n        # hardcoded resource\n        chance_resist = dataset.genie_civs[0][\"resources\"][77].value / 100\n        resistance_raw_api_object.add_raw_member(\"chance_resist\",\n                                                 chance_resist,\n                                                 resistance_parent)\n\n        if isinstance(line, GenieUnitLineGroup):\n            guaranteed_rounds = dataset.genie_civs[0][\"resources\"][178].value\n            protected_rounds = dataset.genie_civs[0][\"resources\"][179].value\n\n        else:\n            guaranteed_rounds = dataset.genie_civs[0][\"resources\"][180].value\n            protected_rounds = dataset.genie_civs[0][\"resources\"][181].value\n\n        # Guaranteed rounds\n        resistance_raw_api_object.add_raw_member(\"guaranteed_resist_rounds\",\n                                                 guaranteed_rounds,\n                                                 convert_parent)\n\n        # Protected rounds\n        resistance_raw_api_object.add_raw_member(\"protected_rounds\",\n                                                 protected_rounds,\n                                                 convert_parent)\n\n        # Protection recharge\n        resistance_raw_api_object.add_raw_member(\"protection_round_recharge_time\",\n                                                 0.0,\n                                                 convert_parent)\n\n        line.add_raw_api_object(resistance_raw_api_object)\n        resistance_forward_ref = ForwardRef(line, resistance_ref)\n        resistances.append(resistance_forward_ref)\n\n        return resistances\n\n    @staticmethod\n    def get_heal_resistances(\n        line: GenieGameEntityGroup,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates resistances that are used for healing (unit command: 105)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        dataset = line.data\n\n        resistances = []\n\n        resistance_parent = \"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"\n        heal_parent = \"engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n\n        resistance_ref = f\"{ability_ref}.Heal\"\n        resistance_raw_api_object = RawAPIObject(resistance_ref,\n                                                 \"Heal\",\n                                                 dataset.nyan_api_objects)\n        resistance_raw_api_object.add_raw_parent(heal_parent)\n        resistance_location = ForwardRef(line, ability_ref)\n        resistance_raw_api_object.set_location(resistance_location)\n\n        # Type\n        type_ref = \"util.attribute_change_type.types.Heal\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        resistance_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 resistance_parent)\n\n        # Block rate\n        # =================================================================================\n        rate_name = f\"{ability_ref}.Heal.BlockRate\"\n        rate_raw_api_object = RawAPIObject(rate_name, \"BlockRate\", dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, resistance_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           0.0,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # =================================================================================\n        rate_forward_ref = ForwardRef(line, rate_name)\n        resistance_raw_api_object.add_raw_member(\"block_rate\",\n                                                 rate_forward_ref,\n                                                 resistance_parent)\n\n        line.add_raw_api_object(resistance_raw_api_object)\n        resistance_forward_ref = ForwardRef(line, resistance_ref)\n        resistances.append(resistance_forward_ref)\n\n        return resistances\n\n    @staticmethod\n    def get_repair_resistances(\n        line: GenieGameEntityGroup,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates resistances that are used for repairing (unit command: 106)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        resistances = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        resistance_parent = \"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"\n        repair_parent = \"engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n\n        resistance_ref = f\"{ability_ref}.Repair\"\n        resistance_raw_api_object = RawAPIObject(resistance_ref,\n                                                 \"Repair\",\n                                                 dataset.nyan_api_objects)\n        resistance_raw_api_object.add_raw_parent(repair_parent)\n        resistance_location = ForwardRef(line, ability_ref)\n        resistance_raw_api_object.set_location(resistance_location)\n\n        # Type\n        type_ref = f\"util.attribute_change_type.types.{game_entity_name}Repair\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        resistance_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 resistance_parent)\n\n        # Block rate\n        # =================================================================================\n        rate_name = f\"{ability_ref}.Repair.BlockRate\"\n        rate_raw_api_object = RawAPIObject(rate_name, \"BlockRate\", dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, resistance_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           0.0,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # =================================================================================\n        rate_forward_ref = ForwardRef(line, rate_name)\n        resistance_raw_api_object.add_raw_member(\"block_rate\",\n                                                 rate_forward_ref,\n                                                 resistance_parent)\n\n        # Stacking of villager repair HP increase\n        construct_property = dataset.pregen_nyan_objects[\"resistance.property.types.BuildingRepair\"].get_nyan_object(\n        )\n        properties = {\n            api_objects[\"engine.resistance.property.type.Stacked\"]: construct_property\n        }\n\n        # Add the predefined property\n        resistance_raw_api_object.add_raw_member(\"properties\",\n                                                 properties,\n                                                 \"engine.resistance.Resistance\")\n\n        line.add_raw_api_object(resistance_raw_api_object)\n        resistance_forward_ref = ForwardRef(line, resistance_ref)\n        resistances.append(resistance_forward_ref)\n\n        return resistances\n\n    @staticmethod\n    def get_construct_resistances(\n        line: GenieGameEntityGroup,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates resistances that are used for constructing (unit command: 101)\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        resistances = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        progress_resistance_parent = \"engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\"\n        progress_construct_parent = \"engine.resistance.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\"\n        attr_resistance_parent = \"engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\"\n        attr_construct_parent = \"engine.resistance.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\"\n\n        # Progress\n        resistance_ref = f\"{ability_ref}.ConstructProgress\"\n        resistance_raw_api_object = RawAPIObject(resistance_ref,\n                                                 \"ConstructProgress\",\n                                                 dataset.nyan_api_objects)\n        resistance_raw_api_object.add_raw_parent(progress_construct_parent)\n        resistance_location = ForwardRef(line, ability_ref)\n        resistance_raw_api_object.set_location(resistance_location)\n\n        # Type\n        type_ref = f\"util.construct_type.types.{game_entity_name}Construct\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        resistance_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 progress_resistance_parent)\n\n        line.add_raw_api_object(resistance_raw_api_object)\n        resistance_forward_ref = ForwardRef(line, resistance_ref)\n        resistances.append(resistance_forward_ref)\n\n        # Stacking of villager construction times\n        construct_property = dataset.pregen_nyan_objects[\"resistance.property.types.BuildingConstruct\"].get_nyan_object(\n        )\n        properties = {\n            api_objects[\"engine.resistance.property.type.Stacked\"]: construct_property\n        }\n\n        # Add the predefined property\n        resistance_raw_api_object.add_raw_member(\"properties\",\n                                                 properties,\n                                                 \"engine.resistance.Resistance\")\n\n        # Health\n        resistance_ref = f\"{ability_ref}.ConstructHP\"\n        resistance_raw_api_object = RawAPIObject(resistance_ref,\n                                                 \"ConstructHP\",\n                                                 dataset.nyan_api_objects)\n        resistance_raw_api_object.add_raw_parent(attr_construct_parent)\n        resistance_location = ForwardRef(line, ability_ref)\n        resistance_raw_api_object.set_location(resistance_location)\n\n        # Type\n        type_ref = f\"util.attribute_change_type.types.{game_entity_name}Construct\"\n        change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n        resistance_raw_api_object.add_raw_member(\"type\",\n                                                 change_type,\n                                                 attr_resistance_parent)\n\n        # Stacking of villager construction HP increase\n        construct_property = dataset.pregen_nyan_objects[\"resistance.property.types.BuildingConstruct\"].get_nyan_object(\n        )\n        properties = {\n            api_objects[\"engine.resistance.property.type.Stacked\"]: construct_property\n        }\n\n        # Add the predefined property\n        resistance_raw_api_object.add_raw_member(\"properties\",\n                                                 properties,\n                                                 \"engine.resistance.Resistance\")\n\n        line.add_raw_api_object(resistance_raw_api_object)\n        resistance_forward_ref = ForwardRef(line, resistance_ref)\n        resistances.append(resistance_forward_ref)\n\n        return resistances\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/media_subprocessor.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-few-public-methods,too-many-statements\n\"\"\"\nConvert media information to metadata definitions and export\nrequests. Subroutine of the main AoC processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom openage.convert.value_object.read.media_types import MediaType\n\nfrom ....entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode\nfrom ....entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode\nfrom ....entity_object.export.media_export_request import MediaExportRequest\nfrom ....entity_object.export.metadata_export import SpriteMetadataExport\nfrom ....entity_object.export.metadata_export import TextureMetadataExport\nfrom ....entity_object.export.metadata_export import TerrainMetadataExport\nfrom ....value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass AoCMediaSubprocessor:\n    \"\"\"\n    Creates the exports requests for media files from AoC.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create all export requests for the dataset.\n        \"\"\"\n        cls.create_graphics_requests(full_data_set)\n        # cls.create_blend_requests(full_data_set)\n        cls.create_sound_requests(full_data_set)\n\n    @staticmethod\n    def create_graphics_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for graphics referenced by CombinedSprite objects.\n        \"\"\"\n        combined_sprites = full_data_set.combined_sprites.values()\n        handled_graphic_ids = set()\n\n        for sprite in combined_sprites:\n            ref_graphics = sprite.get_graphics()\n            graphic_targetdirs = sprite.resolve_graphics_location()\n\n            # Animation metadata file definiton\n            sprite_meta_filename = f\"{sprite.get_filename()}.sprite\"\n            sprite_meta_export = SpriteMetadataExport(sprite.resolve_sprite_location(),\n                                                      sprite_meta_filename)\n            full_data_set.metadata_exports.append(sprite_meta_export)\n\n            for graphic in ref_graphics:\n                graphic_id = graphic.get_id()\n                if graphic_id in handled_graphic_ids:\n                    continue\n\n                # Texture image file definiton\n                targetdir = graphic_targetdirs[graphic_id]\n                source_filename = f\"{str(graphic['slp_id'].value)}.slp\"\n                target_filename = f\"{sprite.get_filename()}_{str(graphic['slp_id'].value)}.png\"\n\n                export_request = MediaExportRequest(MediaType.GRAPHICS,\n                                                    targetdir,\n                                                    source_filename,\n                                                    target_filename)\n                full_data_set.graphics_exports.update({graphic_id: export_request})\n\n                # Texture metadata file definiton\n                # Same file stem as the image file and same targetdir\n                texture_meta_filename = f\"{target_filename[:-4]}.texture\"\n                texture_meta_export = TextureMetadataExport(targetdir,\n                                                            texture_meta_filename)\n                full_data_set.metadata_exports.append(texture_meta_export)\n\n                # Add texture image filename to texture metadata\n                texture_meta_export.add_imagefile(target_filename)\n\n                # Add metadata from graphics to animation metadata\n                sequence_type = graphic[\"sequence_type\"].value\n                if sequence_type == 0x00:\n                    layer_mode = SpriteLayerMode.OFF\n\n                elif sequence_type & 0x08:\n                    layer_mode = SpriteLayerMode.ONCE\n\n                else:\n                    layer_mode = SpriteLayerMode.LOOP\n\n                layer_pos = graphic[\"layer\"].value\n                frame_rate = round(graphic[\"frame_rate\"].value, ndigits=6)\n                if frame_rate < 0.000001:\n                    frame_rate = None\n\n                replay_delay = round(graphic[\"replay_delay\"].value, ndigits=6)\n                if replay_delay < 0.000001:\n                    replay_delay = None\n\n                frame_count = graphic[\"frame_count\"].value\n                angle_count = graphic[\"angle_count\"].value\n                mirror_mode = graphic[\"mirroring_mode\"].value\n                sprite_meta_export.add_graphics_metadata(target_filename,\n                                                         texture_meta_filename,\n                                                         layer_mode,\n                                                         layer_pos,\n                                                         frame_rate,\n                                                         replay_delay,\n                                                         frame_count,\n                                                         angle_count,\n                                                         mirror_mode)\n\n                # Notify metadata export about SLP metadata when the file is exported\n                export_request.add_observer(texture_meta_export)\n                export_request.add_observer(sprite_meta_export)\n\n                handled_graphic_ids.add(graphic_id)\n\n        combined_terrains = full_data_set.combined_terrains.values()\n        for texture in combined_terrains:\n            slp_id = texture.get_terrain()[\"slp_id\"].value\n\n            targetdir = texture.resolve_graphics_location()\n            source_filename = f\"{str(slp_id)}.slp\"\n            target_filename = f\"{texture.get_filename()}.png\"\n\n            export_request = MediaExportRequest(MediaType.TERRAIN,\n                                                targetdir,\n                                                source_filename,\n                                                target_filename)\n            full_data_set.graphics_exports.update({slp_id: export_request})\n\n            texture_meta_filename = f\"{texture.get_filename()}.texture\"\n            texture_meta_export = TextureMetadataExport(targetdir,\n                                                        texture_meta_filename)\n            full_data_set.metadata_exports.append(texture_meta_export)\n\n            # Add texture image filename to texture metadata\n            texture_meta_export.add_imagefile(target_filename)\n            texture_meta_export.update(\n                None,\n                {\n                    f\"{target_filename}\": {\n                        \"size\": (481, 481),  # TODO: Get actual size = sqrt(slp_frame_count)\n                        \"subtex_metadata\": [\n                            {\n                                \"x\":  0,\n                                \"y\":  0,\n                                \"w\":  481,\n                                \"h\":  481,\n                                \"cx\": 0,\n                                \"cy\": 0,\n                            }\n                        ]\n                    }}\n            )\n\n            terrain_meta_filename = f\"{texture.get_filename()}.terrain\"\n            terrain_meta_export = TerrainMetadataExport(targetdir,\n                                                        terrain_meta_filename)\n            full_data_set.metadata_exports.append(terrain_meta_export)\n\n            terrain_meta_export.add_graphics_metadata(target_filename,\n                                                      texture_meta_filename,\n                                                      TerrainLayerMode.OFF,\n                                                      0,\n                                                      0.0,\n                                                      0.0,\n                                                      1)\n\n    @staticmethod\n    def create_blend_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for Blendomatic objects.\n\n        TODO: Blendomatic contains multiple files. Better handling?\n        \"\"\"\n        export_request = MediaExportRequest(\n            MediaType.BLEND,\n            \"data/blend/\",\n            full_data_set.game_version.edition.media_paths[MediaType.BLEND][0],\n            \"blendmode\"\n        )\n        full_data_set.blend_exports.update({0: export_request})\n\n    @staticmethod\n    def create_sound_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for sounds referenced by CombinedSound objects.\n        \"\"\"\n        combined_sounds = full_data_set.combined_sounds.values()\n\n        for sound in combined_sounds:\n            sound_id = sound.get_file_id()\n\n            targetdir = sound.resolve_sound_location()\n            source_filename = f\"{str(sound_id)}.wav\"\n            target_filename = f\"{sound.get_filename()}.opus\"\n\n            export_request = MediaExportRequest(MediaType.SOUNDS,\n                                                targetdir,\n                                                source_filename,\n                                                target_filename)\n\n            full_data_set.sound_exports.update({sound_id: export_request})\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/modifier_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches,too-many-nested-blocks,too-many-statements\n\n\"\"\"\nDerives and adds abilities to lines or civ groups. Subroutine of the\nnyan subprocessor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup, \\\n    GenieBuildingLineGroup, GenieVillagerGroup, GenieAmbientGroup, \\\n    GenieVariantGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.nyan.nyan_structs import NyanObject\n\n\nclass AoCModifierSubprocessor:\n    \"\"\"\n    Creates raw API objects for modifiers in AoC.\n    \"\"\"\n\n    @staticmethod\n    def elevation_attack_modifiers(converter_obj_group: GenieGameEntityGroup) -> list[NyanObject]:\n        \"\"\"\n        Adds the pregenerated elevation damage multipliers to a line or civ group.\n\n        :param converter_obj_group: ConverterObjectGroup that gets the modifier.\n        :type converter_obj_group: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the modifier.\n        :rtype: list\n        \"\"\"\n        dataset = converter_obj_group.data\n        modifiers = [\n            dataset.pregen_nyan_objects[\n                \"util.modifier.elevation_difference.AttackHigh\"\n            ].get_nyan_object(),\n            dataset.pregen_nyan_objects[\n                \"util.modifier.elevation_difference.AttackLow\"\n            ].get_nyan_object()\n        ]\n\n        return modifiers\n\n    @staticmethod\n    def flyover_effect_modifier(converter_obj_group: GenieGameEntityGroup) -> NyanObject:\n        \"\"\"\n        Adds the pregenerated fly-over-cliff damage multiplier to a line or civ group.\n\n        :param converter_obj_group: ConverterObjectGroup that gets the modifier.\n        :type converter_obj_group: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the modifier.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        dataset = converter_obj_group.data\n        modifier = dataset.pregen_nyan_objects[\n            \"util.modifier.flyover_cliff.AttackFlyover\"\n        ].get_nyan_object()\n\n        return modifier\n\n    @staticmethod\n    def gather_rate_modifier(converter_obj_group: GenieGameEntityGroup) -> list[ForwardRef]:\n        \"\"\"\n        Adds Gather modifiers to a line or civ group.\n\n        :param converter_obj_group: ConverterObjectGroup that gets the modifier.\n        :type converter_obj_group: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the modifier.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        dataset = converter_obj_group.data\n\n        modifiers = []\n\n        if isinstance(converter_obj_group, GenieGameEntityGroup):\n            if isinstance(converter_obj_group, GenieVillagerGroup):\n                gatherers = converter_obj_group.variants[0].line\n\n            else:\n                gatherers = [converter_obj_group.line[0]]\n\n            head_unit_id = converter_obj_group.get_head_unit_id()\n\n            name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n            target_obj_name = name_lookup_dict[head_unit_id][0]\n\n            for gatherer in gatherers:\n                unit_commands = gatherer[\"unit_commands\"].value\n\n                for command in unit_commands:\n                    # Find a gather ability.\n                    type_id = command[\"type\"].value\n\n                    gather_task_ids = internal_name_lookups.get_gather_lookups(\n                        dataset.game_version).keys()\n                    if type_id not in gather_task_ids:\n                        continue\n\n                    work_value = command[\"work_value1\"].value\n\n                    # Check if the work value is 1 (with some rounding error margin)\n                    # if not, we have to create a modifier\n                    if 0.9999 < work_value < 1.0001:\n                        continue\n\n                    # Search for the lines with the matching class/unit id\n                    class_id = command[\"class_id\"].value\n                    unit_id = command[\"unit_id\"].value\n\n                    entity_lines = {}\n                    entity_lines.update(dataset.unit_lines)\n                    entity_lines.update(dataset.building_lines)\n                    entity_lines.update(dataset.ambient_groups)\n                    entity_lines.update(dataset.variant_groups)\n\n                    if unit_id != -1:\n                        lines = [entity_lines[unit_id]]\n\n                    elif class_id != -1:\n                        lines = []\n                        for line in entity_lines.values():\n                            if line.get_class_id() == class_id:\n                                lines.append(line)\n\n                    else:\n                        raise ValueError(\"Gather task has no valid target ID.\")\n\n                    # Create a modifier for each matching resource spot\n                    for resource_line in lines:\n                        head_unit_id = resource_line.get_head_unit_id()\n                        if isinstance(resource_line, GenieBuildingLineGroup):\n                            resource_line_name = name_lookup_dict[head_unit_id][0]\n\n                        elif isinstance(resource_line, GenieAmbientGroup):\n                            resource_line_name = name_lookup_dict[head_unit_id][0]\n\n                        elif isinstance(resource_line, GenieVariantGroup):\n                            resource_line_name = name_lookup_dict[head_unit_id][1]\n\n                        modifier_ref = f\"{target_obj_name}.{resource_line_name}GatheringRate\"\n                        modifier_raw_api_object = RawAPIObject(modifier_ref,\n                                                               \"%sGatheringRate\",\n                                                               dataset.nyan_api_objects)\n                        modifier_raw_api_object.add_raw_parent(\n                            \"engine.modifier.multiplier.type.GatheringRate\")\n                        modifier_location = ForwardRef(converter_obj_group, target_obj_name)\n                        modifier_raw_api_object.set_location(modifier_location)\n\n                        # Multiplier\n                        modifier_raw_api_object.add_raw_member(\n                            \"multiplier\",\n                            work_value,\n                            \"engine.modifier.multiplier.MultiplierModifier\"\n                        )\n\n                        # Resource spot\n                        spot_ref = (f\"{resource_line_name}.Harvestable.\"\n                                    f\"{resource_line_name}ResourceSpot\")\n                        spot_forward_ref = ForwardRef(resource_line, spot_ref)\n                        modifier_raw_api_object.add_raw_member(\n                            \"resource_spot\",\n                            spot_forward_ref,\n                            \"engine.modifier.multiplier.type.GatheringRate\"\n                        )\n\n                        converter_obj_group.add_raw_api_object(modifier_raw_api_object)\n                        modifier_forward_ref = ForwardRef(converter_obj_group,\n                                                          modifier_raw_api_object.get_id())\n                        modifiers.append(modifier_forward_ref)\n\n        return modifiers\n\n    @staticmethod\n    def move_speed_modifier(converter_obj_group: GenieGameEntityGroup, value: float) -> ForwardRef:\n        \"\"\"\n        Adds a MoveSpeed modifier to a line or civ group.\n\n        :param converter_obj_group: ConverterObjectGroup that gets the modifier.\n        :type converter_obj_group: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the modifier.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        dataset = converter_obj_group.data\n        if isinstance(converter_obj_group, GenieGameEntityGroup):\n            head_unit_id = converter_obj_group.get_head_unit_id()\n            name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n            target_obj_name = name_lookup_dict[head_unit_id][0]\n\n        else:\n            # Civs\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            target_obj_name = civ_lookup_dict[converter_obj_group.get_id()][0]\n\n        modifier_ref = f\"{target_obj_name}.MoveSpeed\"\n        modifier_raw_api_object = RawAPIObject(modifier_ref, \"MoveSpeed\", dataset.nyan_api_objects)\n        modifier_raw_api_object.add_raw_parent(\"engine.modifier.multiplier.type.MoveSpeed\")\n        modifier_location = ForwardRef(converter_obj_group, target_obj_name)\n        modifier_raw_api_object.set_location(modifier_location)\n\n        modifier_raw_api_object.add_raw_member(\"multiplier\",\n                                               value,\n                                               \"engine.modifier.multiplier.MultiplierModifier\")\n\n        converter_obj_group.add_raw_api_object(modifier_raw_api_object)\n\n        modifier_forward_ref = ForwardRef(converter_obj_group, modifier_raw_api_object.get_id())\n\n        return modifier_forward_ref\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/modpack_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches,too-few-public-methods,too-many-statements\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....nyan.import_tree import ImportTree\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ....entity_object.export.formats.nyan_file import NyanFile\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n    from openage.convert.entity_object.conversion.converter_object import RawAPIObject\n\n\nclass AoCModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the AoC conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        aoe2_base = cls._get_aoe2_base(full_data_set)\n\n        return [aoe2_base]\n\n    @classmethod\n    def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe2-base modpack.\n        \"\"\"\n        modpack = Modpack(\"aoe2_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"aoe2_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"aoe2_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        cls.organize_nyan_objects(modpack, full_data_set)\n        cls.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n\n    @staticmethod\n    def organize_nyan_objects(modpack: Modpack, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Put available nyan objects into a given modpack.\n        \"\"\"\n        created_nyan_files: dict[str, NyanFile] = {}\n\n        # Access all raw API objects\n        raw_api_objects: list[RawAPIObject] = []\n        raw_api_objects.extend(full_data_set.pregen_nyan_objects.values())\n\n        for unit_line in full_data_set.unit_lines.values():\n            raw_api_objects.extend(unit_line.get_raw_api_objects().values())\n\n        for building_line in full_data_set.building_lines.values():\n            raw_api_objects.extend(building_line.get_raw_api_objects().values())\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            raw_api_objects.extend(ambient_group.get_raw_api_objects().values())\n\n        for variant_group in full_data_set.variant_groups.values():\n            raw_api_objects.extend(variant_group.get_raw_api_objects().values())\n\n        for tech_group in full_data_set.tech_groups.values():\n            raw_api_objects.extend(tech_group.get_raw_api_objects().values())\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            raw_api_objects.extend(terrain_group.get_raw_api_objects().values())\n\n        for civ_group in full_data_set.civ_groups.values():\n            raw_api_objects.extend(civ_group.get_raw_api_objects().values())\n\n        # Create the files\n        for raw_api_object in raw_api_objects:\n            obj_location = raw_api_object.get_location()\n\n            if isinstance(obj_location, ForwardRef):\n                # Resolve location and add nested object\n                nyan_object = obj_location.resolve()\n                nyan_object.add_nested_object(raw_api_object.get_nyan_object())\n                continue\n\n            obj_filename = raw_api_object.get_filename()\n            nyan_file_path = f\"{modpack.name}/{obj_location}{obj_filename}\"\n\n            if nyan_file_path in created_nyan_files:\n                nyan_file = created_nyan_files[nyan_file_path]\n\n            else:\n                nyan_file = NyanFile(obj_location, obj_filename,\n                                     modpack.name)\n                created_nyan_files.update({nyan_file.get_relative_file_path(): nyan_file})\n                modpack.add_data_export(nyan_file)\n\n            nyan_file.add_nyan_object(raw_api_object.get_nyan_object())\n\n        # Create an import tree from the files\n        import_tree = ImportTree()\n\n        for nyan_file in created_nyan_files.values():\n            import_tree.expand_from_file(nyan_file)\n\n        for nyan_object in full_data_set.nyan_api_objects.values():\n            import_tree.expand_from_object(nyan_object)\n\n        for nyan_file in created_nyan_files.values():\n            nyan_file.set_import_tree(import_tree)\n\n        AoCModpackSubprocessor._set_static_aliases(modpack, import_tree)\n\n    @staticmethod\n    def organize_media_objects(modpack: Modpack, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Put export requests and sprite files into as given modpack.\n        \"\"\"\n        for graphic_export in full_data_set.graphics_exports.values():\n            modpack.add_media_export(graphic_export)\n\n        for blend_export in full_data_set.blend_exports.values():\n            modpack.add_media_export(blend_export)\n\n        for sound_export in full_data_set.sound_exports.values():\n            modpack.add_media_export(sound_export)\n\n        for metadata_file in full_data_set.metadata_exports:\n            modpack.add_metadata_export(metadata_file)\n\n    @staticmethod\n    def _set_static_aliases(modpack: Modpack, import_tree: ImportTree) -> None:\n        \"\"\"\n        Set the aliases for the nyan import tree. The alias names\n        and affected nodes are hardcoded in this function.\n        \"\"\"\n        # Abilities from the openage API\n        import_tree.add_alias((\"engine\", \"ability\", \"type\"), \"ability\")\n        import_tree.add_alias((\"engine\", \"ability\", \"property\", \"type\"), \"ability_prop\")\n\n        # Auxiliary objects\n        import_tree.add_alias(\n            (\"engine\", \"util\", \"activity\", \"condition\", \"type\"), \"activity_condition\"\n        )\n        import_tree.add_alias((\"engine\", \"util\", \"activity\", \"event\", \"type\"), \"activity_event\")\n        import_tree.add_alias((\"engine\", \"util\", \"activity\", \"node\", \"type\"), \"activity_node\")\n        import_tree.add_alias((\"engine\", \"util\", \"accuracy\"), \"accuracy\")\n        import_tree.add_alias(\n            (\"engine\", \"util\", \"animation_override\"), \"animation_override\"\n        )\n        import_tree.add_alias((\"engine\", \"util\", \"attribute\"), \"attribute\")\n        import_tree.add_alias((\"engine\", \"util\", \"attribute_change_type\", \"type\"),\n                              \"attribute_change_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"calculation_type\", \"type\"), \"calculation_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"setup\"), \"civ\")\n        import_tree.add_alias((\"engine\", \"util\", \"convert_type\"), \"convert_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"cost\", \"type\"), \"cost_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"create\"), \"create\")\n        import_tree.add_alias((\"engine\", \"util\", \"diplomatic_stance\"), \"diplo_stance\")\n        import_tree.add_alias(\n            (\"engine\", \"util\", \"diplomatic_stance\", \"type\"),\n            \"diplo_stance_type\"\n        )\n        import_tree.add_alias((\"engine\", \"util\", \"distribution_type\", \"type\"), \"distribution_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"dropoff_type\", \"type\"), \"dropoff_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"exchange_mode\", \"type\"), \"exchange_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"exchange_rate\"), \"exchange_rate\")\n        import_tree.add_alias((\"engine\", \"util\", \"formation\"), \"formation\")\n        import_tree.add_alias((\"engine\", \"util\", \"game_entity\"), \"game_entity\")\n        import_tree.add_alias(\n            (\"engine\", \"util\", \"game_entity_formation\"), \"ge_formation\"\n        )\n        import_tree.add_alias((\"engine\", \"util\", \"game_entity_stance\", \"type\"), \"ge_stance\")\n        import_tree.add_alias((\"engine\", \"util\", \"game_entity_type\", \"type\"), \"ge_type\")\n        import_tree.add_alias(\n            (\"engine\", \"util\", \"game_entity_type\"), \"game_entity_type\"\n        )\n        import_tree.add_alias((\"engine\", \"util\", \"graphics\"), \"graphics\")\n        import_tree.add_alias((\"engine\", \"util\", \"herdable_mode\", \"type\"), \"herdable_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"hitbox\"), \"hitbox\")\n        import_tree.add_alias((\"engine\", \"util\", \"move_mode\", \"type\"), \"move_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"language\"), \"lang\")\n        import_tree.add_alias((\"engine\", \"util\", \"language\", \"translated\", \"type\"), \"translated\")\n        import_tree.add_alias((\"engine\", \"util\", \"logic\", \"gate\", \"type\"), \"logic_gate\")\n        import_tree.add_alias((\"engine\", \"util\", \"logic\", \"literal\", \"type\"), \"literal\")\n        import_tree.add_alias((\"engine\", \"util\", \"logic\", \"literal_scope\", \"type\"), \"literal_scope\")\n        import_tree.add_alias((\"engine\", \"util\", \"patch\"), \"patch\")\n        import_tree.add_alias((\"engine\", \"util\", \"patch\", \"property\", \"type\"), \"patch_prop\")\n        import_tree.add_alias((\"engine\", \"util\", \"path_type\"), \"path_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"payment_mode\", \"type\"), \"payment_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"placement_mode\", \"type\"), \"placement_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"price_mode\", \"type\"), \"price_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"price_pool\"), \"price_pool\")\n        import_tree.add_alias((\"engine\", \"util\", \"production_mode\", \"type\"), \"production_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"progress\"), \"progress\")\n        import_tree.add_alias((\"engine\", \"util\", \"progress\", \"property\", \"type\"), \"progress_prop\")\n        import_tree.add_alias((\"engine\", \"util\", \"progress_status\"), \"progress_status\")\n        import_tree.add_alias((\"engine\", \"util\", \"progress_type\", \"type\"), \"progress_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"research\"), \"research\")\n        import_tree.add_alias((\"engine\", \"util\", \"resource\"), \"resource\")\n        import_tree.add_alias((\"engine\", \"util\", \"resource_spot\"), \"resource_spot\")\n        import_tree.add_alias((\"engine\", \"util\", \"selection_box\", \"type\"), \"selection_box\")\n        import_tree.add_alias((\"engine\", \"util\", \"sound\",), \"sound\")\n        import_tree.add_alias((\"engine\", \"util\", \"state_machine\"), \"state_machine\")\n        import_tree.add_alias((\"engine\", \"util\", \"storage\"), \"storage\")\n        import_tree.add_alias((\"engine\", \"util\", \"target_mode\", \"type\"), \"target_mode\")\n        import_tree.add_alias((\"engine\", \"util\", \"tech\"), \"tech\")\n        import_tree.add_alias((\"engine\", \"util\", \"terrain\"), \"terrain\")\n        import_tree.add_alias((\"engine\", \"util\", \"terrain_type\"), \"terrain_type\")\n        import_tree.add_alias((\"engine\", \"util\", \"trade_route\", \"type\"), \"trade_route\")\n        import_tree.add_alias((\"engine\", \"util\", \"variant\", \"type\"), \"variant\")\n\n        # Effect objects\n        import_tree.add_alias((\"engine\", \"effect\", \"property\", \"type\"), \"effect_prop\")\n        import_tree.add_alias(\n            (\"engine\", \"effect\", \"continuous\", \"flat_attribute_change\", \"type\"),\n            \"econt_flac\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"effect\", \"continuous\", \"time_relative_progress\", \"type\"),\n            \"econt_trp\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"effect\", \"continuous\", \"time_relative_attribute\", \"type\"),\n            \"econt_tra\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"effect\", \"discrete\", \"convert\", \"type\"),\n            \"edisc_conv\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"effect\", \"discrete\", \"flat_attribute_change\", \"type\"),\n            \"edisc_flac\"\n        )\n        import_tree.add_alias((\"engine\", \"resistance\", \"property\", \"type\"), \"resist_prop\")\n        import_tree.add_alias(\n            (\"engine\", \"resistance\", \"continuous\", \"flat_attribute_change\", \"type\"),\n            \"rcont_flac\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"resistance\", \"continuous\", \"time_relative_progress\", \"type\"),\n            \"rcont_trp\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"resistance\", \"continuous\", \"time_relative_attribute\", \"type\"),\n            \"rcont_tra\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"resistance\", \"discrete\", \"convert\", \"type\"),\n            \"rdisc_conv\"\n        )\n        import_tree.add_alias(\n            (\"engine\", \"resistance\", \"discrete\", \"flat_attribute_change\", \"type\"),\n            \"rdisc_flac\"\n        )\n\n        # Modifier objects\n        import_tree.add_alias(\n            (\"engine\", \"modifier\", \"effect\", \"flat_attribute_change\", \"type\"),\n            \"me_flac\"\n        )\n\n        # Aliases for objects from the modpack itself\n\n        # Prefix aliases to prevent naming conflucts with 'engine'\n        prefix = modpack.name + \"_\"\n\n        # Auxiliary objects\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"attribute\", \"types\"),\n            prefix + \"attribute\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"attribute_change_type\", \"types\"),\n            prefix + \"attr_change_type\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"construct_type\", \"types\"),\n            prefix + \"construct_type\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"convert_type\", \"types\"),\n            prefix + \"convert_type\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"diplomatic_stance\", \"types\"),\n            prefix + \"diplo_stance\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"game_entity_type\", \"types\"),\n            prefix + \"ge_type\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"formation\", \"types\"),\n            prefix + \"formation\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"formation\", \"subformations\"),\n            prefix + \"subformations\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"language\", \"language\"),\n            prefix + \"lang\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"logic\", \"death\", \"death\"),\n            \"death_condition\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"logic\", \"garrison_empty\",\n             \"garrison_empty\"),\n            \"empty_garrison_condition\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"path_type\", \"types\"),\n            prefix + \"path_type\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"resource\", \"market_trading\"),\n            prefix + \"market_trading\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"resource\", \"types\"),\n            prefix + \"resource\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"terrain_type\", \"types\"),\n            prefix + \"terrain_type\"\n        )\n\n        # Effect objects\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"effect\", \"discrete\", \"flat_attribute_change\",\n             \"fallback\"),\n            \"attack_fallback\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"effect\", \"discrete\", \"flat_attribute_change\",\n             \"min_damage\"),\n            \"min_damage\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"effect\", \"discrete\", \"flat_attribute_change\",\n             \"min_heal\"),\n            \"min_heal\"\n        )\n\n        # Modifier objects\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"modifier\", \"elevation_difference\",\n             \"elevation_difference\"),\n            prefix + \"mme_elev_high\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"modifier\", \"elevation_difference\",\n             \"elevation_difference\"),\n            prefix + \"mme_elev_low\"\n        )\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"util\", \"modifier\", \"flyover_cliff\",\n             \"flyover_cliff\"),\n            prefix + \"mme_cliff_attack\"\n        )\n\n        # Terrain objects\n        import_tree.add_alias(\n            (modpack.name, \"data\", \"terrain\", \"foundation\", \"foundation\"),\n            prefix + \"foundation\"\n        )\n\n        # Generic aliases\n        # TODO: Make this less hacky\n        fqon = (modpack.name, \"data\", \"game_entity\", \"generic\")\n        current_node = import_tree.root\n        for part in fqon:\n            current_node = current_node.get_child(part)\n\n        for child in current_node.children.values():\n            current_node = child\n\n            # These are folders and should have unique names\n            alias_name = f\"ge_{current_node.name}\"\n\n            for subchild in current_node.children.values():\n                if subchild.name in (\"graphics\", \"sounds\", \"projectiles\"):\n                    continue\n\n                if subchild.name.endswith(\"upgrade\"):\n                    alias = f\"{alias_name}_{subchild.name}\"\n                    subchild.set_alias(alias)\n                    continue\n\n                # One level deeper: This should be the nyan file\n                current_node = subchild\n\n                alias = f\"ge_{current_node.name}\"\n\n                # Use the file name as alias for the file\n                current_node.set_alias(alias)\n\n        fqon = (modpack.name, \"data\", \"tech\", \"generic\")\n        current_node = import_tree.root\n        for part in fqon:\n            current_node = current_node.get_child(part)\n\n        for child in current_node.children.values():\n            current_node = child\n\n            # These are folders and should have unique names\n            alias_name = \"tech_\" + current_node.name\n\n            # One level deeper: This should be the nyan file\n            current_node = current_node.children[current_node.name]\n\n            # Set the folder name as alias for the file\n            current_node.set_alias(alias_name)\n\n        fqon = (modpack.name, \"data\", \"civ\")\n        current_node = import_tree.root\n        for part in fqon:\n            current_node = current_node.get_child(part)\n\n        for child in current_node.children.values():\n            current_node = child\n\n            # These are folders and should have unique names\n            alias_name = \"civ_\" + current_node.name\n\n            # One level deeper: This should be the nyan file\n            current_node = current_node.children[current_node.name]\n\n            # Set the folder name as alias for the file\n            current_node.set_alias(alias_name)\n\n        fqon = (modpack.name, \"data\", \"terrain\")\n        current_node = import_tree.root\n        for part in fqon:\n            current_node = current_node.get_child(part)\n\n        for child in current_node.children.values():\n            current_node = child\n\n            # These are folders and should have unique names\n            alias_name = \"terrain_\" + current_node.name\n\n            # One level deeper: This should be the nyan file\n            current_node = current_node.children[current_node.name]\n\n            # Set the folder name as alias for the file\n            current_node.set_alias(alias_name)\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/nyan_subprocessor.py",
    "content": "# Copyright 2019-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nConvert API-like objects to nyan objects. Subroutine of the\nmain AoC processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade\nfrom ....entity_object.conversion.aoc.genie_unit import GenieGarrisonMode, \\\n    GenieMonkGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup\nfrom ....entity_object.conversion.combined_terrain import CombinedTerrain\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom .ability_subprocessor import AoCAbilitySubprocessor\nfrom .auxiliary_subprocessor import AoCAuxiliarySubprocessor\nfrom .civ_subprocessor import AoCCivSubprocessor\nfrom .modifier_subprocessor import AoCModifierSubprocessor\nfrom .tech_subprocessor import AoCTechSubprocessor\nfrom .upgrade_ability_subprocessor import AoCUpgradeAbilitySubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n    from openage.convert.entity_object.conversion.aoc.genie_terrain import GenieTerrainGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup, \\\n        GenieUnitLineGroup, GenieBuildingLineGroup, GenieAmbientGroup, GenieVariantGroup\n\n\nclass AoCNyanSubprocessor:\n    \"\"\"\n    Transform an AoC dataset to nyan objects.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects from the given dataset.\n        \"\"\"\n        cls._process_game_entities(full_data_set)\n        cls._create_nyan_objects(full_data_set)\n        cls._create_nyan_members(full_data_set)\n\n        cls._check_objects(full_data_set)\n\n    @classmethod\n    def _check_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Check if objects are valid.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.check_readiness()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.check_readiness()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.check_readiness()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.check_readiness()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.check_readiness()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.check_readiness()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.check_readiness()\n\n    @classmethod\n    def _create_nyan_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Creates nyan objects from the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_objects()\n            unit_line.execute_raw_member_pushs()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_objects()\n            building_line.execute_raw_member_pushs()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_objects()\n            ambient_group.execute_raw_member_pushs()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_objects()\n            variant_group.execute_raw_member_pushs()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_objects()\n            tech_group.execute_raw_member_pushs()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_objects()\n            terrain_group.execute_raw_member_pushs()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_objects()\n            civ_group.execute_raw_member_pushs()\n\n    @classmethod\n    def _create_nyan_members(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Fill nyan member values of the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_members()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_members()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_members()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_members()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_members()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_members()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_members()\n\n    @classmethod\n    def _process_game_entities(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create the RawAPIObject representation of the objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            cls.unit_line_to_game_entity(unit_line)\n\n        for building_line in full_data_set.building_lines.values():\n            cls.building_line_to_game_entity(building_line)\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            cls.ambient_group_to_game_entity(ambient_group)\n\n        for variant_group in full_data_set.variant_groups.values():\n            cls.variant_group_to_game_entity(variant_group)\n\n        for tech_group in full_data_set.tech_groups.values():\n            if tech_group.is_researchable():\n                cls.tech_group_to_tech(tech_group)\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            cls.terrain_group_to_terrain(terrain_group)\n\n        for civ_group in full_data_set.civ_groups.values():\n            cls.civ_group_to_civ(civ_group)\n\n    @staticmethod\n    def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a unit line.\n\n        :param unit_line: Unit line that gets converted to a game entity.\n        :type unit_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = unit_line.get_head_unit()\n        current_unit_id = unit_line.get_head_unit_id()\n\n        dataset = unit_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_unit_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_unit_id][1])\n        unit_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a unit two types\n        #    - util.game_entity_type.types.Unit (if unit_type >= 70)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_unit[\"unit_type\"].value\n\n        if unit_type >= 70:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(unit_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.turn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(unit_line))\n\n        # Creation\n        if len(unit_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(unit_line))\n\n        # Config\n        ability = AoCAbilitySubprocessor.use_contingent_ability(unit_line)\n        if ability:\n            abilities_set.append(ability)\n\n        if unit_line.get_head_unit_id() in (125, 692):\n            # Healing/Recharging attribute points (monks, berserks)\n            abilities_set.extend(AoCAbilitySubprocessor.regenerate_attribute_ability(unit_line))\n\n        # Applying effects and shooting projectiles\n        if unit_line.is_projectile_shooter():\n            abilities_set.append(AoCAbilitySubprocessor.shoot_projectile_ability(unit_line, 7))\n            AoCNyanSubprocessor.projectiles_from_line(unit_line)\n\n        elif unit_line.is_melee() or unit_line.is_ranged():\n            if unit_line.has_command(7):\n                # Attack\n                abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          7,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(101):\n                # Build\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            101,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(104):\n                # convert\n                abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          104,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(105):\n                # Heal\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            105,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(106):\n                # Repair\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            106,\n                                                                                            unit_line.is_ranged()))\n\n        # Formation/Stance\n        if not isinstance(unit_line, GenieVillagerGroup):\n            abilities_set.append(AoCAbilitySubprocessor.formation_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(unit_line))\n\n        # Storage abilities\n        if unit_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(unit_line))\n\n            garrison_mode = unit_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.MONK:\n                abilities_set.append(AoCAbilitySubprocessor.collect_storage_ability(unit_line))\n\n        if len(unit_line.garrison_locations) > 0:\n            ability = AoCAbilitySubprocessor.enter_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n            ability = AoCAbilitySubprocessor.exit_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n        if isinstance(unit_line, GenieMonkGroup):\n            abilities_set.append(AoCAbilitySubprocessor.transfer_storage_ability(unit_line))\n\n        # Resource abilities\n        if unit_line.is_gatherer():\n            abilities_set.append(AoCAbilitySubprocessor.drop_resources_ability(unit_line))\n            abilities_set.extend(AoCAbilitySubprocessor.gather_ability(unit_line))\n\n        # Resource storage\n        if unit_line.is_gatherer() or unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.resource_storage_ability(unit_line))\n\n        if isinstance(unit_line, GenieVillagerGroup):\n            # Farm restocking\n            abilities_set.append(AoCAbilitySubprocessor.restock_ability(unit_line, 50))\n\n        if unit_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(unit_line))\n\n        if unit_type == 70 and unit_line.get_class_id() not in (9, 10, 58):\n            # Excludes trebuchets and animals\n            abilities_set.append(AoCAbilitySubprocessor.herd_ability(unit_line))\n\n        if unit_line.get_class_id() == 58:\n            abilities_set.append(AoCAbilitySubprocessor.herdable_ability(unit_line))\n\n        # Trade abilities\n        if unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.trade_ability(unit_line))\n\n        # =======================================================================\n        # TODO: Transform\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        if unit_line.has_command(7) and not unit_line.is_projectile_shooter():\n            modifiers_set.extend(AoCModifierSubprocessor.elevation_attack_modifiers(unit_line))\n\n        if unit_line.is_gatherer():\n            modifiers_set.extend(AoCModifierSubprocessor.gather_rate_modifier(unit_line))\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if unit_line.is_creatable():\n            AoCAuxiliarySubprocessor.get_creatable_game_entity(unit_line)\n\n    @staticmethod\n    def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a building line.\n\n        :param building_line: Building line that gets converted to a game entity.\n        :type building_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_building = building_line.line[0]\n        current_building_id = building_line.get_head_unit_id()\n        dataset = building_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_building_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_building_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_building_id][1])\n        building_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a building two types\n        #    - util.game_entity_type.types.Building (if unit_type >= 80)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # and additionally\n        #    - util.game_entity_type.types.DropSite (only if this is used as a drop site)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_building[\"unit_type\"].value\n\n        if unit_type >= 80:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_building[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        if building_line.is_dropsite():\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.DropSite\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.attribute_change_tracker_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(building_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(building_line))\n\n        # Config abilities\n        if building_line.is_creatable():\n            abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line))\n\n        if not building_line.is_passable():\n            abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line))\n\n        if building_line.has_foundation():\n            if building_line.get_class_id() == 49:\n                # Use OverlayTerrain for the farm terrain\n                abilities_set.append(AoCAbilitySubprocessor.overlay_terrain_ability(building_line))\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line,\n                                                                               terrain_id=27))\n\n            else:\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line))\n\n        # Creation/Research abilities\n        if len(building_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.production_queue_ability(building_line))\n\n        if len(building_line.researches) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.research_ability(building_line))\n\n        # Effect abilities\n        if building_line.is_projectile_shooter():\n            abilities_set.append(AoCAbilitySubprocessor.shoot_projectile_ability(building_line, 7))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(building_line))\n            AoCNyanSubprocessor.projectiles_from_line(building_line)\n\n        # Storage abilities\n        if building_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(building_line))\n\n            garrison_mode = building_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.NATURAL:\n                abilities_set.append(\n                    AoCAbilitySubprocessor.send_back_to_task_ability(building_line))\n\n            if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                abilities_set.append(AoCAbilitySubprocessor.rally_point_ability(building_line))\n\n        # Resource abilities\n        if building_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(building_line))\n\n        if building_line.is_dropsite():\n            abilities_set.append(AoCAbilitySubprocessor.drop_site_ability(building_line))\n\n        ability = AoCAbilitySubprocessor.provide_contingent_ability(building_line)\n        if ability:\n            abilities_set.append(ability)\n\n        # Trade abilities\n        if building_line.is_trade_post():\n            abilities_set.append(AoCAbilitySubprocessor.trade_post_ability(building_line))\n\n        if building_line.get_id() == 84:\n            # Market trading\n            abilities_set.extend(AoCAbilitySubprocessor.exchange_resources_ability(building_line))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        raw_api_object.add_raw_member(\"modifiers\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if building_line.is_creatable():\n            AoCAuxiliarySubprocessor.get_creatable_game_entity(building_line)\n\n    @staticmethod\n    def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None:\n        \"\"\"\n        Creates raw API objects for an ambient group.\n\n        :param ambient_group: Unit line that gets converted to a game entity.\n        :type ambient_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        ambient_unit = ambient_group.get_head_unit()\n        ambient_id = ambient_group.get_head_unit_id()\n\n        dataset = ambient_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[ambient_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[ambient_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[ambient_id][1])\n        ambient_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give an ambient the types\n        #    - util.game_entity_type.types.Ambient\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n\n        type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Ambient\"].get_nyan_object(\n        )\n        types_set.append(type_obj)\n\n        unit_class = ambient_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        interaction_mode = ambient_unit[\"interaction_mode\"].value\n\n        if interaction_mode >= 0:\n            abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.resistance_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.visibility_ability(ambient_group))\n\n        if interaction_mode >= 2:\n            abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group))\n\n            if not ambient_group.is_passable():\n                abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group))\n\n        if ambient_group.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(ambient_group))\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n    @staticmethod\n    def variant_group_to_game_entity(variant_group: GenieVariantGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a variant group.\n\n        :param ambient_group: Unit line that gets converted to a game entity.\n        :type ambient_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        variant_main_unit = variant_group.get_head_unit()\n        variant_id = variant_group.get_head_unit_id()\n\n        dataset = variant_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[variant_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[variant_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[variant_id][1])\n        variant_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give variants the types\n        #    - util.game_entity_type.types.Ambient\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n\n        type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Ambient\"].get_nyan_object(\n        )\n        types_set.append(type_obj)\n\n        unit_class = variant_main_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(variant_group))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(variant_group))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(variant_group))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(variant_group))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(variant_group))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(variant_group))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(variant_group))\n\n        if variant_main_unit.has_member(\"speed\") and variant_main_unit[\"speed\"].value > 0.0001\\\n                and variant_main_unit.has_member(\"command_sound_id\"):\n            # TODO: Let variant groups be converted without having command_sound_id member\n            abilities_set.append(AoCAbilitySubprocessor.move_ability(variant_group))\n\n        if variant_group.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(variant_group))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Variants\n        # =======================================================================\n        variants_set = []\n\n        variant_type = name_lookup_dict[variant_id][3]\n\n        index = 0\n        for variant in variant_group.line:\n            # Create a diff\n            diff_variant = variant_main_unit.diff(variant)\n\n            if variant_type == \"random\":\n                variant_type_ref = \"engine.util.variant.type.RandomVariant\"\n\n            elif variant_type == \"angle\":\n                variant_type_ref = \"engine.util.variant.type.PerspectiveVariant\"\n\n            elif variant_type == \"misc\":\n                variant_type_ref = \"engine.util.variant.type.MiscVariant\"\n\n            variant_name = f\"Variant{str(index)}\"\n            variant_ref = f\"{game_entity_name}.{variant_name}\"\n            variant_raw_api_object = RawAPIObject(variant_ref,\n                                                  variant_name,\n                                                  dataset.nyan_api_objects)\n            variant_raw_api_object.add_raw_parent(variant_type_ref)\n            variant_location = ForwardRef(variant_group, game_entity_name)\n            variant_raw_api_object.set_location(variant_location)\n\n            # Create patches for the diff\n            patches = []\n\n            patches.extend(AoCUpgradeAbilitySubprocessor.death_ability(variant_group,\n                                                                       variant_group,\n                                                                       variant_ref,\n                                                                       diff_variant))\n            patches.extend(AoCUpgradeAbilitySubprocessor.despawn_ability(variant_group,\n                                                                         variant_group,\n                                                                         variant_ref,\n                                                                         diff_variant))\n            patches.extend(AoCUpgradeAbilitySubprocessor.idle_ability(variant_group,\n                                                                      variant_group,\n                                                                      variant_ref,\n                                                                      diff_variant))\n            patches.extend(AoCUpgradeAbilitySubprocessor.named_ability(variant_group,\n                                                                       variant_group,\n                                                                       variant_ref,\n                                                                       diff_variant))\n\n            if variant_main_unit.has_member(\"speed\") and variant_main_unit[\"speed\"].value > 0.0001\\\n                    and variant_main_unit.has_member(\"command_sound_id\"):\n                # TODO: Let variant groups be converted without having command_sound_id member:\n                patches.extend(AoCUpgradeAbilitySubprocessor.move_ability(variant_group,\n                                                                          variant_group,\n                                                                          variant_ref,\n                                                                          diff_variant))\n\n            # Changes\n            variant_raw_api_object.add_raw_member(\"changes\",\n                                                  patches,\n                                                  \"engine.util.variant.Variant\")\n\n            # Prority\n            variant_raw_api_object.add_raw_member(\"priority\",\n                                                  1,\n                                                  \"engine.util.variant.Variant\")\n\n            if variant_type == \"random\":\n                variant_raw_api_object.add_raw_member(\"chance_share\",\n                                                      1 / len(variant_group.line),\n                                                      \"engine.util.variant.type.RandomVariant\")\n\n            elif variant_type == \"angle\":\n                variant_raw_api_object.add_raw_member(\"angle\",\n                                                      index,\n                                                      \"engine.util.variant.type.PerspectiveVariant\")\n\n            variants_forward_ref = ForwardRef(variant_group, variant_ref)\n            variants_set.append(variants_forward_ref)\n            variant_group.add_raw_api_object(variant_raw_api_object)\n\n            index += 1\n\n        raw_api_object.add_raw_member(\"variants\", variants_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n    @staticmethod\n    def tech_group_to_tech(tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a tech group.\n\n        :param tech_group: Tech group that gets converted to a tech.\n        :type tech_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        tech_id = tech_group.get_id()\n\n        # Skip Dark Age tech\n        if tech_id == 104:\n            return\n\n        dataset = tech_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = tech_lookup_dict[tech_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.tech.Tech\")\n\n        if isinstance(tech_group, UnitLineUpgrade):\n            unit_line = dataset.unit_lines[tech_group.get_line_id()]\n            head_unit_id = unit_line.get_head_unit_id()\n            obj_location = f\"data/game_entity/generic/{name_lookup_dict[head_unit_id][1]}/\"\n\n        else:\n            obj_location = f\"data/tech/generic/{tech_lookup_dict[tech_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(tech_lookup_dict[tech_id][1])\n        tech_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        raw_api_object.add_raw_member(\"types\", [], \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(tech_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(tech_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(tech_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(tech_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(tech_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(tech_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # Updates\n        # =======================================================================\n        patches = []\n        patches.extend(AoCTechSubprocessor.get_patches(tech_group))\n        raw_api_object.add_raw_member(\"updates\", patches, \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the tech group itself, but use its values)\n        # =======================================================================\n        if tech_group.is_researchable():\n            AoCAuxiliarySubprocessor.get_researchable_tech(tech_group)\n\n    @staticmethod\n    def terrain_group_to_terrain(terrain_group: GenieTerrainGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a terrain group.\n\n        :param terrain_group: Terrain group that gets converted to a tech.\n        :type terrain_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        terrain_index = terrain_group.get_id()\n\n        dataset = terrain_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_lookup_dict = internal_name_lookups.get_terrain_lookups(dataset.game_version)\n        terrain_type_lookup_dict = internal_name_lookups.get_terrain_type_lookups(\n            dataset.game_version)\n\n        # Start with the Terrain object\n        terrain_name = terrain_lookup_dict[terrain_index][1]\n        raw_api_object = RawAPIObject(terrain_name, terrain_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.terrain.Terrain\")\n        obj_location = f\"data/terrain/{terrain_lookup_dict[terrain_index][2]}/\"\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(terrain_lookup_dict[terrain_index][2])\n        terrain_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        terrain_types = []\n\n        for terrain_type in terrain_type_lookup_dict.values():\n            if terrain_index in terrain_type[0]:\n                type_name = f\"util.terrain_type.types.{terrain_type[2]}\"\n                type_obj = dataset.pregen_nyan_objects[type_name].get_nyan_object()\n                terrain_types.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", terrain_types, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{terrain_name}.{terrain_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{terrain_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(terrain_group, terrain_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(terrain_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.terrain.Terrain\")\n        terrain_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Sound\n        # =======================================================================\n        sound_name = f\"{terrain_name}.Sound\"\n        sound_raw_api_object = RawAPIObject(sound_name, \"Sound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(terrain_group, terrain_name)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Sounds for terrains don't exist in AoC\n        sounds = []\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(terrain_group, sound_name)\n        raw_api_object.add_raw_member(\"sound\",\n                                      sound_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n        terrain_group.add_raw_api_object(sound_raw_api_object)\n\n        # =======================================================================\n        # Ambience\n        # =======================================================================\n        terrain = terrain_group.get_terrain()\n        ambients_count = terrain[\"terrain_units_used_count\"].value\n\n        ambience = []\n        for ambient_index in range(ambients_count):\n            ambient_id = terrain[\"terrain_unit_id\"][ambient_index].value\n\n            if ambient_id == -1:\n                continue\n\n            ambient_line = dataset.unit_ref[ambient_id]\n            ambient_name = name_lookup_dict[ambient_line.get_head_unit_id()][0]\n\n            ambient_ref = f\"{terrain_name}.Ambient{str(ambient_index)}\"\n            ambient_raw_api_object = RawAPIObject(ambient_ref,\n                                                  f\"Ambient{str(ambient_index)}\",\n                                                  dataset.nyan_api_objects)\n            ambient_raw_api_object.add_raw_parent(\"engine.util.terrain.TerrainAmbient\")\n            ambient_location = ForwardRef(terrain_group, terrain_name)\n            ambient_raw_api_object.set_location(ambient_location)\n\n            # Game entity reference\n            ambient_line_forward_ref = ForwardRef(ambient_line, ambient_name)\n            ambient_raw_api_object.add_raw_member(\"object\",\n                                                  ambient_line_forward_ref,\n                                                  \"engine.util.terrain.TerrainAmbient\")\n\n            # Max density\n            max_density = terrain[\"terrain_unit_density\"][ambient_index].value\n            ambient_raw_api_object.add_raw_member(\"max_density\",\n                                                  max_density,\n                                                  \"engine.util.terrain.TerrainAmbient\")\n\n            terrain_group.add_raw_api_object(ambient_raw_api_object)\n            terrain_ambient_forward_ref = ForwardRef(terrain_group, ambient_ref)\n            ambience.append(terrain_ambient_forward_ref)\n\n        raw_api_object.add_raw_member(\"ambience\", ambience, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Path Costs\n        # =======================================================================\n        path_costs = {}\n        restrictions = dataset.genie_terrain_restrictions\n\n        # Land grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Land\"].get_nyan_object()\n        land_restrictions = restrictions[0x07]\n        if land_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Water grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Water\"].get_nyan_object()\n        water_restrictions = restrictions[0x03]\n        if water_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Air grid (default accessible)\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Air\"].get_nyan_object()\n        path_costs[path_type] = 1\n\n        raw_api_object.add_raw_member(\"path_costs\", path_costs, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Graphic\n        # =======================================================================\n        if terrain_group.has_subterrain():\n            subterrain = terrain_group.get_subterrain()\n            terrain_id = subterrain.get_id()\n\n        else:\n            terrain_id = terrain_group.get_id()\n\n        # Create animation object\n        graphic_name = f\"{terrain_name}.TerrainTexture\"\n        graphic_raw_api_object = RawAPIObject(graphic_name, \"TerrainTexture\",\n                                              dataset.nyan_api_objects)\n        graphic_raw_api_object.add_raw_parent(\"engine.util.graphics.Terrain\")\n        graphic_location = ForwardRef(terrain_group, terrain_name)\n        graphic_raw_api_object.set_location(graphic_location)\n\n        if terrain_id in dataset.combined_terrains.keys():\n            terrain_graphic = dataset.combined_terrains[terrain_id]\n\n        else:\n            terrain_graphic = CombinedTerrain(terrain_id,\n                                              f\"texture_{terrain_lookup_dict[terrain_index][2]}\",\n                                              dataset)\n            dataset.combined_terrains.update({terrain_graphic.get_id(): terrain_graphic})\n\n        terrain_graphic.add_reference(graphic_raw_api_object)\n\n        graphic_raw_api_object.add_raw_member(\"sprite\", terrain_graphic,\n                                              \"engine.util.graphics.Terrain\")\n\n        terrain_group.add_raw_api_object(graphic_raw_api_object)\n        graphic_forward_ref = ForwardRef(terrain_group, graphic_name)\n        raw_api_object.add_raw_member(\"terrain_graphic\", graphic_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n    @staticmethod\n    def civ_group_to_civ(civ_group: GenieCivilizationGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a civ group.\n\n        :param civ_group: Terrain group that gets converted to a tech.\n        :type civ_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        civ_id = civ_group.get_id()\n\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = civ_lookup_dict[civ_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.setup.PlayerSetup\")\n\n        obj_location = f\"data/civ/{civ_lookup_dict[civ_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(civ_lookup_dict[civ_id][1])\n        civ_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(civ_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(civ_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(civ_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(civ_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(civ_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(civ_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # TODO: Leader names\n        # =======================================================================\n        raw_api_object.add_raw_member(\"leader_names\",\n                                      [],\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers = AoCCivSubprocessor.get_modifiers(civ_group)\n        raw_api_object.add_raw_member(\"modifiers\",\n                                      modifiers,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Starting resources\n        # =======================================================================\n        resource_amounts = AoCCivSubprocessor.get_starting_resources(civ_group)\n        raw_api_object.add_raw_member(\"starting_resources\",\n                                      resource_amounts,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Game setup\n        # =======================================================================\n        game_setup = AoCCivSubprocessor.get_civ_setup(civ_group)\n        raw_api_object.add_raw_member(\"game_setup\",\n                                      game_setup,\n                                      \"engine.util.setup.PlayerSetup\")\n\n    @staticmethod\n    def projectiles_from_line(line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Creates Projectile(GameEntity) raw API objects for a unit/building line.\n\n        :param line: Line for which the projectiles are extracted.\n        :type line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        game_entity_filename = name_lookup_dict[current_unit_id][1]\n\n        projectiles_location = f\"data/game_entity/generic/{game_entity_filename}/projectiles/\"\n\n        projectile_indices = []\n        projectile_primary = current_unit[\"projectile_id0\"].value\n        if projectile_primary > -1:\n            projectile_indices.append(0)\n\n        projectile_secondary = current_unit[\"projectile_id1\"].value\n        if projectile_secondary > -1:\n            projectile_indices.append(1)\n\n        for projectile_num in projectile_indices:\n            obj_ref = f\"{game_entity_name}.ShootProjectile.Projectile{projectile_num}\"\n            obj_name = f\"Projectile{projectile_num}\"\n            proj_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n            proj_raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n            proj_raw_api_object.set_location(projectiles_location)\n            proj_raw_api_object.set_filename(f\"{game_entity_filename}_projectiles\")\n\n            # =======================================================================\n            # Types\n            # =======================================================================\n            types_set = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Projectile\"].get_nyan_object()]\n            proj_raw_api_object.add_raw_member(\n                \"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Abilities\n            # =======================================================================\n            abilities_set = []\n            abilities_set.append(AoCAbilitySubprocessor.projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.move_projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(\n                line, 7, False, projectile_num))\n            # TODO: Death, Despawn\n            proj_raw_api_object.add_raw_member(\n                \"abilities\", abilities_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Modifiers\n            # =======================================================================\n            modifiers_set = []\n\n            modifiers_set.append(AoCModifierSubprocessor.flyover_effect_modifier(line))\n            modifiers_set.extend(AoCModifierSubprocessor.elevation_attack_modifiers(line))\n\n            proj_raw_api_object.add_raw_member(\n                \"modifiers\", modifiers_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Variants\n            # =======================================================================\n            variants_set = []\n            proj_raw_api_object.add_raw_member(\n                \"variants\", variants_set, \"engine.util.game_entity.GameEntity\")\n\n            line.add_raw_api_object(proj_raw_api_object)\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/pregen_processor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-locals,too-many-statements\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates nyan objects for things that are hardcoded into the Genie Engine,\nbut configurable in openage. E.g. HP.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom ....entity_object.conversion.converter_object import RawAPIObject, \\\n    ConverterObjectGroup\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n\n\nclass AoCPregenSubprocessor:\n    \"\"\"\n    Creates raw API objects for hardcoded settings in AoC.\n    \"\"\"\n\n    @classmethod\n    def generate(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects for hardcoded properties.\n        \"\"\"\n        # Stores pregenerated raw API objects as a container\n        pregen_converter_group = ConverterObjectGroup(\"pregen\")\n\n        cls.generate_activities(full_data_set, pregen_converter_group)\n        cls.generate_attributes(full_data_set, pregen_converter_group)\n        cls.generate_diplomatic_stances(full_data_set, pregen_converter_group)\n        cls.generate_team_property(full_data_set, pregen_converter_group)\n        cls.generate_entity_types(full_data_set, pregen_converter_group)\n        cls.generate_effect_types(full_data_set, pregen_converter_group)\n        cls.generate_exchange_objects(full_data_set, pregen_converter_group)\n        cls.generate_formation_types(full_data_set, pregen_converter_group)\n        cls.generate_language_objects(full_data_set, pregen_converter_group)\n        cls.generate_misc_effect_objects(full_data_set, pregen_converter_group)\n        cls.generate_modifiers(full_data_set, pregen_converter_group)\n        cls.generate_terrain_types(full_data_set, pregen_converter_group)\n        cls.generate_path_types(full_data_set, pregen_converter_group)\n        cls.generate_resources(full_data_set, pregen_converter_group)\n        cls.generate_death_condition(full_data_set, pregen_converter_group)\n\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        # Create nyan objects from the raw API objects\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_object()\n\n        # This has to be a separate for-loop because of possible object interdependencies\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_members()\n\n            if not pregen_object.is_ready():\n                raise RuntimeError(f\"{repr(pregen_object)}: Pregenerated object is not ready \"\n                                   \"for export. Member or object not initialized.\")\n\n    @staticmethod\n    def generate_activities(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate the activities for game entity behaviour.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        activity_parent = \"engine.util.activity.Activity\"\n        activity_location = \"data/util/activity/\"\n\n        # Node types\n        start_parent = \"engine.util.activity.node.type.Start\"\n        end_parent = \"engine.util.activity.node.type.End\"\n        ability_parent = \"engine.util.activity.node.type.Ability\"\n        xor_parent = \"engine.util.activity.node.type.XORGate\"\n        xor_event_parent = \"engine.util.activity.node.type.XOREventGate\"\n\n        # Condition types\n        condition_parent = \"engine.util.activity.condition.Condition\"\n        condition_queue_parent = \"engine.util.activity.condition.type.CommandInQueue\"\n        condition_next_move_parent = \"engine.util.activity.condition.type.NextCommandMove\"\n\n        # =======================================================================\n        # Default (Start -> Ability(Idle) -> End)\n        # =======================================================================\n        default_ref_in_modpack = \"util.activity.types.Default\"\n        default_raw_api_object = RawAPIObject(default_ref_in_modpack,\n                                              \"Default\", api_objects,\n                                              activity_location)\n        default_raw_api_object.set_filename(\"types\")\n        default_raw_api_object.add_raw_parent(activity_parent)\n\n        start_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.activity.types.Default.Start\")\n        default_raw_api_object.add_raw_member(\"start\", start_forward_ref,\n                                              activity_parent)\n\n        pregen_converter_group.add_raw_api_object(default_raw_api_object)\n        pregen_nyan_objects.update({default_ref_in_modpack: default_raw_api_object})\n\n        unit_forward_ref = ForwardRef(pregen_converter_group, default_ref_in_modpack)\n\n        # Start\n        start_ref_in_modpack = \"util.activity.types.Default.Start\"\n        start_raw_api_object = RawAPIObject(start_ref_in_modpack,\n                                            \"Start\", api_objects)\n        start_raw_api_object.set_location(unit_forward_ref)\n        start_raw_api_object.add_raw_parent(start_parent)\n\n        idle_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.activity.types.Default.Idle\")\n        start_raw_api_object.add_raw_member(\"next\", idle_forward_ref,\n                                            start_parent)\n\n        pregen_converter_group.add_raw_api_object(start_raw_api_object)\n        pregen_nyan_objects.update({start_ref_in_modpack: start_raw_api_object})\n\n        # Idle\n        idle_ref_in_modpack = \"util.activity.types.Default.Idle\"\n        idle_raw_api_object = RawAPIObject(idle_ref_in_modpack,\n                                           \"Idle\", api_objects)\n        idle_raw_api_object.set_location(unit_forward_ref)\n        idle_raw_api_object.add_raw_parent(ability_parent)\n\n        end_forward_ref = ForwardRef(pregen_converter_group,\n                                     \"util.activity.types.Default.End\")\n        idle_raw_api_object.add_raw_member(\"next\", end_forward_ref,\n                                           ability_parent)\n        idle_raw_api_object.add_raw_member(\"ability\",\n                                           api_objects[\"engine.ability.type.Idle\"],\n                                           ability_parent)\n\n        pregen_converter_group.add_raw_api_object(idle_raw_api_object)\n        pregen_nyan_objects.update({idle_ref_in_modpack: idle_raw_api_object})\n\n        # End\n        end_ref_in_modpack = \"util.activity.types.Default.End\"\n        end_raw_api_object = RawAPIObject(end_ref_in_modpack,\n                                          \"End\", api_objects)\n        end_raw_api_object.set_location(unit_forward_ref)\n        end_raw_api_object.add_raw_parent(end_parent)\n\n        pregen_converter_group.add_raw_api_object(end_raw_api_object)\n        pregen_nyan_objects.update({end_ref_in_modpack: end_raw_api_object})\n\n        # =======================================================================\n        # Units\n        # =======================================================================\n        unit_ref_in_modpack = \"util.activity.types.Unit\"\n        unit_raw_api_object = RawAPIObject(unit_ref_in_modpack,\n                                           \"Unit\", api_objects,\n                                           activity_location)\n        unit_raw_api_object.set_filename(\"types\")\n        unit_raw_api_object.add_raw_parent(activity_parent)\n\n        start_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.activity.types.Unit.Start\")\n        unit_raw_api_object.add_raw_member(\"start\", start_forward_ref,\n                                           activity_parent)\n\n        pregen_converter_group.add_raw_api_object(unit_raw_api_object)\n        pregen_nyan_objects.update({unit_ref_in_modpack: unit_raw_api_object})\n\n        unit_forward_ref = ForwardRef(pregen_converter_group, unit_ref_in_modpack)\n\n        # Start\n        start_ref_in_modpack = \"util.activity.types.Unit.Start\"\n        start_raw_api_object = RawAPIObject(start_ref_in_modpack,\n                                            \"Start\", api_objects)\n        start_raw_api_object.set_location(unit_forward_ref)\n        start_raw_api_object.add_raw_parent(start_parent)\n\n        idle_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.activity.types.Unit.Idle\")\n        start_raw_api_object.add_raw_member(\"next\", idle_forward_ref,\n                                            start_parent)\n\n        pregen_converter_group.add_raw_api_object(start_raw_api_object)\n        pregen_nyan_objects.update({start_ref_in_modpack: start_raw_api_object})\n\n        # Idle\n        idle_ref_in_modpack = \"util.activity.types.Unit.Idle\"\n        idle_raw_api_object = RawAPIObject(idle_ref_in_modpack,\n                                           \"Idle\", api_objects)\n        idle_raw_api_object.set_location(unit_forward_ref)\n        idle_raw_api_object.add_raw_parent(ability_parent)\n\n        queue_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.activity.types.Unit.CheckQueue\")\n        idle_raw_api_object.add_raw_member(\"next\", queue_forward_ref,\n                                           ability_parent)\n        idle_raw_api_object.add_raw_member(\"ability\",\n                                           api_objects[\"engine.ability.type.Idle\"],\n                                           ability_parent)\n\n        pregen_converter_group.add_raw_api_object(idle_raw_api_object)\n        pregen_nyan_objects.update({idle_ref_in_modpack: idle_raw_api_object})\n\n        # Check if command is in queue\n        queue_ref_in_modpack = \"util.activity.types.Unit.CheckQueue\"\n        queue_raw_api_object = RawAPIObject(queue_ref_in_modpack,\n                                            \"CheckQueue\", api_objects)\n        queue_raw_api_object.set_location(unit_forward_ref)\n        queue_raw_api_object.add_raw_parent(xor_parent)\n\n        condition_forward_ref = ForwardRef(pregen_converter_group,\n                                           \"util.activity.types.Unit.CommandInQueue\")\n        queue_raw_api_object.add_raw_member(\"next\",\n                                            [condition_forward_ref],\n                                            xor_parent)\n        command_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.activity.types.Unit.WaitForCommand\")\n        queue_raw_api_object.add_raw_member(\"default\",\n                                            command_forward_ref,\n                                            xor_parent)\n\n        pregen_converter_group.add_raw_api_object(queue_raw_api_object)\n        pregen_nyan_objects.update({queue_ref_in_modpack: queue_raw_api_object})\n\n        # condition for command in queue\n        condition_ref_in_modpack = \"util.activity.types.Unit.CommandInQueue\"\n        condition_raw_api_object = RawAPIObject(condition_ref_in_modpack,\n                                                \"CommandInQueue\", api_objects)\n        condition_raw_api_object.set_location(queue_forward_ref)\n        condition_raw_api_object.add_raw_parent(condition_queue_parent)\n\n        branch_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.activity.types.Unit.BranchCommand\")\n        condition_raw_api_object.add_raw_member(\"next\",\n                                                branch_forward_ref,\n                                                condition_parent)\n\n        pregen_converter_group.add_raw_api_object(condition_raw_api_object)\n        pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object})\n\n        # Wait for Command\n        command_ref_in_modpack = \"util.activity.types.Unit.WaitForCommand\"\n        command_raw_api_object = RawAPIObject(command_ref_in_modpack,\n                                              \"WaitForCommand\", api_objects)\n        command_raw_api_object.set_location(unit_forward_ref)\n        command_raw_api_object.add_raw_parent(xor_event_parent)\n\n        event_api_object = api_objects[\"engine.util.activity.event.type.CommandInQueue\"]\n        branch_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.activity.types.Unit.BranchCommand\")\n        command_raw_api_object.add_raw_member(\"next\",\n                                              {event_api_object: branch_forward_ref},\n                                              xor_event_parent)\n\n        pregen_converter_group.add_raw_api_object(command_raw_api_object)\n        pregen_nyan_objects.update({command_ref_in_modpack: command_raw_api_object})\n\n        # Branch on command type\n        branch_ref_in_modpack = \"util.activity.types.Unit.BranchCommand\"\n        branch_raw_api_object = RawAPIObject(branch_ref_in_modpack,\n                                             \"BranchCommand\", api_objects)\n        branch_raw_api_object.set_location(unit_forward_ref)\n        branch_raw_api_object.add_raw_parent(xor_parent)\n\n        condition_forward_ref = ForwardRef(pregen_converter_group,\n                                           \"util.activity.types.Unit.NextCommandMove\")\n        branch_raw_api_object.add_raw_member(\"next\",\n                                             [condition_forward_ref],\n                                             xor_parent)\n        idle_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.activity.types.Unit.Idle\")\n        branch_raw_api_object.add_raw_member(\"default\",\n                                             idle_forward_ref,\n                                             xor_parent)\n\n        pregen_converter_group.add_raw_api_object(branch_raw_api_object)\n        pregen_nyan_objects.update({branch_ref_in_modpack: branch_raw_api_object})\n\n        # condition for branching to move\n        condition_ref_in_modpack = \"util.activity.types.Unit.NextCommandMove\"\n        condition_raw_api_object = RawAPIObject(condition_ref_in_modpack,\n                                                \"NextCommandMove\", api_objects)\n        condition_raw_api_object.set_location(branch_forward_ref)\n        condition_raw_api_object.add_raw_parent(condition_next_move_parent)\n\n        move_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.activity.types.Unit.Move\")\n        condition_raw_api_object.add_raw_member(\"next\",\n                                                move_forward_ref,\n                                                condition_parent)\n\n        pregen_converter_group.add_raw_api_object(condition_raw_api_object)\n        pregen_nyan_objects.update({condition_ref_in_modpack: condition_raw_api_object})\n\n        # Move\n        move_ref_in_modpack = \"util.activity.types.Unit.Move\"\n        move_raw_api_object = RawAPIObject(move_ref_in_modpack,\n                                           \"Move\", api_objects)\n        move_raw_api_object.set_location(unit_forward_ref)\n        move_raw_api_object.add_raw_parent(ability_parent)\n\n        wait_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.activity.types.Unit.Wait\")\n        move_raw_api_object.add_raw_member(\"next\", wait_forward_ref,\n                                           ability_parent)\n        move_raw_api_object.add_raw_member(\"ability\",\n                                           api_objects[\"engine.ability.type.Move\"],\n                                           ability_parent)\n\n        pregen_converter_group.add_raw_api_object(move_raw_api_object)\n        pregen_nyan_objects.update({move_ref_in_modpack: move_raw_api_object})\n\n        # Wait (for Move or Command)\n        wait_ref_in_modpack = \"util.activity.types.Unit.Wait\"\n        wait_raw_api_object = RawAPIObject(wait_ref_in_modpack,\n                                           \"Wait\", api_objects)\n        wait_raw_api_object.set_location(unit_forward_ref)\n        wait_raw_api_object.add_raw_parent(xor_event_parent)\n\n        wait_finish = api_objects[\"engine.util.activity.event.type.WaitAbility\"]\n        wait_command = api_objects[\"engine.util.activity.event.type.CommandInQueue\"]\n        wait_raw_api_object.add_raw_member(\"next\",\n                                           {\n                                               wait_finish: idle_forward_ref,\n                                               # TODO: don't go back to move, go to xor gate that\n                                               # branches depending on command\n                                               wait_command: branch_forward_ref\n                                           },\n                                           xor_event_parent)\n\n        pregen_converter_group.add_raw_api_object(wait_raw_api_object)\n        pregen_nyan_objects.update({wait_ref_in_modpack: wait_raw_api_object})\n\n        # End\n        end_ref_in_modpack = \"util.activity.types.Unit.End\"\n        end_raw_api_object = RawAPIObject(end_ref_in_modpack,\n                                          \"End\", api_objects)\n        end_raw_api_object.set_location(unit_forward_ref)\n        end_raw_api_object.add_raw_parent(end_parent)\n\n        pregen_converter_group.add_raw_api_object(end_raw_api_object)\n        pregen_nyan_objects.update({end_ref_in_modpack: end_raw_api_object})\n\n    @staticmethod\n    def generate_attributes(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate Attribute objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # TODO: Fill translations\n        # =======================================================================\n        attribute_parent = \"engine.util.attribute.Attribute\"\n        attributes_location = \"data/util/attribute/\"\n\n        # =======================================================================\n        # HP\n        # =======================================================================\n        health_ref_in_modpack = \"util.attribute.types.Health\"\n        health_raw_api_object = RawAPIObject(health_ref_in_modpack,\n                                             \"Health\", api_objects,\n                                             attributes_location)\n        health_raw_api_object.set_filename(\"types\")\n        health_raw_api_object.add_raw_parent(attribute_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.attribute.types.Health.HealthName\")\n        health_raw_api_object.add_raw_member(\"name\", name_forward_ref,\n                                             attribute_parent)\n        abbrv_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.attribute.types.Health.HealthAbbreviation\")\n        health_raw_api_object.add_raw_member(\"abbreviation\", abbrv_forward_ref,\n                                             attribute_parent)\n\n        pregen_converter_group.add_raw_api_object(health_raw_api_object)\n        pregen_nyan_objects.update({health_ref_in_modpack: health_raw_api_object})\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        health_name_ref_in_modpack = \"util.attribute.types.Health.HealthName\"\n        health_name_value = RawAPIObject(health_name_ref_in_modpack, \"HealthName\",\n                                         api_objects, attributes_location)\n        health_name_value.set_filename(\"types\")\n        health_name_value.add_raw_parent(name_value_parent)\n        health_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        pregen_converter_group.add_raw_api_object(health_name_value)\n        pregen_nyan_objects.update({health_name_ref_in_modpack: health_name_value})\n\n        abbrv_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        health_abbrv_ref_in_modpack = \"util.attribute.types.Health.HealthAbbreviation\"\n        health_abbrv_value = RawAPIObject(health_abbrv_ref_in_modpack, \"HealthAbbreviation\",\n                                          api_objects, attributes_location)\n        health_abbrv_value.set_filename(\"types\")\n        health_abbrv_value.add_raw_parent(abbrv_value_parent)\n        health_abbrv_value.add_raw_member(\"translations\", [], abbrv_value_parent)\n\n        pregen_converter_group.add_raw_api_object(health_abbrv_value)\n        pregen_nyan_objects.update({health_abbrv_ref_in_modpack: health_abbrv_value})\n\n        # =======================================================================\n        # Faith\n        # =======================================================================\n        faith_ref_in_modpack = \"util.attribute.types.Faith\"\n        faith_raw_api_object = RawAPIObject(faith_ref_in_modpack,\n                                            \"Faith\", api_objects,\n                                            attributes_location)\n        faith_raw_api_object.set_filename(\"types\")\n        faith_raw_api_object.add_raw_parent(attribute_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.attribute.types.Faith.FaithName\")\n        faith_raw_api_object.add_raw_member(\"name\", name_forward_ref,\n                                            attribute_parent)\n        abbrv_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.attribute.types.Faith.FaithAbbreviation\")\n        faith_raw_api_object.add_raw_member(\"abbreviation\", abbrv_forward_ref,\n                                            attribute_parent)\n\n        pregen_converter_group.add_raw_api_object(faith_raw_api_object)\n        pregen_nyan_objects.update({faith_ref_in_modpack: faith_raw_api_object})\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        faith_name_ref_in_modpack = \"util.attribute.types.Faith.FaithName\"\n        faith_name_value = RawAPIObject(faith_name_ref_in_modpack, \"FaithName\",\n                                        api_objects, attributes_location)\n        faith_name_value.set_filename(\"types\")\n        faith_name_value.add_raw_parent(name_value_parent)\n        faith_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        pregen_converter_group.add_raw_api_object(faith_name_value)\n        pregen_nyan_objects.update({faith_name_ref_in_modpack: faith_name_value})\n\n        abbrv_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        faith_abbrv_ref_in_modpack = \"util.attribute.types.Faith.FaithAbbreviation\"\n        faith_abbrv_value = RawAPIObject(faith_abbrv_ref_in_modpack, \"FaithAbbreviation\",\n                                         api_objects, attributes_location)\n        faith_abbrv_value.set_filename(\"types\")\n        faith_abbrv_value.add_raw_parent(abbrv_value_parent)\n        faith_abbrv_value.add_raw_member(\"translations\", [], abbrv_value_parent)\n\n        pregen_converter_group.add_raw_api_object(faith_abbrv_value)\n        pregen_nyan_objects.update({faith_abbrv_ref_in_modpack: faith_abbrv_value})\n\n    @staticmethod\n    def generate_diplomatic_stances(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate DiplomaticStance objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        stance_parent = \"engine.util.diplomatic_stance.DiplomaticStance\"\n        stance_location = \"data/util/diplomatic_stance/\"\n\n        # =======================================================================\n        # Enemy\n        # =======================================================================\n        enemy_ref_in_modpack = \"util.diplomatic_stance.types.Enemy\"\n        enemy_raw_api_object = RawAPIObject(enemy_ref_in_modpack,\n                                            \"Enemy\", api_objects,\n                                            stance_location)\n        enemy_raw_api_object.set_filename(\"types\")\n        enemy_raw_api_object.add_raw_parent(stance_parent)\n\n        pregen_converter_group.add_raw_api_object(enemy_raw_api_object)\n        pregen_nyan_objects.update({enemy_ref_in_modpack: enemy_raw_api_object})\n\n        # =======================================================================\n        # Neutral\n        # =======================================================================\n        neutral_ref_in_modpack = \"util.diplomatic_stance.types.Neutral\"\n        neutral_raw_api_object = RawAPIObject(neutral_ref_in_modpack,\n                                              \"Neutral\", api_objects,\n                                              stance_location)\n        neutral_raw_api_object.set_filename(\"types\")\n        neutral_raw_api_object.add_raw_parent(stance_parent)\n\n        pregen_converter_group.add_raw_api_object(neutral_raw_api_object)\n        pregen_nyan_objects.update({neutral_ref_in_modpack: neutral_raw_api_object})\n\n        # =======================================================================\n        # Friendly\n        # =======================================================================\n        friendly_ref_in_modpack = \"util.diplomatic_stance.types.Friendly\"\n        friendly_raw_api_object = RawAPIObject(friendly_ref_in_modpack,\n                                               \"Friendly\", api_objects,\n                                               stance_location)\n        friendly_raw_api_object.set_filename(\"types\")\n        friendly_raw_api_object.add_raw_parent(stance_parent)\n\n        pregen_converter_group.add_raw_api_object(friendly_raw_api_object)\n        pregen_nyan_objects.update({friendly_ref_in_modpack: friendly_raw_api_object})\n\n        # =======================================================================\n        # Gaia\n        # =======================================================================\n        gaia_ref_in_modpack = \"util.diplomatic_stance.types.Gaia\"\n        gaia_raw_api_object = RawAPIObject(gaia_ref_in_modpack,\n                                           \"Gaia\", api_objects,\n                                           stance_location)\n        gaia_raw_api_object.set_filename(\"types\")\n        gaia_raw_api_object.add_raw_parent(stance_parent)\n\n        pregen_converter_group.add_raw_api_object(gaia_raw_api_object)\n        pregen_nyan_objects.update({gaia_ref_in_modpack: gaia_raw_api_object})\n\n    @staticmethod\n    def generate_team_property(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate the property used in team patches objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        prop_ref_in_modpack = \"util.patch.property.types.Team\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"Team\",\n                                           api_objects,\n                                           \"data/util/patch/property/\")\n        prop_raw_api_object.set_filename(\"types\")\n        prop_raw_api_object.add_raw_parent(\"engine.util.patch.property.type.Diplomatic\")\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        stances = [\n            full_data_set.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"],\n            ForwardRef(pregen_converter_group, \"util.diplomatic_stance.types.Friendly\")\n        ]\n        prop_raw_api_object.add_raw_member(\"stances\",\n                                           stances,\n                                           \"engine.util.patch.property.type.Diplomatic\")\n\n    @staticmethod\n    def generate_entity_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate GameEntityType objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        class_lookup_dict = internal_name_lookups.get_class_lookups(full_data_set.game_version)\n\n        type_parent = \"engine.util.game_entity_type.GameEntityType\"\n        types_location = \"data/util/game_entity_type/\"\n\n        # =======================================================================\n        # Ambient\n        # =======================================================================\n        ambient_ref_in_modpack = \"util.game_entity_type.types.Ambient\"\n        ambient_raw_api_object = RawAPIObject(ambient_ref_in_modpack,\n                                              \"Ambient\", api_objects,\n                                              types_location)\n        ambient_raw_api_object.set_filename(\"types\")\n        ambient_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(ambient_raw_api_object)\n        pregen_nyan_objects.update({ambient_ref_in_modpack: ambient_raw_api_object})\n\n        # =======================================================================\n        # Building\n        # =======================================================================\n        building_ref_in_modpack = \"util.game_entity_type.types.Building\"\n        building_raw_api_object = RawAPIObject(building_ref_in_modpack,\n                                               \"Building\", api_objects,\n                                               types_location)\n        building_raw_api_object.set_filename(\"types\")\n        building_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(building_raw_api_object)\n        pregen_nyan_objects.update({building_ref_in_modpack: building_raw_api_object})\n\n        # =======================================================================\n        # Item\n        # =======================================================================\n        item_ref_in_modpack = \"util.game_entity_type.types.Item\"\n        item_raw_api_object = RawAPIObject(item_ref_in_modpack,\n                                           \"Item\", api_objects,\n                                           types_location)\n        item_raw_api_object.set_filename(\"types\")\n        item_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(item_raw_api_object)\n        pregen_nyan_objects.update({item_ref_in_modpack: item_raw_api_object})\n\n        # =======================================================================\n        # Projectile\n        # =======================================================================\n        projectile_ref_in_modpack = \"util.game_entity_type.types.Projectile\"\n        projectile_raw_api_object = RawAPIObject(projectile_ref_in_modpack,\n                                                 \"Projectile\", api_objects,\n                                                 types_location)\n        projectile_raw_api_object.set_filename(\"types\")\n        projectile_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(projectile_raw_api_object)\n        pregen_nyan_objects.update({projectile_ref_in_modpack: projectile_raw_api_object})\n\n        # =======================================================================\n        # Unit\n        # =======================================================================\n        unit_ref_in_modpack = \"util.game_entity_type.types.Unit\"\n        unit_raw_api_object = RawAPIObject(unit_ref_in_modpack,\n                                           \"Unit\", api_objects,\n                                           types_location)\n        unit_raw_api_object.set_filename(\"types\")\n        unit_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(unit_raw_api_object)\n        pregen_nyan_objects.update({unit_ref_in_modpack: unit_raw_api_object})\n\n        # =======================================================================\n        # DropSite\n        # =======================================================================\n        drop_site_ref_in_modpack = \"util.game_entity_type.types.DropSite\"\n        drop_site_raw_api_object = RawAPIObject(drop_site_ref_in_modpack,\n                                                \"DropSite\", api_objects,\n                                                types_location)\n        drop_site_raw_api_object.set_filename(\"types\")\n        drop_site_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(drop_site_raw_api_object)\n        pregen_nyan_objects.update({drop_site_ref_in_modpack: drop_site_raw_api_object})\n\n        # =======================================================================\n        # Others (generated from class ID)\n        # =======================================================================\n        converter_groups = []\n        converter_groups.extend(full_data_set.unit_lines.values())\n        converter_groups.extend(full_data_set.building_lines.values())\n        converter_groups.extend(full_data_set.ambient_groups.values())\n        converter_groups.extend(full_data_set.variant_groups.values())\n\n        for unit_line in converter_groups:\n            unit_class = unit_line.get_class_id()\n            class_name = class_lookup_dict[unit_class]\n            class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n\n            new_game_entity_type = RawAPIObject(class_obj_name, class_name,\n                                                full_data_set.nyan_api_objects,\n                                                types_location)\n            new_game_entity_type.set_filename(\"types\")\n            new_game_entity_type.add_raw_parent(\"engine.util.game_entity_type.GameEntityType\")\n            new_game_entity_type.create_nyan_object()\n\n            pregen_converter_group.add_raw_api_object(new_game_entity_type)\n            pregen_nyan_objects.update({class_obj_name: new_game_entity_type})\n\n    @staticmethod\n    def generate_effect_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate types for effects and resistances.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(full_data_set.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(\n            full_data_set.game_version)\n\n        # =======================================================================\n        # Armor types\n        # =======================================================================\n        type_parent = \"engine.util.attribute_change_type.AttributeChangeType\"\n        types_location = \"data/util/attribute_change_type/\"\n\n        for type_name in armor_lookup_dict.values():\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{type_name}\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               type_name, api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Heal\n        # =======================================================================\n        type_ref_in_modpack = \"util.attribute_change_type.types.Heal\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"Heal\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Repair (one for each repairable entity)\n        # =======================================================================\n        repairable_lines = []\n        repairable_lines.extend(full_data_set.building_lines.values())\n        for unit_line in full_data_set.unit_lines.values():\n            if unit_line.is_repairable():\n                repairable_lines.append(unit_line)\n\n        for repairable_line in repairable_lines:\n            game_entity_name = name_lookup_dict[repairable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{game_entity_name}Repair\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Repair\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Construct (two for each constructable entity)\n        # =======================================================================\n        constructable_lines = []\n        constructable_lines.extend(full_data_set.building_lines.values())\n\n        for constructable_line in constructable_lines:\n            game_entity_name = name_lookup_dict[constructable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{game_entity_name}Construct\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Construct\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        type_parent = \"engine.util.progress_type.type.Construct\"\n        types_location = \"data/util/construct_type/\"\n\n        for constructable_line in constructable_lines:\n            game_entity_name = name_lookup_dict[constructable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.construct_type.types.{game_entity_name}Construct\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Construct\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # ConvertType: UnitConvert\n        # =======================================================================\n        type_parent = \"engine.util.convert_type.ConvertType\"\n        types_location = \"data/util/convert_type/\"\n\n        type_ref_in_modpack = \"util.convert_type.types.UnitConvert\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"UnitConvert\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # ConvertType: BuildingConvert\n        # =======================================================================\n        type_parent = \"engine.util.convert_type.ConvertType\"\n        types_location = \"data/util/convert_type/\"\n\n        type_ref_in_modpack = \"util.convert_type.types.BuildingConvert\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"BuildingConvert\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n    @staticmethod\n    def generate_exchange_objects(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate objects for market trading (ExchangeResources).\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Exchange mode Buy\n        # =======================================================================\n        exchange_mode_parent = \"engine.util.exchange_mode.type.Buy\"\n        exchange_mode_location = \"data/util/resource/\"\n\n        exchange_mode_ref_in_modpack = \"util.resource.market_trading.MarketBuyExchangeMode\"\n        exchange_mode_raw_api_object = RawAPIObject(exchange_mode_ref_in_modpack,\n                                                    \"MarketBuyExchangePool\",\n                                                    api_objects,\n                                                    exchange_mode_location)\n        exchange_mode_raw_api_object.set_filename(\"market_trading\")\n        exchange_mode_raw_api_object.add_raw_parent(exchange_mode_parent)\n\n        # Fee (30% on top)\n        exchange_mode_raw_api_object.add_raw_member(\"fee_multiplier\",\n                                                    1.3,\n                                                    \"engine.util.exchange_mode.ExchangeMode\")\n\n        pregen_converter_group.add_raw_api_object(exchange_mode_raw_api_object)\n        pregen_nyan_objects.update({exchange_mode_ref_in_modpack: exchange_mode_raw_api_object})\n\n        # =======================================================================\n        # Exchange mode Sell\n        # =======================================================================\n        exchange_mode_parent = \"engine.util.exchange_mode.type.Sell\"\n        exchange_mode_location = \"data/util/resource/\"\n\n        exchange_mode_ref_in_modpack = \"util.resource.market_trading.MarketSellExchangeMode\"\n        exchange_mode_raw_api_object = RawAPIObject(exchange_mode_ref_in_modpack,\n                                                    \"MarketSellExchangeMode\",\n                                                    api_objects,\n                                                    exchange_mode_location)\n        exchange_mode_raw_api_object.set_filename(\"market_trading\")\n        exchange_mode_raw_api_object.add_raw_parent(exchange_mode_parent)\n\n        # Fee (30% reduced)\n        exchange_mode_raw_api_object.add_raw_member(\"fee_multiplier\",\n                                                    0.7,\n                                                    \"engine.util.exchange_mode.ExchangeMode\")\n\n        pregen_converter_group.add_raw_api_object(exchange_mode_raw_api_object)\n        pregen_nyan_objects.update({exchange_mode_ref_in_modpack: exchange_mode_raw_api_object})\n\n        # =======================================================================\n        # Market Food price pool\n        # =======================================================================\n        exchange_pool_parent = \"engine.util.price_pool.PricePool\"\n        exchange_pool_location = \"data/util/resource/\"\n\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketFoodPricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketFoodPricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Market Wood price pool\n        # =======================================================================\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketWoodPricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketWoodPricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Market Stone price pool\n        # =======================================================================\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketStonePricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketStonePricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Food\n        # =======================================================================\n        exchange_rate_parent = \"engine.util.exchange_rate.ExchangeRate\"\n        exchange_rate_location = \"data/util/resource/\"\n\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketFoodExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketFoodExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.0,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketFoodPricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Wood\n        # =======================================================================\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketWoodExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketWoodExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.0,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketWoodPricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Stone\n        # =======================================================================\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketStoneExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketStoneExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.3,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketStonePricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Buy Price mode\n        # =======================================================================\n        price_mode_parent = \"engine.util.price_mode.type.Dynamic\"\n        price_mode_location = \"data/util/resource/\"\n\n        price_mode_ref_in_modpack = \"util.resource.market_trading.MarketBuyPriceMode\"\n        price_mode_raw_api_object = RawAPIObject(price_mode_ref_in_modpack,\n                                                 \"MarketBuyPriceMode\",\n                                                 api_objects,\n                                                 price_mode_location)\n        price_mode_raw_api_object.set_filename(\"market_trading\")\n        price_mode_raw_api_object.add_raw_parent(price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"change_value\",\n                                                 0.03,\n                                                 price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"min_price\",\n                                                 0.3,\n                                                 price_mode_parent)\n\n        # Max price\n        price_mode_raw_api_object.add_raw_member(\"max_price\",\n                                                 99.9,\n                                                 price_mode_parent)\n\n        pregen_converter_group.add_raw_api_object(price_mode_raw_api_object)\n        pregen_nyan_objects.update({price_mode_ref_in_modpack: price_mode_raw_api_object})\n\n        # =======================================================================\n        # Sell Price mode\n        # =======================================================================\n        price_mode_parent = \"engine.util.price_mode.type.Dynamic\"\n        price_mode_location = \"data/util/resource/\"\n\n        price_mode_ref_in_modpack = \"util.resource.market_trading.MarketSellPriceMode\"\n        price_mode_raw_api_object = RawAPIObject(price_mode_ref_in_modpack,\n                                                 \"MarketSellPriceMode\",\n                                                 api_objects,\n                                                 price_mode_location)\n        price_mode_raw_api_object.set_filename(\"market_trading\")\n        price_mode_raw_api_object.add_raw_parent(price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"change_value\",\n                                                 -0.03,\n                                                 price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"min_price\",\n                                                 0.3,\n                                                 price_mode_parent)\n\n        # Max price\n        price_mode_raw_api_object.add_raw_member(\"max_price\",\n                                                 99.9,\n                                                 price_mode_parent)\n\n        pregen_converter_group.add_raw_api_object(price_mode_raw_api_object)\n        pregen_nyan_objects.update({price_mode_ref_in_modpack: price_mode_raw_api_object})\n\n    @staticmethod\n    def generate_formation_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate Formation and Subformation objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Line formation\n        # =======================================================================\n        formation_parent = \"engine.util.formation.Formation\"\n        formation_location = \"data/util/formation/\"\n\n        formation_ref_in_modpack = \"util.formation.types.Line\"\n        formation_raw_api_object = RawAPIObject(formation_ref_in_modpack,\n                                                \"Line\",\n                                                api_objects,\n                                                formation_location)\n        formation_raw_api_object.set_filename(\"types\")\n        formation_raw_api_object.add_raw_parent(formation_parent)\n\n        subformations = [\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Cavalry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Infantry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Ranged\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Siege\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Support\"),\n        ]\n        formation_raw_api_object.add_raw_member(\"subformations\",\n                                                subformations,\n                                                formation_parent)\n\n        pregen_converter_group.add_raw_api_object(formation_raw_api_object)\n        pregen_nyan_objects.update({formation_ref_in_modpack: formation_raw_api_object})\n        # =======================================================================\n        # Staggered formation\n        # =======================================================================\n        formation_ref_in_modpack = \"util.formation.types.Staggered\"\n        formation_raw_api_object = RawAPIObject(formation_ref_in_modpack,\n                                                \"Staggered\",\n                                                api_objects,\n                                                formation_location)\n        formation_raw_api_object.set_filename(\"types\")\n        formation_raw_api_object.add_raw_parent(formation_parent)\n\n        subformations = [\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Cavalry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Infantry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Ranged\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Siege\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Support\"),\n        ]\n        formation_raw_api_object.add_raw_member(\"subformations\",\n                                                subformations,\n                                                formation_parent)\n\n        pregen_converter_group.add_raw_api_object(formation_raw_api_object)\n        pregen_nyan_objects.update({formation_ref_in_modpack: formation_raw_api_object})\n        # =======================================================================\n        # Box formation\n        # =======================================================================\n        formation_ref_in_modpack = \"util.formation.types.Box\"\n        formation_raw_api_object = RawAPIObject(formation_ref_in_modpack,\n                                                \"Box\",\n                                                api_objects,\n                                                formation_location)\n        formation_raw_api_object.set_filename(\"types\")\n        formation_raw_api_object.add_raw_parent(formation_parent)\n\n        subformations = [\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Cavalry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Infantry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Ranged\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Siege\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Support\"),\n        ]\n        formation_raw_api_object.add_raw_member(\"subformations\",\n                                                subformations,\n                                                formation_parent)\n\n        pregen_converter_group.add_raw_api_object(formation_raw_api_object)\n        pregen_nyan_objects.update({formation_ref_in_modpack: formation_raw_api_object})\n        # =======================================================================\n        # Flank formation\n        # =======================================================================\n        formation_ref_in_modpack = \"util.formation.types.Flank\"\n        formation_raw_api_object = RawAPIObject(formation_ref_in_modpack,\n                                                \"Flank\",\n                                                api_objects,\n                                                formation_location)\n        formation_raw_api_object.set_filename(\"types\")\n        formation_raw_api_object.add_raw_parent(formation_parent)\n\n        subformations = [\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Cavalry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Infantry\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Ranged\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Siege\"),\n            ForwardRef(pregen_converter_group, \"util.formation.subformation.types.Support\"),\n        ]\n        formation_raw_api_object.add_raw_member(\"subformations\",\n                                                subformations,\n                                                formation_parent)\n\n        pregen_converter_group.add_raw_api_object(formation_raw_api_object)\n        pregen_nyan_objects.update({formation_ref_in_modpack: formation_raw_api_object})\n\n        # =======================================================================\n        # Cavalry subformation\n        # =======================================================================\n        subformation_parent = \"engine.util.formation.Subformation\"\n        subformation_location = \"data/util/formation/\"\n\n        subformation_ref_in_modpack = \"util.formation.subformation.types.Cavalry\"\n        subformation_raw_api_object = RawAPIObject(subformation_ref_in_modpack,\n                                                   \"Cavalry\",\n                                                   api_objects,\n                                                   subformation_location)\n        subformation_raw_api_object.set_filename(\"subformations\")\n        subformation_raw_api_object.add_raw_parent(subformation_parent)\n\n        subformation_raw_api_object.add_raw_member(\"ordering_priority\",\n                                                   5,\n                                                   subformation_parent)\n\n        pregen_converter_group.add_raw_api_object(subformation_raw_api_object)\n        pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object})\n\n        # =======================================================================\n        # Infantry subformation\n        # =======================================================================\n        subformation_ref_in_modpack = \"util.formation.subformation.types.Infantry\"\n        subformation_raw_api_object = RawAPIObject(subformation_ref_in_modpack,\n                                                   \"Infantry\",\n                                                   api_objects,\n                                                   subformation_location)\n        subformation_raw_api_object.set_filename(\"subformations\")\n        subformation_raw_api_object.add_raw_parent(subformation_parent)\n\n        subformation_raw_api_object.add_raw_member(\"ordering_priority\",\n                                                   4,\n                                                   subformation_parent)\n\n        pregen_converter_group.add_raw_api_object(subformation_raw_api_object)\n        pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object})\n\n        # =======================================================================\n        # Ranged subformation\n        # =======================================================================\n        subformation_ref_in_modpack = \"util.formation.subformation.types.Ranged\"\n        subformation_raw_api_object = RawAPIObject(subformation_ref_in_modpack,\n                                                   \"Ranged\",\n                                                   api_objects,\n                                                   subformation_location)\n        subformation_raw_api_object.set_filename(\"subformations\")\n        subformation_raw_api_object.add_raw_parent(subformation_parent)\n\n        subformation_raw_api_object.add_raw_member(\"ordering_priority\",\n                                                   3,\n                                                   subformation_parent)\n\n        pregen_converter_group.add_raw_api_object(subformation_raw_api_object)\n        pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object})\n\n        # =======================================================================\n        # Siege subformation\n        # =======================================================================\n        subformation_ref_in_modpack = \"util.formation.subformation.types.Siege\"\n        subformation_raw_api_object = RawAPIObject(subformation_ref_in_modpack,\n                                                   \"Siege\",\n                                                   api_objects,\n                                                   subformation_location)\n        subformation_raw_api_object.set_filename(\"subformations\")\n        subformation_raw_api_object.add_raw_parent(subformation_parent)\n\n        subformation_raw_api_object.add_raw_member(\"ordering_priority\",\n                                                   2,\n                                                   subformation_parent)\n\n        pregen_converter_group.add_raw_api_object(subformation_raw_api_object)\n        pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object})\n\n        # =======================================================================\n        # Support subformation\n        # =======================================================================\n        subformation_ref_in_modpack = \"util.formation.subformation.types.Support\"\n        subformation_raw_api_object = RawAPIObject(subformation_ref_in_modpack,\n                                                   \"Support\",\n                                                   api_objects,\n                                                   subformation_location)\n        subformation_raw_api_object.set_filename(\"subformations\")\n        subformation_raw_api_object.add_raw_parent(subformation_parent)\n\n        subformation_raw_api_object.add_raw_member(\"ordering_priority\",\n                                                   1,\n                                                   subformation_parent)\n\n        pregen_converter_group.add_raw_api_object(subformation_raw_api_object)\n        pregen_nyan_objects.update({subformation_ref_in_modpack: subformation_raw_api_object})\n\n    @staticmethod\n    def generate_language_objects(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate language objects from the string resources\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        language_parent = \"engine.util.language.Language\"\n        language_location = \"data/util/language/\"\n\n        languages = full_data_set.strings.get_tables().keys()\n\n        for language in languages:\n            language_ref_in_modpack = f\"util.language.{language}\"\n            language_raw_api_object = RawAPIObject(language_ref_in_modpack,\n                                                   language,\n                                                   api_objects,\n                                                   language_location)\n            language_raw_api_object.set_filename(\"language\")\n            language_raw_api_object.add_raw_parent(language_parent)\n\n            language_raw_api_object.add_raw_member(\"ietf_string\",\n                                                   language,\n                                                   language_parent)\n\n            pregen_converter_group.add_raw_api_object(language_raw_api_object)\n            pregen_nyan_objects.update({language_ref_in_modpack: language_raw_api_object})\n\n    @staticmethod\n    def generate_misc_effect_objects(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate fallback types and other standard objects for effects and resistances.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Min change value (lower cealing for attack effects)\n        # =======================================================================\n        min_change_parent = \"engine.util.attribute.AttributeAmount\"\n        min_change_location = \"data/effect/discrete/flat_attribute_change/\"\n\n        change_ref_in_modpack = \"effect.discrete.flat_attribute_change.min_damage.AoE2MinChangeAmount\"\n        change_raw_api_object = RawAPIObject(change_ref_in_modpack,\n                                             \"AoE2MinChangeAmount\",\n                                             api_objects,\n                                             min_change_location)\n        change_raw_api_object.set_filename(\"min_damage\")\n        change_raw_api_object.add_raw_parent(min_change_parent)\n\n        attribute = ForwardRef(pregen_converter_group, \"util.attribute.types.Health\")\n        change_raw_api_object.add_raw_member(\"type\",\n                                             attribute,\n                                             min_change_parent)\n        change_raw_api_object.add_raw_member(\"amount\",\n                                             0,\n                                             min_change_parent)\n\n        pregen_converter_group.add_raw_api_object(change_raw_api_object)\n        pregen_nyan_objects.update({change_ref_in_modpack: change_raw_api_object})\n\n        # =======================================================================\n        # Min change value (lower cealing for heal effects)\n        # =======================================================================\n        min_change_parent = \"engine.util.attribute.AttributeRate\"\n        min_change_location = \"data/effect/discrete/flat_attribute_change/\"\n\n        change_ref_in_modpack = \"effect.discrete.flat_attribute_change.min_heal.AoE2MinChangeAmount\"\n        change_raw_api_object = RawAPIObject(change_ref_in_modpack,\n                                             \"AoE2MinChangeAmount\",\n                                             api_objects,\n                                             min_change_location)\n        change_raw_api_object.set_filename(\"min_heal\")\n        change_raw_api_object.add_raw_parent(min_change_parent)\n\n        attribute = ForwardRef(pregen_converter_group, \"util.attribute.types.Health\")\n        change_raw_api_object.add_raw_member(\"type\",\n                                             attribute,\n                                             min_change_parent)\n        change_raw_api_object.add_raw_member(\"rate\",\n                                             0,\n                                             min_change_parent)\n\n        pregen_converter_group.add_raw_api_object(change_raw_api_object)\n        pregen_nyan_objects.update({change_ref_in_modpack: change_raw_api_object})\n\n        # =======================================================================\n        # Fallback effect for attacking (= minimum damage)\n        # =======================================================================\n        effect_parent = \"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"\n        fallback_parent = \"engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n        fallback_location = \"data/effect/discrete/flat_attribute_change/\"\n\n        fallback_ref_in_modpack = \"effect.discrete.flat_attribute_change.fallback.AoE2AttackFallback\"\n        fallback_raw_api_object = RawAPIObject(fallback_ref_in_modpack,\n                                               \"AoE2AttackFallback\",\n                                               api_objects,\n                                               fallback_location)\n        fallback_raw_api_object.set_filename(\"fallback\")\n        fallback_raw_api_object.add_raw_parent(fallback_parent)\n\n        # Type\n        type_ref = \"engine.util.attribute_change_type.type.Fallback\"\n        change_type = api_objects[type_ref]\n        fallback_raw_api_object.add_raw_member(\"type\",\n                                               change_type,\n                                               effect_parent)\n\n        # Min value (optional)\n        # =================================================================================\n        amount_name = f\"{fallback_ref_in_modpack}.LowerCealing\"\n        amount_raw_api_object = RawAPIObject(amount_name, \"LowerCealing\", api_objects)\n        amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n        amount_location = ForwardRef(pregen_converter_group, fallback_ref_in_modpack)\n        amount_raw_api_object.set_location(amount_location)\n\n        attribute = ForwardRef(pregen_converter_group, \"util.attribute.types.Health\")\n        amount_raw_api_object.add_raw_member(\"type\",\n                                             attribute,\n                                             \"engine.util.attribute.AttributeAmount\")\n        amount_raw_api_object.add_raw_member(\"amount\",\n                                             1,\n                                             \"engine.util.attribute.AttributeAmount\")\n\n        pregen_converter_group.add_raw_api_object(amount_raw_api_object)\n        pregen_nyan_objects.update({amount_name: amount_raw_api_object})\n        # =================================================================================\n        amount_forward_ref = ForwardRef(pregen_converter_group, amount_name)\n        fallback_raw_api_object.add_raw_member(\"min_change_value\",\n                                               amount_forward_ref,\n                                               effect_parent)\n\n        # Max value (optional; not needed\n\n        # Change value\n        # =================================================================================\n        amount_name = f\"{fallback_ref_in_modpack}.ChangeAmount\"\n        amount_raw_api_object = RawAPIObject(amount_name, \"ChangeAmount\", api_objects)\n        amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n        amount_location = ForwardRef(pregen_converter_group, fallback_ref_in_modpack)\n        amount_raw_api_object.set_location(amount_location)\n\n        attribute = ForwardRef(pregen_converter_group, \"util.attribute.types.Health\")\n        amount_raw_api_object.add_raw_member(\"type\",\n                                             attribute,\n                                             \"engine.util.attribute.AttributeAmount\")\n        amount_raw_api_object.add_raw_member(\"amount\",\n                                             1,\n                                             \"engine.util.attribute.AttributeAmount\")\n\n        pregen_converter_group.add_raw_api_object(amount_raw_api_object)\n        pregen_nyan_objects.update({amount_name: amount_raw_api_object})\n\n        # =================================================================================\n        amount_forward_ref = ForwardRef(pregen_converter_group, amount_name)\n        fallback_raw_api_object.add_raw_member(\"change_value\",\n                                               amount_forward_ref,\n                                               effect_parent)\n\n        # Ignore protection\n        fallback_raw_api_object.add_raw_member(\"ignore_protection\",\n                                               [],\n                                               effect_parent)\n\n        pregen_converter_group.add_raw_api_object(fallback_raw_api_object)\n        pregen_nyan_objects.update({fallback_ref_in_modpack: fallback_raw_api_object})\n\n        # =======================================================================\n        # Fallback resistance\n        # =======================================================================\n        effect_parent = \"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"\n        fallback_parent = \"engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n        fallback_location = \"data/resistance/discrete/flat_attribute_change/\"\n\n        fallback_ref_in_modpack = \"resistance.discrete.flat_attribute_change.fallback.AoE2AttackFallback\"\n        fallback_raw_api_object = RawAPIObject(fallback_ref_in_modpack,\n                                               \"AoE2AttackFallback\",\n                                               api_objects,\n                                               fallback_location)\n        fallback_raw_api_object.set_filename(\"fallback\")\n        fallback_raw_api_object.add_raw_parent(fallback_parent)\n\n        # Type\n        type_ref = \"engine.util.attribute_change_type.type.Fallback\"\n        change_type = api_objects[type_ref]\n        fallback_raw_api_object.add_raw_member(\"type\",\n                                               change_type,\n                                               effect_parent)\n\n        # Block value\n        # =================================================================================\n        amount_name = f\"{fallback_ref_in_modpack}.BlockAmount\"\n        amount_raw_api_object = RawAPIObject(amount_name, \"BlockAmount\", api_objects)\n        amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n        amount_location = ForwardRef(pregen_converter_group, fallback_ref_in_modpack)\n        amount_raw_api_object.set_location(amount_location)\n\n        attribute = ForwardRef(pregen_converter_group, \"util.attribute.types.Health\")\n        amount_raw_api_object.add_raw_member(\"type\",\n                                             attribute,\n                                             \"engine.util.attribute.AttributeAmount\")\n        amount_raw_api_object.add_raw_member(\"amount\",\n                                             0,\n                                             \"engine.util.attribute.AttributeAmount\")\n\n        pregen_converter_group.add_raw_api_object(amount_raw_api_object)\n        pregen_nyan_objects.update({amount_name: amount_raw_api_object})\n\n        # =================================================================================\n        amount_forward_ref = ForwardRef(pregen_converter_group, amount_name)\n        fallback_raw_api_object.add_raw_member(\"block_value\",\n                                               amount_forward_ref,\n                                               effect_parent)\n\n        pregen_converter_group.add_raw_api_object(fallback_raw_api_object)\n        pregen_nyan_objects.update({fallback_ref_in_modpack: fallback_raw_api_object})\n\n        # =======================================================================\n        # Property Construct\n        # =======================================================================\n        prop_ref_in_modpack = \"resistance.property.types.BuildingConstruct\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"BuildingConstruct\",\n                                           api_objects,\n                                           \"data/resistance/property/\")\n        prop_raw_api_object.set_filename(\"types\")\n        prop_raw_api_object.add_raw_parent(\"engine.resistance.property.type.Stacked\")\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        prop_raw_api_object.add_raw_member(\"stack_limit\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           \"engine.resistance.property.type.Stacked\")\n\n        prop_raw_api_object.add_raw_member(\"distribution_type\",\n                                           api_objects[\"engine.util.distribution_type.type.Mean\"],\n                                           \"engine.resistance.property.type.Stacked\")\n\n        # Calculation type Construct\n        # =======================================================================\n        calc_parent = \"engine.util.calculation_type.type.Hyperbolic\"\n\n        calc_ref_in_modpack = \"util.calculation_type.construct_calculation.ConstructCalcType\"\n        calc_raw_api_object = RawAPIObject(calc_ref_in_modpack,\n                                           \"BuildingConstruct\",\n                                           api_objects)\n        calc_location = ForwardRef(pregen_converter_group, prop_ref_in_modpack)\n        calc_raw_api_object.set_location(calc_location)\n        calc_raw_api_object.add_raw_parent(calc_parent)\n\n        pregen_converter_group.add_raw_api_object(calc_raw_api_object)\n        pregen_nyan_objects.update({calc_ref_in_modpack: calc_raw_api_object})\n\n        # Formula: (scale_factor / (count_effectors - shift_x)) + shift_y\n        # AoE2: (3 / (vil_count + 2))\n\n        # Shift x\n        calc_raw_api_object.add_raw_member(\"shift_x\",\n                                           -2,\n                                           calc_parent)\n\n        # Shift y\n        calc_raw_api_object.add_raw_member(\"shift_y\",\n                                           0,\n                                           calc_parent)\n\n        # Scale\n        calc_raw_api_object.add_raw_member(\"scale_factor\",\n                                           3,\n                                           calc_parent)\n\n        calc_forward_ref = ForwardRef(pregen_converter_group, calc_ref_in_modpack)\n        prop_raw_api_object.add_raw_member(\"calculation_type\",\n                                           calc_forward_ref,\n                                           \"engine.resistance.property.type.Stacked\")\n\n        # =======================================================================\n        # Property Repair\n        # =======================================================================\n        prop_ref_in_modpack = \"resistance.property.types.BuildingRepair\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"BuildingRepair\",\n                                           api_objects,\n                                           \"data/resistance/property/\")\n        prop_raw_api_object.set_filename(\"types\")\n        prop_raw_api_object.add_raw_parent(\"engine.resistance.property.type.Stacked\")\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        prop_raw_api_object.add_raw_member(\"stack_limit\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           \"engine.resistance.property.type.Stacked\")\n\n        prop_raw_api_object.add_raw_member(\"distribution_type\",\n                                           api_objects[\"engine.util.distribution_type.type.Mean\"],\n                                           \"engine.resistance.property.type.Stacked\")\n\n        # =======================================================================\n        # Calculation type Repair\n        # =======================================================================\n        calc_parent = \"engine.util.calculation_type.type.Linear\"\n\n        calc_ref_in_modpack = \"util.calculation_type.construct_calculation.BuildingRepair\"\n        calc_raw_api_object = RawAPIObject(calc_ref_in_modpack,\n                                           \"BuildingRepair\",\n                                           api_objects)\n        calc_location = ForwardRef(pregen_converter_group, prop_ref_in_modpack)\n        calc_raw_api_object.set_location(calc_location)\n        calc_raw_api_object.add_raw_parent(calc_parent)\n\n        pregen_converter_group.add_raw_api_object(calc_raw_api_object)\n        pregen_nyan_objects.update({calc_ref_in_modpack: calc_raw_api_object})\n\n        # Formula: (scale_factor * (count_effectors - shift_x)) + shift_y\n        # AoE2: (0.333334 * (vil_count + 2))\n\n        # Shift x\n        calc_raw_api_object.add_raw_member(\"shift_x\",\n                                           -2,\n                                           calc_parent)\n\n        # Shift y\n        calc_raw_api_object.add_raw_member(\"shift_y\",\n                                           0,\n                                           calc_parent)\n\n        # Scale\n        calc_raw_api_object.add_raw_member(\"scale_factor\",\n                                           1 / 3,\n                                           calc_parent)\n\n        calc_forward_ref = ForwardRef(pregen_converter_group, calc_ref_in_modpack)\n        prop_raw_api_object.add_raw_member(\"calculation_type\",\n                                           calc_forward_ref,\n                                           \"engine.resistance.property.type.Stacked\")\n\n    @staticmethod\n    def generate_modifiers(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate standard modifiers.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        modifier_parent = \"engine.modifier.Modifier\"\n        mprop_parent = \"engine.modifier.property.type.Multiplier\"\n        type_parent = \"engine.modifier.effect.flat_attribute_change.type.Flyover\"\n        types_location = \"data/util/modifier/flyover_cliff/\"\n\n        # =======================================================================\n        # Flyover effect multiplier\n        # =======================================================================\n        modifier_ref_in_modpack = \"util.modifier.flyover_cliff.AttackFlyover\"\n        modifier_raw_api_object = RawAPIObject(modifier_ref_in_modpack,\n                                               \"AttackFlyover\", api_objects,\n                                               types_location)\n        modifier_raw_api_object.set_filename(\"flyover_cliff\")\n        modifier_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(modifier_raw_api_object)\n        pregen_nyan_objects.update({modifier_ref_in_modpack: modifier_raw_api_object})\n\n        # Relative angle to cliff must not be smaller than 90°\n        modifier_raw_api_object.add_raw_member(\"relative_angle\",\n                                               90,\n                                               type_parent)\n\n        # Affects all cliffs\n        types = [ForwardRef(pregen_converter_group, \"util.game_entity_type.types.Cliff\")]\n        modifier_raw_api_object.add_raw_member(\"flyover_types\",\n                                               types,\n                                               type_parent)\n        modifier_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                               [],\n                                               type_parent)\n\n        # Multiplier property: Increases effect value by 25%\n        # --------------------------------------------------\n        prop_ref_in_modpack = \"util.modifier.flyover_cliff.AttackFlyover.Multiplier\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"Multiplier\", api_objects,\n                                           types_location)\n        prop_location = ForwardRef(pregen_converter_group, modifier_ref_in_modpack)\n        prop_raw_api_object.set_location(prop_location)\n        prop_raw_api_object.add_raw_parent(mprop_parent)\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        prop_raw_api_object.add_raw_member(\"multiplier\",\n                                           1.25,\n                                           mprop_parent)\n        # --------------------------------------------------\n        # Assign property to modifier\n        prop_forward_ref = ForwardRef(pregen_converter_group, prop_ref_in_modpack)\n        properties = {api_objects[mprop_parent]: prop_forward_ref}\n        modifier_raw_api_object.add_raw_member(\"properties\",\n                                               properties,\n                                               modifier_parent)\n\n        # =======================================================================\n        # Elevation difference effect multiplier (higher unit)\n        # =======================================================================\n        type_parent = \"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\"\n        types_location = \"data/util/modifier/elevation_difference/\"\n\n        modifier_ref_in_modpack = \"util.modifier.elevation_difference.AttackHigh\"\n        modifier_raw_api_object = RawAPIObject(modifier_ref_in_modpack,\n                                               \"AttackHigh\", api_objects,\n                                               types_location)\n        modifier_raw_api_object.set_filename(\"elevation_difference\")\n        modifier_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(modifier_raw_api_object)\n        pregen_nyan_objects.update({modifier_ref_in_modpack: modifier_raw_api_object})\n\n        # Multiplier property: Increases effect value to 125%\n        # --------------------------------------------------\n        prop_ref_in_modpack = \"util.modifier.elevation_difference.AttackHigh.Multiplier\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"Multiplier\", api_objects,\n                                           types_location)\n        prop_location = ForwardRef(pregen_converter_group, modifier_ref_in_modpack)\n        prop_raw_api_object.set_location(prop_location)\n        prop_raw_api_object.add_raw_parent(mprop_parent)\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        prop_raw_api_object.add_raw_member(\"multiplier\",\n                                           1.25,\n                                           mprop_parent)\n        # --------------------------------------------------\n        # Assign property to modifier\n        prop_forward_ref = ForwardRef(pregen_converter_group, prop_ref_in_modpack)\n        properties = {api_objects[mprop_parent]: prop_forward_ref}\n        modifier_raw_api_object.add_raw_member(\"properties\",\n                                               properties,\n                                               modifier_parent)\n\n        # =======================================================================\n        # Elevation difference effect multiplier (lower unit)\n        # =======================================================================\n        type_parent = \"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\"\n        types_location = \"data/util/modifier/elevation_difference/\"\n\n        modifier_ref_in_modpack = \"util.modifier.elevation_difference.AttackLow\"\n        modifier_raw_api_object = RawAPIObject(modifier_ref_in_modpack,\n                                               \"AttackLow\", api_objects,\n                                               types_location)\n        modifier_raw_api_object.set_filename(\"elevation_difference\")\n        modifier_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(modifier_raw_api_object)\n        pregen_nyan_objects.update({modifier_ref_in_modpack: modifier_raw_api_object})\n\n        # Multiplier property: Decreases effect value to 75%\n        # --------------------------------------------------\n        prop_ref_in_modpack = \"util.modifier.elevation_difference.AttackLow.Multiplier\"\n        prop_raw_api_object = RawAPIObject(prop_ref_in_modpack,\n                                           \"Multiplier\", api_objects,\n                                           types_location)\n        prop_location = ForwardRef(pregen_converter_group, modifier_ref_in_modpack)\n        prop_raw_api_object.set_location(prop_location)\n        prop_raw_api_object.add_raw_parent(mprop_parent)\n\n        pregen_converter_group.add_raw_api_object(prop_raw_api_object)\n        pregen_nyan_objects.update({prop_ref_in_modpack: prop_raw_api_object})\n\n        prop_raw_api_object.add_raw_member(\"multiplier\",\n                                           1.25,\n                                           mprop_parent)\n        # --------------------------------------------------\n        # Assign property to modifier\n        prop_forward_ref = ForwardRef(pregen_converter_group, prop_ref_in_modpack)\n        properties = {api_objects[mprop_parent]: prop_forward_ref}\n        modifier_raw_api_object.add_raw_member(\"properties\",\n                                               properties,\n                                               modifier_parent)\n\n    @staticmethod\n    def generate_terrain_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate TerrainType objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        terrain_type_lookup_dict = internal_name_lookups.get_terrain_type_lookups(\n            full_data_set.game_version)\n\n        type_parent = \"engine.util.terrain_type.TerrainType\"\n        types_location = \"data/util/terrain_type/\"\n\n        terrain_type_lookups = terrain_type_lookup_dict.values()\n\n        for terrain_type in terrain_type_lookups:\n            type_name = terrain_type[2]\n            type_ref_in_modpack = f\"util.terrain_type.types.{type_name}\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               type_name, api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n    @staticmethod\n    def generate_path_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate PathType objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        path_type_parent = \"engine.util.path_type.PathType\"\n        path_types_location = \"data/util/path_type/\"\n\n        # =======================================================================\n        # Land\n        # =======================================================================\n        path_type_ref_in_modpack = \"util.path.types.Land\"\n        path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack,\n                                                \"Land\",\n                                                api_objects,\n                                                path_types_location)\n        path_type_raw_api_object.set_filename(\"types\")\n        path_type_raw_api_object.add_raw_parent(path_type_parent)\n\n        pregen_converter_group.add_raw_api_object(path_type_raw_api_object)\n        pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object})\n\n        # =======================================================================\n        # Water\n        # =======================================================================\n        path_type_ref_in_modpack = \"util.path.types.Water\"\n        path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack,\n                                                \"Water\",\n                                                api_objects,\n                                                path_types_location)\n        path_type_raw_api_object.set_filename(\"types\")\n        path_type_raw_api_object.add_raw_parent(path_type_parent)\n\n        pregen_converter_group.add_raw_api_object(path_type_raw_api_object)\n        pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object})\n\n        # =======================================================================\n        # Air\n        # =======================================================================\n        path_type_ref_in_modpack = \"util.path.types.Air\"\n        path_type_raw_api_object = RawAPIObject(path_type_ref_in_modpack,\n                                                \"Air\",\n                                                api_objects,\n                                                path_types_location)\n        path_type_raw_api_object.set_filename(\"types\")\n        path_type_raw_api_object.add_raw_parent(path_type_parent)\n\n        pregen_converter_group.add_raw_api_object(path_type_raw_api_object)\n        pregen_nyan_objects.update({path_type_ref_in_modpack: path_type_raw_api_object})\n\n    @staticmethod\n    def generate_resources(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate Attribute objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        resource_parent = \"engine.util.resource.Resource\"\n        resources_location = \"data/util/resource/\"\n\n        # =======================================================================\n        # Food\n        # =======================================================================\n        food_ref_in_modpack = \"util.resource.types.Food\"\n        food_raw_api_object = RawAPIObject(food_ref_in_modpack,\n                                           \"Food\", api_objects,\n                                           resources_location)\n        food_raw_api_object.set_filename(\"types\")\n        food_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(food_raw_api_object)\n        pregen_nyan_objects.update({food_ref_in_modpack: food_raw_api_object})\n\n        food_raw_api_object.add_raw_member(\"max_storage\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        food_name_ref_in_modpack = \"util.attribute.types.Food.FoodName\"\n        food_name_value = RawAPIObject(food_name_ref_in_modpack, \"FoodName\",\n                                       api_objects, resources_location)\n        food_name_value.set_filename(\"types\")\n        food_name_value.add_raw_parent(name_value_parent)\n        food_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      food_name_ref_in_modpack)\n        food_raw_api_object.add_raw_member(\"name\",\n                                           name_forward_ref,\n                                           resource_parent)\n\n        pregen_converter_group.add_raw_api_object(food_name_value)\n        pregen_nyan_objects.update({food_name_ref_in_modpack: food_name_value})\n\n        # =======================================================================\n        # Wood\n        # =======================================================================\n        wood_ref_in_modpack = \"util.resource.types.Wood\"\n        wood_raw_api_object = RawAPIObject(wood_ref_in_modpack,\n                                           \"Wood\", api_objects,\n                                           resources_location)\n        wood_raw_api_object.set_filename(\"types\")\n        wood_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(wood_raw_api_object)\n        pregen_nyan_objects.update({wood_ref_in_modpack: wood_raw_api_object})\n\n        wood_raw_api_object.add_raw_member(\"max_storage\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        wood_name_ref_in_modpack = \"util.attribute.types.Wood.WoodName\"\n        wood_name_value = RawAPIObject(wood_name_ref_in_modpack, \"WoodName\",\n                                       api_objects, resources_location)\n        wood_name_value.set_filename(\"types\")\n        wood_name_value.add_raw_parent(name_value_parent)\n        wood_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      wood_name_ref_in_modpack)\n        wood_raw_api_object.add_raw_member(\"name\",\n                                           name_forward_ref,\n                                           resource_parent)\n\n        pregen_converter_group.add_raw_api_object(wood_name_value)\n        pregen_nyan_objects.update({wood_name_ref_in_modpack: wood_name_value})\n\n        # =======================================================================\n        # Stone\n        # =======================================================================\n        stone_ref_in_modpack = \"util.resource.types.Stone\"\n        stone_raw_api_object = RawAPIObject(stone_ref_in_modpack,\n                                            \"Stone\", api_objects,\n                                            resources_location)\n        stone_raw_api_object.set_filename(\"types\")\n        stone_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(stone_raw_api_object)\n        pregen_nyan_objects.update({stone_ref_in_modpack: stone_raw_api_object})\n\n        stone_raw_api_object.add_raw_member(\"max_storage\",\n                                            MemberSpecialValue.NYAN_INF,\n                                            resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        stone_name_ref_in_modpack = \"util.attribute.types.Stone.StoneName\"\n        stone_name_value = RawAPIObject(stone_name_ref_in_modpack, \"StoneName\",\n                                        api_objects, resources_location)\n        stone_name_value.set_filename(\"types\")\n        stone_name_value.add_raw_parent(name_value_parent)\n        stone_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      stone_name_ref_in_modpack)\n        stone_raw_api_object.add_raw_member(\"name\",\n                                            name_forward_ref,\n                                            resource_parent)\n\n        pregen_converter_group.add_raw_api_object(stone_name_value)\n        pregen_nyan_objects.update({stone_name_ref_in_modpack: stone_name_value})\n\n        # =======================================================================\n        # Gold\n        # =======================================================================\n        gold_ref_in_modpack = \"util.resource.types.Gold\"\n        gold_raw_api_object = RawAPIObject(gold_ref_in_modpack,\n                                           \"Gold\", api_objects,\n                                           resources_location)\n        gold_raw_api_object.set_filename(\"types\")\n        gold_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(gold_raw_api_object)\n        pregen_nyan_objects.update({gold_ref_in_modpack: gold_raw_api_object})\n\n        gold_raw_api_object.add_raw_member(\"max_storage\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        gold_name_ref_in_modpack = \"util.attribute.types.Gold.GoldName\"\n        gold_name_value = RawAPIObject(gold_name_ref_in_modpack, \"GoldName\",\n                                       api_objects, resources_location)\n        gold_name_value.set_filename(\"types\")\n        gold_name_value.add_raw_parent(name_value_parent)\n        gold_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      gold_name_ref_in_modpack)\n        gold_raw_api_object.add_raw_member(\"name\",\n                                           name_forward_ref,\n                                           resource_parent)\n\n        pregen_converter_group.add_raw_api_object(gold_name_value)\n        pregen_nyan_objects.update({gold_name_ref_in_modpack: gold_name_value})\n\n        # =======================================================================\n        # Population Space\n        # =======================================================================\n        resource_contingent_parent = \"engine.util.resource.ResourceContingent\"\n\n        pop_ref_in_modpack = \"util.resource.types.PopulationSpace\"\n        pop_raw_api_object = RawAPIObject(pop_ref_in_modpack,\n                                          \"PopulationSpace\", api_objects,\n                                          resources_location)\n        pop_raw_api_object.set_filename(\"types\")\n        pop_raw_api_object.add_raw_parent(resource_contingent_parent)\n\n        pregen_converter_group.add_raw_api_object(pop_raw_api_object)\n        pregen_nyan_objects.update({pop_ref_in_modpack: pop_raw_api_object})\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        pop_name_ref_in_modpack = \"util.attribute.types.PopulationSpace.PopulationSpaceName\"\n        pop_name_value = RawAPIObject(pop_name_ref_in_modpack, \"PopulationSpaceName\",\n                                      api_objects, resources_location)\n        pop_name_value.set_filename(\"types\")\n        pop_name_value.add_raw_parent(name_value_parent)\n        pop_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      pop_name_ref_in_modpack)\n        pop_raw_api_object.add_raw_member(\"name\",\n                                          name_forward_ref,\n                                          resource_parent)\n        pop_raw_api_object.add_raw_member(\"max_storage\",\n                                          MemberSpecialValue.NYAN_INF,\n                                          resource_parent)\n        pop_raw_api_object.add_raw_member(\"min_amount\",\n                                          0,\n                                          resource_contingent_parent)\n        pop_raw_api_object.add_raw_member(\"max_amount\",\n                                          200,\n                                          resource_contingent_parent)\n\n        pregen_converter_group.add_raw_api_object(pop_name_value)\n        pregen_nyan_objects.update({pop_name_ref_in_modpack: pop_name_value})\n\n    @staticmethod\n    def generate_death_condition(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate DeathCondition objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Death condition\n        # =======================================================================\n        logic_parent = \"engine.util.logic.LogicElement\"\n        literal_parent = \"engine.util.logic.literal.Literal\"\n        interval_parent = \"engine.util.logic.literal.type.AttributeBelowValue\"\n        literal_location = \"data/util/logic/death/\"\n\n        death_ref_in_modpack = \"util.logic.literal.death.StandardHealthDeathLiteral\"\n        literal_raw_api_object = RawAPIObject(death_ref_in_modpack,\n                                              \"StandardHealthDeathLiteral\",\n                                              api_objects,\n                                              literal_location)\n        literal_raw_api_object.set_filename(\"death\")\n        literal_raw_api_object.add_raw_parent(interval_parent)\n\n        # Literal will not default to 'True' when it was fulfilled once\n        literal_raw_api_object.add_raw_member(\"only_once\", False, logic_parent)\n\n        # Scope\n        scope_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.logic.literal_scope.death.StandardHealthDeathScope\")\n        literal_raw_api_object.add_raw_member(\"scope\",\n                                              scope_forward_ref,\n                                              literal_parent)\n\n        # Attribute\n        health_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.attribute.types.Health\")\n        literal_raw_api_object.add_raw_member(\"attribute\",\n                                              health_forward_ref,\n                                              interval_parent)\n\n        # sidenote: Apparently this is actually HP<1 in Genie\n        # (https://youtu.be/FdBk8zGbE7U?t=7m16s)\n        literal_raw_api_object.add_raw_member(\"threshold\",\n                                              1,\n                                              interval_parent)\n\n        pregen_converter_group.add_raw_api_object(literal_raw_api_object)\n        pregen_nyan_objects.update({death_ref_in_modpack: literal_raw_api_object})\n\n        # LiteralScope\n        scope_parent = \"engine.util.logic.literal_scope.LiteralScope\"\n        self_scope_parent = \"engine.util.logic.literal_scope.type.Self\"\n\n        death_scope_ref_in_modpack = \"util.logic.literal_scope.death.StandardHealthDeathScope\"\n        scope_raw_api_object = RawAPIObject(death_scope_ref_in_modpack,\n                                            \"StandardHealthDeathScope\",\n                                            api_objects)\n        scope_location = ForwardRef(pregen_converter_group, death_ref_in_modpack)\n        scope_raw_api_object.set_location(scope_location)\n        scope_raw_api_object.add_raw_parent(self_scope_parent)\n\n        scope_diplomatic_stances = [api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        scope_raw_api_object.add_raw_member(\"stances\",\n                                            scope_diplomatic_stances,\n                                            scope_parent)\n\n        pregen_converter_group.add_raw_api_object(scope_raw_api_object)\n        pregen_nyan_objects.update({death_scope_ref_in_modpack: scope_raw_api_object})\n\n        # =======================================================================\n        # Garrison empty condition\n        # =======================================================================\n        logic_parent = \"engine.util.logic.LogicElement\"\n        literal_parent = \"engine.util.logic.literal.Literal\"\n        interval_parent = \"engine.util.logic.literal.type.AttributeBelowValue\"\n        literal_location = \"data/util/logic/garrison_empty/\"\n\n        garrison_literal_ref_in_modpack = \"util.logic.literal.garrison.BuildingDamageEmpty\"\n        literal_raw_api_object = RawAPIObject(garrison_literal_ref_in_modpack,\n                                              \"BuildingDamageEmptyLiteral\",\n                                              api_objects,\n                                              literal_location)\n        literal_raw_api_object.set_filename(\"garrison_empty\")\n        literal_raw_api_object.add_raw_parent(interval_parent)\n\n        # Literal will not default to 'True' when it was fulfilled once\n        literal_raw_api_object.add_raw_member(\"only_once\", False, logic_parent)\n\n        # Scope\n        scope_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.logic.literal_scope.garrison.BuildingDamageEmptyScope\")\n        literal_raw_api_object.add_raw_member(\"scope\",\n                                              scope_forward_ref,\n                                              literal_parent)\n\n        # Attribute\n        health_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.attribute.types.Health\")\n        literal_raw_api_object.add_raw_member(\"attribute\",\n                                              health_forward_ref,\n                                              interval_parent)\n\n        # Threshhold\n        literal_raw_api_object.add_raw_member(\"threshold\",\n                                              0.2,\n                                              interval_parent)\n\n        pregen_converter_group.add_raw_api_object(literal_raw_api_object)\n        pregen_nyan_objects.update({garrison_literal_ref_in_modpack: literal_raw_api_object})\n\n        # LiteralScope\n        scope_parent = \"engine.util.logic.literal_scope.LiteralScope\"\n        self_scope_parent = \"engine.util.logic.literal_scope.type.Self\"\n\n        garrison_scope_ref_in_modpack = \"util.logic.literal_scope.garrison.BuildingDamageEmptyScope\"\n        scope_raw_api_object = RawAPIObject(garrison_scope_ref_in_modpack,\n                                            \"BuildingDamageEmptyScope\",\n                                            api_objects)\n        scope_location = ForwardRef(pregen_converter_group, garrison_literal_ref_in_modpack)\n        scope_raw_api_object.set_location(scope_location)\n        scope_raw_api_object.add_raw_parent(self_scope_parent)\n\n        scope_diplomatic_stances = [api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        scope_raw_api_object.add_raw_member(\"stances\",\n                                            scope_diplomatic_stances,\n                                            scope_parent)\n\n        pregen_converter_group.add_raw_api_object(scope_raw_api_object)\n        pregen_nyan_objects.update({garrison_scope_ref_in_modpack: scope_raw_api_object})\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/processor.py",
    "content": "# Copyright 2019-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-branches,too-many-statements\n# pylint: disable=too-many-locals,too-many-public-methods\n\"\"\"\nConvert data from AoC to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....log import info\nfrom ....entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\nfrom ....entity_object.conversion.aoc.genie_civ import GenieCivilizationObject\nfrom ....entity_object.conversion.aoc.genie_connection import GenieAgeConnection, \\\n    GenieBuildingConnection, GenieUnitConnection, GenieTechConnection\nfrom ....entity_object.conversion.aoc.genie_effect import GenieEffectObject, \\\n    GenieEffectBundle\nfrom ....entity_object.conversion.aoc.genie_graphic import GenieGraphic\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....entity_object.conversion.aoc.genie_sound import GenieSound\nfrom ....entity_object.conversion.aoc.genie_tech import AgeUpgrade, \\\n    UnitUnlock, UnitLineUpgrade, CivBonus\nfrom ....entity_object.conversion.aoc.genie_tech import BuildingLineUpgrade\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechObject\nfrom ....entity_object.conversion.aoc.genie_tech import StatUpgrade, InitiatedTech, \\\n    BuildingUnlock\nfrom ....entity_object.conversion.aoc.genie_terrain import GenieTerrainGroup, \\\n    GenieTerrainObject, GenieTerrainRestriction\nfrom ....entity_object.conversion.aoc.genie_unit import GenieAmbientGroup, \\\n    GenieGarrisonMode\nfrom ....entity_object.conversion.aoc.genie_unit import GenieStackBuildingGroup, \\\n    GenieBuildingLineGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n    GenieUnitTransformGroup, GenieMonkGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitObject\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitTaskGroup, \\\n    GenieVillagerGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVariantGroup\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ....value_object.conversion.aoc.internal_nyan_names import AMBIENT_GROUP_LOOKUPS, \\\n    VARIANT_GROUP_LOOKUPS\nfrom .media_subprocessor import AoCMediaSubprocessor\nfrom .modpack_subprocessor import AoCModpackSubprocessor\nfrom .nyan_subprocessor import AoCNyanSubprocessor\nfrom .pregen_processor import AoCPregenSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass AoCProcessor:\n    \"\"\"\n    Main processor for converting data from AoC.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game speification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openae formats (nyan, sprite, terrain)\n        dataset = cls._processor(dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        cls.extract_genie_units(gamespec, dataset)\n        cls.extract_genie_techs(gamespec, dataset)\n        cls.extract_genie_effect_bundles(gamespec, dataset)\n        cls.sanitize_effect_bundles(dataset)\n        cls.extract_genie_civs(gamespec, dataset)\n        cls.extract_age_connections(gamespec, dataset)\n        cls.extract_building_connections(gamespec, dataset)\n        cls.extract_unit_connections(gamespec, dataset)\n        cls.extract_tech_connections(gamespec, dataset)\n        cls.extract_genie_graphics(gamespec, dataset)\n        cls.extract_genie_sounds(gamespec, dataset)\n        cls.extract_genie_terrains(gamespec, dataset)\n        cls.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(cls, full_data_set: GenieObjectContainer) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        cls.create_unit_lines(full_data_set)\n        cls.create_extra_unit_lines(full_data_set)\n        cls.create_building_lines(full_data_set)\n        cls.create_villager_groups(full_data_set)\n        cls.create_ambient_groups(full_data_set)\n        cls.create_variant_groups(full_data_set)\n        cls.create_terrain_groups(full_data_set)\n        cls.create_tech_groups(full_data_set)\n        cls.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        cls.link_building_upgrades(full_data_set)\n        cls.link_creatables(full_data_set)\n        cls.link_researchables(full_data_set)\n        cls.link_civ_uniques(full_data_set)\n        cls.link_gatherers_to_dropsites(full_data_set)\n        cls.link_garrison(full_data_set)\n        cls.link_trade_posts(full_data_set)\n        cls.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        AoCPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating nyan objects...\")\n\n        AoCNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        AoCMediaSubprocessor.convert(full_data_set)\n\n        return AoCModpackSubprocessor.get_modpacks(full_data_set)\n\n    @staticmethod\n    def extract_genie_units(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract units from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # Units are stored in the civ container.\n        # All civs point to the same units (?) except for Gaia which has more.\n        # Gaia also seems to have the most units, so we only read from Gaia\n        #\n        # call hierarchy: wrapper[0]->civs[0]->units\n        raw_units = gamespec[0][\"civs\"][0][\"units\"].value\n\n        # Unit headers store the things units can do\n        raw_unit_headers = gamespec[0][\"unit_headers\"].value\n\n        for raw_unit in raw_units:\n            unit_id = raw_unit[\"id0\"].value\n            unit_members = raw_unit.value\n\n            # Turn attack and armor into containers to make diffing work\n            if \"attacks\" in unit_members.keys():\n                attacks_member = unit_members.pop(\"attacks\")\n                attacks_member = attacks_member.get_container(\"type_id\")\n                armors_member = unit_members.pop(\"armors\")\n                armors_member = armors_member.get_container(\"type_id\")\n\n                unit_members.update({\"attacks\": attacks_member})\n                unit_members.update({\"armors\": armors_member})\n\n            unit = GenieUnitObject(unit_id, full_data_set, members=unit_members)\n            full_data_set.genie_units.update({unit.get_id(): unit})\n\n            # Commands\n            unit_commands = raw_unit_headers[unit_id][\"unit_commands\"]\n            unit.add_member(unit_commands)\n\n    @staticmethod\n    def extract_genie_techs(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract techs from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # Techs are stored as \"researches\".\n        #\n        # call hierarchy: wrapper[0]->researches\n        raw_techs = gamespec[0][\"researches\"].value\n\n        index = 0\n        for raw_tech in raw_techs:\n            tech_id = index\n            tech_members = raw_tech.value\n\n            tech = GenieTechObject(tech_id, full_data_set, members=tech_members)\n            full_data_set.genie_techs.update({tech.get_id(): tech})\n\n            index += 1\n\n    @staticmethod\n    def extract_genie_effect_bundles(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract effects and effect bundles from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->effect_bundles\n        raw_effect_bundles = gamespec[0][\"effect_bundles\"].value\n\n        index_bundle = 0\n        for raw_effect_bundle in raw_effect_bundles:\n            bundle_id = index_bundle\n\n            # call hierarchy: effect_bundle->effects\n            raw_effects = raw_effect_bundle[\"effects\"].value\n\n            effects = {}\n\n            index_effect = 0\n            for raw_effect in raw_effects:\n                effect_id = index_effect\n                effect_members = raw_effect.value\n\n                effect = GenieEffectObject(effect_id, bundle_id, full_data_set,\n                                           members=effect_members)\n\n                effects.update({effect_id: effect})\n\n                index_effect += 1\n\n            # Pass everything to the bundle\n            effect_bundle_members = raw_effect_bundle.value\n            # Remove effects we store them as separate objects\n            effect_bundle_members.pop(\"effects\")\n\n            bundle = GenieEffectBundle(bundle_id, effects, full_data_set,\n                                       members=effect_bundle_members)\n            full_data_set.genie_effect_bundles.update({bundle.get_id(): bundle})\n\n            index_bundle += 1\n\n    @staticmethod\n    def extract_genie_civs(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract civs from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->civs\n        raw_civs = gamespec[0][\"civs\"].value\n\n        index = 0\n        for raw_civ in raw_civs:\n            civ_id = index\n\n            civ_members = raw_civ.value\n            units_member = civ_members.pop(\"units\")\n            units_member = units_member.get_container(\"id0\")\n\n            civ_members.update({\"units\": units_member})\n\n            civ = GenieCivilizationObject(civ_id, full_data_set, members=civ_members)\n            full_data_set.genie_civs.update({civ.get_id(): civ})\n\n            index += 1\n\n    @staticmethod\n    def extract_age_connections(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract age connections from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->age_connections\n        raw_connections = gamespec[0][\"age_connections\"].value\n\n        for raw_connection in raw_connections:\n            age_id = raw_connection[\"id\"].value\n            connection_members = raw_connection.value\n\n            connection = GenieAgeConnection(age_id, full_data_set, members=connection_members)\n            full_data_set.age_connections.update({connection.get_id(): connection})\n\n    @staticmethod\n    def extract_building_connections(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract building connections from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->building_connections\n        raw_connections = gamespec[0][\"building_connections\"].value\n\n        for raw_connection in raw_connections:\n            building_id = raw_connection[\"id\"].value\n            connection_members = raw_connection.value\n\n            connection = GenieBuildingConnection(building_id, full_data_set,\n                                                 members=connection_members)\n            full_data_set.building_connections.update({connection.get_id(): connection})\n\n    @staticmethod\n    def extract_unit_connections(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract unit connections from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->unit_connections\n        raw_connections = gamespec[0][\"unit_connections\"].value\n\n        for raw_connection in raw_connections:\n            unit_id = raw_connection[\"id\"].value\n            connection_members = raw_connection.value\n\n            connection = GenieUnitConnection(unit_id, full_data_set, members=connection_members)\n            full_data_set.unit_connections.update({connection.get_id(): connection})\n\n    @staticmethod\n    def extract_tech_connections(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract tech connections from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->tech_connections\n        raw_connections = gamespec[0][\"tech_connections\"].value\n\n        for raw_connection in raw_connections:\n            tech_id = raw_connection[\"id\"].value\n            connection_members = raw_connection.value\n\n            connection = GenieTechConnection(tech_id, full_data_set, members=connection_members)\n            full_data_set.tech_connections.update({connection.get_id(): connection})\n\n    @staticmethod\n    def extract_genie_graphics(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract graphic definitions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->graphics\n        raw_graphics = gamespec[0][\"graphics\"].value\n\n        for raw_graphic in raw_graphics:\n            # Can be ignored if there is no filename associated\n            filename = raw_graphic[\"filename\"].value\n            if not filename:\n                continue\n\n            graphic_id = raw_graphic[\"graphic_id\"].value\n            graphic_members = raw_graphic.value\n\n            graphic = GenieGraphic(graphic_id, full_data_set, members=graphic_members)\n            slp_id = raw_graphic[\"slp_id\"].value\n            if str(slp_id) not in full_data_set.existing_graphics:\n                graphic.exists = False\n\n            full_data_set.genie_graphics.update({graphic.get_id(): graphic})\n\n        # Detect subgraphics\n        for genie_graphic in full_data_set.genie_graphics.values():\n            genie_graphic.detect_subgraphics()\n\n    @staticmethod\n    def extract_genie_sounds(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract sound definitions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->sounds\n        raw_sounds = gamespec[0][\"sounds\"].value\n\n        for raw_sound in raw_sounds:\n            sound_id = raw_sound[\"sound_id\"].value\n            sound_members = raw_sound.value\n\n            sound = GenieSound(sound_id, full_data_set, members=sound_members)\n            full_data_set.genie_sounds.update({sound.get_id(): sound})\n\n    @staticmethod\n    def extract_genie_terrains(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract terrains from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->terrains\n        raw_terrains = gamespec[0][\"terrains\"].value\n\n        for index, raw_terrain in enumerate(raw_terrains):\n            terrain_index = index\n            terrain_members = raw_terrain.value\n\n            terrain = GenieTerrainObject(terrain_index, full_data_set, members=terrain_members)\n            full_data_set.genie_terrains.update({terrain.get_id(): terrain})\n\n    @staticmethod\n    def extract_genie_restrictions(\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> None:\n        \"\"\"\n        Extract terrain restrictions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->terrains\n        raw_restrictions = gamespec[0][\"terrain_restrictions\"].value\n\n        for index, raw_restriction in enumerate(raw_restrictions):\n            restriction_index = index\n            restriction_members = raw_restriction.value\n\n            restriction = GenieTerrainRestriction(restriction_index,\n                                                  full_data_set,\n                                                  members=restriction_members)\n            full_data_set.genie_terrain_restrictions.update({restriction.get_id(): restriction})\n\n    @staticmethod\n    def create_unit_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Sort units into lines, based on information in the unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        unit_connections = full_data_set.unit_connections\n\n        # First only handle the line heads (firstunits in a line)\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            unit = full_data_set.genie_units[unit_id]\n            line_mode = connection[\"line_mode\"].value\n\n            if line_mode != 2:\n                # It's an upgrade. Skip and handle later\n                continue\n\n            # Check for special cases first\n            if unit.has_member(\"transform_unit_id\")\\\n                    and unit[\"transform_unit_id\"].value > -1:\n                # Trebuchet\n                unit_line = GenieUnitTransformGroup(unit_id, unit_id, full_data_set)\n                full_data_set.transform_groups.update({unit_line.get_id(): unit_line})\n\n            elif unit_id == 125:\n                # Monks\n                # Switch to monk with relic is hardcoded :(\n                unit_line = GenieMonkGroup(unit_id, unit_id, 286, full_data_set)\n                full_data_set.monk_groups.update({unit_line.get_id(): unit_line})\n\n            elif unit.has_member(\"task_group\")\\\n                    and unit[\"task_group\"].value > 0:\n                # Villager\n                # done somewhere else because they are special^TM\n                continue\n\n            else:\n                # Normal units\n                unit_line = GenieUnitLineGroup(unit_id, full_data_set)\n\n            unit_line.add_unit(unit)\n            full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n            full_data_set.unit_ref.update({unit_id: unit_line})\n\n        # Second, handle all upgraded units\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            unit = full_data_set.genie_units[unit_id]\n            line_mode = connection[\"line_mode\"].value\n\n            if line_mode != 3:\n                # This unit is not an upgrade and was handled in the last for-loop\n                continue\n\n            # Search other_connections for the previous unit in line\n            connected_types = connection[\"other_connections\"].value\n            for index, _ in enumerate(connected_types):\n                connected_type = connected_types[index][\"other_connection\"].value\n                if connected_type == 2:\n                    # 2 == Unit\n                    connected_index = index\n                    break\n\n            else:\n                raise RuntimeError(f\"Unit {unit_id} is not first in line, but no previous \"\n                                   \"unit can be found in other_connections\")\n\n            connected_ids = connection[\"other_connected_ids\"].value\n            previous_unit_id = connected_ids[connected_index].value\n\n            # Search for the first unit ID in the line recursively\n            previous_id = previous_unit_id\n            previous_connection = unit_connections[previous_unit_id]\n            while previous_connection[\"line_mode\"] != 2:\n                if previous_id in full_data_set.unit_ref.keys():\n                    # Short-circuit here, if we the previous unit was already handled\n                    break\n\n                connected_types = previous_connection[\"other_connections\"].value\n                connected_index = -1\n                for index, _ in enumerate(connected_types):\n                    connected_type = connected_types[index][\"other_connection\"].value\n                    if connected_type == 2:\n                        # 2 == Unit\n                        connected_index = index\n                        break\n\n                connected_ids = previous_connection[\"other_connected_ids\"].value\n                previous_id = connected_ids[connected_index].value\n                previous_connection = unit_connections[previous_id]\n\n            unit_line = full_data_set.unit_ref[previous_id]\n            unit_line.add_unit(unit, after=previous_unit_id)\n            full_data_set.unit_ref.update({unit_id: unit_line})\n\n    @staticmethod\n    def create_extra_unit_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create additional units that are not in the unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                                contains all relevant data for the conversion\n                                process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        extra_units = (48, 65, 594, 833)  # Wildlife\n\n        for unit_id in extra_units:\n            unit_line = GenieUnitLineGroup(unit_id, full_data_set)\n            unit_line.add_unit(full_data_set.genie_units[unit_id])\n            full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n            full_data_set.unit_ref.update({unit_id: unit_line})\n\n    @staticmethod\n    def create_building_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Establish building lines, based on information in the building connections.\n        Because of how Genie building lines work, this will only find the first\n        building in the line. Subsequent buildings in the line have to be determined\n        by effects in AgeUpTechs.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        building_connections = full_data_set.building_connections\n\n        for connection in building_connections.values():\n            building_id = connection[\"id\"].value\n            building = full_data_set.genie_units[building_id]\n            previous_building_id = None\n            stack_building = False\n\n            # Buildings have no actual lines, so we use\n            # their unit ID as the line ID.\n            line_id = building_id\n\n            # Check if we have to create a GenieStackBuildingGroup\n            if building.has_member(\"stack_unit_id\") and \\\n                    building[\"stack_unit_id\"].value > -1:\n                stack_building = True\n\n            if building.has_member(\"head_unit_id\") and \\\n                    building[\"head_unit_id\"].value > -1:\n                # we don't care about head units because we process\n                # them with their stack unit\n                continue\n\n            # Check if the building is part of an existing line.\n            # To do this, we look for connected techs and\n            # check if any tech has an upgrade effect.\n            connected_types = connection[\"other_connections\"].value\n            connected_tech_indices = []\n            for index, _ in enumerate(connected_types):\n                connected_type = connected_types[index][\"other_connection\"].value\n                if connected_type == 3:\n                    # 3 == Tech\n                    connected_tech_indices.append(index)\n\n            connected_ids = connection[\"other_connected_ids\"].value\n\n            for index in connected_tech_indices:\n                connected_tech_id = connected_ids[index].value\n                connected_tech = full_data_set.genie_techs[connected_tech_id]\n                effect_bundle_id = connected_tech[\"tech_effect_id\"].value\n                effect_bundle = full_data_set.genie_effect_bundles[effect_bundle_id]\n\n                upgrade_effects = effect_bundle.get_effects(effect_type=3)\n\n                if len(upgrade_effects) == 0:\n                    continue\n\n                # Search upgrade effects for the line_id\n                for upgrade in upgrade_effects:\n                    upgrade_source = upgrade[\"attr_a\"].value\n                    upgrade_target = upgrade[\"attr_b\"].value\n\n                    # Check if the upgrade target is correct\n                    if upgrade_target == building_id:\n                        # Line id is the source building id\n                        line_id = upgrade_source\n                        break\n\n                else:\n                    # If no upgrade was found, then search remaining techs\n                    continue\n\n                # Find the previous building\n                for c_index, _ in enumerate(connected_types):\n                    connected_type = connected_types[c_index][\"other_connection\"].value\n                    if connected_type == 1:\n                        # 1 == Building\n                        connected_index = c_index\n                        break\n\n                else:\n                    raise RuntimeError(f\"Building {building_id} is not first in line, but no \"\n                                       \"previous building could be found in other_connections\")\n\n                previous_building_id = connected_ids[connected_index].value\n                break\n\n            if line_id == building_id:\n                # First building in line\n                if stack_building:\n                    stack_unit_id = building[\"stack_unit_id\"].value\n                    building_line = GenieStackBuildingGroup(stack_unit_id, line_id, full_data_set)\n\n                else:\n                    building_line = GenieBuildingLineGroup(line_id, full_data_set)\n\n                full_data_set.building_lines.update({building_line.get_id(): building_line})\n                building_line.add_unit(building, after=previous_building_id)\n                full_data_set.unit_ref.update({building_id: building_line})\n\n            else:\n                # It's an upgraded building\n                building_line = full_data_set.building_lines[line_id]\n                building_line.add_unit(building, after=previous_building_id)\n                full_data_set.unit_ref.update({building_id: building_line})\n\n    @staticmethod\n    def sanitize_effect_bundles(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Remove garbage data from effect bundles.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        effect_bundles = full_data_set.genie_effect_bundles\n\n        for bundle in effect_bundles.values():\n            sanitized_effects = {}\n\n            effects = bundle.get_effects()\n\n            index = 0\n            for effect in effects:\n                effect_type = effect[\"type_id\"].value\n                if effect_type < 0:\n                    # Effect has no type\n                    continue\n\n                if effect_type == 3:\n                    if effect[\"attr_b\"].value < 0:\n                        # Upgrade to invalid unit\n                        continue\n\n                if effect_type == 102:\n                    if effect[\"attr_d\"].value < 0:\n                        # Tech disable effect with no tech id specified\n                        continue\n\n                sanitized_effects.update({index: effect})\n                index += 1\n\n            bundle.effects = sanitized_effects\n            bundle.sanitized = True\n\n    @staticmethod\n    def create_tech_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create techs from tech connections and unit upgrades/unlocks\n        from unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        tech_connections = full_data_set.tech_connections\n\n        # In tech connection are age ups, building unlocks/upgrades and stat upgrades\n        for connection in tech_connections.values():\n            connected_buildings = connection[\"buildings\"].value\n            tech_id = connection[\"id\"].value\n            tech = full_data_set.genie_techs[tech_id]\n\n            effect_id = tech[\"tech_effect_id\"].value\n            if effect_id < 0:\n                continue\n\n            tech_effects = full_data_set.genie_effect_bundles[effect_id]\n\n            # Check if the tech is an age upgrade\n            tech_found = False\n            resource_effects = tech_effects.get_effects(effect_type=1)\n            for effect in resource_effects:\n                # Resource ID 6: Current Age\n                if effect[\"attr_a\"].value != 6:\n                    continue\n\n                age_id = effect[\"attr_b\"].value\n                age_up = AgeUpgrade(tech_id, age_id, full_data_set)\n                full_data_set.tech_groups.update({age_up.get_id(): age_up})\n                full_data_set.age_upgrades.update({age_up.get_id(): age_up})\n                tech_found = True\n                break\n\n            if tech_found:\n                continue\n\n            if len(connected_buildings) > 0:\n                # Building unlock or upgrade\n                unlock_effects = tech_effects.get_effects(effect_type=2)\n                upgrade_effects = tech_effects.get_effects(effect_type=2)\n                if len(unlock_effects) > 0:\n                    unlock = unlock_effects[0]\n                    unlock_id = unlock[\"attr_a\"].value\n\n                    building_unlock = BuildingUnlock(tech_id, unlock_id, full_data_set)\n                    full_data_set.tech_groups.update(\n                        {building_unlock.get_id(): building_unlock}\n                    )\n                    full_data_set.building_unlocks.update(\n                        {building_unlock.get_id(): building_unlock}\n                    )\n                    continue\n\n                if len(upgrade_effects) > 0:\n                    upgrade = upgrade_effects[0]\n                    line_id = upgrade[\"attr_a\"].value\n                    upgrade_id = upgrade[\"attr_b\"].value\n\n                    building_upgrade = BuildingLineUpgrade(\n                        tech_id,\n                        line_id,\n                        upgrade_id,\n                        full_data_set\n                    )\n                    full_data_set.tech_groups.update(\n                        {building_upgrade.get_id(): building_upgrade}\n                    )\n                    full_data_set.building_upgrades.update(\n                        {building_upgrade.get_id(): building_upgrade}\n                    )\n                    continue\n\n            # Create a stat upgrade for other techs\n            stat_up = StatUpgrade(tech_id, full_data_set)\n            full_data_set.tech_groups.update({stat_up.get_id(): stat_up})\n            full_data_set.stat_upgrades.update({stat_up.get_id(): stat_up})\n\n        # Unit upgrades and unlocks are stored in unit connections\n        unit_connections = full_data_set.unit_connections\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            required_research_id = connection[\"required_research\"].value\n            enabling_research_id = connection[\"enabling_research\"].value\n            line_mode = connection[\"line_mode\"].value\n            line_id = full_data_set.unit_ref[unit_id].get_id()\n\n            if required_research_id == -1 and enabling_research_id == -1:\n                # Unit is unlocked from the start\n                continue\n\n            if line_mode == 2:\n                # Unit is first in line, there should be an unlock tech id\n                # This is usually the enabling tech id\n                unlock_tech_id = enabling_research_id\n                if unlock_tech_id == -1:\n                    # Battle elephant is a curious exception wher it's the required tech id\n                    unlock_tech_id = required_research_id\n\n                unit_unlock = UnitUnlock(unlock_tech_id, line_id, full_data_set)\n                full_data_set.tech_groups.update({unit_unlock.get_id(): unit_unlock})\n                full_data_set.unit_unlocks.update({unit_unlock.get_id(): unit_unlock})\n\n            elif line_mode == 3:\n                # Units further down the line receive line upgrades\n                unit_upgrade = UnitLineUpgrade(required_research_id, line_id,\n                                               unit_id, full_data_set)\n                full_data_set.tech_groups.update({unit_upgrade.get_id(): unit_upgrade})\n                full_data_set.unit_upgrades.update({unit_upgrade.get_id(): unit_upgrade})\n\n        # Initiated techs are stored with buildings\n        genie_units = full_data_set.genie_units\n\n        for genie_unit in genie_units.values():\n            if not genie_unit.has_member(\"research_id\"):\n                continue\n\n            building_id = genie_unit[\"id0\"].value\n            initiated_tech_id = genie_unit[\"research_id\"].value\n\n            if initiated_tech_id == -1:\n                continue\n\n            if building_id not in full_data_set.building_lines.keys():\n                # Skips upgraded buildings (which initiate the same techs)\n                continue\n\n            initiated_tech = InitiatedTech(initiated_tech_id, building_id, full_data_set)\n            full_data_set.tech_groups.update({initiated_tech.get_id(): initiated_tech})\n            full_data_set.initiated_techs.update({initiated_tech.get_id(): initiated_tech})\n\n        # Civ boni have to be aquired from techs\n        # Civ boni = ONLY passive boni (not unit unlocks, unit upgrades or team bonus)\n        genie_techs = full_data_set.genie_techs\n\n        for index, _ in enumerate(genie_techs):\n            tech_id = index\n\n            # Civ ID must be positive and non-zero\n            civ_id = genie_techs[index][\"civilization_id\"].value\n            if civ_id <= 0:\n                continue\n\n            # Passive boni are not researched anywhere\n            research_location_id = genie_techs[index][\"research_location_id\"].value\n            if research_location_id > 0:\n                continue\n\n            # Passive boni are not available in full tech mode\n            full_tech_mode = genie_techs[index][\"full_tech_mode\"].value\n            if full_tech_mode:\n                continue\n\n            civ_bonus = CivBonus(tech_id, civ_id, full_data_set)\n            full_data_set.tech_groups.update({civ_bonus.get_id(): civ_bonus})\n            full_data_set.civ_boni.update({civ_bonus.get_id(): civ_bonus})\n\n    @staticmethod\n    def create_civ_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create civilization groups from civ objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        civ_objects = full_data_set.genie_civs\n\n        for index in range(len(civ_objects)):\n            civ_id = index\n\n            civ_group = GenieCivilizationGroup(civ_id, full_data_set)\n            full_data_set.civ_groups.update({civ_group.get_id(): civ_group})\n\n            index += 1\n\n    @staticmethod\n    def create_villager_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create task groups and assign the relevant male and female group to a\n        villager group.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        units = full_data_set.genie_units\n        task_group_ids = set()\n        unit_ids = set()\n\n        # Find task groups in the dataset\n        for unit in units.values():\n            if unit.has_member(\"task_group\"):\n                task_group_id = unit[\"task_group\"].value\n\n            else:\n                task_group_id = 0\n\n            if task_group_id == 0:\n                # no task group\n                continue\n\n            if task_group_id in task_group_ids:\n                task_group = full_data_set.task_groups[task_group_id]\n                task_group.add_unit(unit)\n\n            else:\n                if task_group_id == 1:\n                    line_id = GenieUnitTaskGroup.male_line_id\n\n                elif task_group_id == 2:\n                    line_id = GenieUnitTaskGroup.female_line_id\n\n                task_group = GenieUnitTaskGroup(line_id, task_group_id, full_data_set)\n                task_group.add_unit(unit)\n                full_data_set.task_groups.update({task_group_id: task_group})\n\n            task_group_ids.add(task_group_id)\n            unit_ids.add(unit[\"id0\"].value)\n\n        # Create the villager task group\n        villager = GenieVillagerGroup(118, task_group_ids, full_data_set)\n        full_data_set.unit_lines.update({villager.get_id(): villager})\n        full_data_set.villager_groups.update({villager.get_id(): villager})\n        for unit_id in unit_ids:\n            full_data_set.unit_ref.update({unit_id: villager})\n\n    @staticmethod\n    def create_ambient_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create ambient groups, mostly for resources and scenery.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        ambient_ids = AMBIENT_GROUP_LOOKUPS.keys()\n        genie_units = full_data_set.genie_units\n\n        for ambient_id in ambient_ids:\n            ambient_group = GenieAmbientGroup(ambient_id, full_data_set)\n            ambient_group.add_unit(genie_units[ambient_id])\n            full_data_set.ambient_groups.update({ambient_group.get_id(): ambient_group})\n            full_data_set.unit_ref.update({ambient_id: ambient_group})\n\n    @staticmethod\n    def create_variant_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create variant groups.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        variants = VARIANT_GROUP_LOOKUPS\n\n        for group_id, variant in variants.items():\n            variant_group = GenieVariantGroup(group_id, full_data_set)\n            full_data_set.variant_groups.update({variant_group.get_id(): variant_group})\n\n            for variant_id in variant[2]:\n                variant_group.add_unit(full_data_set.genie_units[variant_id])\n                full_data_set.unit_ref.update({variant_id: variant_group})\n\n    @staticmethod\n    def create_terrain_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create terrain groups.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        terrains = full_data_set.genie_terrains.values()\n\n        for terrain in terrains:\n            slp_id = terrain[\"slp_id\"].value\n            replacement_id = terrain[\"terrain_replacement_id\"].value\n\n            if slp_id == -1 and replacement_id == -1:\n                # No graphics and no graphics replacement means this terrain is unused\n                continue\n\n            enabled = terrain[\"enabled\"].value\n\n            if enabled:\n                terrain_group = GenieTerrainGroup(terrain.get_id(), full_data_set)\n                full_data_set.terrain_groups.update({terrain.get_id(): terrain_group})\n\n    @staticmethod\n    def link_building_upgrades(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Find building upgrades in the AgeUp techs and append them to the building lines.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        age_ups = full_data_set.age_upgrades\n\n        # Order of age ups should be correct\n        for age_up in age_ups.values():\n            for effect in age_up.effects.get_effects():\n                type_id = effect.get_type()\n\n                if type_id != 3:\n                    continue\n\n                upgrade_source_id = effect[\"attr_a\"].value\n                upgrade_target_id = effect[\"attr_b\"].value\n\n                if upgrade_source_id not in full_data_set.building_lines.keys():\n                    continue\n\n                upgraded_line = full_data_set.building_lines[upgrade_source_id]\n                upgrade_target = full_data_set.genie_units[upgrade_target_id]\n\n                upgraded_line.add_unit(upgrade_target)\n                full_data_set.unit_ref.update({upgrade_target_id: upgraded_line})\n\n    @staticmethod\n    def link_creatables(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link creatable units and buildings to their creating entity. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        # Link units to buildings\n        unit_lines = full_data_set.unit_lines\n\n        for unit_line in unit_lines.values():\n            if unit_line.is_creatable():\n                train_location_id = unit_line.get_train_location_id()\n                full_data_set.building_lines[train_location_id].add_creatable(unit_line)\n\n        # Link buildings to villagers and fishing ships\n        building_lines = full_data_set.building_lines\n\n        for building_line in building_lines.values():\n            if building_line.is_creatable():\n                train_location_id = building_line.get_train_location_id()\n\n                if train_location_id in full_data_set.villager_groups.keys():\n                    full_data_set.villager_groups[train_location_id].add_creatable(building_line)\n\n                else:\n                    # try normal units\n                    full_data_set.unit_lines[train_location_id].add_creatable(building_line)\n\n    @staticmethod\n    def link_researchables(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link techs to their buildings. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        tech_groups = full_data_set.tech_groups\n\n        for tech in tech_groups.values():\n            if tech.is_researchable():\n                research_location_id = tech.get_research_location_id()\n                full_data_set.building_lines[research_location_id].add_researchable(tech)\n\n    @staticmethod\n    def link_civ_uniques(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link civ bonus techs, unique units and unique techs to their civs.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        for bonus in full_data_set.civ_boni.values():\n            civ_id = bonus.get_civilization()\n            full_data_set.civ_groups[civ_id].add_civ_bonus(bonus)\n\n        for unit_line in full_data_set.unit_lines.values():\n            if unit_line.is_unique():\n                head_unit_id = unit_line.get_head_unit_id()\n                head_unit_connection = full_data_set.unit_connections[head_unit_id]\n                enabling_research_id = head_unit_connection[\"enabling_research\"].value\n                enabling_research = full_data_set.genie_techs[enabling_research_id]\n                enabling_civ_id = enabling_research[\"civilization_id\"].value\n\n                full_data_set.civ_groups[enabling_civ_id].add_unique_entity(unit_line)\n\n        for building_line in full_data_set.building_lines.values():\n            if building_line.is_unique():\n                head_unit_id = building_line.get_head_unit_id()\n                head_building_connection = full_data_set.building_connections[head_unit_id]\n                enabling_research_id = head_building_connection[\"enabling_research\"].value\n                enabling_research = full_data_set.genie_techs[enabling_research_id]\n                enabling_civ_id = enabling_research[\"civilization_id\"].value\n\n                full_data_set.civ_groups[enabling_civ_id].add_unique_entity(building_line)\n\n        for tech_group in full_data_set.tech_groups.values():\n            if tech_group.is_unique() and tech_group.is_researchable():\n                civ_id = tech_group.get_civilization()\n                full_data_set.civ_groups[civ_id].add_unique_tech(tech_group)\n\n    @staticmethod\n    def link_gatherers_to_dropsites(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link gatherers to the buildings they drop resources off. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        villager_groups = full_data_set.villager_groups\n\n        for villager in villager_groups.values():\n            for unit in villager.variants[0].line:\n                drop_site_members = unit[\"drop_sites\"].value\n                unit_id = unit[\"id0\"].value\n\n                for drop_site_member in drop_site_members:\n                    drop_site_id = drop_site_member.value\n\n                    if drop_site_id > -1:\n                        drop_site = full_data_set.building_lines[drop_site_id]\n                        drop_site.add_gatherer_id(unit_id)\n\n    @staticmethod\n    def link_garrison(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link a garrison unit to the lines that are stored and vice versa. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        garrisoned_lines = {}\n        garrisoned_lines.update(full_data_set.unit_lines)\n        garrisoned_lines.update(full_data_set.ambient_groups)\n\n        garrison_lines = {}\n        garrison_lines.update(full_data_set.unit_lines)\n        garrison_lines.update(full_data_set.building_lines)\n\n        # Search through all units and look at their garrison commands\n        for unit_line in garrisoned_lines.values():\n            garrison_classes = []\n            garrison_units = []\n\n            if unit_line.has_command(3):\n                unit_commands = unit_line.get_head_unit()[\"unit_commands\"].value\n                for command in unit_commands:\n                    type_id = command[\"type\"].value\n\n                    if type_id != 3:\n                        continue\n\n                    class_id = command[\"class_id\"].value\n                    if class_id > -1:\n                        garrison_classes.append(class_id)\n\n                        if class_id == 3:\n                            # Towers because Ensemble didn't like consistent rules\n                            garrison_classes.append(52)\n\n                    unit_id = command[\"unit_id\"].value\n                    if unit_id > -1:\n                        garrison_units.append(unit_id)\n\n            for garrison_line in garrison_lines.values():\n                if not garrison_line.is_garrison():\n                    continue\n\n                # Natural garrison\n                garrison_mode = garrison_line.get_garrison_mode()\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    if unit_line.get_head_unit().has_member(\"creatable_type\"):\n                        creatable_type = unit_line.get_head_unit()[\"creatable_type\"].value\n\n                    else:\n                        creatable_type = 0\n\n                    if garrison_line.get_head_unit().has_member(\"garrison_type\"):\n                        garrison_type = garrison_line.get_head_unit()[\"garrison_type\"].value\n\n                    else:\n                        garrison_type = 0\n\n                    if creatable_type == 1 and not garrison_type & 0x01:\n                        continue\n\n                    if creatable_type == 2 and not garrison_type & 0x02:\n                        continue\n\n                    if creatable_type == 3 and not garrison_type & 0x04:\n                        continue\n\n                    if creatable_type == 6 and not garrison_type & 0x08:\n                        continue\n\n                    if garrison_line.get_class_id() in garrison_classes:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n                        continue\n\n                    if garrison_line.get_head_unit_id() in garrison_units:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n                        continue\n\n                # Transports/ unit garrisons (no conditions)\n                elif garrison_mode in (GenieGarrisonMode.TRANSPORT,\n                                       GenieGarrisonMode.UNIT_GARRISON):\n                    if garrison_line.get_class_id() in garrison_classes:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n\n                # Self produced units (these cannot be determined from commands)\n                elif garrison_mode == GenieGarrisonMode.SELF_PRODUCED:\n                    if unit_line in garrison_line.creates:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n\n                # Monk inventories\n                elif garrison_mode == GenieGarrisonMode.MONK:\n                    # Search for a pickup command\n                    unit_commands = garrison_line.get_head_unit()[\"unit_commands\"].value\n                    for command in unit_commands:\n                        type_id = command[\"type\"].value\n\n                        if type_id != 132:\n                            continue\n\n                        unit_id = command[\"unit_id\"].value\n                        if unit_id == unit_line.get_head_unit_id():\n                            unit_line.garrison_locations.append(garrison_line)\n                            garrison_line.garrison_entities.append(unit_line)\n\n    @staticmethod\n    def link_trade_posts(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link a trade post building to the lines that it trades with.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        unit_lines = full_data_set.unit_lines.values()\n\n        for unit_line in unit_lines:\n            if unit_line.has_command(111):\n                head_unit = unit_line.get_head_unit()\n                unit_commands = head_unit[\"unit_commands\"].value\n                trade_post_id = -1\n                for command in unit_commands:\n                    # Find the trade command and the trade post id\n                    type_id = command[\"type\"].value\n\n                    if type_id != 111:\n                        continue\n\n                    trade_post_id = command[\"unit_id\"].value\n\n                    # Notify buiding\n                    if trade_post_id in full_data_set.building_lines.keys():\n                        full_data_set.building_lines[trade_post_id].add_trading_line(unit_line)\n\n    @staticmethod\n    def link_repairables(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Set units/buildings as repairable\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        villager_groups = full_data_set.villager_groups\n\n        repair_lines = {}\n        repair_lines.update(full_data_set.unit_lines)\n        repair_lines.update(full_data_set.building_lines)\n\n        repair_classes = []\n        for villager in villager_groups.values():\n            repair_unit = villager.get_units_with_command(106)[0]\n            unit_commands = repair_unit[\"unit_commands\"].value\n            for command in unit_commands:\n                type_id = command[\"type\"].value\n\n                if type_id != 106:\n                    continue\n\n                class_id = command[\"class_id\"].value\n                if class_id == -1:\n                    # Buildings/Siege\n                    repair_classes.append(3)\n                    repair_classes.append(13)\n                    repair_classes.append(52)\n                    repair_classes.append(54)\n                    repair_classes.append(55)\n\n                else:\n                    repair_classes.append(class_id)\n\n        for repair_line in repair_lines.values():\n            if repair_line.get_class_id() in repair_classes:\n                repair_line.repairable = True\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/tech_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates patches for technologies.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom openage.log import warn\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup, \\\n    CivTeamBonus, CivBonus\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n    GenieBuildingLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom .upgrade_ability_subprocessor import AoCUpgradeAbilitySubprocessor\nfrom .upgrade_attribute_subprocessor import AoCUpgradeAttributeSubprocessor\nfrom .upgrade_resource_subprocessor import AoCUpgradeResourceSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject\n\n\nclass AoCTechSubprocessor:\n    \"\"\"\n    Creates raw API objects and patches for techs and civ setups in AoC.\n    \"\"\"\n\n    upgrade_attribute_funcs = {\n        0: AoCUpgradeAttributeSubprocessor.hp_upgrade,\n        1: AoCUpgradeAttributeSubprocessor.los_upgrade,\n        2: AoCUpgradeAttributeSubprocessor.garrison_capacity_upgrade,\n        3: AoCUpgradeAttributeSubprocessor.unit_size_x_upgrade,\n        4: AoCUpgradeAttributeSubprocessor.unit_size_y_upgrade,\n        5: AoCUpgradeAttributeSubprocessor.move_speed_upgrade,\n        6: AoCUpgradeAttributeSubprocessor.rotation_speed_upgrade,\n        8: AoCUpgradeAttributeSubprocessor.armor_upgrade,\n        9: AoCUpgradeAttributeSubprocessor.attack_upgrade,\n        10: AoCUpgradeAttributeSubprocessor.reload_time_upgrade,\n        11: AoCUpgradeAttributeSubprocessor.accuracy_upgrade,\n        12: AoCUpgradeAttributeSubprocessor.max_range_upgrade,\n        13: AoCUpgradeAttributeSubprocessor.work_rate_upgrade,\n        14: AoCUpgradeAttributeSubprocessor.carry_capacity_upgrade,\n        16: AoCUpgradeAttributeSubprocessor.projectile_unit_upgrade,\n        17: AoCUpgradeAttributeSubprocessor.graphics_angle_upgrade,\n        18: AoCUpgradeAttributeSubprocessor.terrain_defense_upgrade,\n        19: AoCUpgradeAttributeSubprocessor.ballistics_upgrade,\n        20: AoCUpgradeAttributeSubprocessor.min_range_upgrade,\n        21: AoCUpgradeAttributeSubprocessor.resource_storage_1_upgrade,\n        22: AoCUpgradeAttributeSubprocessor.blast_radius_upgrade,\n        23: AoCUpgradeAttributeSubprocessor.search_radius_upgrade,\n        42: AoCUpgradeAttributeSubprocessor.standing_wonders_upgrade,\n        46: AoCUpgradeAttributeSubprocessor.tribute_inefficiency_upgrade,\n        48: AoCUpgradeAttributeSubprocessor.tc_available_upgrade,\n        49: AoCUpgradeAttributeSubprocessor.gold_counter_upgrade,\n        57: AoCUpgradeAttributeSubprocessor.kidnap_storage_upgrade,\n        100: AoCUpgradeAttributeSubprocessor.resource_cost_upgrade,\n        101: AoCUpgradeAttributeSubprocessor.creation_time_upgrade,\n        102: AoCUpgradeAttributeSubprocessor.min_projectiles_upgrade,\n        103: AoCUpgradeAttributeSubprocessor.cost_food_upgrade,\n        104: AoCUpgradeAttributeSubprocessor.cost_wood_upgrade,\n        105: AoCUpgradeAttributeSubprocessor.cost_gold_upgrade,\n        106: AoCUpgradeAttributeSubprocessor.cost_stone_upgrade,\n        107: AoCUpgradeAttributeSubprocessor.max_projectiles_upgrade,\n        108: AoCUpgradeAttributeSubprocessor.garrison_heal_upgrade,\n    }\n\n    upgrade_resource_funcs = {\n        4: AoCUpgradeResourceSubprocessor.starting_population_space_upgrade,\n        27: AoCUpgradeResourceSubprocessor.monk_conversion_upgrade,\n        28: AoCUpgradeResourceSubprocessor.building_conversion_upgrade,\n        32: AoCUpgradeResourceSubprocessor.bonus_population_upgrade,\n        35: AoCUpgradeResourceSubprocessor.faith_recharge_rate_upgrade,\n        36: AoCUpgradeResourceSubprocessor.farm_food_upgrade,\n        46: AoCUpgradeResourceSubprocessor.tribute_inefficiency_upgrade,\n        47: AoCUpgradeResourceSubprocessor.gather_gold_efficiency_upgrade,\n        50: AoCUpgradeResourceSubprocessor.reveal_ally_upgrade,\n        77: AoCUpgradeResourceSubprocessor.conversion_resistance_upgrade,\n        78: AoCUpgradeResourceSubprocessor.trade_penalty_upgrade,\n        79: AoCUpgradeResourceSubprocessor.gather_stone_efficiency_upgrade,\n        84: AoCUpgradeResourceSubprocessor.starting_villagers_upgrade,\n        85: AoCUpgradeResourceSubprocessor.chinese_tech_discount_upgrade,\n        86: AoCUpgradeResourceSubprocessor.research_time_upgrade,\n        89: AoCUpgradeResourceSubprocessor.heal_rate_upgrade,\n        90: AoCUpgradeResourceSubprocessor.heal_range_upgrade,\n        91: AoCUpgradeResourceSubprocessor.starting_food_upgrade,\n        92: AoCUpgradeResourceSubprocessor.starting_wood_upgrade,\n        94: AoCUpgradeResourceSubprocessor.starting_gold_upgrade,\n        96: AoCUpgradeResourceSubprocessor.berserk_heal_rate_upgrade,\n        97: AoCUpgradeResourceSubprocessor.herding_dominance_upgrade,\n        178: AoCUpgradeResourceSubprocessor.conversion_resistance_min_rounds_upgrade,\n        179: AoCUpgradeResourceSubprocessor.conversion_resistance_max_rounds_upgrade,\n        183: AoCUpgradeResourceSubprocessor.reveal_enemy_upgrade,\n        189: AoCUpgradeResourceSubprocessor.gather_wood_efficiency_upgrade,\n        190: AoCUpgradeResourceSubprocessor.gather_food_efficiency_upgrade,\n        191: AoCUpgradeResourceSubprocessor.relic_gold_bonus_upgrade,\n        192: AoCUpgradeResourceSubprocessor.heresy_upgrade,\n        193: AoCUpgradeResourceSubprocessor.theocracy_upgrade,\n        194: AoCUpgradeResourceSubprocessor.crenellations_upgrade,\n        196: AoCUpgradeResourceSubprocessor.wonder_time_increase_upgrade,\n        197: AoCUpgradeResourceSubprocessor.spies_discount_upgrade,\n    }\n\n    @classmethod\n    def get_patches(cls, converter_group: ConverterObjectGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for a converter group, depending on the type\n        of its effects.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n        team_bonus = False\n\n        if isinstance(converter_group, CivTeamBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n            team_bonus = True\n\n        elif isinstance(converter_group, CivBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n\n        else:\n            effects = converter_group.get_effects()\n\n        team_effect = False\n        for effect in effects:\n            type_id = effect.get_type()\n\n            if team_bonus or type_id in (10, 11, 12, 13, 14, 15, 16):\n                team_effect = True\n                type_id -= 10\n\n            if type_id in (0, 4, 5):\n                patches.extend(cls.attribute_modify_effect(converter_group,\n                                                           effect,\n                                                           team=team_effect))\n\n            elif type_id in (1, 6):\n                patches.extend(cls.resource_modify_effect(converter_group,\n                                                          effect,\n                                                          team=team_effect))\n\n            elif type_id == 2:\n                # Enabling/disabling units: Handled in creatable conditions\n                pass\n\n            elif type_id == 3:\n                patches.extend(cls.upgrade_unit_effect(converter_group, effect))\n\n            elif type_id == 101:\n                patches.extend(cls.tech_cost_modify_effect(converter_group,\n                                                           effect,\n                                                           team=team_effect))\n\n            elif type_id == 102:\n                # Tech disable: Only used for civ tech tree\n                pass\n\n            elif type_id == 103:\n                patches.extend(cls.tech_time_modify_effect(converter_group,\n                                                           effect,\n                                                           team=team_effect))\n\n            team_effect = False\n\n        return patches\n\n    @staticmethod\n    def attribute_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying attributes of entities.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type in (0, 10):\n            operator = MemberOperator.ASSIGN\n\n        elif effect_type in (4, 14):\n            operator = MemberOperator.ADD\n\n        elif effect_type in (5, 15):\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid attribute effect\")\n\n        unit_id = effect[\"attr_a\"].value\n        class_id = effect[\"attr_b\"].value\n        attribute_type = effect[\"attr_c\"].value\n        value = effect[\"attr_d\"].value\n\n        if attribute_type == -1:\n            return patches\n\n        affected_entities = []\n        if unit_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.contains_entity(unit_id):\n                    affected_entities.append(line)\n\n                elif attribute_type == 19:\n                    if line.is_projectile_shooter() and line.has_projectile(unit_id):\n                        affected_entities.append(line)\n\n        elif class_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.get_class_id() == class_id:\n                    affected_entities.append(line)\n\n        else:\n            return patches\n\n        upgrade_func = AoCTechSubprocessor.upgrade_attribute_funcs[attribute_type]\n        for affected_entity in affected_entities:\n            patches.extend(upgrade_func(converter_group, affected_entity, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def resource_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying resources.\n        \"\"\"\n        patches = []\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type in (1, 11):\n            mode = effect[\"attr_b\"].value\n\n            if mode == 0:\n                operator = MemberOperator.ASSIGN\n\n            else:\n                operator = MemberOperator.ADD\n\n        elif effect_type in (6, 16):\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid resource effect\")\n\n        resource_id = effect[\"attr_a\"].value\n        value = effect[\"attr_d\"].value\n\n        if resource_id in (-1, 6, 21):\n            # -1 = invalid ID\n            # 6  = set current age (unused)\n            # 21 = tech count (unused)\n            return patches\n\n        upgrade_func = AoCTechSubprocessor.upgrade_resource_funcs[resource_id]\n        patches.extend(upgrade_func(converter_group, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def upgrade_unit_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for upgrading entities in a line.\n        \"\"\"\n        patches = []\n        tech_id = converter_group.get_id()\n        dataset = converter_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        upgrade_source_id = effect[\"attr_a\"].value\n        upgrade_target_id = effect[\"attr_b\"].value\n\n        if upgrade_source_id not in dataset.unit_ref.keys() or\\\n                upgrade_target_id not in dataset.unit_ref.keys():\n            # Skip annexes or transform units\n            return patches\n\n        line = dataset.unit_ref[upgrade_source_id]\n        upgrade_source_pos = line.get_unit_position(upgrade_source_id)\n        try:\n            upgrade_target_pos = line.get_unit_position(upgrade_target_id)\n\n        except KeyError:\n            # TODO: Implement branching line upgrades\n            warn(f\"Could not create upgrade from unit {upgrade_source_id} to {upgrade_target_id}\")\n            return patches\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Building upgrades always reference the head unit\n            # so we use the decremented target id instead\n            upgrade_source_pos = upgrade_target_pos - 1\n\n        elif upgrade_target_pos - upgrade_source_pos != 1:\n            # Skip effects that upgrades entities not next to each other in\n            # the line.\n            return patches\n\n        upgrade_source = line.line[upgrade_source_pos]\n        upgrade_target = line.line[upgrade_target_pos]\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        diff = upgrade_source.diff(upgrade_target)\n\n        patches.extend(AoCUpgradeAbilitySubprocessor.death_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.despawn_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.idle_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.live_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.los_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.named_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.resistance_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.selectable_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.turn_ability(\n            converter_group, line, tech_name, diff))\n\n        if line.is_projectile_shooter():\n            patches.extend(AoCUpgradeAbilitySubprocessor.shoot_projectile_ability(converter_group, line,\n                                                                                  tech_name,\n                                                                                  upgrade_source,\n                                                                                  upgrade_target,\n                                                                                  7, diff))\n        elif line.is_melee() or line.is_ranged():\n            if line.has_command(7):\n                # Attack\n                patches.extend(AoCUpgradeAbilitySubprocessor.apply_discrete_effect_ability(converter_group,\n                                                                                           line, tech_name,\n                                                                                           7,\n                                                                                           line.is_ranged(),\n                                                                                           diff))\n\n        if isinstance(line, GenieUnitLineGroup):\n            patches.extend(AoCUpgradeAbilitySubprocessor.move_ability(converter_group, line,\n                                                                      tech_name, diff))\n\n        if isinstance(line, GenieBuildingLineGroup):\n            patches.extend(AoCUpgradeAbilitySubprocessor.attribute_change_tracker_ability(converter_group, line,\n                                                                                          tech_name, diff))\n\n        return patches\n\n    @staticmethod\n    def tech_cost_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying tech costs.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        tech_id = effect[\"attr_a\"].value\n        resource_id = effect[\"attr_b\"].value\n        mode = effect[\"attr_c\"].value\n        amount = int(effect[\"attr_d\"].value)\n\n        if tech_id not in tech_lookup_dict:\n            # Skips some legacy techs from AoK such as the tech for bombard cannon\n            return patches\n\n        tech_group = dataset.tech_groups[tech_id]\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        if resource_id == 0:\n            resource_name = \"Food\"\n\n        elif resource_id == 1:\n            resource_name = \"Wood\"\n\n        elif resource_id == 2:\n            resource_name = \"Stone\"\n\n        elif resource_id == 3:\n            resource_name = \"Gold\"\n\n        else:\n            raise ValueError(\"no valid resource ID found\")\n\n        # Check if the tech actually costs an amount of the defined resource\n        for resource_amount in tech_group.tech[\"research_resource_costs\"].value:\n            cost_resource_id = resource_amount[\"type_id\"].value\n\n            if cost_resource_id == resource_id:\n                break\n\n        else:\n            # Skip patch generation if no matching resource cost was found\n            return patches\n\n        if mode == 0:\n            operator = MemberOperator.ASSIGN\n\n        else:\n            operator = MemberOperator.ADD\n\n        patch_target_ref = f\"{tech_name}.ResearchableTech.{tech_name}Cost.{resource_name}Amount\"\n        patch_target_forward_ref = ForwardRef(tech_group, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{tech_name}CostWrapper\"\n        wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{tech_name}Cost\"\n        nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       amount,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def tech_time_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying tech research times.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        tech_id = effect[\"attr_a\"].value\n        mode = effect[\"attr_c\"].value\n        research_time = effect[\"attr_d\"].value\n\n        if tech_id not in tech_lookup_dict:\n            # Skips some legacy techs from AoK such as the tech for bombard cannon\n            return patches\n\n        tech_group = dataset.tech_groups[tech_id]\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        if mode == 0:\n            operator = MemberOperator.ASSIGN\n\n        else:\n            operator = MemberOperator.ADD\n\n        patch_target_ref = f\"{tech_name}.ResearchableTech\"\n        patch_target_forward_ref = ForwardRef(tech_group, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{tech_name}ResearchTimeWrapper\"\n        wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{tech_name}ResearchTime\"\n        nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"research_time\",\n                                                       research_time,\n                                                       \"engine.util.research.ResearchableTech\",\n                                                       operator)\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/upgrade_ability_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,invalid-name\n# pylint: disable=too-many-public-methods,too-many-branches,too-many-arguments\n#\n# TODO:\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for abilities.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom math import degrees\n\nfrom .....nyan.nyan_structs import MemberOperator, MemberSpecialValue\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup, \\\n    GenieVariantGroup, GenieUnitLineGroup\nfrom ....entity_object.conversion.combined_sound import CombinedSound\nfrom ....entity_object.conversion.combined_sprite import CombinedSprite\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ....value_object.read.value_members import NoDiffMember\nfrom .upgrade_effect_subprocessor import AoCUpgradeEffectSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject, \\\n        ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup, \\\n        GenieUnitObject\n\n\nclass AoCUpgradeAbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for ability upgrade effects in AoC.\n    \"\"\"\n\n    @staticmethod\n    def apply_continuous_effect_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        command_id: int,\n        ranged: bool = False,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ApplyContinuousEffect ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        ability_name = command_lookup_dict[command_id][0]\n\n        data_changed = False\n        diff_animation = diff[\"attack_sprite_id\"]\n        diff_comm_sound = diff[\"command_sound_id\"]\n        diff_frame_delay = diff[\"frame_delay\"]\n        if any(not isinstance(value, NoDiffMember) for value in (diff_frame_delay)):\n            data_changed = True\n\n        # Command types Heal, Construct, Repair are not upgraded by lines\n\n        diff_min_range = None\n        diff_max_range = None\n        if not data_changed and ranged:\n            diff_min_range = diff[\"weapon_range_min\"]\n            diff_max_range = diff[\"weapon_range_max\"]\n            if any(not isinstance(value, NoDiffMember) for value in (\n                diff_min_range,\n                diff_max_range\n            )):\n                data_changed = True\n\n        if not isinstance(diff_animation, NoDiffMember):\n            diff_animation_id = diff_animation.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_animation_id]\n            )\n            patches.append(anim_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if not isinstance(diff_comm_sound, NoDiffMember):\n            diff_comm_sound_id = diff_comm_sound.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_comm_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{ability_name}Wrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_frame_delay, NoDiffMember):\n                if not isinstance(diff_animation, NoDiffMember):\n                    attack_graphic_id = diff_animation.value\n\n                else:\n                    attack_graphic_id = diff_animation.ref.value\n\n                attack_graphic = dataset.genie_graphics[attack_graphic_id]\n                frame_rate = attack_graphic.get_frame_rate()\n                frame_delay = diff_frame_delay.value\n                application_delay = frame_rate * frame_delay\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"application_delay\",\n                                                               application_delay,\n                                                               \"engine.ability.type.ApplyContinuousEffect\",\n                                                               MemberOperator.ASSIGN)\n\n            if ranged:\n                if not isinstance(diff_min_range, NoDiffMember):\n                    min_range = diff_min_range.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"min_range\",\n                                                                   min_range,\n                                                                   \"engine.ability.type.RangedContinuousEffect\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_max_range, NoDiffMember):\n                    max_range = diff_max_range.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                                   max_range,\n                                                                   \"engine.ability.type.RangedContinuousEffect\",\n                                                                   MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def apply_discrete_effect_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        command_id: int,\n        ranged: bool = False,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ApplyDiscreteEffect ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        ability_name = command_lookup_dict[command_id][0]\n\n        data_changed = False\n        diff_animation = diff[\"attack_sprite_id\"]\n        diff_comm_sound = diff[\"command_sound_id\"]\n        diff_reload_time = diff[\"attack_speed\"]\n        diff_frame_delay = diff[\"frame_delay\"]\n        if any(not isinstance(value, NoDiffMember) for value in (diff_reload_time,\n                                                                 diff_frame_delay)):\n            data_changed = True\n\n        diff_min_range = None\n        diff_max_range = None\n        if ranged:\n            diff_min_range = diff[\"weapon_range_min\"]\n            diff_max_range = diff[\"weapon_range_max\"]\n            if any(not isinstance(value, NoDiffMember) for value in (\n                diff_min_range,\n                diff_max_range\n            )):\n                data_changed = True\n\n        if not isinstance(diff_animation, NoDiffMember):\n            diff_animation_id = diff_animation.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_animation_id]\n            )\n            patches.append(anim_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if not isinstance(diff_comm_sound, NoDiffMember):\n            diff_comm_sound_id = diff_comm_sound.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_comm_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{ability_name}Wrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_reload_time, NoDiffMember):\n                reload_time = diff_reload_time.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"reload_time\",\n                                                               reload_time,\n                                                               \"engine.ability.type.ApplyDiscreteEffect\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_frame_delay, NoDiffMember):\n                if not isinstance(diff_animation, NoDiffMember):\n                    attack_graphic_id = diff_animation.value\n\n                else:\n                    attack_graphic_id = diff_animation.ref.value\n\n                attack_graphic = dataset.genie_graphics[attack_graphic_id]\n                frame_rate = attack_graphic.get_frame_rate()\n                frame_delay = diff_frame_delay.value\n                application_delay = frame_rate * frame_delay\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"application_delay\",\n                                                               application_delay,\n                                                               \"engine.ability.type.ApplyDiscreteEffect\",\n                                                               MemberOperator.ASSIGN)\n\n            if ranged:\n                if not isinstance(diff_min_range, NoDiffMember):\n                    min_range = diff_min_range.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"min_range\",\n                                                                   min_range,\n                                                                   \"engine.ability.type.RangedApplyDiscreteEffect\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_max_range, NoDiffMember):\n                    max_range = diff_max_range.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                                   max_range,\n                                                                   \"engine.ability.type.RangedApplyDiscreteEffect\",\n                                                                   MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        # Seperate because effects get their own wrappers from the subprocessor\n        data_changed = False\n        diff_attacks = None\n        if not data_changed and command_id == 7:\n            diff_attacks = diff[\"attacks\"]\n            if not isinstance(diff_attacks, NoDiffMember):\n                data_changed = True\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            if command_id == 7 and not isinstance(diff_attacks, NoDiffMember):\n                patches.extend(AoCUpgradeEffectSubprocessor.get_attack_effects(converter_group,\n                                                                               line, diff,\n                                                                               patch_target_ref))\n\n        return patches\n\n    @staticmethod\n    def attribute_change_tracker_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the AttributeChangeTracker ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_damage_graphics = diff[\"damage_graphics\"]\n            if isinstance(diff_damage_graphics, NoDiffMember):\n                return patches\n\n            diff_damage_animations = diff_damage_graphics.value\n\n        else:\n            return patches\n\n        percentage = 0\n        for diff_damage_animation in diff_damage_animations:\n            if isinstance(diff_damage_animation, NoDiffMember) or\\\n                    isinstance(diff_damage_animation[\"graphic_id\"], NoDiffMember):\n                continue\n\n            # This should be a NoDiffMember\n            percentage = diff_damage_animation[\"damage_percent\"].ref.value\n\n            patch_target_ref = (f\"{game_entity_name}.AttributeChangeTracker.\"\n                                f\"ChangeProgress{percentage}.AnimationOverlay\")\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}DamageGraphic{percentage}Wrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}DamageGraphic{str(percentage)}\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            animations_set = []\n            diff_animation_id = diff_damage_animation[\"graphic_id\"].value\n            if diff_animation_id > -1:\n                # Patch the new animation in\n                animation_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation(\n                    converter_group,\n                    line,\n                    diff_animation_id,\n                    nyan_patch_ref,\n                    \"Idle\",\n                    f\"idle_damage_override_{percentage}_\"\n                )\n                animations_set.append(animation_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"overlays\",\n                                                           animations_set,\n                                                           \"engine.util.progress.property.type.AnimationOverlay\",\n                                                           MemberOperator.ASSIGN)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def death_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Death ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_animation = diff[\"dying_graphic\"]\n            if isinstance(diff_animation, NoDiffMember):\n                return patches\n\n            # TODO: If the head unit has an invalid -1 graphic, it doesnt get the Animated\n            #       property for the ability in the ability subprocessor, so\n            #       we can't patch it here.\n            #\n            #       We have to find a solution for this, e.g. patch in the Animated ability\n            #       here or in the ability subprocessor.\n            if line.get_head_unit()[\"dying_graphic\"].value == -1:\n                return patches\n\n            diff_animation_id = diff_animation.value\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Death\"\n        nyan_patch_name = f\"Change{game_entity_name}Death\"\n\n        # Nyan patch\n        wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n            converter_group,\n            line,\n            patch_target_ref,\n            nyan_patch_name,\n            container_obj_ref,\n            \"Death\",\n            \"death_\",\n            [diff_animation_id]\n        )\n        patches.append(anim_patch_forward_ref)\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper.set_location((\"data/game_entity/generic/\"\n                                  f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        return patches\n\n    @staticmethod\n    def despawn_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Despawn ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_dead_unit = diff[\"dead_unit_id\"]\n            if isinstance(diff_dead_unit, NoDiffMember):\n                return patches\n\n            diff_animation_id = dataset.genie_units[diff_dead_unit.value][\"idle_graphic0\"].value\n\n            # TODO: If the head unit has an invalid -1 graphic, it doesnt get the Animated\n            #       property for the ability in the ability subprocessor, so\n            #       we can't patch it here.\n            #\n            #       We have to find a solution for this, e.g. patch in the Animated ability\n            #       here or in the ability subprocessor.\n            dead_unit_id = line.get_head_unit()[\"dead_unit_id\"].value\n            if dead_unit_id == -1:\n                return patches\n\n            dead_unit = dataset.genie_units[dead_unit_id]\n            dead_unit_animation_id = dead_unit[\"idle_graphic0\"].value\n            if dead_unit_animation_id == -1:\n                return patches\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Despawn\"\n        nyan_patch_name = f\"Change{game_entity_name}Despawn\"\n\n        # Nyan patch\n        wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n            converter_group,\n            line,\n            patch_target_ref,\n            nyan_patch_name,\n            container_obj_ref,\n            \"Despawn\",\n            \"despawn_\",\n            [diff_animation_id]\n        )\n        patches.append(anim_patch_forward_ref)\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper.set_location((\"data/game_entity/generic/\"\n                                  f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        return patches\n\n    @staticmethod\n    def idle_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Idle ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_animation = diff[\"idle_graphic0\"]\n            if isinstance(diff_animation, NoDiffMember):\n                return patches\n\n            # TODO: If the head unit has an invalid -1 graphic, it doesnt get the Animated\n            #       property for the ability in the ability subprocessor, so\n            #       we can't patch it here.\n            #\n            #       We have to find a solution for this, e.g. patch in the Animated ability\n            #       here or in the ability subprocessor.\n            if line.get_head_unit()[\"idle_graphic0\"].value == -1:\n                return patches\n\n            diff_animation_id = diff_animation.value\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Idle\"\n        nyan_patch_name = f\"Change{game_entity_name}Idle\"\n\n        # Nyan patch\n        wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n            converter_group,\n            line,\n            patch_target_ref,\n            nyan_patch_name,\n            container_obj_ref,\n            \"Idle\",\n            \"idle_\",\n            [diff_animation_id]\n        )\n        patches.append(anim_patch_forward_ref)\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper.set_location((\"data/game_entity/generic/\"\n                                  f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        return patches\n\n    @staticmethod\n    def live_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Live ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_hp = diff[\"hit_points\"]\n            if isinstance(diff_hp, NoDiffMember):\n                return patches\n\n            diff_hp_value = diff_hp.value\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Live.Health\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}HealthWrapper\"\n        wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                 f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        else:\n            wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}Health\"\n        nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # HP max value\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_value\",\n                                                       diff_hp_value,\n                                                       \"engine.util.attribute.AttributeSetting\",\n                                                       MemberOperator.ADD)\n\n        # HP starting value\n        nyan_patch_raw_api_object.add_raw_patch_member(\"starting_value\",\n                                                       diff_hp_value,\n                                                       \"engine.util.attribute.AttributeSetting\",\n                                                       MemberOperator.ADD)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def los_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the LineOfSight ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_line_of_sight = diff[\"line_of_sight\"]\n            if isinstance(diff_line_of_sight, NoDiffMember):\n                return patches\n\n            diff_los_range = diff_line_of_sight.value\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.LineOfSight\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}LineOfSightWrapper\"\n        wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                 f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        else:\n            wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}LineOfSight\"\n        nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # Line of Sight\n        nyan_patch_raw_api_object.add_raw_patch_member(\"range\",\n                                                       diff_los_range,\n                                                       \"engine.ability.type.LineOfSight\",\n                                                       MemberOperator.ADD)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def move_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Move ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        data_changed = False\n        diff_move_animation = diff[\"move_graphics\"]\n        diff_comm_sound = diff[\"command_sound_id\"]\n        diff_move_speed = diff[\"speed\"]\n        if any(not isinstance(value, NoDiffMember) for value in (diff_move_speed,)):\n            data_changed = True\n\n        if not isinstance(diff_move_animation, NoDiffMember):\n            diff_animation_id = diff_move_animation.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.Move\"\n            nyan_patch_name = f\"Change{game_entity_name}Move\"\n            wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                \"Move\",\n                \"move_\",\n                [diff_animation_id]\n            )\n            patches.append(anim_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if not isinstance(diff_comm_sound, NoDiffMember):\n            diff_comm_sound_id = diff_comm_sound.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.Move\"\n            nyan_patch_name = f\"Change{game_entity_name}Move\"\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                \"Move\",\n                \"move_\",\n                [diff_comm_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.Move\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}MoveWrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}Move\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_move_speed, NoDiffMember):\n                diff_speed_value = diff_move_speed.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"speed\",\n                                                               diff_speed_value,\n                                                               \"engine.ability.type.Move\",\n                                                               MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def named_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Named ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        group_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            obj_prefix = tech_lookup_dict[group_id][0]\n\n        else:\n            obj_prefix = game_entity_name\n\n        diff_name = diff[\"language_dll_name\"]\n        if not isinstance(diff_name, NoDiffMember):\n            patch_target_ref = f\"{game_entity_name}.Named.{game_entity_name}Name\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}NameWrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[group_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}Name\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            name_string_id = diff_name.value\n            translations = AoCUpgradeAbilitySubprocessor.create_language_strings(\n                converter_group,\n                name_string_id,\n                nyan_patch_ref,\n                f\"{obj_prefix}Name\"\n            )\n            nyan_patch_raw_api_object.add_raw_patch_member(\"translations\",\n                                                           translations,\n                                                           \"engine.util.language.translated.type.TranslatedString\",\n                                                           MemberOperator.ASSIGN)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def resistance_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Resistance ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        diff_armors = diff[\"armors\"]\n        if not isinstance(diff_armors, NoDiffMember):\n            patch_target_ref = f\"{game_entity_name}.Resistance\"\n            patches.extend(AoCUpgradeEffectSubprocessor.get_attack_resistances(converter_group,\n                                                                               line, diff,\n                                                                               patch_target_ref))\n\n        # TODO: Other resistance types\n\n        return patches\n\n    @staticmethod\n    def selectable_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Selectable ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        # First patch: Sound for the SelectableSelf ability\n        changed = False\n        if diff:\n            diff_selection_sound = diff[\"selection_sound_id\"]\n            if not isinstance(diff_selection_sound, NoDiffMember):\n                changed = True\n\n        if isinstance(line, GenieUnitLineGroup):\n            ability_name = \"SelectableSelf\"\n\n        else:\n            ability_name = \"Selectable\"\n\n        if changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n\n            # Change sound\n            diff_selection_sound_id = diff_selection_sound.value\n            # Nyan patch\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                \"select_\",\n                [diff_selection_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        # Second patch: Selection box\n        changed = False\n        if diff:\n            diff_radius_x = diff[\"selection_shape_x\"]\n            diff_radius_y = diff[\"selection_shape_y\"]\n            if any(not isinstance(value, NoDiffMember) for value in (diff_radius_x,\n                                                                     diff_radius_y)):\n                changed = True\n\n        if changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}.Rectangle\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{ability_name}RectangleWrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}Rectangle\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_radius_x, NoDiffMember):\n                diff_width_value = diff_radius_x.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"width\",\n                                                               diff_width_value,\n                                                               \"engine.util.selection_box.type.Rectangle\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_radius_y, NoDiffMember):\n                diff_height_value = diff_radius_y.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"height\",\n                                                               diff_height_value,\n                                                               \"engine.util.selection_box.type.Rectangle\",\n                                                               MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def shoot_projectile_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        upgrade_source: GenieUnitObject,\n        upgrade_target: GenieUnitObject,\n        command_id: int,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Selectable ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        ability_name = command_lookup_dict[command_id][0]\n\n        data_changed = False\n        if diff:\n            diff_animation = diff[\"attack_sprite_id\"]\n            diff_comm_sound = diff[\"command_sound_id\"]\n            diff_min_projectiles = diff[\"projectile_min_count\"]\n            diff_max_projectiles = diff[\"projectile_max_count\"]\n            diff_min_range = diff[\"weapon_range_min\"]\n            diff_max_range = diff[\"weapon_range_min\"]\n            diff_reload_time = diff[\"attack_speed\"]\n            # spawn delay also depends on animation\n            diff_spawn_delay = diff[\"frame_delay\"]\n            diff_spawn_area_offsets = diff[\"weapon_offset\"]\n            diff_spawn_area_width = diff[\"projectile_spawning_area_width\"]\n            diff_spawn_area_height = diff[\"projectile_spawning_area_length\"]\n            diff_spawn_area_randomness = diff[\"projectile_spawning_area_randomness\"]\n\n            if any(not isinstance(value, NoDiffMember) for value in (\n                diff_min_projectiles,\n                diff_max_projectiles,\n                diff_min_range,\n                diff_max_range,\n                diff_reload_time,\n                diff_spawn_delay,\n                diff_spawn_area_offsets,\n                diff_spawn_area_width,\n                diff_spawn_area_height,\n                diff_spawn_area_randomness\n            )):\n                data_changed = True\n\n        if not isinstance(diff_animation, NoDiffMember):\n            diff_animation_id = diff_animation.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_animation_id]\n            )\n            patches.append(anim_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if not isinstance(diff_comm_sound, NoDiffMember):\n            diff_comm_sound_id = diff_comm_sound.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_comm_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{ability_name}Wrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_min_projectiles, NoDiffMember):\n                min_projectiles = diff_min_projectiles.value\n                source_min_count = upgrade_source[\"projectile_min_count\"].value\n                source_max_count = upgrade_source[\"projectile_max_count\"].value\n                target_min_count = upgrade_target[\"projectile_min_count\"].value\n                target_max_count = upgrade_target[\"projectile_max_count\"].value\n\n                # Account for a special case where the number of projectiles are 0\n                # in the .dat, but the game still counts this as 1 when a projectile\n                # is defined.\n                if source_min_count == 0 and source_max_count == 0:\n                    min_projectiles -= 1\n\n                if target_min_count == 0 and target_max_count == 0:\n                    min_projectiles += 1\n\n                if min_projectiles != 0:\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"min_projectiles\",\n                                                                   min_projectiles,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n            if not isinstance(diff_max_projectiles, NoDiffMember):\n                max_projectiles = diff_max_projectiles.value\n                source_min_count = upgrade_source[\"projectile_min_count\"].value\n                source_max_count = upgrade_source[\"projectile_max_count\"].value\n                target_min_count = upgrade_target[\"projectile_min_count\"].value\n                target_max_count = upgrade_target[\"projectile_max_count\"].value\n\n                # Account for a special case where the number of projectiles are 0\n                # in the .dat, but the game still counts this as 1 when a projectile\n                # is defined.\n                if source_min_count == 0 and source_max_count == 0:\n                    max_projectiles -= 1\n\n                if target_min_count == 0 and target_max_count == 0:\n                    max_projectiles += 1\n\n                if max_projectiles != 0:\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"max_projectiles\",\n                                                                   max_projectiles,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n            if not isinstance(diff_min_range, NoDiffMember):\n                min_range = diff_min_range.value\n                nyan_patch_raw_api_object.add_raw_patch_member(\"min_range\",\n                                                               min_range,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_max_range, NoDiffMember):\n                max_range = diff_max_range.value\n                nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                               max_range,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_reload_time, NoDiffMember):\n                reload_time = diff_reload_time.value\n                nyan_patch_raw_api_object.add_raw_patch_member(\"reload_time\",\n                                                               reload_time,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_spawn_delay, NoDiffMember):\n                if not isinstance(diff_animation, NoDiffMember):\n                    attack_graphic_id = diff_animation.value\n\n                else:\n                    attack_graphic_id = diff_animation.ref.value\n\n                attack_graphic = dataset.genie_graphics[attack_graphic_id]\n                frame_rate = attack_graphic.get_frame_rate()\n                frame_delay = diff_spawn_delay.value\n                spawn_delay = frame_rate * frame_delay\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"spawn_delay\",\n                                                               spawn_delay,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ASSIGN)\n\n            if not isinstance(diff_spawn_area_offsets, NoDiffMember):\n                diff_spawn_area_x = diff_spawn_area_offsets[0]\n                diff_spawn_area_y = diff_spawn_area_offsets[1]\n                diff_spawn_area_z = diff_spawn_area_offsets[2]\n\n                if not isinstance(diff_spawn_area_x, NoDiffMember):\n                    spawn_area_x = diff_spawn_area_x.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_x\",\n                                                                   spawn_area_x,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_spawn_area_y, NoDiffMember):\n                    spawn_area_y = diff_spawn_area_y.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_y\",\n                                                                   spawn_area_y,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_spawn_area_z, NoDiffMember):\n                    spawn_area_z = diff_spawn_area_z.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_z\",\n                                                                   spawn_area_z,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n            if not isinstance(diff_spawn_area_width, NoDiffMember):\n                spawn_area_width = diff_spawn_area_width.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_width\",\n                                                               spawn_area_width,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_spawn_area_height, NoDiffMember):\n                spawn_area_height = diff_spawn_area_height.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_height\",\n                                                               spawn_area_height,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_spawn_area_randomness, NoDiffMember):\n                spawn_area_randomness = diff_spawn_area_randomness.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_randomness\",\n                                                               spawn_area_randomness,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def turn_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Turn ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if diff:\n            diff_turn_speed = diff[\"turn_speed\"]\n            if isinstance(diff_turn_speed, NoDiffMember):\n                return patches\n\n            diff_turn_speed_value = diff_turn_speed.value\n\n        else:\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Turn\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}TurnWrapper\"\n        wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # Store building upgrades next to their game entity definition,\n            # not in the Age up techs.\n            wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                 f\"{name_lookup_dict[head_unit_id][1]}/\"))\n            wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        else:\n            wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}Turn\"\n        nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # Speed\n        turn_speed_unmodified = diff_turn_speed_value\n        turn_speed = MemberSpecialValue.NYAN_INF\n        # Ships/Trebuchets turn slower\n        if turn_speed_unmodified > 0:\n            turn_yaw = diff[\"max_yaw_per_sec_moving\"].value\n            turn_speed = degrees(turn_yaw)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"turn_speed\",\n                                                       turn_speed,\n                                                       \"engine.ability.type.Turn\",\n                                                       MemberOperator.ASSIGN)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def create_animation(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        animation_id: int,\n        container_obj_ref: str,\n        animation_name: str,\n        filename_prefix: str\n    ) -> ForwardRef:\n        \"\"\"\n        Generates an animation for an ability.\n        \"\"\"\n        dataset = converter_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        if isinstance(converter_group, GenieVariantGroup):\n            group_name = str(animation_id)\n\n        else:\n            tech_id = converter_group.get_id()\n            group_name = tech_lookup_dict[tech_id][1]\n\n        animation_ref = f\"{container_obj_ref}.{animation_name}Animation\"\n        animation_obj_name = f\"{animation_name}Animation\"\n        animation_raw_api_object = RawAPIObject(animation_ref, animation_obj_name,\n                                                dataset.nyan_api_objects)\n        animation_raw_api_object.add_raw_parent(\"engine.util.graphics.Animation\")\n        animation_location = ForwardRef(converter_group, container_obj_ref)\n        animation_raw_api_object.set_location(animation_location)\n\n        if animation_id in dataset.combined_sprites.keys():\n            animation_sprite = dataset.combined_sprites[animation_id]\n\n        else:\n            if isinstance(line, GenieBuildingLineGroup):\n                animation_filename = (f\"{filename_prefix}\"\n                                      f\"{name_lookup_dict[line.get_head_unit_id()][1]}_\"\n                                      f\"{group_name}\")\n\n            else:\n                animation_filename = f\"{filename_prefix}{group_name}\"\n\n            animation_sprite = CombinedSprite(animation_id,\n                                              animation_filename,\n                                              dataset)\n            dataset.combined_sprites.update({animation_sprite.get_id(): animation_sprite})\n\n        animation_sprite.add_reference(animation_raw_api_object)\n\n        animation_raw_api_object.add_raw_member(\"sprite\", animation_sprite,\n                                                \"engine.util.graphics.Animation\")\n\n        converter_group.add_raw_api_object(animation_raw_api_object)\n\n        animation_forward_ref = ForwardRef(converter_group, animation_ref)\n\n        return animation_forward_ref\n\n    @staticmethod\n    def create_sound(\n        converter_group: ConverterObjectGroup,\n        sound_id: int,\n        container_obj_ref: str,\n        sound_name: str,\n        filename_prefix: str\n    ) -> ForwardRef:\n        \"\"\"\n        Generates a sound for an ability.\n        \"\"\"\n        dataset = converter_group.data\n\n        sound_ref = f\"{container_obj_ref}.{sound_name}Sound\"\n        sound_obj_name = f\"{sound_name}Sound\"\n        sound_raw_api_object = RawAPIObject(sound_ref, sound_obj_name,\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(converter_group, container_obj_ref)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Search for the sound if it exists\n        sounds_set = []\n\n        genie_sound = dataset.genie_sounds[sound_id]\n        file_ids = genie_sound.get_sounds(civ_id=-1)\n\n        for file_id in file_ids:\n            if file_id in dataset.combined_sounds:\n                sound = dataset.combined_sounds[file_id]\n\n            else:\n                sound_filename = f\"{filename_prefix}sound_{str(file_id)}\"\n\n                sound = CombinedSound(sound_id,\n                                      file_id,\n                                      sound_filename,\n                                      dataset)\n                dataset.combined_sounds.update({file_id: sound})\n\n            sound.add_reference(sound_raw_api_object)\n            sounds_set.append(sound)\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            sounds_set,\n                                            \"engine.util.sound.Sound\")\n\n        converter_group.add_raw_api_object(sound_raw_api_object)\n\n        sound_forward_ref = ForwardRef(converter_group, sound_ref)\n\n        return sound_forward_ref\n\n    @staticmethod\n    def create_language_strings(\n        converter_group: ConverterObjectGroup,\n        string_id: int,\n        obj_ref: str,\n        obj_name_prefix: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Generates a language string for an ability.\n        \"\"\"\n        dataset = converter_group.data\n        string_resources = dataset.strings.get_tables()\n\n        string_objs = []\n        for language, strings in string_resources.items():\n            if string_id in strings.keys():\n                string_name = f\"{obj_name_prefix}String\"\n                string_ref = f\"{obj_ref}.{string_name}\"\n                string_raw_api_object = RawAPIObject(string_ref, string_name,\n                                                     dataset.nyan_api_objects)\n                string_raw_api_object.add_raw_parent(\"engine.util.language.LanguageTextPair\")\n                string_location = ForwardRef(converter_group, obj_ref)\n                string_raw_api_object.set_location(string_location)\n\n                # Language identifier\n                lang_forward_ref = dataset.pregen_nyan_objects[f\"util.language.{language}\"].get_nyan_object(\n                )\n                string_raw_api_object.add_raw_member(\"language\",\n                                                     lang_forward_ref,\n                                                     \"engine.util.language.LanguageTextPair\")\n\n                # String\n                string_raw_api_object.add_raw_member(\"string\",\n                                                     strings[string_id],\n                                                     \"engine.util.language.LanguageTextPair\")\n\n                converter_group.add_raw_api_object(string_raw_api_object)\n                string_forward_ref = ForwardRef(converter_group, string_ref)\n                string_objs.append(string_forward_ref)\n\n        return string_objs\n\n    @staticmethod\n    def create_animation_patch(\n        converter_group: ConverterObjectGroup,\n        line: ConverterObjectGroup,\n        ability_ref: str,\n        patch_name_prefix: str,\n        container_obj_ref: str,\n        animation_name_prefix: str,\n        filename_prefix: str,\n        animation_ids: list[int]\n    ) -> tuple[RawAPIObject, ForwardRef]:\n        \"\"\"\n        Create a patch for the Animated property of an ability.\n\n        :param converter_group: Converter group for storing the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param ability_ref: Reference of the ability that has the Animated property.\n        :type ability_ref: str\n        :param patch_name_prefix: Prefix to the name of the patch.\n        :type patch_name_prefix: str\n        :param container_obj_ref: Reference of the API object that should contain the\n                                  patch as a nested object.\n        :type container_obj_ref: str\n        :param animation_name_prefix: Prefix to the name of the animation.\n        :type animation_name_prefix: str\n        :param filename_prefix: Prefix to the filename of the animation.\n        :type filename_prefix: str\n        :param animation_ids: IDs of the animations to patch in.\n        :type animation_ids: list[int]\n        \"\"\"\n        dataset = converter_group.data\n\n        patch_target_ref = f\"{ability_ref}.Animated\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"{patch_name_prefix}AnimationWrapper\"\n        wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n        wrapper_location = ForwardRef(converter_group, container_obj_ref)\n        wrapper_raw_api_object.set_location(wrapper_location)\n\n        # Nyan patch\n        nyan_patch_name = f\"{patch_name_prefix}Animation\"\n        nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        animations: list[ForwardRef] = []\n        for idx, anim_id in enumerate(animation_ids):\n            if anim_id < 0:\n                continue\n\n            if len(animation_ids) == 1:\n                # don't append index if there is only one animation\n                anim_obj_name = animation_name_prefix\n\n            else:\n                anim_obj_name = f\"{animation_name_prefix}{idx}\"\n\n            anim_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation(\n                converter_group,\n                line,\n                anim_id,\n                nyan_patch_ref,\n                anim_obj_name,\n                filename_prefix\n            )\n            animations.append(anim_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"animations\",\n                                                       animations,\n                                                       \"engine.ability.property.type.Animated\",\n                                                       MemberOperator.ASSIGN)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n\n        return wrapper_raw_api_object, wrapper_forward_ref\n\n    @staticmethod\n    def create_command_sound_patch(\n        converter_group: ConverterObjectGroup,\n        line: ConverterObjectGroup,\n        ability_ref: str,\n        patch_name_prefix: str,\n        container_obj_ref: str,\n        sound_name_prefix: str,\n        filename_prefix: str,\n        sound_ids: list[int]\n    ) -> tuple[RawAPIObject, ForwardRef]:\n        \"\"\"\n        Create a patch for the CommandSound property of an ability.\n\n        :param converter_group: Converter group for storing the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        dataset = converter_group.data\n\n        patch_target_ref = f\"{ability_ref}.CommandSound\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"{patch_name_prefix}CommandSoundWrapper\"\n        wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n        wrapper_location = ForwardRef(converter_group, container_obj_ref)\n        wrapper_raw_api_object.set_location(wrapper_location)\n\n        # Nyan patch\n        nyan_patch_name = f\"{patch_name_prefix}CommandSound\"\n        nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        sounds: list[ForwardRef] = []\n        for idx, sound_id in enumerate(sound_ids):\n            if sound_id < 0:\n                continue\n\n            if len(sound_ids) == 1:\n                # don't append index if there is only one sound\n                sound_obj_name = sound_name_prefix\n\n            else:\n                sound_obj_name = f\"{sound_name_prefix}{idx}\"\n\n            sound_forward_ref = AoCUpgradeAbilitySubprocessor.create_sound(\n                converter_group,\n                sound_id,\n                nyan_patch_ref,\n                sound_obj_name,\n                filename_prefix\n            )\n            sounds.append(sound_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"sounds\",\n                                                       sounds,\n                                                       \"engine.ability.property.type.CommandSound\",\n                                                       MemberOperator.ASSIGN)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n\n        return wrapper_raw_api_object, wrapper_forward_ref\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/upgrade_attribute_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for attribute modification effects in AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass AoCUpgradeAttributeSubprocessor:\n    \"\"\"\n    Creates raw API objects for attribute upgrade effects in AoC.\n    \"\"\"\n\n    @staticmethod\n    def accuracy_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the accuracy modify effect (ID: 11).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.ShootProjectile.Projectile0.Projectile.Accuracy\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}AccuracyWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}Accuracy\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"accuracy\",\n                                                       value,\n                                                       \"engine.util.accuracy.Accuracy\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def armor_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the armor modify effect (ID: 8).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        if value > 0:\n            armor_class = int(value) >> 8\n            armor_amount = int(value) & 0x0F\n\n        else:\n            # Sign is for armor amount\n            value *= -1\n            armor_class = int(value) >> 8\n            armor_amount = int(value) & 0x0F\n            armor_amount *= -1\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        class_name = armor_lookup_dict[armor_class]\n\n        if line.has_armor(armor_class):\n            patch_target_ref = f\"{game_entity_name}.Resistance.{class_name}.BlockAmount\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        else:\n            # TODO: Create new attack resistance\n            return patches\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}{class_name}ResistanceWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}{class_name}Resistance\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       armor_amount,\n                                                       \"engine.util.attribute.AttributeAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def attack_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the attack modify effect (ID: 9).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        attack_amount = int(value) & 0x0F\n        armor_class = int(value) >> 8\n\n        if armor_class == -1:\n            return patches\n\n        if not line.has_armor(armor_class):\n            # TODO: Happens sometimes in AoE1; what do we do?\n            return patches\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        class_name = armor_lookup_dict[armor_class]\n\n        if line.is_projectile_shooter():\n            primary_projectile_id = line.get_head_unit(\n            )[\"projectile_id0\"].value\n            if primary_projectile_id == -1:\n                # Upgrade is skipped if the primary projectile is not defined\n                return patches\n\n            patch_target_ref = (f\"{game_entity_name}.ShootProjectile.Projectile0.\"\n                                f\"Attack.Batch.{class_name}.ChangeAmount\")\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        else:\n            patch_target_ref = f\"{game_entity_name}.Attack.Batch.{class_name}.ChangeAmount\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        if not line.has_attack(armor_class):\n            # TODO: Create new attack effect\n            return patches\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}{class_name}AttackWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}{class_name}Attack\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       attack_amount,\n                                                       \"engine.util.attribute.AttributeAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def ballistics_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: int,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ballistics modify effect (ID: 19).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        if value == 0:\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.CurrentPosition\"]\n\n        elif value == 1:\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.ExpectedPosition\"]\n\n        elif value == 2:\n            # No Ballistics, only for Arambai\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.ExpectedPosition\"]\n\n        elif value == 3:\n            # Ballistics, only for Arambai\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.ExpectedPosition\"]\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        projectile_id0 = head_unit[\"projectile_id0\"].value\n        projectile_id1 = head_unit[\"projectile_id1\"].value\n\n        if projectile_id0 > -1:\n            patch_target_ref = f\"{game_entity_name}.ShootProjectile.Projectile0.Projectile\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}Projectile0TargetModeWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}Projectile0TargetMode\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"target_mode\",\n                                                           target_mode,\n                                                           \"engine.ability.type.Projectile\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        if projectile_id1 > -1:\n            patch_target_ref = f\"{game_entity_name}.ShootProjectile.Projectile1.Projectile\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}Projectile1TargetModeWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}Projectile1TargetMode\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"target_mode\",\n                                                           target_mode,\n                                                           \"engine.ability.type.Projectile\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def attack_warning_sound_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the attack warning sound modify effect (ID: 26).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def blast_radius_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the blast radius modify effect (ID: 22).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def carry_capacity_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the carry capacity modify effect (ID: 14).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def cost_food_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the food cost modify effect (ID: 103).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        # Check if the unit line actually costs food\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            if resource_id == 0:\n                break\n\n        else:\n            # Skip patch generation if no food cost was found\n            return patches\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.FoodAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}FoodCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}FoodCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cost_wood_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the wood cost modify effect (ID: 104).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        # Check if the unit line actually costs wood\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            if resource_id == 1:\n                break\n\n        else:\n            # Skip patch generation if no wood cost was found\n            return patches\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.WoodAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}WoodCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}WoodCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cost_gold_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the gold cost modify effect (ID: 105).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        # Check if the unit line actually costs gold\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            if resource_id == 3:\n                break\n\n        else:\n            # Skip patch generation if no gold cost was found\n            return patches\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.GoldAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}GoldCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}GoldCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cost_stone_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the stone cost modify effect (ID: 106).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        # Check if the unit line actually costs stone\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            if resource_id == 2:\n                break\n\n        else:\n            # Skip patch generation if no stone cost was found\n            return patches\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.StoneAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}StoneCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}StoneCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def creation_time_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the creation time modify effect (ID: 101).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.CreatableGameEntity\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}CreationTimeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}CreationTime\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"creation_time\",\n                                                       value,\n                                                       \"engine.util.create.CreatableGameEntity\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def garrison_capacity_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the garrison capacity modify effect (ID: 2).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if not line.is_garrison():\n            # TODO: Patch ability in\n            return patches\n\n        patch_target_ref = f\"{game_entity_name}.Storage.{game_entity_name}Container\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}CreationTimeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}CreationTime\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"slots\",\n                                                       value,\n                                                       \"engine.util.storage.EntityContainer\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def garrison_heal_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the garrison heal rate modify effect (ID: 108).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def graphics_angle_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: int,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the graphics angle modify effect (ID: 17).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def gold_counter_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the gold counter effect (ID: 49).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def hp_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the HP modify effect (ID: 0).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Live.Health\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MaxHealthWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MaxHealth\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_value\",\n                                                       value,\n                                                       \"engine.util.attribute.AttributeSetting\",\n                                                       operator)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"starting_value\",\n                                                       value,\n                                                       \"engine.util.attribute.AttributeSetting\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def ignore_armor_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ignore armor effect (ID: 63).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def imperial_tech_id_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the imperial tech ID effect (ID: 24).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def kidnap_storage_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the kidnap storage effect (ID: 57).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def los_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the line of sight modify effect (ID: 1).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.LineOfSight\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}LineOfSightWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}LineOfSight\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"range\",\n                                                       value,\n                                                       \"engine.ability.type.LineOfSight\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def max_projectiles_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the max projectiles modify effect (ID: 107).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Attack\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MaxProjectilesWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MaxProjectiles\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_projectiles\",\n                                                       value,\n                                                       \"engine.ability.type.ShootProjectile\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def min_projectiles_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the min projectiles modify effect (ID: 102).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Attack\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MinProjectilesWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MinProjectiles\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"min_projectiles\",\n                                                       value,\n                                                       \"engine.ability.type.ShootProjectile\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def max_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the max range modify effect (ID: 12).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if line.is_projectile_shooter():\n            patch_target_ref = f\"{game_entity_name}.Attack\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.ShootProjectile\"\n\n        elif line.is_melee():\n            if line.is_ranged():\n                patch_target_ref = f\"{game_entity_name}.Attack\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n                patch_target_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n            else:\n                # excludes ram upgrades\n                return patches\n\n        elif line.has_command(104):\n            patch_target_ref = f\"{game_entity_name}.Convert\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        else:\n            # no matching ability\n            return patches\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MaxRangeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MaxRange\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                       value,\n                                                       patch_target_parent,\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def min_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the min range modify effect (ID: 20).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if line.is_projectile_shooter():\n            patch_target_ref = f\"{game_entity_name}.Attack\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.ShootProjectile\"\n\n        elif line.is_melee():\n            patch_target_ref = f\"{game_entity_name}.Attack\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        elif line.has_command(104):\n            patch_target_ref = f\"{game_entity_name}.Convert\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        else:\n            return []\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MinRangeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MinRange\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"min_range\",\n                                                       value,\n                                                       patch_target_parent,\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def move_speed_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the move speed modify effect (ID: 5).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Move\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}MoveSpeedWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}MoveSpeed\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"speed\",\n                                                       value,\n                                                       \"engine.ability.type.Move\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def projectile_unit_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the projectile modify effect (ID: 16).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def reload_time_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the reload time modify effect (ID: 10).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if line.is_projectile_shooter():\n            patch_target_ref = f\"{game_entity_name}.Attack\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.ShootProjectile\"\n\n        elif line.is_melee():\n            patch_target_ref = f\"{game_entity_name}.Attack\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.ApplyDiscreteEffect\"\n\n        elif line.has_command(104):\n            patch_target_ref = f\"{game_entity_name}.Convert\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            patch_target_parent = \"engine.ability.type.ApplyDiscreteEffect\"\n\n        else:\n            # No matching ability\n            return patches\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}ReloadTimeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}ReloadTime\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"reload_time\",\n                                                       value,\n                                                       patch_target_parent,\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def resource_cost_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the resource modify effect (ID: 100).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource_name = \"Wood\"\n\n            elif resource_id == 2:\n                resource_name = \"Stone\"\n\n            elif resource_id == 3:\n                resource_name = \"Gold\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                                f\"{game_entity_name}Cost.{resource_name}Amount\")\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{resource_name}CostWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{resource_name}Cost\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                           value,\n                                                           \"engine.util.resource.ResourceAmount\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def resource_storage_1_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the resource storage 1 modify effect (ID: 21).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        if line.is_harvestable():\n            patch_target_ref = f\"{game_entity_name}.Harvestable.{game_entity_name}ResourceSpot\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            wrapper_name = f\"Change{game_entity_name}HarvestableAmountWrapper\"\n            nyan_patch_name = f\"Change{game_entity_name}HarvestableAmount\"\n\n        else:\n            patch_target_ref = f\"{game_entity_name}.ProvideContingent.PopSpace\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n            wrapper_name = f\"Change{game_entity_name}PopSpaceWrapper\"\n            nyan_patch_name = f\"Change{game_entity_name}PopSpace\"\n\n        # Wrapper\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        if line.is_harvestable():\n            nyan_patch_raw_api_object.add_raw_patch_member(\"max_amount\",\n                                                           value,\n                                                           \"engine.util.resource_spot.ResourceSpot\",\n                                                           operator)\n\n        else:\n            nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                           value,\n                                                           \"engine.util.resource.ResourceAmount\",\n                                                           operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def rotation_speed_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the move speed modify effect (ID: 6).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def search_radius_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the search radius modify effect (ID: 23).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        if isinstance(line, GenieBuildingLineGroup) and not line.is_projectile_shooter():\n            # Does not have the ability\n            return patches\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        stance_names = [\"Aggressive\", \"Defensive\", \"StandGround\", \"Passive\"]\n\n        for stance_name in stance_names:\n            patch_target_ref = f\"{game_entity_name}.GameEntityStance.{stance_name}\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{stance_name}SearchRangeWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{stance_name}SearchRange\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"search_range\",\n                                                           value,\n                                                           \"engine.util.game_entity_stance.GameEntityStance\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def standing_wonders_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the standing wonders effect (ID: 42).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def tc_available_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the TC available effect (ID: 48).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def terrain_defense_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the terrain defense modify effect (ID: 18).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def train_button_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the train button modify effect (ID: 43).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def tribute_inefficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the tribute inefficiency effect (ID: 46).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def unit_size_x_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the unit size x modify effect (ID: 3).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def unit_size_y_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the unit size y modify effect (ID: 4).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def work_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the work rate modify effect (ID: 13).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/upgrade_effect_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nUpgrades effects and resistances for the Apply*Effect and Resistance\nabilities.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ....value_object.read.value_members import NoDiffMember, \\\n    LeftMissingMember, RightMissingMember\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass AoCUpgradeEffectSubprocessor:\n    \"\"\"\n    Creates raw API objects for attack/resistance upgrades in AoC.\n    \"\"\"\n\n    @staticmethod\n    def get_attack_effects(\n        tech_group: GenieTechEffectBundleGroup,\n        line: GenieGameEntityGroup,\n        diff: ConverterObject,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Upgrades effects that are used for attacking (unit command: 7)\n\n        :param tech_group: Tech that gets the patch.\n        :type tech_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the effects.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = tech_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        diff_attacks = diff[\"attacks\"].value\n        for diff_attack in diff_attacks.values():\n            if isinstance(diff_attack, NoDiffMember):\n                continue\n\n            if isinstance(diff_attack, LeftMissingMember):\n                # Create a new attack effect, then patch it in\n                attack = diff_attack.ref\n\n                armor_class = attack[\"type_id\"].value\n                attack_amount = attack[\"amount\"].value\n\n                if armor_class == -1:\n                    continue\n\n                class_name = armor_lookup_dict[armor_class]\n\n                # FlatAttributeChangeDecrease\n                effect_parent = \"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"\n                attack_parent = \"engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n\n                patch_target_ref = f\"{ability_ref}.Batch\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Add{class_name}AttackEffectWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Add{class_name}AttackEffect\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                # New attack effect\n                # ============================================================================\n                attack_ref = f\"{nyan_patch_ref}.{class_name}\"\n                attack_raw_api_object = RawAPIObject(attack_ref,\n                                                     class_name,\n                                                     dataset.nyan_api_objects)\n                attack_raw_api_object.add_raw_parent(attack_parent)\n                attack_location = ForwardRef(tech_group, nyan_patch_ref)\n                attack_raw_api_object.set_location(attack_location)\n\n                # Type\n                type_ref = f\"util.attribute_change_type.types.{class_name}\"\n                change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n                attack_raw_api_object.add_raw_member(\"type\",\n                                                     change_type,\n                                                     effect_parent)\n\n                # Min value (optional)\n                min_value = dataset.pregen_nyan_objects[(\"effect.discrete.flat_attribute_change.\"\n                                                         \"min_damage.AoE2MinChangeAmount\")].get_nyan_object()\n                attack_raw_api_object.add_raw_member(\"min_change_value\",\n                                                     min_value,\n                                                     effect_parent)\n\n                # Max value (optional; not added because there is none in AoE2)\n\n                # Change value\n                # =================================================================================\n                amount_name = f\"{nyan_patch_ref}.{class_name}.ChangeAmount\"\n                amount_raw_api_object = RawAPIObject(\n                    amount_name, \"ChangeAmount\", dataset.nyan_api_objects)\n                amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n                amount_location = ForwardRef(line, attack_ref)\n                amount_raw_api_object.set_location(amount_location)\n\n                attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object(\n                )\n                amount_raw_api_object.add_raw_member(\"type\",\n                                                     attribute,\n                                                     \"engine.util.attribute.AttributeAmount\")\n                amount_raw_api_object.add_raw_member(\"amount\",\n                                                     attack_amount,\n                                                     \"engine.util.attribute.AttributeAmount\")\n\n                line.add_raw_api_object(amount_raw_api_object)\n                # =================================================================================\n                amount_forward_ref = ForwardRef(line, amount_name)\n                attack_raw_api_object.add_raw_member(\"change_value\",\n                                                     amount_forward_ref,\n                                                     effect_parent)\n\n                # Ignore protection\n                attack_raw_api_object.add_raw_member(\"ignore_protection\",\n                                                     [],\n                                                     effect_parent)\n\n                # Effect is added to the line, so it can be referenced by other upgrades\n                line.add_raw_api_object(attack_raw_api_object)\n                # ============================================================================\n                attack_forward_ref = ForwardRef(line, attack_ref)\n                nyan_patch_raw_api_object.add_raw_patch_member(\"effects\",\n                                                               [attack_forward_ref],\n                                                               \"engine.util.effect_batch.EffectBatch\",\n                                                               MemberOperator.ADD)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n            elif isinstance(diff_attack, RightMissingMember):\n                # Patch the effect out of the ability\n                attack = diff_attack.ref\n\n                armor_class = attack[\"type_id\"].value\n                class_name = armor_lookup_dict[armor_class]\n\n                patch_target_ref = f\"{ability_ref}.Batch\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Remove{class_name}AttackEffectWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Remove{class_name}AttackEffect\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                attack_ref = f\"{ability_ref}.{class_name}\"\n                attack_forward_ref = ForwardRef(line, attack_ref)\n                nyan_patch_raw_api_object.add_raw_patch_member(\"effects\",\n                                                               [attack_forward_ref],\n                                                               \"engine.util.effect_batch.EffectBatch\",\n                                                               MemberOperator.SUBTRACT)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n            else:\n                diff_armor_class = diff_attack[\"type_id\"]\n                if not isinstance(diff_armor_class, NoDiffMember):\n                    # If this happens then the attacks are out of order\n                    # and we have to try something else\n                    raise ValueError(f\"Could not create effect upgrade for line {repr(line)}: \"\n                                     \"Out of order\")\n\n                armor_class = diff_armor_class.ref.value\n                attack_amount = diff_attack[\"amount\"].value\n\n                class_name = armor_lookup_dict[armor_class]\n\n                patch_target_ref = f\"{ability_ref}.Batch.{class_name}.ChangeAmount\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Change{class_name}AttackWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Change{class_name}Attack\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                               attack_amount,\n                                                               \"engine.util.attribute.AttributeAmount\",\n                                                               MemberOperator.ADD)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def get_attack_resistances(\n        tech_group: GenieTechEffectBundleGroup,\n        line: GenieGameEntityGroup,\n        diff: ConverterObject,\n        ability_ref: str\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Upgrades resistances that are used for attacking (unit command: 7)\n\n        :param tech_group: Tech that gets the patch.\n        :type tech_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :param ability_ref: Reference of the ability raw API object the effects are added to.\n        :type ability_ref: str\n        :returns: The forward references for the resistances.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = tech_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        diff_armors = diff[\"armors\"].value\n        for diff_armor in diff_armors.values():\n            if isinstance(diff_armor, NoDiffMember):\n                continue\n\n            if isinstance(diff_armor, LeftMissingMember):\n                # Create a new attack resistance, then patch it in\n                armor = diff_armor.ref\n\n                armor_class = armor[\"type_id\"].value\n                armor_amount = armor[\"amount\"].value\n\n                if armor_class == -1:\n                    continue\n\n                class_name = armor_lookup_dict[armor_class]\n\n                # FlatAttributeChangeDecrease\n                resistance_parent = \"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"\n                armor_parent = \"engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n\n                patch_target_ref = f\"{ability_ref}\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Add{class_name}AttackResistanceWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Add{class_name}AttackResistance\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                # New attack effect\n                # ============================================================================\n                attack_ref = f\"{nyan_patch_ref}.{class_name}\"\n                attack_raw_api_object = RawAPIObject(attack_ref,\n                                                     class_name,\n                                                     dataset.nyan_api_objects)\n                attack_raw_api_object.add_raw_parent(armor_parent)\n                attack_location = ForwardRef(tech_group, nyan_patch_ref)\n                attack_raw_api_object.set_location(attack_location)\n\n                # Type\n                type_ref = f\"util.attribute_change_type.types.{class_name}\"\n                change_type = dataset.pregen_nyan_objects[type_ref].get_nyan_object()\n                attack_raw_api_object.add_raw_member(\"type\",\n                                                     change_type,\n                                                     resistance_parent)\n\n                # Block value\n                # =================================================================================\n                amount_name = f\"{nyan_patch_ref}.{class_name}.BlockAmount\"\n                amount_raw_api_object = RawAPIObject(\n                    amount_name, \"BlockAmount\", dataset.nyan_api_objects)\n                amount_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeAmount\")\n                amount_location = ForwardRef(line, attack_ref)\n                amount_raw_api_object.set_location(amount_location)\n\n                attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object(\n                )\n                amount_raw_api_object.add_raw_member(\"type\",\n                                                     attribute,\n                                                     \"engine.util.attribute.AttributeAmount\")\n                amount_raw_api_object.add_raw_member(\"amount\",\n                                                     armor_amount,\n                                                     \"engine.util.attribute.AttributeAmount\")\n\n                line.add_raw_api_object(amount_raw_api_object)\n                # =================================================================================\n                amount_forward_ref = ForwardRef(line, amount_name)\n                attack_raw_api_object.add_raw_member(\"block_value\",\n                                                     amount_forward_ref,\n                                                     resistance_parent)\n\n                # Resistance is added to the line, so it can be referenced by other upgrades\n                line.add_raw_api_object(attack_raw_api_object)\n                # ============================================================================\n                attack_forward_ref = ForwardRef(line, attack_ref)\n                nyan_patch_raw_api_object.add_raw_patch_member(\"resistances\",\n                                                               [attack_forward_ref],\n                                                               \"engine.ability.type.Resistance\",\n                                                               MemberOperator.ADD)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n            elif isinstance(diff_armor, RightMissingMember):\n                # Patch the resistance out of the ability\n                armor = diff_armor.ref\n\n                armor_class = armor[\"type_id\"].value\n                class_name = armor_lookup_dict[armor_class]\n\n                patch_target_ref = f\"{ability_ref}\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Remove{class_name}AttackResistanceWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Remove{class_name}AttackResistance\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                attack_ref = f\"{ability_ref}.{class_name}\"\n                attack_forward_ref = ForwardRef(line, attack_ref)\n                nyan_patch_raw_api_object.add_raw_patch_member(\"resistances\",\n                                                               [attack_forward_ref],\n                                                               \"engine.ability.type.Resistance\",\n                                                               MemberOperator.SUBTRACT)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n            else:\n                diff_armor_class = diff_armor[\"type_id\"]\n                if not isinstance(diff_armor_class, NoDiffMember):\n                    # If this happens then the armors are out of order\n                    # and we have to try something else\n                    raise ValueError(f\"Could not create effect upgrade for line {repr(line)}: \"\n                                     \"Out of order\")\n\n                armor_class = diff_armor_class.ref.value\n                armor_amount = diff_armor[\"amount\"].value\n\n                class_name = armor_lookup_dict[armor_class]\n\n                patch_target_ref = f\"{ability_ref}.{class_name}.BlockAmount\"\n                patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n                # Wrapper\n                wrapper_name = f\"Change{class_name}ResistanceWrapper\"\n                wrapper_ref = f\"{tech_name}.{wrapper_name}\"\n                wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                      wrapper_name,\n                                                      dataset.nyan_api_objects)\n                wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n                if isinstance(line, GenieBuildingLineGroup):\n                    # Store building upgrades next to their game entity definition,\n                    # not in the Age up techs.\n                    wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                         f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                    wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n                else:\n                    wrapper_raw_api_object.set_location(ForwardRef(tech_group, tech_name))\n\n                # Nyan patch\n                nyan_patch_name = f\"Change{class_name}Resistance\"\n                nyan_patch_ref = f\"{tech_name}.{wrapper_name}.{nyan_patch_name}\"\n                nyan_patch_location = ForwardRef(tech_group, wrapper_ref)\n                nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                         nyan_patch_name,\n                                                         dataset.nyan_api_objects,\n                                                         nyan_patch_location)\n                nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n                nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                               armor_amount,\n                                                               \"engine.util.attribute.AttributeAmount\",\n                                                               MemberOperator.ADD)\n\n                patch_forward_ref = ForwardRef(tech_group, nyan_patch_ref)\n                wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                      patch_forward_ref,\n                                                      \"engine.util.patch.Patch\")\n\n                tech_group.add_raw_api_object(wrapper_raw_api_object)\n                tech_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n                wrapper_forward_ref = ForwardRef(tech_group, wrapper_ref)\n                patches.append(wrapper_forward_ref)\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc/upgrade_resource_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods,invalid-name\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for resource modification effects in AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass AoCUpgradeResourceSubprocessor:\n    \"\"\"\n    Creates raw API objects for resource upgrade effects in AoC.\n    \"\"\"\n\n    @staticmethod\n    def berserk_heal_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the berserk heal rate modify effect (ID: 96).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        berserk_id = 692\n        dataset = converter_group.data\n        line = dataset.unit_lines[berserk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[berserk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.RegenerateHealth.HealthRate\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}HealthRegenerationWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}HealthRegeneration\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # Regeneration is on a counter, so we have to invert the value\n        value = 1 / value\n        nyan_patch_raw_api_object.add_raw_patch_member(\"rate\",\n                                                       value,\n                                                       \"engine.util.attribute.AttributeRate\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def bonus_population_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the bonus population effect (ID: 32).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        dataset = converter_group.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        patch_target_ref = \"util.resource.types.PopulationSpace\"\n        patch_target = dataset.pregen_nyan_objects[patch_target_ref].get_nyan_object()\n\n        # Wrapper\n        wrapper_name = \"ChangePopulationCapWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = \"ChangePopulationCap\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceContingent\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def building_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the building conversion effect (ID: 28).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        monk_id = 125\n        dataset = converter_group.data\n        line = dataset.unit_lines[monk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[monk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Convert\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Building conversion\n\n        # Wrapper\n        wrapper_name = \"EnableBuildingConversionWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = \"EnableBuildingConversion\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # New allowed types\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()]\n        nyan_patch_raw_api_object.add_raw_patch_member(\"allowed_types\",\n                                                       allowed_types,\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.ADD)\n\n        # Blacklisted buildings\n        tc_line = dataset.building_lines[109]\n        farm_line = dataset.building_lines[50]\n        fish_trap_line = dataset.building_lines[199]\n        monastery_line = dataset.building_lines[104]\n        castle_line = dataset.building_lines[82]\n        palisade_line = dataset.building_lines[72]\n        stone_wall_line = dataset.building_lines[117]\n        stone_gate_line = dataset.building_lines[64]\n        wonder_line = dataset.building_lines[276]\n\n        blacklisted_forward_refs = [ForwardRef(tc_line, \"TownCenter\"),\n                                    ForwardRef(farm_line, \"Farm\"),\n                                    ForwardRef(fish_trap_line, \"FishingTrap\"),\n                                    ForwardRef(monastery_line, \"Monastery\"),\n                                    ForwardRef(castle_line, \"Castle\"),\n                                    ForwardRef(palisade_line, \"PalisadeWall\"),\n                                    ForwardRef(stone_wall_line, \"StoneWall\"),\n                                    ForwardRef(stone_gate_line, \"StoneGate\"),\n                                    ForwardRef(wonder_line, \"Wonder\"),\n                                    ]\n        nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                       blacklisted_forward_refs,\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.ADD)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        # Siege unit conversion\n\n        # Wrapper\n        wrapper_name = \"EnableSiegeUnitConversionWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = \"EnableSiegeUnitConversion\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # Blacklisted units\n        blacklisted_entities = []\n        for unit_line in dataset.unit_lines.values():\n            if unit_line.get_class_id() in (13, 55):\n                # Siege units\n                blacklisted_name = name_lookup_dict[unit_line.get_head_unit_id()][0]\n                blacklisted_entities.append(ForwardRef(unit_line, blacklisted_name))\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                       blacklisted_entities,\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.SUBTRACT)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def chinese_tech_discount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the chinese tech discount effect (ID: 85).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def construction_speed_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the construction speed modify effect (ID: 195).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_resistance_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion resistance modify effect (ID: 77).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_resistance_min_rounds_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion resistance modify effect (ID: 178).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_resistance_max_rounds_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion resistance modify effect (ID: 179).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def crenellations_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the crenellations effect (ID: 194).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def faith_recharge_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the faith_recharge_rate modify effect (ID: 35).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        monk_id = 125\n        dataset = converter_group.data\n        line = dataset.unit_lines[monk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[monk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.RegenerateFaith.FaithRate\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}FaithRegenerationWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}FaithRegeneration\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"rate\",\n                                                       value,\n                                                       \"engine.util.attribute.AttributeRate\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def farm_food_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the farm food modify effect (ID: 36).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        farm_id = 50\n        dataset = converter_group.data\n        line = dataset.building_lines[farm_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[farm_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Harvestable.{game_entity_name}ResourceSpot\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}FoodAmountWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}FoodAmount\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_amount\",\n                                                       value,\n                                                       \"engine.util.resource_spot.ResourceSpot\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def gather_food_efficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the food gathering efficiency modify effect (ID: 190).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def gather_wood_efficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the wood gathering efficiency modify effect (ID: 189).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def gather_gold_efficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the gold gathering efficiency modify effect (ID: 47).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def gather_stone_efficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the stone gathering efficiency modify effect (ID: 79).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def heal_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the heal range modify effect (ID: 90).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        monk_id = 125\n        dataset = converter_group.data\n        line = dataset.unit_lines[monk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[monk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Heal\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}HealRangeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}HealRange\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                       value,\n                                                       \"engine.ability.type.RangedContinuousEffect\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def heal_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the heal rate modify effect (ID: 89).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def herding_dominance_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the herding dominance effect (ID: 97).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def heresy_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the heresy effect (ID: 192).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def monk_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the monk conversion effect (ID: 27).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        monk_id = 125\n        dataset = converter_group.data\n        line = dataset.unit_lines[monk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[monk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Convert\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Enable{game_entity_name}ConversionWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Enable{game_entity_name}Conversion\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        monk_forward_ref = ForwardRef(line, game_entity_name)\n        nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                       [monk_forward_ref],\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.SUBTRACT)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def relic_gold_bonus_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the relic gold bonus modify effect (ID: 191).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        return patches\n\n    @staticmethod\n    def research_time_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the research time modify effect (ID: 86).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def reveal_ally_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the reveal ally modify effect (ID: 50).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def reveal_enemy_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the reveal enemy modify effect (ID: 183).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def siege_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the siege conversion effect (ID: 29).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO\n\n        return patches\n\n    @staticmethod\n    def ship_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ship conversion effect (ID: 87).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused in AoC\n\n        return patches\n\n    @staticmethod\n    def spies_discount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the spies discount effect (ID: 197).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_food_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting food modify effect (ID: 91).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_wood_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting wood modify effect (ID: 92).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_stone_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting stone modify effect (ID: 93).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_gold_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting gold modify effect (ID: 94).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_villagers_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting villagers modify effect (ID: 84).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def starting_population_space_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the starting popspace modify effect (ID: 4).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        dataset = converter_group.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        patch_target_ref = \"util.resource.types.PopulationSpace\"\n        patch_target = dataset.pregen_nyan_objects[patch_target_ref].get_nyan_object()\n\n        # Wrapper\n        wrapper_name = \"ChangeInitialPopulationLimitWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = \"ChangeInitialPopulationLimit\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"min_amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceContingent\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def theocracy_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the theocracy effect (ID: 193).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def trade_penalty_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the trade penalty modify effect (ID: 78).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def tribute_inefficiency_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the tribute inefficiency modify effect (ID: 46).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def wonder_time_increase_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the wonder time modify effect (ID: 196).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc_demo/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n    modpack_subprocessor.py\n\tprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc_demo/__init__.py",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for AoE2: The Conquerors Demo.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc_demo/modpack_subprocessor.py",
    "content": "# Copyright 2023-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass DemoModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the AoC Demo conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        demo_base = cls._get_demo_base(full_data_set)\n\n        return [demo_base]\n\n    @classmethod\n    def _get_demo_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe2-base modpack.\n        \"\"\"\n        modpack = Modpack(\"trial_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"trial_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"trial_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/aoc_demo/processor.py",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nConvert data from the AoC Demo to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....log import info\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ..aoc.processor import AoCProcessor\nfrom ..aoc.nyan_subprocessor import AoCNyanSubprocessor\nfrom ..aoc.media_subprocessor import AoCMediaSubprocessor\nfrom .modpack_subprocessor import DemoModpackSubprocessor\n\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.entity_object.conversion.aoc.genie_object_container \\\n        import GenieObjectContainer\n\n\nclass DemoProcessor:\n    \"\"\"\n    Main processor for converting data from the AoC Demo.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game speification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n        # pylint: disable=protected-access\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = AoCProcessor._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openae formats (nyan, sprite, terrain)\n        dataset = AoCProcessor._processor(dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        info(\"Creating nyan objects...\")\n\n        AoCNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        AoCMediaSubprocessor.convert(full_data_set)\n\n        return DemoModpackSubprocessor.get_modpacks(full_data_set)\n"
  },
  {
    "path": "openage/convert/processor/conversion/de1/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tmedia_subprocessor.py\n    modpack_subprocessor.py\n\tprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/de1/__init__.py",
    "content": ""
  },
  {
    "path": "openage/convert/processor/conversion/de1/media_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals\n\"\"\"\nConvert media information to metadata definitions and export\nrequests. Subroutine of the main DE1 processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.export.formats.sprite_metadata import LayerMode\nfrom ....entity_object.export.media_export_request import MediaExportRequest\nfrom ....entity_object.export.metadata_export import SpriteMetadataExport\nfrom ....entity_object.export.metadata_export import TextureMetadataExport\nfrom ....value_object.read.media_types import MediaType\nfrom ..aoc.media_subprocessor import AoCMediaSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass DE1MediaSubprocessor:\n    \"\"\"\n    Creates the exports requests for media files from DE1.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create all export requests for the dataset.\n        \"\"\"\n        cls.create_graphics_requests(full_data_set)\n        AoCMediaSubprocessor.create_sound_requests(full_data_set)\n\n    @staticmethod\n    def create_graphics_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for graphics referenced by CombinedSprite objects.\n        \"\"\"\n        combined_sprites = full_data_set.combined_sprites.values()\n        handled_graphic_ids = set()\n\n        for sprite in combined_sprites:\n            ref_graphics = sprite.get_graphics()\n            graphic_targetdirs = sprite.resolve_graphics_location()\n\n            # Animation metadata file definiton\n            sprite_meta_filename = f\"{sprite.get_filename()}.sprite\"\n            sprite_meta_export = SpriteMetadataExport(sprite.resolve_sprite_location(),\n                                                      sprite_meta_filename)\n            full_data_set.metadata_exports.append(sprite_meta_export)\n\n            for graphic in ref_graphics:\n                graphic_id = graphic.get_id()\n                if graphic_id in handled_graphic_ids:\n                    continue\n\n                targetdir = graphic_targetdirs[graphic_id]\n\n                # DE1 stores most graphics filenames as 'whatever_<x#>'\n                # where '<x#>' must be replaced by x1, x2 or x4\n                # which corresponds to the graphics resolution variant\n                source_str = graphic['filename'].value[:-4]\n\n                # TODO: Also convert x2 and x4 variants\n                source_filename = f\"{source_str}x1.slp\"\n                target_filename = f\"{sprite.get_filename()}_{str(graphic['slp_id'].value)}.png\"\n\n                export_request = MediaExportRequest(MediaType.GRAPHICS,\n                                                    targetdir,\n                                                    source_filename,\n                                                    target_filename)\n                full_data_set.graphics_exports.update({graphic_id: export_request})\n\n                # Texture metadata file definiton\n                # Same file stem as the image file and same targetdir\n                texture_meta_filename = f\"{target_filename[:-4]}.texture\"\n                texture_meta_export = TextureMetadataExport(targetdir,\n                                                            texture_meta_filename)\n                full_data_set.metadata_exports.append(texture_meta_export)\n\n                # Add texture image filename to texture metadata\n                texture_meta_export.add_imagefile(target_filename)\n\n                # Add metadata from graphics to animation metadata\n                sequence_type = graphic[\"sequence_type\"].value\n                if sequence_type == 0x00:\n                    layer_mode = LayerMode.OFF\n\n                elif sequence_type & 0x08:\n                    layer_mode = LayerMode.ONCE\n\n                else:\n                    layer_mode = LayerMode.LOOP\n\n                layer_pos = graphic[\"layer\"].value\n                frame_rate = round(graphic[\"frame_rate\"].value, ndigits=6)\n                if frame_rate < 0.000001:\n                    frame_rate = None\n\n                replay_delay = round(graphic[\"replay_delay\"].value, ndigits=6)\n                if replay_delay < 0.000001:\n                    replay_delay = None\n\n                frame_count = graphic[\"frame_count\"].value\n                angle_count = graphic[\"angle_count\"].value\n                # mirror_mode = graphic[\"mirroring_mode\"].value\n                sprite_meta_export.add_graphics_metadata(target_filename,\n                                                         texture_meta_filename,\n                                                         layer_mode,\n                                                         layer_pos,\n                                                         frame_rate,\n                                                         replay_delay,\n                                                         frame_count,\n                                                         angle_count,\n                                                         mirror_mode=0,\n                                                         start_angle=270)\n\n                # Notify metadata export about SLP metadata when the file is exported\n                export_request.add_observer(texture_meta_export)\n                export_request.add_observer(sprite_meta_export)\n\n                handled_graphic_ids.add(graphic_id)\n\n        # TODO: Terrains\n"
  },
  {
    "path": "openage/convert/processor/conversion/de1/modpack_subprocessor.py",
    "content": "# Copyright 2023-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass DE1ModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the DE2 conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        de1_base = cls._get_aoe1_base(full_data_set)\n\n        return [de1_base]\n\n    @classmethod\n    def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe2-base modpack.\n        \"\"\"\n        modpack = Modpack(\"de1_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"de1_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"de1_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/de1/processor.py",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConvert data from DE1 to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....log import info\nfrom ....entity_object.conversion.aoc.genie_graphic import GenieGraphic\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ..aoc.processor import AoCProcessor\nfrom ..ror.nyan_subprocessor import RoRNyanSubprocessor\nfrom ..ror.pregen_subprocessor import RoRPregenSubprocessor\nfrom ..ror.processor import RoRProcessor\nfrom .media_subprocessor import DE1MediaSubprocessor\nfrom .modpack_subprocessor import DE1ModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass DE1Processor:\n    \"\"\"\n    Main processor for converting data from DE1.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game specification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openage formats (nyan, sprite, terrain)\n        dataset = cls._processor(gamespec, dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        RoRProcessor.extract_genie_units(gamespec, dataset)\n        AoCProcessor.extract_genie_techs(gamespec, dataset)\n        AoCProcessor.extract_genie_effect_bundles(gamespec, dataset)\n        AoCProcessor.sanitize_effect_bundles(dataset)\n        AoCProcessor.extract_genie_civs(gamespec, dataset)\n        cls.extract_genie_graphics(gamespec, dataset)\n        RoRProcessor.extract_genie_sounds(gamespec, dataset)\n        AoCProcessor.extract_genie_terrains(gamespec, dataset)\n        AoCProcessor.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(\n        cls,\n        gamespec: ArrayMember,\n        full_data_set: GenieObjectContainer\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        RoRProcessor.create_tech_groups(full_data_set)\n        RoRProcessor.create_entity_lines(gamespec, full_data_set)\n        RoRProcessor.create_ambient_groups(full_data_set)\n        RoRProcessor.create_variant_groups(full_data_set)\n        AoCProcessor.create_terrain_groups(full_data_set)\n        AoCProcessor.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        AoCProcessor.link_creatables(full_data_set)\n        AoCProcessor.link_researchables(full_data_set)\n        AoCProcessor.link_gatherers_to_dropsites(full_data_set)\n        RoRProcessor.link_garrison(full_data_set)\n        AoCProcessor.link_trade_posts(full_data_set)\n        RoRProcessor.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        RoRPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        info(\"Creating nyan objects...\")\n\n        RoRNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        DE1MediaSubprocessor.convert(full_data_set)\n\n        return DE1ModpackSubprocessor.get_modpacks(full_data_set)\n\n    @staticmethod\n    def extract_genie_graphics(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract graphic definitions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->graphics\n        raw_graphics = gamespec[0][\"graphics\"].value\n\n        for raw_graphic in raw_graphics:\n            # Can be ignored if there is no filename associated\n            filename = raw_graphic[\"filename\"].value\n            if not filename:\n                continue\n\n            # DE1 stores most graphics filenames as 'whatever_<x#>'\n            # where '<x#>' must be replaced by x1, x2 or x4\n            # which corresponds to the graphics resolution variant\n            #\n            # we look for the x1 variant\n            if filename.endswith(\"<x#>\"):\n                filename = f\"{filename[:-4]}x1\"\n\n            graphic_id = raw_graphic[\"graphic_id\"].value\n            graphic_members = raw_graphic.value\n\n            graphic = GenieGraphic(graphic_id, full_data_set, members=graphic_members)\n            if filename.lower() not in full_data_set.existing_graphics:\n                graphic.exists = False\n\n            full_data_set.genie_graphics.update({graphic.get_id(): graphic})\n\n        # Detect subgraphics\n        for genie_graphic in full_data_set.genie_graphics.values():\n            genie_graphic.detect_subgraphics()\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/CMakeLists.txt",
    "content": "add_py_modules(\n\tability_subprocessor.py\n\t__init__.py\n\tciv_subprocessor.py\n\tmedia_subprocessor.py\n\tmodpack_subprocessor.py\n\tnyan_subprocessor.py\n\tprocessor.py\n\ttech_subprocessor.py\n\tupgrade_attribute_subprocessor.py\n\tupgrade_resource_subprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for AoE2: Definitive Edition.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/ability_subprocessor.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods,too-many-locals\n\n\"\"\"\nDerives and adds abilities to lines. Subroutine of the\nnyan subprocessor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass DE2AbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for abilities in DE2.\n    \"\"\"\n\n    @staticmethod\n    def regenerate_attribute_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the RegenerateAttribute ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the ability.\n        :rtype: list\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        attribute = None\n        attribute_name = \"\"\n        if current_unit_id == 125:\n            # Monk; regenerates Faith\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Faith\"].get_nyan_object()\n            attribute_name = \"Faith\"\n\n        elif current_unit_id == 692:\n            # Berserk: regenerates Health\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            attribute_name = \"Health\"\n\n        else:\n            return []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_name = f\"Regenerate{attribute_name}\"\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.RegenerateAttribute\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Attribute rate\n        # ===============================================================================\n        rate_name = f\"{attribute_name}Rate\"\n        rate_ref = f\"{game_entity_name}.{ability_name}.{rate_name}\"\n        rate_raw_api_object = RawAPIObject(rate_ref, rate_name, dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, ability_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        # Attribute\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        # Rate\n        attribute_rate = 0\n        if current_unit_id == 125:\n            # stored in civ resources\n            attribute_rate = dataset.genie_civs[0][\"resources\"][35].value\n\n        elif current_unit_id == 692:\n            # stored in unit, but has to get converted to amount/second\n            heal_timer = current_unit[\"heal_timer\"].value\n            attribute_rate = 1 / heal_timer\n\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           attribute_rate,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # ===============================================================================\n        rate_forward_ref = ForwardRef(line, rate_ref)\n        ability_raw_api_object.add_raw_member(\"rate\",\n                                              rate_forward_ref,\n                                              \"engine.ability.type.RegenerateAttribute\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return [ability_forward_ref]\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/civ_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,too-many-branches\n\n\"\"\"\nCreates patches and modifiers for civs.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.civ_subprocessor import AoCCivSubprocessor\nfrom .tech_subprocessor import DE2TechSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n\n\nclass DE2CivSubprocessor:\n    \"\"\"\n    Creates raw API objects for civs in DE2.\n    \"\"\"\n\n    @classmethod\n    def get_civ_setup(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for the civ setup which configures architecture sets\n        unique units, unique techs, team boni and unique stat upgrades.\n        \"\"\"\n        patches = []\n\n        patches.extend(AoCCivSubprocessor.setup_unique_units(civ_group))\n        patches.extend(AoCCivSubprocessor.setup_unique_techs(civ_group))\n        patches.extend(AoCCivSubprocessor.setup_tech_tree(civ_group))\n        patches.extend(cls.setup_civ_bonus(civ_group))\n\n        if len(civ_group.get_team_bonus_effects()) > 0:\n            patches.extend(DE2TechSubprocessor.get_patches(civ_group.team_bonus))\n\n        return patches\n\n    @classmethod\n    def setup_civ_bonus(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns global modifiers of a civ.\n        \"\"\"\n        patches = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        # key: tech_id; value patched in patches\n        tech_patches = {}\n\n        for civ_bonus in civ_group.civ_boni.values():\n            if not civ_bonus.replaces_researchable_tech():\n                bonus_patches = DE2TechSubprocessor.get_patches(civ_bonus)\n\n                # civ boni might be unlocked by age ups. if so, patch them into the age up\n                # patches are queued here\n                required_tech_count = civ_bonus.tech[\"required_tech_count\"].value\n                if required_tech_count > 0 and len(bonus_patches) > 0:\n                    if required_tech_count == 1:\n                        tech_id = civ_bonus.tech[\"required_techs\"][0].value\n\n                    elif required_tech_count == 2:\n                        # Try to patch them into the second listed tech\n                        # This tech is usually unlocked by an age up\n                        tech_id = civ_bonus.tech[\"required_techs\"][1].value\n\n                        if tech_id == 232:\n                            # Synergies with other civs (usually chinese farming bonus)\n                            # TODO: This must be solved in a better way than in the Genie dataset.\n                            continue\n\n                        if tech_id not in dataset.tech_groups.keys():\n                            # Circumvents a \"funny\" duplicate castle age up tech for Incas\n                            # The required tech of the duplicate is the age up we are looking for\n                            tech_id = dataset.genie_techs[tech_id][\"required_techs\"][0].value\n\n                        if not dataset.tech_groups[tech_id].is_researchable():\n                            # Fall back to the first tech if the second is not researchable\n                            tech_id = civ_bonus.tech[\"required_techs\"][0].value\n\n                    if tech_id == 104:\n                        # Skip Dark Age; it is not a tech in openage\n                        patches.extend(bonus_patches)\n\n                    if tech_id not in dataset.tech_groups.keys() or\\\n                            not dataset.tech_groups[tech_id].is_researchable():\n                        # TODO: Bonus unlocked by something else\n                        continue\n\n                    if tech_id in tech_patches:\n                        tech_patches[tech_id].extend(bonus_patches)\n\n                    else:\n                        tech_patches[tech_id] = bonus_patches\n\n                else:\n                    patches.extend(bonus_patches)\n\n        for tech_id, patches in tech_patches.items():\n            tech_group = dataset.tech_groups[tech_id]\n            tech_name = tech_lookup_dict[tech_id][0]\n\n            patch_target_ref = f\"{tech_name}\"\n            patch_target_forward_ref = ForwardRef(tech_group, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"{tech_name}CivBonusWrapper\"\n            wrapper_ref = f\"{civ_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(civ_group, civ_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"{tech_name}CivBonus\"\n            nyan_patch_ref = f\"{civ_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(civ_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"updates\",\n                                                           patches,\n                                                           \"engine.util.tech.Tech\",\n                                                           MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(civ_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            civ_group.add_raw_api_object(wrapper_raw_api_object)\n            civ_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(civ_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/media_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals\n\"\"\"\nConvert media information to metadata definitions and export\nrequests. Subroutine of the main DE2 processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.export.formats.sprite_metadata import LayerMode\nfrom ....entity_object.export.media_export_request import MediaExportRequest\nfrom ....entity_object.export.metadata_export import SpriteMetadataExport\nfrom ....entity_object.export.metadata_export import TextureMetadataExport\nfrom ....value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass DE2MediaSubprocessor:\n    \"\"\"\n    Creates the exports requests for media files from DE2.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create all export requests for the dataset.\n        \"\"\"\n        cls.create_graphics_requests(full_data_set)\n        cls.create_sound_requests(full_data_set)\n\n    @staticmethod\n    def create_graphics_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for graphics referenced by CombinedSprite objects.\n        \"\"\"\n        combined_sprites = full_data_set.combined_sprites.values()\n        handled_graphic_ids = set()\n\n        for sprite in combined_sprites:\n            ref_graphics = sprite.get_graphics()\n            graphic_targetdirs = sprite.resolve_graphics_location()\n\n            # Animation metadata file definiton\n            sprite_meta_filename = f\"{sprite.get_filename()}.sprite\"\n            sprite_meta_export = SpriteMetadataExport(sprite.resolve_sprite_location(),\n                                                      sprite_meta_filename)\n            full_data_set.metadata_exports.append(sprite_meta_export)\n\n            for graphic in ref_graphics:\n                graphic_id = graphic.get_id()\n                if graphic_id in handled_graphic_ids:\n                    continue\n\n                targetdir = graphic_targetdirs[graphic_id]\n                source_filename = f\"{str(graphic['filename'].value)}.sld\"\n                target_filename = f\"{sprite.get_filename()}_{str(graphic['slp_id'].value)}.png\"\n\n                export_request = MediaExportRequest(MediaType.GRAPHICS,\n                                                    targetdir,\n                                                    source_filename,\n                                                    target_filename)\n                full_data_set.graphics_exports.update({graphic_id: export_request})\n\n                # Texture metadata file definiton\n                # Same file stem as the image file and same targetdir\n                texture_meta_filename = f\"{target_filename[:-4]}.texture\"\n                texture_meta_export = TextureMetadataExport(targetdir,\n                                                            texture_meta_filename)\n                full_data_set.metadata_exports.append(texture_meta_export)\n\n                # Add texture image filename to texture metadata\n                texture_meta_export.add_imagefile(target_filename)\n\n                # Add metadata from graphics to animation metadata\n                sequence_type = graphic[\"sequence_type\"].value\n                if sequence_type == 0x00:\n                    layer_mode = LayerMode.OFF\n\n                elif sequence_type & 0x08:\n                    layer_mode = LayerMode.ONCE\n\n                else:\n                    layer_mode = LayerMode.LOOP\n\n                layer_pos = graphic[\"layer\"].value\n                frame_rate = round(graphic[\"frame_rate\"].value, ndigits=6)\n                if frame_rate < 0.000001:\n                    frame_rate = None\n\n                replay_delay = round(graphic[\"replay_delay\"].value, ndigits=6)\n                if replay_delay < 0.000001:\n                    replay_delay = None\n\n                frame_count = graphic[\"frame_count\"].value\n                angle_count = graphic[\"angle_count\"].value\n                # mirror_mode = graphic[\"mirroring_mode\"].value\n                sprite_meta_export.add_graphics_metadata(target_filename,\n                                                         texture_meta_filename,\n                                                         layer_mode,\n                                                         layer_pos,\n                                                         frame_rate,\n                                                         replay_delay,\n                                                         frame_count,\n                                                         angle_count,\n                                                         mirror_mode=0,\n                                                         start_angle=270)\n\n                # Notify metadata export about SMX metadata when the file is exported\n                export_request.add_observer(texture_meta_export)\n                export_request.add_observer(sprite_meta_export)\n\n                handled_graphic_ids.add(graphic_id)\n\n        # TODO: Terrain exports (DDS files)\n\n    @staticmethod\n    def create_sound_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for sounds referenced by CombinedSound objects.\n        \"\"\"\n        # TODO: Sound exports (Wwise files)\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/modpack_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass DE2ModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the DE2 conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        de2_base = cls._get_aoe2_base(full_data_set)\n\n        return [de2_base]\n\n    @classmethod\n    def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe2-base modpack.\n        \"\"\"\n        modpack = Modpack(\"de2_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"de2_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"de2_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/nyan_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\"\"\"\nConvert API-like objects to nyan objects. Subroutine of the\nmain DE2 processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieGarrisonMode, GenieMonkGroup\nfrom ....entity_object.conversion.combined_terrain import CombinedTerrain\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.ability_subprocessor import AoCAbilitySubprocessor\nfrom ..aoc.auxiliary_subprocessor import AoCAuxiliarySubprocessor\nfrom ..aoc.civ_subprocessor import AoCCivSubprocessor\nfrom ..aoc.modifier_subprocessor import AoCModifierSubprocessor\nfrom ..aoc.nyan_subprocessor import AoCNyanSubprocessor\nfrom .ability_subprocessor import DE2AbilitySubprocessor\nfrom .civ_subprocessor import DE2CivSubprocessor\nfrom .tech_subprocessor import DE2TechSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n    from openage.convert.entity_object.conversion.aoc.genie_terrain import GenieTerrainGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n        GenieBuildingLineGroup\n\n\nclass DE2NyanSubprocessor:\n    \"\"\"\n    Transform a DE2 dataset to nyan objects.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects from the given dataset.\n        \"\"\"\n        cls._process_game_entities(full_data_set)\n        cls._create_nyan_objects(full_data_set)\n        cls._create_nyan_members(full_data_set)\n\n        cls._check_objects(full_data_set)\n\n    @classmethod\n    def _check_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Check if objects are valid.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.check_readiness()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.check_readiness()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.check_readiness()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.check_readiness()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.check_readiness()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.check_readiness()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.check_readiness()\n\n    @classmethod\n    def _create_nyan_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Creates nyan objects from the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_objects()\n            unit_line.execute_raw_member_pushs()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_objects()\n            building_line.execute_raw_member_pushs()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_objects()\n            ambient_group.execute_raw_member_pushs()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_objects()\n            variant_group.execute_raw_member_pushs()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_objects()\n            tech_group.execute_raw_member_pushs()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_objects()\n            terrain_group.execute_raw_member_pushs()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_objects()\n            civ_group.execute_raw_member_pushs()\n\n    @classmethod\n    def _create_nyan_members(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Fill nyan member values of the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_members()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_members()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_members()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_members()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_members()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_members()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_members()\n\n    @classmethod\n    def _process_game_entities(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create the RawAPIObject representation of the objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            cls.unit_line_to_game_entity(unit_line)\n\n        for building_line in full_data_set.building_lines.values():\n            cls.building_line_to_game_entity(building_line)\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            AoCNyanSubprocessor.ambient_group_to_game_entity(ambient_group)\n\n        for variant_group in full_data_set.variant_groups.values():\n            AoCNyanSubprocessor.variant_group_to_game_entity(variant_group)\n\n        for tech_group in full_data_set.tech_groups.values():\n            if tech_group.is_researchable():\n                cls.tech_group_to_tech(tech_group)\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            cls.terrain_group_to_terrain(terrain_group)\n\n        for civ_group in full_data_set.civ_groups.values():\n            cls.civ_group_to_civ(civ_group)\n\n    @staticmethod\n    def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a unit line.\n\n        :param unit_line: Unit line that gets converted to a game entity.\n        :type unit_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = unit_line.get_head_unit()\n        current_unit_id = unit_line.get_head_unit_id()\n\n        dataset = unit_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_unit_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_unit_id][1])\n        unit_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a unit two types\n        #    - util.game_entity_type.types.Unit (if unit_type >= 70)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_unit[\"unit_type\"].value\n\n        if unit_type >= 70:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(unit_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.turn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(unit_line))\n\n        # Creation\n        if len(unit_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(unit_line))\n\n        # Config\n        ability = AoCAbilitySubprocessor.use_contingent_ability(unit_line)\n        if ability:\n            abilities_set.append(ability)\n\n        if unit_line.get_head_unit_id() in (125, 692):\n            # Healing/Recharging attribute points (monks, berserks)\n            abilities_set.extend(DE2AbilitySubprocessor.regenerate_attribute_ability(unit_line))\n\n        # Applying effects and shooting projectiles\n        if unit_line.is_projectile_shooter():\n            abilities_set.append(AoCAbilitySubprocessor.shoot_projectile_ability(unit_line, 7))\n            AoCNyanSubprocessor.projectiles_from_line(unit_line)\n\n        elif unit_line.is_melee() or unit_line.is_ranged():\n            if unit_line.has_command(7):\n                # Attack\n                abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          7,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(101):\n                # Build\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            101,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(104):\n                # convert\n                abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          104,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(105):\n                # Heal\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            105,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(106):\n                # Repair\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            106,\n                                                                                            unit_line.is_ranged()))\n\n        # Formation/Stance\n        if not isinstance(unit_line, GenieVillagerGroup):\n            abilities_set.append(AoCAbilitySubprocessor.formation_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(unit_line))\n\n        # Storage abilities\n        if unit_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(unit_line))\n\n            garrison_mode = unit_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.MONK:\n                abilities_set.append(AoCAbilitySubprocessor.collect_storage_ability(unit_line))\n\n        if len(unit_line.garrison_locations) > 0:\n            ability = AoCAbilitySubprocessor.enter_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n            ability = AoCAbilitySubprocessor.exit_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n        if isinstance(unit_line, GenieMonkGroup):\n            abilities_set.append(AoCAbilitySubprocessor.transfer_storage_ability(unit_line))\n\n        # Resource abilities\n        if unit_line.is_gatherer():\n            abilities_set.append(AoCAbilitySubprocessor.drop_resources_ability(unit_line))\n            abilities_set.extend(AoCAbilitySubprocessor.gather_ability(unit_line))\n\n        # Resource storage\n        if unit_line.is_gatherer() or unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.resource_storage_ability(unit_line))\n\n        if isinstance(unit_line, GenieVillagerGroup):\n            # Farm restocking\n            abilities_set.append(AoCAbilitySubprocessor.restock_ability(unit_line, 50))\n\n        if unit_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(unit_line))\n\n        if unit_type == 70 and unit_line.get_class_id() not in (9, 10, 58):\n            # Excludes trebuchets and animals\n            abilities_set.append(AoCAbilitySubprocessor.herd_ability(unit_line))\n\n        if unit_line.get_class_id() == 58:\n            abilities_set.append(AoCAbilitySubprocessor.herdable_ability(unit_line))\n\n        # Trade abilities\n        if unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.trade_ability(unit_line))\n\n        # =======================================================================\n        # TODO: Transform\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        if unit_line.has_command(7) and not unit_line.is_projectile_shooter():\n            modifiers_set.extend(AoCModifierSubprocessor.elevation_attack_modifiers(unit_line))\n\n        if unit_line.is_gatherer():\n            modifiers_set.extend(AoCModifierSubprocessor.gather_rate_modifier(unit_line))\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if unit_line.is_creatable():\n            AoCAuxiliarySubprocessor.get_creatable_game_entity(unit_line)\n\n    @staticmethod\n    def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a building line.\n\n        :param building_line: Building line that gets converted to a game entity.\n        :type building_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_building = building_line.line[0]\n        current_building_id = building_line.get_head_unit_id()\n        dataset = building_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_building_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_building_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_building_id][1])\n        building_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a building two types\n        #    - util.game_entity_type.types.Building (if unit_type >= 80)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # and additionally\n        #    - util.game_entity_type.types.DropSite (only if this is used as a drop site)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_building[\"unit_type\"].value\n\n        if unit_type >= 80:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_building[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        if building_line.is_dropsite():\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.DropSite\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.attribute_change_tracker_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(building_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(building_line))\n\n        if building_line.get_head_unit()[\"speed\"].value > 0:\n            abilities_set.append(AoCAbilitySubprocessor.move_ability(building_line))\n\n        # Config abilities\n        if building_line.is_creatable():\n            abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line))\n\n        if not building_line.is_passable():\n            abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line))\n\n        if building_line.has_foundation():\n            if building_line.get_class_id() == 49:\n                # Use OverlayTerrain for the farm terrain\n                abilities_set.append(AoCAbilitySubprocessor.overlay_terrain_ability(building_line))\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line,\n                                                                               terrain_id=27))\n\n            else:\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line))\n\n        # Creation/Research abilities\n        if len(building_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.production_queue_ability(building_line))\n\n        if len(building_line.researches) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.research_ability(building_line))\n\n        # Effect abilities\n        if building_line.is_projectile_shooter():\n            abilities_set.append(AoCAbilitySubprocessor.shoot_projectile_ability(building_line, 7))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(building_line))\n            AoCNyanSubprocessor.projectiles_from_line(building_line)\n\n        # Storage abilities\n        if building_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(building_line))\n\n            garrison_mode = building_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.NATURAL:\n                abilities_set.append(\n                    AoCAbilitySubprocessor.send_back_to_task_ability(building_line))\n\n            if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                abilities_set.append(AoCAbilitySubprocessor.rally_point_ability(building_line))\n\n        # Resource abilities\n        if building_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(building_line))\n\n        if building_line.is_dropsite():\n            abilities_set.append(AoCAbilitySubprocessor.drop_site_ability(building_line))\n\n        ability = AoCAbilitySubprocessor.provide_contingent_ability(building_line)\n        if ability:\n            abilities_set.append(ability)\n\n        # Trade abilities\n        if building_line.is_trade_post():\n            abilities_set.append(AoCAbilitySubprocessor.trade_post_ability(building_line))\n\n        if building_line.get_id() == 84:\n            # Market trading\n            abilities_set.extend(AoCAbilitySubprocessor.exchange_resources_ability(building_line))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        raw_api_object.add_raw_member(\"modifiers\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if building_line.is_creatable():\n            AoCAuxiliarySubprocessor.get_creatable_game_entity(building_line)\n\n    @staticmethod\n    def tech_group_to_tech(tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a tech group.\n\n        :param tech_group: Tech group that gets converted to a tech.\n        :type tech_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        tech_id = tech_group.get_id()\n\n        # Skip Dark Age tech\n        if tech_id == 104:\n            return\n\n        dataset = tech_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = tech_lookup_dict[tech_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.tech.Tech\")\n\n        if isinstance(tech_group, UnitLineUpgrade):\n            unit_line = dataset.unit_lines[tech_group.get_line_id()]\n            head_unit_id = unit_line.get_head_unit_id()\n            obj_location = f\"data/game_entity/generic/{name_lookup_dict[head_unit_id][1]}/\"\n\n        else:\n            obj_location = f\"data/tech/generic/{tech_lookup_dict[tech_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(tech_lookup_dict[tech_id][1])\n        tech_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        raw_api_object.add_raw_member(\"types\", [], \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(tech_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(tech_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(tech_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(tech_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(tech_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(tech_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # Updates\n        # =======================================================================\n        patches = []\n        patches.extend(DE2TechSubprocessor.get_patches(tech_group))\n        raw_api_object.add_raw_member(\"updates\", patches, \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the tech group itself, but use its values)\n        # =======================================================================\n        if tech_group.is_researchable():\n            AoCAuxiliarySubprocessor.get_researchable_tech(tech_group)\n\n    @staticmethod\n    def terrain_group_to_terrain(terrain_group: GenieTerrainGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a terrain group.\n\n        :param terrain_group: Terrain group that gets converted to a tech.\n        :type terrain_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        terrain_index = terrain_group.get_id()\n\n        dataset = terrain_group.data\n\n        # name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_lookup_dict = internal_name_lookups.get_terrain_lookups(dataset.game_version)\n        terrain_type_lookup_dict = internal_name_lookups.get_terrain_type_lookups(\n            dataset.game_version)\n\n        if terrain_index not in terrain_lookup_dict:\n            # TODO: Not all terrains are used in DE2; filter out the unused terrains\n            # in pre-processor\n            return\n\n        # Start with the Terrain object\n        terrain_name = terrain_lookup_dict[terrain_index][1]\n        raw_api_object = RawAPIObject(terrain_name, terrain_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.terrain.Terrain\")\n        obj_location = f\"data/terrain/{terrain_lookup_dict[terrain_index][2]}/\"\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(terrain_lookup_dict[terrain_index][2])\n        terrain_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        terrain_types = []\n\n        for terrain_type in terrain_type_lookup_dict.values():\n            if terrain_index in terrain_type[0]:\n                type_name = f\"util.terrain_type.types.{terrain_type[2]}\"\n                type_obj = dataset.pregen_nyan_objects[type_name].get_nyan_object()\n                terrain_types.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", terrain_types, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{terrain_name}.{terrain_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{terrain_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(terrain_group, terrain_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(terrain_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.terrain.Terrain\")\n        terrain_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Sound\n        # =======================================================================\n        sound_name = f\"{terrain_name}.Sound\"\n        sound_raw_api_object = RawAPIObject(sound_name, \"Sound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(terrain_group, terrain_name)\n        sound_raw_api_object.set_location(sound_location)\n\n        # TODO: Sounds\n        sounds = []\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(terrain_group, sound_name)\n        raw_api_object.add_raw_member(\"sound\",\n                                      sound_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n        terrain_group.add_raw_api_object(sound_raw_api_object)\n\n        # =======================================================================\n        # Ambience\n        # =======================================================================\n        terrain = terrain_group.get_terrain()\n        # ambients_count = terrain[\"terrain_units_used_count\"].value\n\n        ambience = []\n        # TODO: Ambience\n# ===============================================================================\n#         for ambient_index in range(ambients_count):\n#             ambient_id = terrain[\"terrain_unit_id\"][ambient_index].value\n#\n#             if ambient_id == -1:\n#                 continue\n#\n#             ambient_line = dataset.unit_ref[ambient_id]\n#             ambient_name = name_lookup_dict[ambient_line.get_head_unit_id()][0]\n#\n#             ambient_ref = \"%s.Ambient%s\" % (terrain_name, str(ambient_index))\n#             ambient_raw_api_object = RawAPIObject(ambient_ref,\n#                                                   \"Ambient%s\" % (str(ambient_index)),\n#                                                   dataset.nyan_api_objects)\n#             ambient_raw_api_object.add_raw_parent(\"engine.util.terrain.TerrainAmbient\")\n#             ambient_location = ForwardRef(terrain_group, terrain_name)\n#             ambient_raw_api_object.set_location(ambient_location)\n#\n#             # Game entity reference\n#             ambient_line_forward_ref = ForwardRef(ambient_line, ambient_name)\n#             ambient_raw_api_object.add_raw_member(\"object\",\n#                                                   ambient_line_forward_ref,\n#                                                   \"engine.util.terrain.TerrainAmbient\")\n#\n#             # Max density\n#             max_density = terrain[\"terrain_unit_density\"][ambient_index].value\n#             ambient_raw_api_object.add_raw_member(\"max_density\",\n#                                                   max_density,\n#                                                   \"engine.util.terrain.TerrainAmbient\")\n#\n#             terrain_group.add_raw_api_object(ambient_raw_api_object)\n#             terrain_ambient_forward_ref = ForwardRef(terrain_group, ambient_ref)\n#             ambience.append(terrain_ambient_forward_ref)\n# ===============================================================================\n\n        raw_api_object.add_raw_member(\"ambience\", ambience, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Path Costs\n        # =======================================================================\n        path_costs = {}\n        restrictions = dataset.genie_terrain_restrictions\n\n        # Land grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Land\"].get_nyan_object()\n        land_restrictions = restrictions[0x07]\n        if land_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Water grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Water\"].get_nyan_object()\n        water_restrictions = restrictions[0x03]\n        if water_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Air grid (default accessible)\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Air\"].get_nyan_object()\n        path_costs[path_type] = 1\n\n        raw_api_object.add_raw_member(\"path_costs\", path_costs, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Graphic\n        # =======================================================================\n        texture_id = terrain.get_id()\n\n        # Create animation object\n        graphic_name = f\"{terrain_name}.TerrainTexture\"\n        graphic_raw_api_object = RawAPIObject(graphic_name, \"TerrainTexture\",\n                                              dataset.nyan_api_objects)\n        graphic_raw_api_object.add_raw_parent(\"engine.util.graphics.Terrain\")\n        graphic_location = ForwardRef(terrain_group, terrain_name)\n        graphic_raw_api_object.set_location(graphic_location)\n\n        if texture_id in dataset.combined_terrains.keys():\n            terrain_graphic = dataset.combined_terrains[texture_id]\n\n        else:\n            terrain_graphic = CombinedTerrain(texture_id,\n                                              f\"texture_{terrain_lookup_dict[terrain_index][2]}\",\n                                              dataset)\n            dataset.combined_terrains.update({terrain_graphic.get_id(): terrain_graphic})\n\n        terrain_graphic.add_reference(graphic_raw_api_object)\n\n        graphic_raw_api_object.add_raw_member(\"sprite\", terrain_graphic,\n                                              \"engine.util.graphics.Terrain\")\n\n        terrain_group.add_raw_api_object(graphic_raw_api_object)\n        graphic_forward_ref = ForwardRef(terrain_group, graphic_name)\n        raw_api_object.add_raw_member(\"terrain_graphic\", graphic_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n    @staticmethod\n    def civ_group_to_civ(civ_group: GenieCivilizationGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a civ group.\n\n        :param civ_group: Terrain group that gets converted to a tech.\n        :type civ_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        civ_id = civ_group.get_id()\n\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = civ_lookup_dict[civ_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.setup.PlayerSetup\")\n\n        obj_location = f\"data/civ/{civ_lookup_dict[civ_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(civ_lookup_dict[civ_id][1])\n        civ_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(civ_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(civ_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(civ_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(civ_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(civ_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(civ_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # TODO: Leader names\n        # =======================================================================\n        raw_api_object.add_raw_member(\"leader_names\",\n                                      [],\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers = AoCCivSubprocessor.get_modifiers(civ_group)\n        raw_api_object.add_raw_member(\"modifiers\",\n                                      modifiers,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Starting resources\n        # =======================================================================\n        resource_amounts = AoCCivSubprocessor.get_starting_resources(civ_group)\n        raw_api_object.add_raw_member(\"starting_resources\",\n                                      resource_amounts,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Game setup\n        # =======================================================================\n        game_setup = DE2CivSubprocessor.get_civ_setup(civ_group)\n        raw_api_object.add_raw_member(\"game_setup\",\n                                      game_setup,\n                                      \"engine.util.setup.PlayerSetup\")\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/processor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long,too-many-lines,too-many-branches,too-many-statements\n\"\"\"\nConvert data from DE2 to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom openage.convert.value_object.read.value_members import ArrayMember, StorageType\nimport openage.convert.value_object.conversion.aoc.internal_nyan_names as aoc_internal\nimport openage.convert.value_object.conversion.de2.internal_nyan_names as de2_internal\n\nfrom .....log import info\nfrom .....util.ordered_set import OrderedSet\nfrom ....entity_object.conversion.aoc.genie_graphic import GenieGraphic\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup, GenieUnitObject, GenieAmbientGroup, \\\n    GenieVariantGroup\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ..aoc.pregen_processor import AoCPregenSubprocessor\nfrom ..aoc.processor import AoCProcessor\nfrom .media_subprocessor import DE2MediaSubprocessor\nfrom .modpack_subprocessor import DE2ModpackSubprocessor\nfrom .nyan_subprocessor import DE2NyanSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass DE2Processor:\n    \"\"\"\n    Main processor for converting data from DE2.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game speification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openae formats (nyan, sprite, terrain)\n        dataset = cls._processor(dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        cls.extract_genie_units(gamespec, dataset)\n        AoCProcessor.extract_genie_techs(gamespec, dataset)\n        AoCProcessor.extract_genie_effect_bundles(gamespec, dataset)\n        AoCProcessor.sanitize_effect_bundles(dataset)\n        AoCProcessor.extract_genie_civs(gamespec, dataset)\n        AoCProcessor.extract_age_connections(gamespec, dataset)\n        AoCProcessor.extract_building_connections(gamespec, dataset)\n        AoCProcessor.extract_unit_connections(gamespec, dataset)\n        AoCProcessor.extract_tech_connections(gamespec, dataset)\n        cls.extract_genie_graphics(gamespec, dataset)\n        AoCProcessor.extract_genie_sounds(gamespec, dataset)\n        AoCProcessor.extract_genie_terrains(gamespec, dataset)\n        AoCProcessor.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(cls, full_data_set: GenieObjectContainer) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        AoCProcessor.create_unit_lines(full_data_set)\n        AoCProcessor.create_extra_unit_lines(full_data_set)\n        cls.create_extra_building_lines(full_data_set)\n        AoCProcessor.create_building_lines(full_data_set)\n        AoCProcessor.create_villager_groups(full_data_set)\n        cls.create_ambient_groups(full_data_set)\n        cls.create_variant_groups(full_data_set)\n        AoCProcessor.create_terrain_groups(full_data_set)\n        AoCProcessor.create_tech_groups(full_data_set)\n        AoCProcessor.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        AoCProcessor.link_building_upgrades(full_data_set)\n        AoCProcessor.link_creatables(full_data_set)\n        AoCProcessor.link_researchables(full_data_set)\n        AoCProcessor.link_civ_uniques(full_data_set)\n        AoCProcessor.link_gatherers_to_dropsites(full_data_set)\n        AoCProcessor.link_garrison(full_data_set)\n        AoCProcessor.link_trade_posts(full_data_set)\n        AoCProcessor.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        AoCPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        info(\"Creating nyan objects...\")\n\n        DE2NyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        DE2MediaSubprocessor.convert(full_data_set)\n\n        return DE2ModpackSubprocessor.get_modpacks(full_data_set)\n\n    @staticmethod\n    def extract_genie_units(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract units from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # Units are stored in the civ container.\n        # All civs point to the same units (?) except for Gaia which has more.\n        # Gaia also seems to have the most units, so we only read from Gaia\n        #\n        # call hierarchy: wrapper[0]->civs[0]->units\n        raw_units = gamespec[0][\"civs\"][0][\"units\"].value\n\n        # Unit headers store the things units can do\n        raw_unit_headers = gamespec[0][\"unit_headers\"].value\n\n        for raw_unit in raw_units:\n            unit_id = raw_unit[\"id0\"].value\n            unit_members = raw_unit.value\n\n            # Turn attack and armor into containers to make diffing work\n            if \"attacks\" in unit_members.keys():\n                attacks_member = unit_members.pop(\"attacks\")\n                attacks_member = attacks_member.get_container(\"type_id\")\n                armors_member = unit_members.pop(\"armors\")\n                armors_member = armors_member.get_container(\"type_id\")\n\n                unit_members.update({\"attacks\": attacks_member})\n                unit_members.update({\"armors\": armors_member})\n\n            unit = GenieUnitObject(unit_id, full_data_set, members=unit_members)\n            full_data_set.genie_units.update({unit.get_id(): unit})\n\n            # Commands\n            if \"unit_commands\" not in unit_members.keys():\n                # Only ActionUnits with type >= 40 should have commands\n                unit_type = raw_unit[\"unit_type\"].value\n                if unit_type >= 40:\n                    unit_commands = raw_unit_headers[unit_id][\"unit_commands\"]\n                    unit.add_member(unit_commands)\n\n                else:\n                    # Create empty member if no headers are present\n                    unit_commands = ArrayMember(\"unit_commands\",\n                                                StorageType.CONTAINER_MEMBER,\n                                                members=[])\n                    unit.add_member(unit_commands)\n\n    @staticmethod\n    def extract_genie_graphics(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract graphic definitions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->graphics\n        raw_graphics = gamespec[0][\"graphics\"].value\n\n        for raw_graphic in raw_graphics:\n            # Can be ignored if there is no filename associated\n            filename = raw_graphic[\"filename\"].value.lower()\n            if not filename:\n                continue\n\n            graphic_id = raw_graphic[\"graphic_id\"].value\n            graphic_members = raw_graphic.value\n            graphic = GenieGraphic(graphic_id, full_data_set, members=graphic_members)\n\n            if filename not in full_data_set.existing_graphics:\n                graphic.exists = False\n\n            full_data_set.genie_graphics.update({graphic.get_id(): graphic})\n\n        # Detect subgraphics\n        for genie_graphic in full_data_set.genie_graphics.values():\n            genie_graphic.detect_subgraphics()\n\n    @staticmethod\n    def create_ambient_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create ambient groups, mostly for resources and scenery.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        ambient_ids = OrderedSet()\n        ambient_ids.update(aoc_internal.AMBIENT_GROUP_LOOKUPS.keys())\n        ambient_ids.update(de2_internal.AMBIENT_GROUP_LOOKUPS.keys())\n        genie_units = full_data_set.genie_units\n\n        for ambient_id in ambient_ids:\n            ambient_group = GenieAmbientGroup(ambient_id, full_data_set)\n            ambient_group.add_unit(genie_units[ambient_id])\n            full_data_set.ambient_groups.update({ambient_group.get_id(): ambient_group})\n            full_data_set.unit_ref.update({ambient_id: ambient_group})\n\n    @staticmethod\n    def create_variant_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create variant groups.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        variants = {}\n        variants.update(aoc_internal.VARIANT_GROUP_LOOKUPS)\n        variants.update(de2_internal.VARIANT_GROUP_LOOKUPS)\n\n        for group_id, variant in variants.items():\n            variant_group = GenieVariantGroup(group_id, full_data_set)\n            full_data_set.variant_groups.update({variant_group.get_id(): variant_group})\n\n            for variant_id in variant[2]:\n                variant_group.add_unit(full_data_set.genie_units[variant_id])\n                full_data_set.unit_ref.update({variant_id: variant_group})\n\n    @staticmethod\n    def create_extra_building_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create additional units that are not in the building connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        extra_units = (\n            1734,  # Folwark\n            1808,  # Mule Cart\n        )\n\n        for unit_id in extra_units:\n            building_line = GenieBuildingLineGroup(unit_id, full_data_set)\n            building_line.add_unit(full_data_set.genie_units[unit_id])\n            full_data_set.building_lines.update({building_line.get_id(): building_line})\n            full_data_set.unit_ref.update({unit_id: building_line})\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/tech_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches\n\n\"\"\"\nCreates patches for technologies.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import CivTeamBonus, CivBonus\nfrom ..aoc.tech_subprocessor import AoCTechSubprocessor\nfrom ..aoc.upgrade_attribute_subprocessor import AoCUpgradeAttributeSubprocessor\nfrom ..aoc.upgrade_resource_subprocessor import AoCUpgradeResourceSubprocessor\nfrom .upgrade_attribute_subprocessor import DE2UpgradeAttributeSubprocessor\nfrom .upgrade_resource_subprocessor import DE2UpgradeResourceSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject\n    from openage.convert.value_object.conversion.forward_ref import ForwardRef\n\n\nclass DE2TechSubprocessor:\n    \"\"\"\n    Creates raw API objects and patches for techs and civ setups in DE2.\n    \"\"\"\n\n    upgrade_attribute_funcs = {\n        0: AoCUpgradeAttributeSubprocessor.hp_upgrade,\n        1: AoCUpgradeAttributeSubprocessor.los_upgrade,\n        2: AoCUpgradeAttributeSubprocessor.garrison_capacity_upgrade,\n        3: AoCUpgradeAttributeSubprocessor.unit_size_x_upgrade,\n        4: AoCUpgradeAttributeSubprocessor.unit_size_y_upgrade,\n        5: AoCUpgradeAttributeSubprocessor.move_speed_upgrade,\n        6: AoCUpgradeAttributeSubprocessor.rotation_speed_upgrade,\n        8: AoCUpgradeAttributeSubprocessor.armor_upgrade,\n        9: AoCUpgradeAttributeSubprocessor.attack_upgrade,\n        10: AoCUpgradeAttributeSubprocessor.reload_time_upgrade,\n        11: AoCUpgradeAttributeSubprocessor.accuracy_upgrade,\n        12: AoCUpgradeAttributeSubprocessor.max_range_upgrade,\n        13: AoCUpgradeAttributeSubprocessor.work_rate_upgrade,\n        14: AoCUpgradeAttributeSubprocessor.carry_capacity_upgrade,\n        16: AoCUpgradeAttributeSubprocessor.projectile_unit_upgrade,\n        17: AoCUpgradeAttributeSubprocessor.graphics_angle_upgrade,\n        18: AoCUpgradeAttributeSubprocessor.terrain_defense_upgrade,\n        19: AoCUpgradeAttributeSubprocessor.ballistics_upgrade,\n        20: AoCUpgradeAttributeSubprocessor.min_range_upgrade,\n        21: AoCUpgradeAttributeSubprocessor.resource_storage_1_upgrade,\n        22: AoCUpgradeAttributeSubprocessor.blast_radius_upgrade,\n        23: AoCUpgradeAttributeSubprocessor.search_radius_upgrade,\n\n        # TODO: These refer to different atributes in DE2\n        24: AoCUpgradeAttributeSubprocessor.imperial_tech_id_upgrade,\n        26: AoCUpgradeAttributeSubprocessor.attack_warning_sound_upgrade,\n        42: AoCUpgradeAttributeSubprocessor.standing_wonders_upgrade,\n        43: AoCUpgradeAttributeSubprocessor.train_button_upgrade,\n        46: AoCUpgradeAttributeSubprocessor.tribute_inefficiency_upgrade,\n        48: AoCUpgradeAttributeSubprocessor.tc_available_upgrade,\n        49: AoCUpgradeAttributeSubprocessor.gold_counter_upgrade,\n        57: AoCUpgradeAttributeSubprocessor.kidnap_storage_upgrade,\n\n        30: DE2UpgradeAttributeSubprocessor.herdable_capacity_upgrade,\n        51: DE2UpgradeAttributeSubprocessor.bfg_unknown_51_upgrade,\n        59: DE2UpgradeAttributeSubprocessor.charge_attack_upgrade,\n        60: DE2UpgradeAttributeSubprocessor.charge_regen_upgrade,\n        61: DE2UpgradeAttributeSubprocessor.charge_event_upgrade,\n        62: DE2UpgradeAttributeSubprocessor.charge_type_upgrade,\n        63: AoCUpgradeAttributeSubprocessor.ignore_armor_upgrade,\n        71: DE2UpgradeAttributeSubprocessor.bfg_unknown_71_upgrade,\n        73: DE2UpgradeAttributeSubprocessor.bfg_unknown_73_upgrade,\n        100: AoCUpgradeAttributeSubprocessor.resource_cost_upgrade,\n        101: AoCUpgradeAttributeSubprocessor.creation_time_upgrade,\n        102: AoCUpgradeAttributeSubprocessor.min_projectiles_upgrade,\n        103: AoCUpgradeAttributeSubprocessor.cost_food_upgrade,\n        104: AoCUpgradeAttributeSubprocessor.cost_wood_upgrade,\n        105: AoCUpgradeAttributeSubprocessor.cost_gold_upgrade,\n        106: AoCUpgradeAttributeSubprocessor.cost_stone_upgrade,\n        107: AoCUpgradeAttributeSubprocessor.max_projectiles_upgrade,\n        108: AoCUpgradeAttributeSubprocessor.garrison_heal_upgrade,\n        109: DE2UpgradeAttributeSubprocessor.regeneration_rate_upgrade,\n        110: DE2UpgradeAttributeSubprocessor.villager_pop_space_upgrade,\n        111: DE2UpgradeAttributeSubprocessor.min_convert_upgrade,\n        112: DE2UpgradeAttributeSubprocessor.max_convert_upgrade,\n        113: DE2UpgradeAttributeSubprocessor.convert_chance_upgrade,\n    }\n\n    upgrade_resource_funcs = {\n        0: DE2UpgradeResourceSubprocessor.current_food_amount_upgrade,\n        1: DE2UpgradeResourceSubprocessor.current_wood_amount_upgrade,\n        2: DE2UpgradeResourceSubprocessor.current_stone_amount_upgrade,\n        3: DE2UpgradeResourceSubprocessor.current_gold_amount_upgrade,\n        4: AoCUpgradeResourceSubprocessor.starting_population_space_upgrade,\n        27: AoCUpgradeResourceSubprocessor.monk_conversion_upgrade,\n        28: AoCUpgradeResourceSubprocessor.building_conversion_upgrade,\n        29: AoCUpgradeResourceSubprocessor.siege_conversion_upgrade,\n        32: AoCUpgradeResourceSubprocessor.bonus_population_upgrade,\n        35: AoCUpgradeResourceSubprocessor.faith_recharge_rate_upgrade,\n        36: AoCUpgradeResourceSubprocessor.farm_food_upgrade,\n        46: AoCUpgradeResourceSubprocessor.tribute_inefficiency_upgrade,\n        47: AoCUpgradeResourceSubprocessor.gather_gold_efficiency_upgrade,\n        50: AoCUpgradeResourceSubprocessor.reveal_ally_upgrade,\n        69: DE2UpgradeResourceSubprocessor.bfg_unknown_69_upgrade,\n        77: AoCUpgradeResourceSubprocessor.conversion_resistance_upgrade,\n        78: AoCUpgradeResourceSubprocessor.trade_penalty_upgrade,\n        79: AoCUpgradeResourceSubprocessor.gather_stone_efficiency_upgrade,\n        84: AoCUpgradeResourceSubprocessor.starting_villagers_upgrade,\n        85: AoCUpgradeResourceSubprocessor.chinese_tech_discount_upgrade,\n        86: AoCUpgradeResourceSubprocessor.research_time_upgrade,\n        88: DE2UpgradeResourceSubprocessor.fish_trap_food_amount_upgrade,\n        89: AoCUpgradeResourceSubprocessor.heal_rate_upgrade,\n        90: AoCUpgradeResourceSubprocessor.heal_range_upgrade,\n        91: AoCUpgradeResourceSubprocessor.starting_food_upgrade,\n        92: AoCUpgradeResourceSubprocessor.starting_wood_upgrade,\n        93: AoCUpgradeResourceSubprocessor.starting_stone_upgrade,\n        94: AoCUpgradeResourceSubprocessor.starting_gold_upgrade,\n        96: AoCUpgradeResourceSubprocessor.berserk_heal_rate_upgrade,\n        97: AoCUpgradeResourceSubprocessor.herding_dominance_upgrade,\n        176: DE2UpgradeResourceSubprocessor.conversion_min_adjustment_upgrade,\n        177: DE2UpgradeResourceSubprocessor.conversion_max_adjustment_upgrade,\n        178: AoCUpgradeResourceSubprocessor.conversion_resistance_min_rounds_upgrade,\n        179: AoCUpgradeResourceSubprocessor.conversion_resistance_max_rounds_upgrade,\n        180: DE2UpgradeResourceSubprocessor.conversion_min_building_upgrade,\n        181: DE2UpgradeResourceSubprocessor.conversion_max_building_upgrade,\n        182: DE2UpgradeResourceSubprocessor.conversion_building_chance_upgrade,\n        183: AoCUpgradeResourceSubprocessor.reveal_enemy_upgrade,\n        189: AoCUpgradeResourceSubprocessor.gather_wood_efficiency_upgrade,\n        190: AoCUpgradeResourceSubprocessor.gather_food_efficiency_upgrade,\n        191: AoCUpgradeResourceSubprocessor.relic_gold_bonus_upgrade,\n        192: AoCUpgradeResourceSubprocessor.heresy_upgrade,\n        193: AoCUpgradeResourceSubprocessor.theocracy_upgrade,\n        194: AoCUpgradeResourceSubprocessor.crenellations_upgrade,\n        196: AoCUpgradeResourceSubprocessor.wonder_time_increase_upgrade,\n        197: AoCUpgradeResourceSubprocessor.spies_discount_upgrade,\n        208: DE2UpgradeResourceSubprocessor.feitoria_gold_upgrade,\n        209: DE2UpgradeResourceSubprocessor.reveal_enemy_tcs_upgrade,\n        211: DE2UpgradeResourceSubprocessor.elevation_attack_upgrade,\n        212: DE2UpgradeResourceSubprocessor.cliff_attack_upgrade,\n        214: DE2UpgradeResourceSubprocessor.free_kipchaks_upgrade,\n        216: DE2UpgradeResourceSubprocessor.sheep_food_amount_upgrade,\n        218: DE2UpgradeResourceSubprocessor.cuman_tc_upgrade,\n        219: DE2UpgradeResourceSubprocessor.bfg_unknown_219_upgrade,\n        220: DE2UpgradeResourceSubprocessor.relic_food_production_upgrade,\n        234: DE2UpgradeResourceSubprocessor.first_crusade_upgrade,\n        236: DE2UpgradeResourceSubprocessor.burgundian_vineyards_upgrade,\n        237: DE2UpgradeResourceSubprocessor.folwark_collect_upgrade,\n        238: DE2UpgradeResourceSubprocessor.folwark_flag_upgrade,\n        239: DE2UpgradeResourceSubprocessor.folwark_mill_id_upgrade,\n        241: DE2UpgradeResourceSubprocessor.stone_gold_gen_upgrade,\n        242: DE2UpgradeResourceSubprocessor.workshop_food_gen_upgrade,\n        243: DE2UpgradeResourceSubprocessor.workshop_wood_gen_upgrade,\n        244: DE2UpgradeResourceSubprocessor.workshop_stone_gen_upgrade,\n        245: DE2UpgradeResourceSubprocessor.workshop_gold_gen_upgrade,\n        251: DE2UpgradeResourceSubprocessor.trade_food_bonus_upgrade,\n        254: DE2UpgradeResourceSubprocessor.herdable_garrison_upgrade,\n        262: DE2UpgradeResourceSubprocessor.bengali_conversion_resistance_upgrade,\n        266: DE2UpgradeResourceSubprocessor.doi_paper_money_upgrade,\n        267: DE2UpgradeResourceSubprocessor.forager_wood_gather_upgrade,\n        268: DE2UpgradeResourceSubprocessor.resource_decay_upgrade,\n        269: DE2UpgradeResourceSubprocessor.tech_reward_upgrade,\n        272: DE2UpgradeResourceSubprocessor.cliff_defense_upgrade,\n        273: DE2UpgradeResourceSubprocessor.elevation_defense_upgrade,\n        274: DE2UpgradeResourceSubprocessor.chieftains_upgrade,\n        280: DE2UpgradeResourceSubprocessor.conversion_range_upgrade,\n        282: DE2UpgradeResourceSubprocessor.unknown_recharge_rate_upgrade,\n        502: DE2UpgradeResourceSubprocessor.bfg_unknown_502_upgrade,\n        507: DE2UpgradeResourceSubprocessor.bfg_unknown_507_upgrade,\n        521: DE2UpgradeResourceSubprocessor.bfg_unknown_521_upgrade,\n        551: DE2UpgradeResourceSubprocessor.bfg_unknown_551_upgrade,\n    }\n\n    @classmethod\n    def get_patches(cls, converter_group: ConverterObjectGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for a converter group, depending on the type\n        of its effects.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n        team_bonus = False\n\n        if isinstance(converter_group, CivTeamBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n            team_bonus = True\n\n        elif isinstance(converter_group, CivBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n\n        else:\n            effects = converter_group.get_effects()\n\n        team_effect = False\n        for effect in effects:\n            type_id = effect.get_type()\n\n            if team_bonus or type_id in (10, 11, 12, 13, 14, 15, 16):\n                team_effect = True\n                type_id -= 10\n\n            if type_id in (0, 4, 5):\n                patches.extend(cls.attribute_modify_effect(converter_group,\n                                                           effect,\n                                                           team=team_effect))\n\n            elif type_id in (1, 6):\n                patches.extend(cls.resource_modify_effect(converter_group,\n                                                          effect,\n                                                          team=team_effect))\n\n            elif type_id == 2:\n                # Enabling/disabling units: Handled in creatable conditions\n                pass\n\n            elif type_id == 3:\n                patches.extend(AoCTechSubprocessor.upgrade_unit_effect(converter_group,\n                                                                       effect))\n\n            elif type_id == 101:\n                patches.extend(AoCTechSubprocessor.tech_cost_modify_effect(converter_group,\n                                                                           effect,\n                                                                           team=team_effect))\n\n            elif type_id == 102:\n                # Tech disable: Only used for civ tech tree\n                pass\n\n            elif type_id == 103:\n                patches.extend(AoCTechSubprocessor.tech_time_modify_effect(converter_group,\n                                                                           effect,\n                                                                           team=team_effect))\n\n            team_effect = False\n\n        return patches\n\n    @staticmethod\n    def attribute_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying attributes of entities.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type in (0, 10):\n            operator = MemberOperator.ASSIGN\n\n        elif effect_type in (4, 14):\n            operator = MemberOperator.ADD\n\n        elif effect_type in (5, 15):\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid attribute effect\")\n\n        unit_id = effect[\"attr_a\"].value\n        class_id = effect[\"attr_b\"].value\n        attribute_type = effect[\"attr_c\"].value\n        value = effect[\"attr_d\"].value\n\n        if attribute_type == -1:\n            return patches\n\n        affected_entities = []\n        if unit_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.contains_entity(unit_id):\n                    affected_entities.append(line)\n\n                elif attribute_type == 19:\n                    if line.is_projectile_shooter() and line.has_projectile(unit_id):\n                        affected_entities.append(line)\n\n        elif class_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.get_class_id() == class_id:\n                    affected_entities.append(line)\n\n        else:\n            return patches\n\n        upgrade_func = DE2TechSubprocessor.upgrade_attribute_funcs[attribute_type]\n        for affected_entity in affected_entities:\n            patches.extend(upgrade_func(converter_group, affected_entity, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def resource_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying resources.\n        \"\"\"\n        patches = []\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type in (1, 11):\n            mode = effect[\"attr_b\"].value\n\n            if mode == 0:\n                operator = MemberOperator.ASSIGN\n\n            else:\n                operator = MemberOperator.ADD\n\n        elif effect_type in (6, 16):\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid attribute effect\")\n\n        resource_id = effect[\"attr_a\"].value\n        value = effect[\"attr_d\"].value\n\n        if resource_id in (-1, 6, 21):\n            # -1 = invalid ID\n            # 6  = set current age (unused)\n            # 21 = tech count (unused)\n            return patches\n\n        upgrade_func = DE2TechSubprocessor.upgrade_resource_funcs[resource_id]\n        patches.extend(upgrade_func(converter_group, value, operator, team))\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/upgrade_attribute_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument\n\n\"\"\"\nCreates upgrade patches for attribute modification effects in DE2.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.nyan.nyan_structs import MemberOperator\n    from openage.convert.value_object.conversion.forward_ref import ForwardRef\n\n\nclass DE2UpgradeAttributeSubprocessor:\n    \"\"\"\n    Creates raw API objects for attribute upgrade effects in DE2.\n    \"\"\"\n\n    @staticmethod\n    def bfg_unknown_51_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown attribute effect (ID: 51).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_71_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown attribute effect (ID: 71).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_73_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown attribute effect (ID: 73).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def charge_attack_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the charge attack modify effect (ID: 59).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def charge_event_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the charge event modify effect (ID: 61).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def charge_regen_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the charge regen modify effect (ID: 60).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def charge_type_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the charge type modify effect (ID: 62).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def convert_chance_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the convert chance modify effect (ID: 113).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def herdable_capacity_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the herdable garrison capacity modify effect (ID: 30).\n\n        TODO: Move into AK processor.\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def min_convert_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the min convert interval modify effect (ID: 111).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def max_convert_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the max convert interval modify effect (ID: 112).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def regeneration_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the regeneration rate modify effect (ID: 109).\n\n        TODO: Move into AK processor.\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def villager_pop_space_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the villager pop space modify effect (ID: 110).\n\n        TODO: Move into AK processor.\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/de2/upgrade_resource_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods,invalid-name\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument\n\n\"\"\"\nCreates upgrade patches for resource modification effects in DE2.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.nyan.nyan_structs import MemberOperator\n    from openage.convert.value_object.conversion.forward_ref import ForwardRef\n\n\nclass DE2UpgradeResourceSubprocessor:\n    \"\"\"\n    Creates raw API objects for resource upgrade effects in DE2.\n    \"\"\"\n\n    @staticmethod\n    def bengali_conversion_resistance_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the bengali conversion resistance effect (ID: 262).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_69_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 69).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_219_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 219).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_502_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 502).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_507_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 507).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_521_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 521).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def bfg_unknown_551_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for a BfG unknown resource effect (ID: 551).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def burgundian_vineyards_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the burgundian vineyards effect (ID: 236).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def chieftains_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for activating looting (ID: 274).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def cliff_attack_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the cliff attack multiplier effect (ID: 212).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def cliff_defense_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the cliff defense multiplier effect (ID: 272).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_min_adjustment_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion min adjustment modify effect (ID: 176).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_max_adjustment_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion max adjustment modify effect (ID: 177).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_min_building_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion min building modify effect (ID: 180).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_max_building_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion max building modify effect (ID: 181).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_building_chance_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion building chance modify effect (ID: 182).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion range modifer (ID: 280).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def cuman_tc_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the cuman TC modify effect (ID: 218).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def current_food_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the current food amount modify effect (ID: 0).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def current_wood_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the current wood amount modify effect (ID: 1).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def current_stone_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the current stone amount modify effect (ID: 2).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def current_gold_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the current gold amount modify effect (ID: 3).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def doi_paper_money_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Paper Money effect in Dynasties of India (ID: 266).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def elevation_attack_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the elevation attack multiplier effect (ID: 211).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def elevation_defense_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the elevation defense multiplier effect (ID: 273).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def feitoria_gold_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the feitoria gold productivity effect (ID: 208).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement all resources\n\n        return patches\n\n    @staticmethod\n    def first_crusade_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the first crusade effect (ID: 234).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def fish_trap_food_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the fish trap food amount modify effect (ID: 88).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def folwark_collect_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the folwark collect amount modify effect (ID: 237).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def folwark_flag_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the folwark flag effect (ID: 238).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def folwark_mill_id_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the folwark mill ID set effect (ID: 239).\n\n        TODO: Move into AoC processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def forager_wood_gather_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the portugese forage wood gather effect (ID: 267).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def free_kipchaks_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the current gold amount modify effect (ID: 214).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def herdable_garrison_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the herdable garrison effect (ID: 254).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def relic_food_production_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the relic food production effect (ID: 220).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def resource_decay_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the resource decay modifier effect (ID: 268).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def reveal_enemy_tcs_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the reveal enemy TCs effect (ID: 209).\n\n        TODO: Move into Rajas processor\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def sheep_food_amount_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the sheep food amount modify effect (ID: 216).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def stone_gold_gen_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the polish stone gold generation effect (ID: 241).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def tech_reward_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the spanish tech reward effect (ID: 269).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def trade_food_bonus_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the trade food bonus effect (ID: 251).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def unknown_recharge_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the unknown recharge rate bonus effect (ID: 282).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def workshop_food_gen_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the workshop food generation effect (ID: 242).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def workshop_wood_gen_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the workshop wood generation effect (ID: 243).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def workshop_stone_gen_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the workshop stone generation effect (ID: 244).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def workshop_gold_gen_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the workshop gold generation effect (ID: 245).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: MemberOperator\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/hd/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tmedia_subprocessor.py\n\tmodpack_subprocessor.py\n\tprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/hd/__init__.py",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for AoE2: HD Edition.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/hd/media_subprocessor.py",
    "content": "# Copyright 2021-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements\n\n\"\"\"\nConvert media information to metadata definitions and export\nrequests. Subroutine of the main HD processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.export.formats.sprite_metadata import LayerMode as SpriteLayerMode\nfrom ....entity_object.export.formats.terrain_metadata import LayerMode as TerrainLayerMode\nfrom ....entity_object.export.media_export_request import MediaExportRequest\nfrom ....entity_object.export.metadata_export import SpriteMetadataExport\nfrom ....entity_object.export.metadata_export import TextureMetadataExport\nfrom ....entity_object.export.metadata_export import TerrainMetadataExport\nfrom ....value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass HDMediaSubprocessor:\n    \"\"\"\n    Creates the exports requests for media files from HD Edition.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create all export requests for the dataset.\n        \"\"\"\n        cls.create_graphics_requests(full_data_set)\n        cls.create_sound_requests(full_data_set)\n\n    @staticmethod\n    def create_graphics_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for graphics referenced by CombinedSprite objects.\n        \"\"\"\n        combined_sprites = full_data_set.combined_sprites.values()\n        handled_graphic_ids = set()\n\n        for sprite in combined_sprites:\n            ref_graphics = sprite.get_graphics()\n            graphic_targetdirs = sprite.resolve_graphics_location()\n\n            metadata_filename = f\"{sprite.get_filename()}.{'sprite'}\"\n            sprite_meta_export = SpriteMetadataExport(sprite.resolve_sprite_location(),\n                                                      metadata_filename)\n            full_data_set.metadata_exports.append(sprite_meta_export)\n\n            for graphic in ref_graphics:\n                graphic_id = graphic.get_id()\n                if graphic_id in handled_graphic_ids:\n                    continue\n\n                targetdir = graphic_targetdirs[graphic_id]\n                source_filename = f\"{str(graphic['slp_id'].value)}.slp\"\n                target_filename = f\"{sprite.get_filename()}_{str(graphic['slp_id'].value)}.png\"\n\n                export_request = MediaExportRequest(MediaType.GRAPHICS,\n                                                    targetdir,\n                                                    source_filename,\n                                                    target_filename)\n                full_data_set.graphics_exports.update({graphic_id: export_request})\n\n                # Texture metadata file definiton\n                # Same file stem as the image file and same targetdir\n                texture_meta_filename = f\"{target_filename[:-4]}.texture\"\n                texture_meta_export = TextureMetadataExport(targetdir,\n                                                            texture_meta_filename)\n                full_data_set.metadata_exports.append(texture_meta_export)\n\n                # Add texture image filename to texture metadata\n                texture_meta_export.add_imagefile(target_filename)\n\n                # Add metadata from graphics to animation metadata\n                sequence_type = graphic[\"sequence_type\"].value\n                if sequence_type == 0x00:\n                    layer_mode = SpriteLayerMode.OFF\n\n                elif sequence_type & 0x08:\n                    layer_mode = SpriteLayerMode.ONCE\n\n                else:\n                    layer_mode = SpriteLayerMode.LOOP\n\n                layer_pos = graphic[\"layer\"].value\n                frame_rate = round(graphic[\"frame_rate\"].value, ndigits=6)\n                if frame_rate < 0.000001:\n                    frame_rate = None\n\n                replay_delay = round(graphic[\"replay_delay\"].value, ndigits=6)\n                if replay_delay < 0.000001:\n                    replay_delay = None\n\n                frame_count = graphic[\"frame_count\"].value\n                angle_count = graphic[\"angle_count\"].value\n                mirror_mode = graphic[\"mirroring_mode\"].value\n                sprite_meta_export.add_graphics_metadata(target_filename,\n                                                         texture_meta_filename,\n                                                         layer_mode,\n                                                         layer_pos,\n                                                         frame_rate,\n                                                         replay_delay,\n                                                         frame_count,\n                                                         angle_count,\n                                                         mirror_mode)\n\n                # Notify metadata export about SLP metadata when the file is exported\n                export_request.add_observer(texture_meta_export)\n                export_request.add_observer(sprite_meta_export)\n\n                handled_graphic_ids.add(graphic_id)\n\n        combined_terrains = full_data_set.combined_terrains.values()\n        for texture in combined_terrains:\n            slp_id = texture.get_terrain()[\"slp_id\"].value\n            srcfile_prefix = texture.get_terrain()[\"filename\"].value\n\n            targetdir = texture.resolve_graphics_location()\n            source_filename = f\"{str(srcfile_prefix)}_00_color.png\"\n            target_filename = f\"{texture.get_filename()}.png\"\n\n            export_request = MediaExportRequest(MediaType.TERRAIN,\n                                                targetdir,\n                                                source_filename,\n                                                target_filename)\n            full_data_set.graphics_exports.update({slp_id: export_request})\n\n            texture_meta_filename = f\"{texture.get_filename()}.texture\"\n            texture_meta_export = TextureMetadataExport(targetdir,\n                                                        texture_meta_filename)\n            full_data_set.metadata_exports.append(texture_meta_export)\n\n            # Add texture image filename to texture metadata\n            texture_meta_export.add_imagefile(target_filename)\n            texture_meta_export.update(\n                None,\n                {\n                    f\"{target_filename}\": {\n                        \"size\": (512, 512),\n                        \"subtex_metadata\": [\n                            {\n                                \"x\":  0,\n                                \"y\":  0,\n                                \"w\":  512,\n                                \"h\":  512,\n                                \"cx\": 0,\n                                \"cy\": 0,\n                            }\n                        ]\n                    }}\n            )\n\n            terrain_meta_filename = f\"{texture.get_filename()}.terrain\"\n            terrain_meta_export = TerrainMetadataExport(targetdir,\n                                                        terrain_meta_filename)\n            full_data_set.metadata_exports.append(terrain_meta_export)\n\n            terrain_meta_export.add_graphics_metadata(target_filename,\n                                                      texture_meta_filename,\n                                                      TerrainLayerMode.OFF,\n                                                      0,\n                                                      0.0,\n                                                      0.0,\n                                                      1)\n\n    @staticmethod\n    def create_sound_requests(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create export requests for sounds referenced by CombinedSound objects.\n        \"\"\"\n        combined_sounds = full_data_set.combined_sounds.values()\n\n        for sound in combined_sounds:\n            sound_id = sound.get_file_id()\n\n            targetdir = sound.resolve_sound_location()\n            source_filename = f\"{str(sound_id)}.wav\"\n            target_filename = f\"{sound.get_filename()}.opus\"\n\n            export_request = MediaExportRequest(MediaType.SOUNDS,\n                                                targetdir,\n                                                source_filename,\n                                                target_filename)\n\n            full_data_set.sound_exports.update({sound_id: export_request})\n"
  },
  {
    "path": "openage/convert/processor/conversion/hd/modpack_subprocessor.py",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass HDModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the HD conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        hd_base = cls._get_aoe2_base(full_data_set)\n\n        return [hd_base]\n\n    @classmethod\n    def _get_aoe2_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe2_base modpack.\n        \"\"\"\n        modpack = Modpack(\"hd_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"hd_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"hd_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/hd/processor.py",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nConvert data from AoE2:HD to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....log import info\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ..aoc.nyan_subprocessor import AoCNyanSubprocessor\nfrom ..aoc.pregen_processor import AoCPregenSubprocessor\nfrom ..aoc.processor import AoCProcessor\nfrom .media_subprocessor import HDMediaSubprocessor\nfrom .modpack_subprocessor import HDModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass HDProcessor:\n    \"\"\"\n    Main processor for converting data from HD Edition.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game speification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openage formats (nyan, sprite, terrain, etc.)\n        dataset = cls._processor(dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        AoCProcessor.extract_genie_units(gamespec, dataset)\n        AoCProcessor.extract_genie_techs(gamespec, dataset)\n        AoCProcessor.extract_genie_effect_bundles(gamespec, dataset)\n        AoCProcessor.sanitize_effect_bundles(dataset)\n        AoCProcessor.extract_genie_civs(gamespec, dataset)\n        AoCProcessor.extract_age_connections(gamespec, dataset)\n        AoCProcessor.extract_building_connections(gamespec, dataset)\n        AoCProcessor.extract_unit_connections(gamespec, dataset)\n        AoCProcessor.extract_tech_connections(gamespec, dataset)\n        AoCProcessor.extract_genie_graphics(gamespec, dataset)\n        AoCProcessor.extract_genie_sounds(gamespec, dataset)\n        AoCProcessor.extract_genie_terrains(gamespec, dataset)\n        AoCProcessor.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(cls, full_data_set: GenieObjectContainer) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        AoCProcessor.create_unit_lines(full_data_set)\n        AoCProcessor.create_extra_unit_lines(full_data_set)\n        AoCProcessor.create_building_lines(full_data_set)\n        AoCProcessor.create_villager_groups(full_data_set)\n        AoCProcessor.create_ambient_groups(full_data_set)\n        AoCProcessor.create_variant_groups(full_data_set)\n        AoCProcessor.create_terrain_groups(full_data_set)\n        AoCProcessor.create_tech_groups(full_data_set)\n        AoCProcessor.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        AoCProcessor.link_building_upgrades(full_data_set)\n        AoCProcessor.link_creatables(full_data_set)\n        AoCProcessor.link_researchables(full_data_set)\n        AoCProcessor.link_civ_uniques(full_data_set)\n        AoCProcessor.link_gatherers_to_dropsites(full_data_set)\n        AoCProcessor.link_garrison(full_data_set)\n        AoCProcessor.link_trade_posts(full_data_set)\n        AoCProcessor.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        AoCPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating nyan objects...\")\n\n        AoCNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        HDMediaSubprocessor.convert(full_data_set)\n\n        return HDModpackSubprocessor.get_modpacks(full_data_set)\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tability_subprocessor.py\n\tauxiliary_subprocessor.py\n\tciv_subprocessor.py\n\tmedia_subprocessor.py\n\tmodpack_subprocessor.py\n\tnyan_subprocessor.py\n\tpregen_subprocessor.py\n\tprocessor.py\n\ttech_subprocessor.py\n\tupgrade_ability_subprocessor.py\n\tupgrade_attribute_subprocessor.py\n\tupgrade_resource_subprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for AoE1: Rise of Rome.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/ability_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-branches,too-many-statements,too-many-locals\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nDerives and adds abilities to lines. Reimplements only\nabilities that are different from AoC.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom math import degrees\n\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup, \\\n    GenieVillagerGroup, GenieUnitLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.ability_subprocessor import AoCAbilitySubprocessor\nfrom ..aoc.effect_subprocessor import AoCEffectSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass RoRAbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for abilities in RoR.\n    \"\"\"\n\n    @staticmethod\n    def apply_discrete_effect_ability(\n        line: GenieGameEntityGroup,\n        command_id: int,\n        ranged: bool = False,\n        projectile: int = -1\n    ) -> ForwardRef:\n        \"\"\"\n        Adds the ApplyDiscreteEffect ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.get_units_with_command(command_id)[0]\n            current_unit_id = current_unit[\"id0\"].value\n\n        else:\n            current_unit = line.get_head_unit()\n            current_unit_id = line.get_head_unit_id()\n\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        if ranged:\n            ability_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        else:\n            ability_parent = \"engine.ability.type.ApplyDiscreteEffect\"\n\n        if projectile == -1:\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(ability_ref,\n                                                  ability_name,\n                                                  dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            if command_id == 104:\n                # Get animation from commands proceed sprite\n                unit_commands = current_unit[\"unit_commands\"].value\n                for command in unit_commands:\n                    type_id = command[\"type\"].value\n\n                    if type_id != command_id:\n                        continue\n\n                    ability_animation_id = command[\"proceed_sprite_id\"].value\n                    break\n\n                else:\n                    ability_animation_id = -1\n\n            else:\n                ability_animation_id = current_unit[\"attack_sprite_id\"].value\n\n        else:\n            ability_ref = (f\"{game_entity_name}.ShootProjectile.\"\n                           f\"Projectile{projectile}.{ability_name}\")\n            ability_raw_api_object = RawAPIObject(ability_ref,\n                                                  ability_name,\n                                                  dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(line,\n                                          (f\"{game_entity_name}.ShootProjectile.\"\n                                           f\"Projectile{projectile}\"))\n            ability_raw_api_object.set_location(ability_location)\n\n            ability_animation_id = -1\n\n        # Ability properties\n        properties = {}\n\n        # Animated\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"attack_sprite_id\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}{ability_name}\"\n                    filename_prefix = (f\"{command_lookup_dict[command_id][1]}_\"\n                                       f\"{gset_lookup_dict[graphics_set_id][2]}_\")\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                f\"{ability_ref}.Animated\",\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        # Command Sound\n        if projectile == -1:\n            ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n\n        else:\n            ability_comm_sound_id = -1\n\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n\n            if projectile == -1:\n                sound_obj_prefix = ability_name\n\n            else:\n                sound_obj_prefix = \"ProjectileAttack\"\n\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    sound_obj_prefix,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\", sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        if ranged:\n            # Min range\n            min_range = current_unit[\"weapon_range_min\"].value\n            ability_raw_api_object.add_raw_member(\"min_range\",\n                                                  min_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n            # Max range\n            max_range = current_unit[\"weapon_range_max\"].value\n            ability_raw_api_object.add_raw_member(\"max_range\",\n                                                  max_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n        # Effects\n        batch_ref = f\"{ability_ref}.Batch\"\n        batch_raw_api_object = RawAPIObject(batch_ref, \"Batch\", dataset.nyan_api_objects)\n        batch_raw_api_object.add_raw_parent(\"engine.util.effect_batch.type.UnorderedBatch\")\n        batch_location = ForwardRef(line, ability_ref)\n        batch_raw_api_object.set_location(batch_location)\n\n        line.add_raw_api_object(batch_raw_api_object)\n\n        # Effects\n        effects = []\n        if command_id == 7:\n            # Attack\n            if projectile != 1:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref)\n\n            else:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref, projectile=1)\n\n        elif command_id == 104:\n            # TODO: Convert\n            # effects = AoCEffectSubprocessor.get_convert_effects(line, ability_ref)\n            pass\n\n        batch_raw_api_object.add_raw_member(\"effects\",\n                                            effects,\n                                            \"engine.util.effect_batch.EffectBatch\")\n\n        batch_forward_ref = ForwardRef(line, batch_ref)\n        ability_raw_api_object.add_raw_member(\"batches\",\n                                              [batch_forward_ref],\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Reload time\n        if projectile == -1:\n            reload_time = current_unit[\"attack_speed\"].value\n\n        else:\n            reload_time = 0\n\n        ability_raw_api_object.add_raw_member(\"reload_time\",\n                                              reload_time,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Application delay\n        if projectile == -1:\n            apply_graphic = dataset.genie_graphics[ability_animation_id]\n            frame_rate = apply_graphic.get_frame_rate()\n            frame_delay = current_unit[\"frame_delay\"].value\n            application_delay = frame_rate * frame_delay\n\n        else:\n            application_delay = 0\n\n        ability_raw_api_object.add_raw_member(\"application_delay\",\n                                              application_delay,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Allowed types (all buildings/units)\n        if command_id == 104:\n            # Convert\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n            ]\n\n        else:\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n                )\n            ]\n\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        if command_id == 104:\n            # Convert\n            blacklisted_entities = []\n            for unit_line in dataset.unit_lines.values():\n                if unit_line.has_command(104):\n                    # Blacklist other monks\n                    blacklisted_name = name_lookup_dict[unit_line.get_head_unit_id()][0]\n                    blacklisted_entities.append(ForwardRef(unit_line, blacklisted_name))\n                    continue\n\n        else:\n            blacklisted_entities = []\n\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              blacklisted_entities,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def game_entity_stance_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the GameEntityStance ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.GameEntityStance\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"GameEntityStance\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.GameEntityStance\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Stances\n        search_range = current_unit[\"search_radius\"].value\n        stance_names = [\"Aggressive\", \"StandGround\"]\n\n        # Attacking is preferred\n        ability_preferences = []\n        if line.is_projectile_shooter():\n            ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Attack\"))\n\n        elif line.is_melee() or line.is_ranged():\n            if line.has_command(7):\n                ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Attack\"))\n\n            if line.has_command(105):\n                ability_preferences.append(ForwardRef(line, f\"{game_entity_name}.Heal\"))\n\n        # Units are preferred before buildings\n        type_preferences = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n        ]\n\n        stances = []\n        for stance_name in stance_names:\n            stance_api_ref = f\"engine.util.game_entity_stance.type.{stance_name}\"\n\n            stance_ref = f\"{game_entity_name}.GameEntityStance.{stance_name}\"\n            stance_raw_api_object = RawAPIObject(stance_ref, stance_name, dataset.nyan_api_objects)\n            stance_raw_api_object.add_raw_parent(stance_api_ref)\n            stance_location = ForwardRef(line, ability_ref)\n            stance_raw_api_object.set_location(stance_location)\n\n            # Search range\n            stance_raw_api_object.add_raw_member(\"search_range\",\n                                                 search_range,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            # Ability preferences\n            stance_raw_api_object.add_raw_member(\"ability_preference\",\n                                                 ability_preferences,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            # Type preferences\n            stance_raw_api_object.add_raw_member(\"type_preference\",\n                                                 type_preferences,\n                                                 \"engine.util.game_entity_stance.GameEntityStance\")\n\n            line.add_raw_api_object(stance_raw_api_object)\n            stance_forward_ref = ForwardRef(line, stance_ref)\n            stances.append(stance_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"stances\",\n                                              stances,\n                                              \"engine.ability.type.GameEntityStance\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def production_queue_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ProductionQueue ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ProductionQueue\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"ProductionQueue\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ProductionQueue\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Size\n        size = 22\n\n        ability_raw_api_object.add_raw_member(\"size\", size, \"engine.ability.type.ProductionQueue\")\n\n        # Production modes\n        modes = []\n\n        mode_name = f\"{game_entity_name}.ProvideContingent.CreatablesMode\"\n        mode_raw_api_object = RawAPIObject(mode_name, \"CreatablesMode\", dataset.nyan_api_objects)\n        mode_raw_api_object.add_raw_parent(\"engine.util.production_mode.type.Creatables\")\n        mode_location = ForwardRef(line, ability_ref)\n        mode_raw_api_object.set_location(mode_location)\n\n        # RoR allows all creatables in production queue\n        mode_raw_api_object.add_raw_member(\"exclude\",\n                                           [],\n                                           \"engine.util.production_mode.type.Creatables\")\n\n        mode_forward_ref = ForwardRef(line, mode_name)\n        modes.append(mode_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"production_modes\",\n                                              modes,\n                                              \"engine.ability.type.ProductionQueue\")\n\n        line.add_raw_api_object(mode_raw_api_object)\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def projectile_ability(line: GenieGameEntityGroup, position: int = 0) -> ForwardRef:\n        \"\"\"\n        Adds a Projectile ability to projectiles in a line. Which projectile should\n        be added is determined by the 'position' argument.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param position: When 0, gives the first projectile its ability. When 1, the second...\n        :type position: int\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        # First projectile is mandatory\n        obj_ref = f\"{game_entity_name}.ShootProjectile.Projectile{position}\"\n        ability_ref = f\"{game_entity_name}.ShootProjectile.Projectile{position}.Projectile\"\n        ability_raw_api_object = RawAPIObject(ability_ref,\n                                              \"Projectile\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Projectile\")\n        ability_location = ForwardRef(line, obj_ref)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Arc\n        if position == 0:\n            projectile_id = current_unit[\"projectile_id0\"].value\n\n        else:\n            raise ValueError(f\"Invalid projectile position {position}\")\n\n        projectile = dataset.genie_units[projectile_id]\n        arc = degrees(projectile[\"projectile_arc\"].value)\n        ability_raw_api_object.add_raw_member(\"arc\",\n                                              arc,\n                                              \"engine.ability.type.Projectile\")\n\n        # Accuracy\n        accuracy_name = (f\"{game_entity_name}.ShootProjectile.\"\n                         f\"Projectile{position}.Projectile.Accuracy\")\n        accuracy_raw_api_object = RawAPIObject(accuracy_name, \"Accuracy\", dataset.nyan_api_objects)\n        accuracy_raw_api_object.add_raw_parent(\"engine.util.accuracy.Accuracy\")\n        accuracy_location = ForwardRef(line, ability_ref)\n        accuracy_raw_api_object.set_location(accuracy_location)\n\n        accuracy_value = current_unit[\"accuracy\"].value\n        accuracy_raw_api_object.add_raw_member(\"accuracy\",\n                                               accuracy_value,\n                                               \"engine.util.accuracy.Accuracy\")\n\n        accuracy_dispersion = 0\n        accuracy_raw_api_object.add_raw_member(\"accuracy_dispersion\",\n                                               accuracy_dispersion,\n                                               \"engine.util.accuracy.Accuracy\")\n        dropoff_type = dataset.nyan_api_objects[\"engine.util.dropoff_type.type.NoDropoff\"]\n        accuracy_raw_api_object.add_raw_member(\"dispersion_dropoff\",\n                                               dropoff_type,\n                                               \"engine.util.accuracy.Accuracy\")\n\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n        ]\n        accuracy_raw_api_object.add_raw_member(\"target_types\",\n                                               allowed_types,\n                                               \"engine.util.accuracy.Accuracy\")\n        accuracy_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                               [],\n                                               \"engine.util.accuracy.Accuracy\")\n\n        line.add_raw_api_object(accuracy_raw_api_object)\n        accuracy_forward_ref = ForwardRef(line, accuracy_name)\n        ability_raw_api_object.add_raw_member(\"accuracy\",\n                                              [accuracy_forward_ref],\n                                              \"engine.ability.type.Projectile\")\n\n        # Target mode\n        target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.CurrentPosition\"]\n        ability_raw_api_object.add_raw_member(\"target_mode\",\n                                              target_mode,\n                                              \"engine.ability.type.Projectile\")\n\n        # Ingore types; buildings are ignored unless targeted\n        ignore_forward_refs = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"ignored_types\",\n                                              ignore_forward_refs,\n                                              \"engine.ability.type.Projectile\")\n        ability_raw_api_object.add_raw_member(\"unignored_entities\",\n                                              [],\n                                              \"engine.ability.type.Projectile\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def resistance_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Resistance ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.Resistance\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Resistance\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Resistance\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Resistances\n        resistances = []\n        resistances.extend(AoCEffectSubprocessor.get_attack_resistances(line,\n                                                                        ability_ref))\n        if isinstance(line, (GenieUnitLineGroup, GenieBuildingLineGroup)):\n            # TODO: Conversion resistance\n            # resistances.extend(RoREffectSubprocessor.get_convert_resistances(line,\n            #                                                                  ability_ref))\n\n            if isinstance(line, GenieUnitLineGroup) and not line.is_repairable():\n                resistances.extend(AoCEffectSubprocessor.get_heal_resistances(line,\n                                                                              ability_ref))\n\n            if isinstance(line, GenieBuildingLineGroup):\n                resistances.extend(AoCEffectSubprocessor.get_construct_resistances(line,\n                                                                                   ability_ref))\n\n            if line.is_repairable():\n                resistances.extend(AoCEffectSubprocessor.get_repair_resistances(line,\n                                                                                ability_ref))\n\n        ability_raw_api_object.add_raw_member(\"resistances\",\n                                              resistances,\n                                              \"engine.ability.type.Resistance\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> ForwardRef:\n        \"\"\"\n        Adds the ShootProjectile ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ShootProjectile\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Ability properties\n        properties = {}\n\n        # Animation\n        ability_animation_id = current_unit[\"attack_sprite_id\"].value\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\",\n                                                   animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n        # Command Sound\n        ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    ability_name,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\",\n                                                   sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        # Projectile\n        projectiles = []\n        projectile_primary = current_unit[\"projectile_id0\"].value\n        if projectile_primary > -1:\n            projectiles.append(ForwardRef(line,\n                                          f\"{game_entity_name}.ShootProjectile.Projectile0\"))\n\n        ability_raw_api_object.add_raw_member(\"projectiles\",\n                                              projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Projectile count (does not exist in RoR)\n        min_projectiles = 1\n        max_projectiles = 1\n\n        ability_raw_api_object.add_raw_member(\"min_projectiles\",\n                                              min_projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"max_projectiles\",\n                                              max_projectiles,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Range\n        min_range = current_unit[\"weapon_range_min\"].value\n        ability_raw_api_object.add_raw_member(\"min_range\",\n                                              min_range,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        max_range = current_unit[\"weapon_range_max\"].value\n        ability_raw_api_object.add_raw_member(\"max_range\",\n                                              max_range,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Reload time and delay\n        reload_time = current_unit[\"attack_speed\"].value\n        ability_raw_api_object.add_raw_member(\"reload_time\",\n                                              reload_time,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        if ability_animation_id > -1:\n            animation = dataset.genie_graphics[ability_animation_id]\n            frame_rate = animation.get_frame_rate()\n\n        else:\n            frame_rate = 0\n\n        spawn_delay_frames = current_unit[\"frame_delay\"].value\n        spawn_delay = frame_rate * spawn_delay_frames\n        ability_raw_api_object.add_raw_member(\"spawn_delay\",\n                                              spawn_delay,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Projectile delay (unused because RoR has no multiple projectiles)\n        ability_raw_api_object.add_raw_member(\"projectile_delay\",\n                                              0.0,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Turning\n        if isinstance(line, GenieBuildingLineGroup):\n            require_turning = False\n\n        else:\n            require_turning = True\n\n        ability_raw_api_object.add_raw_member(\"require_turning\",\n                                              require_turning,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Manual aiming\n        manual_aiming_allowed = line.get_head_unit_id() in (35, 250)\n        ability_raw_api_object.add_raw_member(\"manual_aiming_allowed\",\n                                              manual_aiming_allowed,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Spawning area\n        spawning_area_offset_x = current_unit[\"weapon_offset\"][0].value\n        spawning_area_offset_y = current_unit[\"weapon_offset\"][1].value\n        spawning_area_offset_z = current_unit[\"weapon_offset\"][2].value\n\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_x\",\n                                              spawning_area_offset_x,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_y\",\n                                              spawning_area_offset_y,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_offset_z\",\n                                              spawning_area_offset_z,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Spawn Area (does not exist in RoR)\n        spawning_area_width = 0\n        spawning_area_height = 0\n        spawning_area_randomness = 0\n\n        ability_raw_api_object.add_raw_member(\"spawning_area_width\",\n                                              spawning_area_width,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_height\",\n                                              spawning_area_height,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"spawning_area_randomness\",\n                                              spawning_area_randomness,\n                                              \"engine.ability.type.ShootProjectile\")\n\n        # Restrictions on targets (only units and buildings allowed)\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(),\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()\n        ]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ShootProjectile\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.ShootProjectile\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/auxiliary_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long,too-many-locals,too-many-branches,too-many-statements\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nDerives complex auxiliary objects from unit lines, techs\nor other objects.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieBuildingLineGroup, GenieUnitLineGroup\nfrom ....entity_object.conversion.combined_sound import CombinedSound\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.auxiliary_subprocessor import AoCAuxiliarySubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass RoRAuxiliarySubprocessor:\n    \"\"\"\n    Creates complexer auxiliary raw API objects for abilities in RoR.\n    \"\"\"\n\n    @staticmethod\n    def get_creatable_game_entity(line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Creates the CreatableGameEntity object for a unit/building line. In comparison\n        to the AoC version, ths replaces some unit class IDs and removes garrison\n        placement modes.\n\n        :param line: Unit/Building line.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.variants[0].line[0]\n\n        else:\n            current_unit = line.line[0]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        obj_ref = f\"{game_entity_name}.CreatableGameEntity\"\n        obj_name = f\"{game_entity_name}Creatable\"\n        creatable_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n        creatable_raw_api_object.add_raw_parent(\"engine.util.create.CreatableGameEntity\")\n\n        # Get train location of line\n        train_location_id = line.get_train_location_id()\n        if isinstance(line, GenieBuildingLineGroup):\n            train_location = dataset.unit_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        else:\n            train_location = dataset.building_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        # Add object to the train location's Create ability\n        creatable_location = ForwardRef(train_location,\n                                        f\"{train_location_name}.Create\")\n\n        creatable_raw_api_object.set_location(creatable_location)\n\n        # Game Entity\n        game_entity_forward_ref = ForwardRef(line, game_entity_name)\n        creatable_raw_api_object.add_raw_member(\"game_entity\",\n                                                game_entity_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # TODO: Variants\n        variants_set = []\n\n        creatable_raw_api_object.add_raw_member(\"variants\", variants_set,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Cost (construction)\n        cost_name = f\"{game_entity_name}.CreatableGameEntity.{game_entity_name}Cost\"\n        cost_raw_api_object = RawAPIObject(cost_name,\n                                           f\"{game_entity_name}Cost\",\n                                           dataset.nyan_api_objects)\n        cost_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n        creatable_forward_ref = ForwardRef(line, obj_ref)\n        cost_raw_api_object.set_location(creatable_forward_ref)\n\n        payment_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Advance\"]\n        cost_raw_api_object.add_raw_member(\"payment_mode\",\n                                           payment_mode,\n                                           \"engine.util.cost.Cost\")\n\n        if isinstance(line, GenieBuildingLineGroup) or line.get_class_id() in (2, 13, 20, 21, 22):\n            # Cost (repair) for buildings\n            cost_repair_name = (f\"{game_entity_name}.CreatableGameEntity.\"\n                                f\"{game_entity_name}RepairCost\")\n            cost_repair_raw_api_object = RawAPIObject(cost_repair_name,\n                                                      f\"{game_entity_name}RepairCost\",\n                                                      dataset.nyan_api_objects)\n            cost_repair_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n            creatable_forward_ref = ForwardRef(line, obj_ref)\n            cost_repair_raw_api_object.set_location(creatable_forward_ref)\n\n            payment_repair_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Adaptive\"]\n            cost_repair_raw_api_object.add_raw_member(\"payment_mode\",\n                                                      payment_repair_mode,\n                                                      \"engine.util.cost.Cost\")\n            line.add_raw_api_object(cost_repair_raw_api_object)\n\n        cost_amounts = []\n        cost_repair_amounts = []\n        for resource_amount in current_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            resource = None\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n                resource_name = \"Wood\"\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object(\n                )\n                resource_name = \"Stone\"\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n                resource_name = \"Gold\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            amount = resource_amount[\"amount\"].value\n\n            cost_amount_name = f\"{cost_name}.{resource_name}Amount\"\n            cost_amount = RawAPIObject(cost_amount_name,\n                                       f\"{resource_name}Amount\",\n                                       dataset.nyan_api_objects)\n            cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            cost_forward_ref = ForwardRef(line, cost_name)\n            cost_amount.set_location(cost_forward_ref)\n\n            cost_amount.add_raw_member(\"type\",\n                                       resource,\n                                       \"engine.util.resource.ResourceAmount\")\n            cost_amount.add_raw_member(\"amount\",\n                                       amount,\n                                       \"engine.util.resource.ResourceAmount\")\n\n            cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n            cost_amounts.append(cost_amount_forward_ref)\n            line.add_raw_api_object(cost_amount)\n\n            if isinstance(line, GenieBuildingLineGroup) or\\\n                    line.get_class_id() in (2, 13, 20, 21, 22):\n                # Cost for repairing = half of the construction cost\n                cost_amount_name = f\"{cost_repair_name}.{resource_name}Amount\"\n                cost_amount = RawAPIObject(cost_amount_name,\n                                           f\"{resource_name}Amount\",\n                                           dataset.nyan_api_objects)\n                cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n                cost_forward_ref = ForwardRef(line, cost_repair_name)\n                cost_amount.set_location(cost_forward_ref)\n\n                cost_amount.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n                cost_amount.add_raw_member(\"amount\",\n                                           amount / 2,\n                                           \"engine.util.resource.ResourceAmount\")\n\n                cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n                cost_repair_amounts.append(cost_amount_forward_ref)\n                line.add_raw_api_object(cost_amount)\n\n        cost_raw_api_object.add_raw_member(\"amount\",\n                                           cost_amounts,\n                                           \"engine.util.cost.type.ResourceCost\")\n\n        if isinstance(line, GenieBuildingLineGroup) or line.get_class_id() in (2, 13, 20, 21, 22):\n            cost_repair_raw_api_object.add_raw_member(\"amount\",\n                                                      cost_repair_amounts,\n                                                      \"engine.util.cost.type.ResourceCost\")\n\n        cost_forward_ref = ForwardRef(line, cost_name)\n        creatable_raw_api_object.add_raw_member(\"cost\",\n                                                cost_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n        # Creation time\n        if isinstance(line, GenieUnitLineGroup):\n            creation_time = current_unit[\"creation_time\"].value\n\n        else:\n            # Buildings are created immediately\n            creation_time = 0\n\n        creatable_raw_api_object.add_raw_member(\"creation_time\",\n                                                creation_time,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Creation sound\n        creation_sound_id = current_unit[\"train_sound_id\"].value\n\n        # Create sound object\n        obj_name = f\"{game_entity_name}.CreatableGameEntity.Sound\"\n        sound_raw_api_object = RawAPIObject(obj_name, \"CreationSound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(line, obj_ref)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Search for the sound if it exists\n        creation_sounds = []\n        if creation_sound_id > -1:\n            # Creation sound should be civ agnostic\n            genie_sound = dataset.genie_sounds[creation_sound_id]\n            file_id = genie_sound.get_sounds(civ_id=-1)[0]\n\n            if file_id in dataset.combined_sounds:\n                creation_sound = dataset.combined_sounds[file_id]\n                creation_sound.add_reference(sound_raw_api_object)\n\n            else:\n                creation_sound = CombinedSound(creation_sound_id,\n                                               file_id,\n                                               f\"creation_sound_{creation_sound_id}\",\n                                               dataset)\n                dataset.combined_sounds.update({file_id: creation_sound})\n                creation_sound.add_reference(sound_raw_api_object)\n\n            creation_sounds.append(creation_sound)\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            creation_sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(line, obj_name)\n        creatable_raw_api_object.add_raw_member(\"creation_sounds\",\n                                                [sound_forward_ref],\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(sound_raw_api_object)\n\n        # Condition\n        unlock_conditions = []\n        enabling_research_id = line.get_enabling_research_id()\n        if enabling_research_id > -1:\n            unlock_conditions.extend(AoCAuxiliarySubprocessor.get_condition(line,\n                                                                            obj_ref,\n                                                                            enabling_research_id))\n\n        creatable_raw_api_object.add_raw_member(\"condition\",\n                                                unlock_conditions,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Placement modes\n        placement_modes = []\n        if isinstance(line, GenieBuildingLineGroup):\n            # Buildings are placed on the map\n            # Place mode\n            obj_name = f\"{game_entity_name}.CreatableGameEntity.Place\"\n            place_raw_api_object = RawAPIObject(obj_name,\n                                                \"Place\",\n                                                dataset.nyan_api_objects)\n            place_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.Place\")\n            place_location = ForwardRef(line,\n                                        f\"{game_entity_name}.CreatableGameEntity\")\n            place_raw_api_object.set_location(place_location)\n\n            # Tile snap distance (uses 1.0 for grid placement)\n            place_raw_api_object.add_raw_member(\"tile_snap_distance\",\n                                                1.0,\n                                                \"engine.util.placement_mode.type.Place\")\n            # Clearance size\n            clearance_size_x = current_unit[\"clearance_size_x\"].value\n            clearance_size_y = current_unit[\"clearance_size_y\"].value\n            place_raw_api_object.add_raw_member(\"clearance_size_x\",\n                                                clearance_size_x,\n                                                \"engine.util.placement_mode.type.Place\")\n            place_raw_api_object.add_raw_member(\"clearance_size_y\",\n                                                clearance_size_y,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Allow rotation\n            place_raw_api_object.add_raw_member(\"allow_rotation\",\n                                                True,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Max elevation difference\n            elevation_mode = current_unit[\"elevation_mode\"].value\n            if elevation_mode == 2:\n                max_elevation_difference = 0\n\n            elif elevation_mode == 3:\n                max_elevation_difference = 1\n\n            else:\n                max_elevation_difference = MemberSpecialValue.NYAN_INF\n\n            place_raw_api_object.add_raw_member(\"max_elevation_difference\",\n                                                max_elevation_difference,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            line.add_raw_api_object(place_raw_api_object)\n\n            place_forward_ref = ForwardRef(line, obj_name)\n            placement_modes.append(place_forward_ref)\n\n        else:\n            placement_modes.append(\n                dataset.nyan_api_objects[\"engine.util.placement_mode.type.Eject\"])\n\n        creatable_raw_api_object.add_raw_member(\"placement_modes\",\n                                                placement_modes,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(creatable_raw_api_object)\n        line.add_raw_api_object(cost_raw_api_object)\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/civ_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods,too-many-statements,too-many-locals\n\n\"\"\"\nCreates patches and modifiers for civs.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n\n\nclass RoRCivSubprocessor:\n    \"\"\"\n    Creates raw API objects for civs in RoR.\n    \"\"\"\n\n    @staticmethod\n    def get_starting_resources(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the starting resources of a civ.\n        \"\"\"\n        resource_amounts = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        # Find starting resource amounts\n        food_amount = civ_group.civ[\"resources\"][0].value\n        wood_amount = civ_group.civ[\"resources\"][1].value\n        gold_amount = civ_group.civ[\"resources\"][2].value\n        stone_amount = civ_group.civ[\"resources\"][3].value\n\n        # Find civ unique starting resources\n        tech_tree = civ_group.get_tech_tree_effects()\n        for effect in tech_tree:\n            type_id = effect.get_type()\n\n            if type_id != 1:\n                continue\n\n            resource_id = effect[\"attr_a\"].value\n            amount = effect[\"attr_d\"].value\n            if resource_id == 0:\n                food_amount += amount\n\n            elif resource_id == 1:\n                wood_amount += amount\n\n            elif resource_id == 2:\n                gold_amount += amount\n\n            elif resource_id == 3:\n                stone_amount += amount\n\n        food_ref = f\"{civ_name}.FoodStartingAmount\"\n        food_raw_api_object = RawAPIObject(food_ref, \"FoodStartingAmount\",\n                                           dataset.nyan_api_objects)\n        food_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        food_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n        food_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_raw_api_object.add_raw_member(\"amount\",\n                                           food_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_forward_ref = ForwardRef(civ_group, food_ref)\n        resource_amounts.append(food_forward_ref)\n\n        wood_ref = f\"{civ_name}.WoodStartingAmount\"\n        wood_raw_api_object = RawAPIObject(wood_ref, \"WoodStartingAmount\",\n                                           dataset.nyan_api_objects)\n        wood_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        wood_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Wood\"].get_nyan_object()\n        wood_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        wood_raw_api_object.add_raw_member(\"amount\",\n                                           wood_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        wood_forward_ref = ForwardRef(civ_group, wood_ref)\n        resource_amounts.append(wood_forward_ref)\n\n        gold_ref = f\"{civ_name}.GoldStartingAmount\"\n        gold_raw_api_object = RawAPIObject(gold_ref, \"GoldStartingAmount\",\n                                           dataset.nyan_api_objects)\n        gold_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        gold_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Gold\"].get_nyan_object()\n        gold_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        gold_raw_api_object.add_raw_member(\"amount\",\n                                           gold_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        gold_forward_ref = ForwardRef(civ_group, gold_ref)\n        resource_amounts.append(gold_forward_ref)\n\n        stone_ref = f\"{civ_name}.StoneStartingAmount\"\n        stone_raw_api_object = RawAPIObject(stone_ref, \"StoneStartingAmount\",\n                                            dataset.nyan_api_objects)\n        stone_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        stone_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Stone\"].get_nyan_object()\n        stone_raw_api_object.add_raw_member(\"type\",\n                                            resource,\n                                            \"engine.util.resource.ResourceAmount\")\n\n        stone_raw_api_object.add_raw_member(\"amount\",\n                                            stone_amount,\n                                            \"engine.util.resource.ResourceAmount\")\n\n        stone_forward_ref = ForwardRef(civ_group, stone_ref)\n        resource_amounts.append(stone_forward_ref)\n\n        civ_group.add_raw_api_object(food_raw_api_object)\n        civ_group.add_raw_api_object(wood_raw_api_object)\n        civ_group.add_raw_api_object(gold_raw_api_object)\n        civ_group.add_raw_api_object(stone_raw_api_object)\n\n        return resource_amounts\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/media_subprocessor.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-few-public-methods\n\"\"\"\nConvert media information to metadata definitions and export\nrequests. Subroutine of the main RoR processor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom openage.convert.processor.conversion.aoc.media_subprocessor import AoCMediaSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass RoRMediaSubprocessor:\n    \"\"\"\n    Creates the exports requests for media files from AoC.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create all export requests for the dataset.\n        \"\"\"\n        AoCMediaSubprocessor.create_graphics_requests(full_data_set)\n        AoCMediaSubprocessor.create_sound_requests(full_data_set)\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/modpack_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass RoRModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the RoR conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        aoe1_base = cls._get_aoe1_base(full_data_set)\n\n        return [aoe1_base]\n\n    @classmethod\n    def _get_aoe1_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the aoe1-base modpack.\n        \"\"\"\n        modpack = Modpack(\"aoe1_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"aoe1_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"aoe1_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/nyan_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nConvert API-like objects to nyan objects. Subroutine of the\nmain RoR processor. Reuses functionality from the AoC subprocessor.\n\"\"\"\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup\nfrom ....entity_object.conversion.combined_terrain import CombinedTerrain\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....entity_object.conversion.ror.genie_tech import RoRUnitLineUpgrade\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.ability_subprocessor import AoCAbilitySubprocessor\nfrom ..aoc.auxiliary_subprocessor import AoCAuxiliarySubprocessor\nfrom ..aoc.civ_subprocessor import AoCCivSubprocessor\nfrom ..aoc.modifier_subprocessor import AoCModifierSubprocessor\nfrom ..aoc.nyan_subprocessor import AoCNyanSubprocessor\nfrom .ability_subprocessor import RoRAbilitySubprocessor\nfrom .auxiliary_subprocessor import RoRAuxiliarySubprocessor\nfrom .civ_subprocessor import RoRCivSubprocessor\nfrom .tech_subprocessor import RoRTechSubprocessor\n\n\nclass RoRNyanSubprocessor:\n    \"\"\"\n    Transform a RoR dataset to nyan objects.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, gamedata):\n        \"\"\"\n        Create nyan objects from the given dataset.\n        \"\"\"\n        cls._process_game_entities(gamedata)\n        cls._create_nyan_objects(gamedata)\n        cls._create_nyan_members(gamedata)\n\n        cls._check_objects(gamedata)\n\n    @classmethod\n    def _check_objects(cls, full_data_set):\n        \"\"\"\n        Check if objects are valid.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.check_readiness()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.check_readiness()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.check_readiness()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.check_readiness()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.check_readiness()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.check_readiness()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.check_readiness()\n\n    @classmethod\n    def _create_nyan_objects(cls, full_data_set):\n        \"\"\"\n        Creates nyan objects from the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_objects()\n            unit_line.execute_raw_member_pushs()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_objects()\n            building_line.execute_raw_member_pushs()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_objects()\n            ambient_group.execute_raw_member_pushs()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_objects()\n            variant_group.execute_raw_member_pushs()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_objects()\n            tech_group.execute_raw_member_pushs()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_objects()\n            terrain_group.execute_raw_member_pushs()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_objects()\n            civ_group.execute_raw_member_pushs()\n\n    @classmethod\n    def _create_nyan_members(cls, full_data_set):\n        \"\"\"\n        Fill nyan member values of the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_members()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_members()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_members()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_members()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_members()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_members()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_members()\n\n    @classmethod\n    def _process_game_entities(cls, full_data_set):\n        \"\"\"\n        Create the RawAPIObject representation of the objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            cls.unit_line_to_game_entity(unit_line)\n\n        for building_line in full_data_set.building_lines.values():\n            cls.building_line_to_game_entity(building_line)\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            cls.ambient_group_to_game_entity(ambient_group)\n\n        for variant_group in full_data_set.variant_groups.values():\n            AoCNyanSubprocessor.variant_group_to_game_entity(variant_group)\n\n        for tech_group in full_data_set.tech_groups.values():\n            if tech_group.is_researchable():\n                cls.tech_group_to_tech(tech_group)\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            cls.terrain_group_to_terrain(terrain_group)\n\n        for civ_group in full_data_set.civ_groups.values():\n            cls.civ_group_to_civ(civ_group)\n\n    @staticmethod\n    def unit_line_to_game_entity(unit_line):\n        \"\"\"\n        Creates raw API objects for a unit line.\n\n        :param unit_line: Unit line that gets converted to a game entity.\n        :type unit_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = unit_line.get_head_unit()\n        current_unit_id = unit_line.get_head_unit_id()\n\n        dataset = unit_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_unit_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_unit_id][1])\n        unit_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a unit two types\n        #    - util.game_entity_type.types.Unit (if unit_type >= 70)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_unit[\"unit_type\"].value\n\n        if unit_type >= 70:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.move_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(unit_line))\n        abilities_set.append(RoRAbilitySubprocessor.resistance_ability(unit_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.turn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(unit_line))\n\n        # Creation\n        if len(unit_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(unit_line))\n\n        # Config\n        ability = AoCAbilitySubprocessor.use_contingent_ability(unit_line)\n        if ability:\n            abilities_set.append(ability)\n\n        if unit_line.has_command(104):\n            # Recharging attribute points (priests)\n            abilities_set.extend(AoCAbilitySubprocessor.regenerate_attribute_ability(unit_line))\n\n        # Applying effects and shooting projectiles\n        if unit_line.is_projectile_shooter():\n            abilities_set.append(RoRAbilitySubprocessor.shoot_projectile_ability(unit_line, 7))\n            RoRNyanSubprocessor.projectiles_from_line(unit_line)\n\n        elif unit_line.is_melee() or unit_line.is_ranged():\n            if unit_line.has_command(7):\n                # Attack\n                abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          7,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(101):\n                # Build\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            101,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(104):\n                # TODO: Success chance is not a resource in RoR\n                # Convert\n                abilities_set.append(RoRAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                          104,\n                                                                                          unit_line.is_ranged()))\n\n            if unit_line.has_command(105):\n                # Heal\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            105,\n                                                                                            unit_line.is_ranged()))\n\n            if unit_line.has_command(106):\n                # Repair\n                abilities_set.append(AoCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                            106,\n                                                                                            unit_line.is_ranged()))\n\n        # Formation/Stance\n        if not isinstance(unit_line, GenieVillagerGroup):\n            abilities_set.append(RoRAbilitySubprocessor.game_entity_stance_ability(unit_line))\n\n        # Storage abilities\n        if unit_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(unit_line))\n\n        if len(unit_line.garrison_locations) > 0:\n            ability = AoCAbilitySubprocessor.enter_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n            ability = AoCAbilitySubprocessor.exit_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n        # Resource abilities\n        if unit_line.is_gatherer():\n            abilities_set.append(AoCAbilitySubprocessor.drop_resources_ability(unit_line))\n            abilities_set.extend(AoCAbilitySubprocessor.gather_ability(unit_line))\n\n        # Resource storage\n        if unit_line.is_gatherer() or unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.resource_storage_ability(unit_line))\n\n        if unit_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(unit_line))\n\n        # Trade abilities\n        if unit_line.has_command(111):\n            abilities_set.append(AoCAbilitySubprocessor.trade_ability(unit_line))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        if unit_line.is_gatherer():\n            modifiers_set.extend(AoCModifierSubprocessor.gather_rate_modifier(unit_line))\n\n        # TODO: Other modifiers?\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        variants_set = []\n\n        raw_api_object.add_raw_member(\"variants\", variants_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if unit_line.is_creatable():\n            RoRAuxiliarySubprocessor.get_creatable_game_entity(unit_line)\n\n    @staticmethod\n    def building_line_to_game_entity(building_line):\n        \"\"\"\n        Creates raw API objects for a building line.\n\n        :param building_line: Building line that gets converted to a game entity.\n        :type building_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_building = building_line.line[0]\n        current_building_id = building_line.get_head_unit_id()\n        dataset = building_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_building_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_building_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_building_id][1])\n        building_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a building two types\n        #    - util.game_entity_type.types.Building (if unit_type >= 80)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # and additionally\n        #    - util.game_entity_type.types.DropSite (only if this is used as a drop site)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_building[\"unit_type\"].value\n\n        if unit_type >= 80:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_building[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        if building_line.is_dropsite():\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.DropSite\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.attribute_change_tracker_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line))\n        abilities_set.append(RoRAbilitySubprocessor.resistance_ability(building_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(building_line))\n\n        # Config abilities\n        if building_line.is_creatable():\n            abilities_set.append(AoCAbilitySubprocessor.constructable_ability(building_line))\n\n        if building_line.has_foundation():\n            abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line))\n\n        # Creation/Research abilities\n        if len(building_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(building_line))\n            abilities_set.append(RoRAbilitySubprocessor.production_queue_ability(building_line))\n\n        if len(building_line.researches) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.research_ability(building_line))\n\n        # Effect abilities\n        if building_line.is_projectile_shooter():\n            abilities_set.append(RoRAbilitySubprocessor.shoot_projectile_ability(building_line, 7))\n            abilities_set.append(RoRAbilitySubprocessor.game_entity_stance_ability(building_line))\n            RoRNyanSubprocessor.projectiles_from_line(building_line)\n\n        # Resource abilities\n        if building_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(building_line))\n\n        if building_line.is_dropsite():\n            abilities_set.append(AoCAbilitySubprocessor.drop_site_ability(building_line))\n\n        ability = AoCAbilitySubprocessor.provide_contingent_ability(building_line)\n        if ability:\n            abilities_set.append(ability)\n\n        # Trade abilities\n        if building_line.is_trade_post():\n            abilities_set.append(AoCAbilitySubprocessor.trade_post_ability(building_line))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        raw_api_object.add_raw_member(\"modifiers\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if building_line.is_creatable():\n            RoRAuxiliarySubprocessor.get_creatable_game_entity(building_line)\n\n    @staticmethod\n    def ambient_group_to_game_entity(ambient_group):\n        \"\"\"\n        Creates raw API objects for an ambient group.\n\n        :param ambient_group: Unit line that gets converted to a game entity.\n        :type ambient_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        ambient_unit = ambient_group.get_head_unit()\n        ambient_id = ambient_group.get_head_unit_id()\n\n        dataset = ambient_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[ambient_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[ambient_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[ambient_id][1])\n        ambient_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give an ambient the types\n        #    - util.game_entity_type.types.Ambient\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n\n        type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Ambient\"].get_nyan_object(\n        )\n        types_set.append(type_obj)\n\n        unit_class = ambient_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        interaction_mode = ambient_unit[\"interaction_mode\"].value\n\n        if interaction_mode >= 0:\n            abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.resistance_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.visibility_ability(ambient_group))\n\n        if interaction_mode >= 2:\n            abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group))\n\n            if not ambient_group.is_passable():\n                abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group))\n\n        if ambient_group.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(ambient_group))\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        variants_set = []\n\n        raw_api_object.add_raw_member(\"variants\", variants_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n    @staticmethod\n    def tech_group_to_tech(tech_group):\n        \"\"\"\n        Creates raw API objects for a tech group.\n\n        :param tech_group: Tech group that gets converted to a tech.\n        :type tech_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        tech_id = tech_group.get_id()\n\n        # Skip Dark Age tech\n        if tech_id == 104:\n            return\n\n        dataset = tech_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = tech_lookup_dict[tech_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.tech.Tech\")\n\n        if isinstance(tech_group, RoRUnitLineUpgrade):\n            unit_line = dataset.unit_lines[tech_group.get_line_id()]\n            head_unit_id = unit_line.get_head_unit_id()\n            obj_location = f\"data/game_entity/generic/{name_lookup_dict[head_unit_id][1]}/\"\n\n        else:\n            obj_location = f\"data/tech/generic/{tech_lookup_dict[tech_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(tech_lookup_dict[tech_id][1])\n        tech_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        raw_api_object.add_raw_member(\"types\", [], \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(tech_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(tech_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(tech_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(tech_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(tech_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(tech_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # Updates\n        # =======================================================================\n        patches = []\n        patches.extend(RoRTechSubprocessor.get_patches(tech_group))\n        raw_api_object.add_raw_member(\"updates\", patches, \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the tech group itself, but use its values)\n        # =======================================================================\n        if tech_group.is_researchable():\n            AoCAuxiliarySubprocessor.get_researchable_tech(tech_group)\n\n    @staticmethod\n    def terrain_group_to_terrain(terrain_group):\n        \"\"\"\n        Creates raw API objects for a terrain group.\n\n        :param terrain_group: Terrain group that gets converted to a tech.\n        :type terrain_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        terrain_index = terrain_group.get_id()\n\n        dataset = terrain_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        terrain_lookup_dict = internal_name_lookups.get_terrain_lookups(dataset.game_version)\n        terrain_type_lookup_dict = internal_name_lookups.get_terrain_type_lookups(\n            dataset.game_version)\n\n        # Start with the Terrain object\n        terrain_name = terrain_lookup_dict[terrain_index][1]\n        raw_api_object = RawAPIObject(terrain_name, terrain_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.terrain.Terrain\")\n        obj_location = f\"data/terrain/{terrain_lookup_dict[terrain_index][2]}/\"\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(terrain_lookup_dict[terrain_index][2])\n        terrain_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        terrain_types = []\n\n        for terrain_type in terrain_type_lookup_dict.values():\n            if terrain_index in terrain_type[0]:\n                type_name = f\"util.terrain_type.types.{terrain_type[2]}\"\n                type_obj = dataset.pregen_nyan_objects[type_name].get_nyan_object()\n                terrain_types.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", terrain_types, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{terrain_name}.{terrain_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{terrain_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(terrain_group, terrain_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(terrain_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.terrain.Terrain\")\n        terrain_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Sound\n        # =======================================================================\n        sound_name = f\"{terrain_name}.Sound\"\n        sound_raw_api_object = RawAPIObject(sound_name, \"Sound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(terrain_group, terrain_name)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Sounds for terrains don't exist in AoC\n        sounds = []\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(terrain_group, sound_name)\n        raw_api_object.add_raw_member(\"sound\",\n                                      sound_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n        terrain_group.add_raw_api_object(sound_raw_api_object)\n\n        # =======================================================================\n        # Ambience\n        # =======================================================================\n        terrain = terrain_group.get_terrain()\n        ambients_count = terrain[\"terrain_units_used_count\"].value\n\n        ambience = []\n        for ambient_index in range(ambients_count):\n            ambient_id = terrain[\"terrain_unit_id\"][ambient_index].value\n\n            if ambient_id not in dataset.unit_ref.keys():\n                # Unit does not exist\n                continue\n\n            ambient_line = dataset.unit_ref[ambient_id]\n            ambient_name = name_lookup_dict[ambient_line.get_head_unit_id()][0]\n\n            ambient_ref = f\"{terrain_name}.Ambient{str(ambient_index)}\"\n            ambient_raw_api_object = RawAPIObject(ambient_ref,\n                                                  f\"Ambient{str(ambient_index)}\",\n                                                  dataset.nyan_api_objects)\n            ambient_raw_api_object.add_raw_parent(\"engine.util.terrain.TerrainAmbient\")\n            ambient_location = ForwardRef(terrain_group, terrain_name)\n            ambient_raw_api_object.set_location(ambient_location)\n\n            # Game entity reference\n            ambient_line_forward_ref = ForwardRef(ambient_line, ambient_name)\n            ambient_raw_api_object.add_raw_member(\"object\",\n                                                  ambient_line_forward_ref,\n                                                  \"engine.util.terrain.TerrainAmbient\")\n\n            # Max density\n            max_density = terrain[\"terrain_unit_density\"][ambient_index].value\n            ambient_raw_api_object.add_raw_member(\"max_density\",\n                                                  max_density,\n                                                  \"engine.util.terrain.TerrainAmbient\")\n\n            terrain_group.add_raw_api_object(ambient_raw_api_object)\n            terrain_ambient_forward_ref = ForwardRef(terrain_group, ambient_ref)\n            ambience.append(terrain_ambient_forward_ref)\n\n        raw_api_object.add_raw_member(\"ambience\", ambience, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Path Costs\n        # =======================================================================\n        path_costs = {}\n        restrictions = dataset.genie_terrain_restrictions\n\n        # Land grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Land\"].get_nyan_object()\n        land_restrictions = restrictions[0x07]\n        if land_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Water grid\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Water\"].get_nyan_object()\n        water_restrictions = restrictions[0x03]\n        if water_restrictions.is_accessible(terrain_index):\n            path_costs[path_type] = 1\n\n        else:\n            path_costs[path_type] = 255\n\n        # Air grid (default accessible)\n        path_type = dataset.pregen_nyan_objects[\"util.path.types.Air\"].get_nyan_object()\n        path_costs[path_type] = 1\n\n        raw_api_object.add_raw_member(\"path_costs\", path_costs, \"engine.util.terrain.Terrain\")\n\n        # =======================================================================\n        # Graphic\n        # =======================================================================\n        if terrain_group.has_subterrain():\n            subterrain = terrain_group.get_subterrain()\n            terrain_id = subterrain.get_id()\n\n        else:\n            terrain_id = terrain_group.get_id()\n\n        # Create animation object\n        graphic_name = f\"{terrain_name}.TerrainTexture\"\n        graphic_raw_api_object = RawAPIObject(graphic_name, \"TerrainTexture\",\n                                              dataset.nyan_api_objects)\n        graphic_raw_api_object.add_raw_parent(\"engine.util.graphics.Terrain\")\n        graphic_location = ForwardRef(terrain_group, terrain_name)\n        graphic_raw_api_object.set_location(graphic_location)\n\n        if terrain_id in dataset.combined_terrains.keys():\n            terrain_graphic = dataset.combined_terrains[terrain_id]\n\n        else:\n            terrain_graphic = CombinedTerrain(terrain_id,\n                                              f\"texture_{terrain_lookup_dict[terrain_index][2]}\",\n                                              dataset)\n            dataset.combined_terrains.update({terrain_graphic.get_id(): terrain_graphic})\n\n        terrain_graphic.add_reference(graphic_raw_api_object)\n\n        graphic_raw_api_object.add_raw_member(\"sprite\", terrain_graphic,\n                                              \"engine.util.graphics.Terrain\")\n\n        terrain_group.add_raw_api_object(graphic_raw_api_object)\n        graphic_forward_ref = ForwardRef(terrain_group, graphic_name)\n        raw_api_object.add_raw_member(\"terrain_graphic\", graphic_forward_ref,\n                                      \"engine.util.terrain.Terrain\")\n\n    @staticmethod\n    def civ_group_to_civ(civ_group):\n        \"\"\"\n        Creates raw API objects for a civ group.\n\n        :param civ_group: Terrain group that gets converted to a tech.\n        :type civ_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        civ_id = civ_group.get_id()\n\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = civ_lookup_dict[civ_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.setup.PlayerSetup\")\n\n        obj_location = f\"data/civ/{civ_lookup_dict[civ_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(civ_lookup_dict[civ_id][1])\n        civ_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(civ_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(civ_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(civ_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(civ_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(civ_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(civ_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # TODO: Leader names\n        # =======================================================================\n        raw_api_object.add_raw_member(\"leader_names\",\n                                      [],\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers = []\n        # modifiers = AoCCivSubprocessor.get_civ_setup(civ_group)\n        raw_api_object.add_raw_member(\"modifiers\",\n                                      modifiers,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Starting resources\n        # =======================================================================\n        resource_amounts = RoRCivSubprocessor.get_starting_resources(civ_group)\n        raw_api_object.add_raw_member(\"starting_resources\",\n                                      resource_amounts,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Game setup\n        # =======================================================================\n        game_setup = AoCCivSubprocessor.get_civ_setup(civ_group)\n        raw_api_object.add_raw_member(\"game_setup\",\n                                      game_setup,\n                                      \"engine.util.setup.PlayerSetup\")\n\n    @staticmethod\n    def projectiles_from_line(line):\n        \"\"\"\n        Creates Projectile(GameEntity) raw API objects for a unit/building line.\n\n        :param line: Line for which the projectiles are extracted.\n        :type line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        game_entity_filename = name_lookup_dict[current_unit_id][1]\n\n        projectiles_location = f\"data/game_entity/generic/{game_entity_filename}/projectiles/\"\n\n        projectile_indices = []\n        projectile_primary = current_unit[\"projectile_id0\"].value\n        if projectile_primary > -1:\n            projectile_indices.append(0)\n\n        for projectile_num in projectile_indices:\n            obj_ref = f\"{game_entity_name}.ShootProjectile.Projectile{projectile_num}\"\n            obj_name = f\"Projectile{projectile_num}\"\n            proj_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n            proj_raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n            proj_raw_api_object.set_location(projectiles_location)\n            proj_raw_api_object.set_filename(f\"{game_entity_filename}_projectiles\")\n\n            # =======================================================================\n            # Types\n            # =======================================================================\n            types_set = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Projectile\"].get_nyan_object()]\n            proj_raw_api_object.add_raw_member(\n                \"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Abilities\n            # =======================================================================\n            abilities_set = []\n            abilities_set.append(RoRAbilitySubprocessor.projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.move_projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(\n                line, 7, False, projectile_num))\n            # TODO: Death, Despawn\n            proj_raw_api_object.add_raw_member(\n                \"abilities\", abilities_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Modifiers\n            # =======================================================================\n            modifiers_set = []\n\n            proj_raw_api_object.add_raw_member(\n                \"modifiers\", modifiers_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Variants\n            # =======================================================================\n            variants_set = []\n            proj_raw_api_object.add_raw_member(\n                \"variants\", variants_set, \"engine.util.game_entity.GameEntity\")\n\n            line.add_raw_api_object(proj_raw_api_object)\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/pregen_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals\n\n\"\"\"\nCreates nyan objects for things that are hardcoded into the Genie Engine,\nbut configurable in openage. E.g. HP.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.converter_object import ConverterObjectGroup, \\\n    RawAPIObject\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.pregen_processor import AoCPregenSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass RoRPregenSubprocessor:\n    \"\"\"\n    Creates raw API objects for hardcoded settings in RoR.\n    \"\"\"\n\n    @classmethod\n    def generate(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects for hardcoded properties.\n        \"\"\"\n        # Stores pregenerated raw API objects as a container\n        pregen_converter_group = ConverterObjectGroup(\"pregen\")\n\n        AoCPregenSubprocessor.generate_activities(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_attributes(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_diplomatic_stances(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_entity_types(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_effect_types(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_language_objects(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_misc_effect_objects(full_data_set, pregen_converter_group)\n        # TODO:\n        # cls._generate_modifiers(gamedata, pregen_converter_group)\n        AoCPregenSubprocessor.generate_terrain_types(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_path_types(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_resources(full_data_set, pregen_converter_group)\n        cls.generate_death_condition(full_data_set, pregen_converter_group)\n\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        # Create nyan objects from the raw API objects\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_object()\n\n        # This has to be a separate for-loop because of possible object interdependencies\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_members()\n\n            if not pregen_object.is_ready():\n                raise RuntimeError(f\"{repr(pregen_object)}: Pregenerated object is not ready \"\n                                   \"for export. Member or object not initialized.\")\n\n    @staticmethod\n    def generate_death_condition(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate DeathCondition objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Death condition\n        # =======================================================================\n        logic_parent = \"engine.util.logic.LogicElement\"\n        literal_parent = \"engine.util.logic.literal.Literal\"\n        interval_parent = \"engine.util.logic.literal.type.AttributeBelowValue\"\n        literal_location = \"data/util/logic/death/\"\n\n        death_ref_in_modpack = \"util.logic.literal.death.StandardHealthDeathLiteral\"\n        literal_raw_api_object = RawAPIObject(death_ref_in_modpack,\n                                              \"StandardHealthDeathLiteral\",\n                                              api_objects,\n                                              literal_location)\n        literal_raw_api_object.set_filename(\"death\")\n        literal_raw_api_object.add_raw_parent(interval_parent)\n\n        # Literal will not default to 'True' when it was fulfilled once\n        literal_raw_api_object.add_raw_member(\"only_once\", False, logic_parent)\n\n        # Scope\n        scope_forward_ref = ForwardRef(pregen_converter_group,\n                                       \"util.logic.literal_scope.death.StandardHealthDeathScope\")\n        literal_raw_api_object.add_raw_member(\"scope\",\n                                              scope_forward_ref,\n                                              literal_parent)\n\n        # Attribute\n        health_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.attribute.types.Health\")\n        literal_raw_api_object.add_raw_member(\"attribute\",\n                                              health_forward_ref,\n                                              interval_parent)\n\n        # sidenote: Apparently this is actually HP<1 in Genie\n        # (https://youtu.be/FdBk8zGbE7U?t=7m16s)\n        literal_raw_api_object.add_raw_member(\"threshold\",\n                                              1,\n                                              interval_parent)\n\n        pregen_converter_group.add_raw_api_object(literal_raw_api_object)\n        pregen_nyan_objects.update({death_ref_in_modpack: literal_raw_api_object})\n\n        # LiteralScope\n        scope_parent = \"engine.util.logic.literal_scope.LiteralScope\"\n        self_scope_parent = \"engine.util.logic.literal_scope.type.Self\"\n\n        death_scope_ref_in_modpack = \"util.logic.literal_scope.death.StandardHealthDeathScope\"\n        scope_raw_api_object = RawAPIObject(death_scope_ref_in_modpack,\n                                            \"StandardHealthDeathScope\",\n                                            api_objects)\n        scope_location = ForwardRef(pregen_converter_group, death_ref_in_modpack)\n        scope_raw_api_object.set_location(scope_location)\n        scope_raw_api_object.add_raw_parent(self_scope_parent)\n\n        scope_diplomatic_stances = [api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        scope_raw_api_object.add_raw_member(\"stances\",\n                                            scope_diplomatic_stances,\n                                            scope_parent)\n\n        pregen_converter_group.add_raw_api_object(scope_raw_api_object)\n        pregen_nyan_objects.update({death_scope_ref_in_modpack: scope_raw_api_object})\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/processor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long,too-many-lines,too-many-branches,too-many-statements,too-many-locals\n\"\"\"\nConvert data from RoR to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....log import info\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....entity_object.conversion.aoc.genie_tech import InitiatedTech\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitObject\nfrom ....entity_object.conversion.ror.genie_sound import RoRSound\nfrom ....entity_object.conversion.ror.genie_tech import RoRStatUpgrade, \\\n    RoRBuildingLineUpgrade, RoRUnitLineUpgrade, RoRBuildingUnlock, RoRUnitUnlock, \\\n    RoRAgeUpgrade\nfrom ....entity_object.conversion.ror.genie_unit import RoRUnitTaskGroup, \\\n    RoRUnitLineGroup, RoRBuildingLineGroup, RoRVillagerGroup, RoRAmbientGroup, \\\n    RoRVariantGroup\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ....value_object.conversion.ror.internal_nyan_names import AMBIENT_GROUP_LOOKUPS, \\\n    VARIANT_GROUP_LOOKUPS\nfrom ..aoc.processor import AoCProcessor\nfrom .media_subprocessor import RoRMediaSubprocessor\nfrom .modpack_subprocessor import RoRModpackSubprocessor\nfrom .nyan_subprocessor import RoRNyanSubprocessor\nfrom .pregen_subprocessor import RoRPregenSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass RoRProcessor:\n    \"\"\"\n    Main processor for converting data from RoR.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game specification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openage formats (nyan, sprite, terrain)\n        dataset = cls._processor(gamespec, dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        cls.extract_genie_units(gamespec, dataset)\n        AoCProcessor.extract_genie_techs(gamespec, dataset)\n        AoCProcessor.extract_genie_effect_bundles(gamespec, dataset)\n        AoCProcessor.sanitize_effect_bundles(dataset)\n        AoCProcessor.extract_genie_civs(gamespec, dataset)\n        AoCProcessor.extract_genie_graphics(gamespec, dataset)\n        cls.extract_genie_sounds(gamespec, dataset)\n        AoCProcessor.extract_genie_terrains(gamespec, dataset)\n        AoCProcessor.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(cls, gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        cls.create_tech_groups(full_data_set)\n        cls.create_entity_lines(gamespec, full_data_set)\n        cls.create_ambient_groups(full_data_set)\n        cls.create_variant_groups(full_data_set)\n        AoCProcessor.create_terrain_groups(full_data_set)\n        AoCProcessor.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        AoCProcessor.link_creatables(full_data_set)\n        AoCProcessor.link_researchables(full_data_set)\n        AoCProcessor.link_gatherers_to_dropsites(full_data_set)\n        cls.link_garrison(full_data_set)\n        AoCProcessor.link_trade_posts(full_data_set)\n        cls.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        RoRPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        info(\"Creating nyan objects...\")\n\n        RoRNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        RoRMediaSubprocessor.convert(full_data_set)\n\n        return RoRModpackSubprocessor.get_modpacks(full_data_set)\n\n    @staticmethod\n    def extract_genie_units(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract units from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # Units are stored in the civ container.\n        # In RoR the normal civs are not subsets of the Gaia civ, so we search units from\n        # Gaia and one player civ (egyptiians).\n        raw_units = []\n\n        # Gaia units\n        # call hierarchy: wrapper[0]->civs[0]->units\n        raw_units.extend(gamespec[0][\"civs\"][0][\"units\"].value)\n\n        # Egyptians\n        # call hierarchy: wrapper[0]->civs[1]->units\n        raw_units.extend(gamespec[0][\"civs\"][1][\"units\"].value)\n\n        for raw_unit in raw_units:\n            unit_id = raw_unit[\"id0\"].value\n\n            if unit_id in full_data_set.genie_units.keys():\n                continue\n\n            unit_members = raw_unit.value\n            # Turn attack and armor into containers to make diffing work\n            if \"attacks\" in unit_members.keys():\n                attacks_member = unit_members.pop(\"attacks\")\n                attacks_member = attacks_member.get_container(\"type_id\")\n                armors_member = unit_members.pop(\"armors\")\n                armors_member = armors_member.get_container(\"type_id\")\n\n                unit_members.update({\"attacks\": attacks_member})\n                unit_members.update({\"armors\": armors_member})\n\n            unit = GenieUnitObject(unit_id, full_data_set, members=unit_members)\n            full_data_set.genie_units.update({unit.get_id(): unit})\n\n        # Sort the dict to make debugging easier :)\n        full_data_set.genie_units = dict(sorted(full_data_set.genie_units.items()))\n\n    @staticmethod\n    def extract_genie_sounds(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Extract sound definitions from the game data.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        \"\"\"\n        # call hierarchy: wrapper[0]->sounds\n        raw_sounds = gamespec[0][\"sounds\"].value\n        for raw_sound in raw_sounds:\n            sound_id = raw_sound[\"sound_id\"].value\n            sound_members = raw_sound.value\n\n            sound = RoRSound(sound_id, full_data_set, members=sound_members)\n            full_data_set.genie_sounds.update({sound.get_id(): sound})\n\n    @staticmethod\n    def create_entity_lines(gamespec: ArrayMember, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Sort units/buildings into lines, based on information from techs and civs.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        # Search a player civ (egyptians) for the starting units\n        player_civ_units = gamespec[0][\"civs\"][1][\"units\"].value\n        task_group_ids = set()\n        villager_unit_ids = set()\n\n        for raw_unit in player_civ_units.values():\n            unit_id = raw_unit[\"id0\"].value\n            enabled = raw_unit[\"enabled\"].value\n            entity = full_data_set.genie_units[unit_id]\n\n            if not enabled:\n                # Unlocked by tech\n                continue\n\n            unit_type = entity[\"unit_type\"].value\n\n            if unit_type == 70:\n                if entity.has_member(\"task_group\") and\\\n                        entity[\"task_group\"].value > 0:\n                    task_group_id = entity[\"task_group\"].value\n                    villager_unit_ids.add(unit_id)\n\n                    if task_group_id in task_group_ids:\n                        task_group = full_data_set.task_groups[task_group_id]\n                        task_group.add_unit(entity)\n\n                    else:\n                        if task_group_id == 1:\n                            line_id = RoRUnitTaskGroup.male_line_id\n\n                        task_group = RoRUnitTaskGroup(line_id, task_group_id, -1, full_data_set)\n                        task_group.add_unit(entity)\n                        task_group_ids.add(task_group_id)\n                        full_data_set.task_groups.update({task_group_id: task_group})\n\n                else:\n                    unit_line = RoRUnitLineGroup(unit_id, -1, full_data_set)\n                    unit_line.add_unit(entity)\n                    full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n                    full_data_set.unit_ref.update({unit_id: unit_line})\n\n            elif unit_type == 80:\n                building_line = RoRBuildingLineGroup(unit_id, -1, full_data_set)\n                building_line.add_unit(entity)\n                full_data_set.building_lines.update({building_line.get_id(): building_line})\n                full_data_set.unit_ref.update({unit_id: building_line})\n\n        # Create the villager task group\n        villager = RoRVillagerGroup(118, task_group_ids, full_data_set)\n        full_data_set.unit_lines.update({villager.get_id(): villager})\n        full_data_set.villager_groups.update({villager.get_id(): villager})\n        for unit_id in villager_unit_ids:\n            full_data_set.unit_ref.update({unit_id: villager})\n\n        # Other units unlocks through techs\n        unit_unlocks = full_data_set.unit_unlocks\n        for unit_unlock in unit_unlocks.values():\n            line_id = unit_unlock.get_line_id()\n            unit = full_data_set.genie_units[line_id]\n\n            unit_line = RoRUnitLineGroup(line_id, unit_unlock.get_id(), full_data_set)\n            unit_line.add_unit(unit)\n            full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n            full_data_set.unit_ref.update({line_id: unit_line})\n\n            # Check if the tech unlocks other lines\n            # TODO: Make this cleaner\n            unlock_effects = unit_unlock.get_effects(effect_type=2)\n            for unlock_effect in unlock_effects:\n                line_id = unlock_effect[\"attr_a\"].value\n                unit = full_data_set.genie_units[line_id]\n\n                if line_id not in full_data_set.unit_lines.keys():\n                    unit_line = RoRUnitLineGroup(line_id, unit_unlock.get_id(), full_data_set)\n                    unit_line.add_unit(unit)\n                    full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n                    full_data_set.unit_ref.update({line_id: unit_line})\n\n        # Upgraded units in a line\n        unit_upgrades = full_data_set.unit_upgrades\n        for unit_upgrade in unit_upgrades.values():\n            line_id = unit_upgrade.get_line_id()\n            target_id = unit_upgrade.get_upgrade_target_id()\n            unit = full_data_set.genie_units[target_id]\n\n            # Find the previous unit in the line\n            required_techs = unit_upgrade.tech[\"required_techs\"].value\n            for required_tech in required_techs:\n                required_tech_id = required_tech.value\n                if required_tech_id in full_data_set.unit_unlocks.keys():\n                    source_id = full_data_set.unit_unlocks[required_tech_id].get_line_id()\n                    break\n\n                if required_tech_id in full_data_set.unit_upgrades.keys():\n                    source_id = full_data_set.unit_upgrades[required_tech_id].get_upgrade_target_id(\n                    )\n                    break\n\n            unit_line = full_data_set.unit_lines[line_id]\n            unit_line.add_unit(unit, after=source_id)\n            full_data_set.unit_ref.update({target_id: unit_line})\n\n        # Other buildings unlocks through techs\n        building_unlocks = full_data_set.building_unlocks\n        for building_unlock in building_unlocks.values():\n            line_id = building_unlock.get_line_id()\n            building = full_data_set.genie_units[line_id]\n\n            building_line = RoRBuildingLineGroup(line_id, building_unlock.get_id(), full_data_set)\n            building_line.add_unit(building)\n            full_data_set.building_lines.update({building_line.get_id(): building_line})\n            full_data_set.unit_ref.update({line_id: building_line})\n\n        # Upgraded buildings through techs\n        building_upgrades = full_data_set.building_upgrades\n        for building_upgrade in building_upgrades.values():\n            line_id = building_upgrade.get_line_id()\n            target_id = building_upgrade.get_upgrade_target_id()\n            unit = full_data_set.genie_units[target_id]\n\n            # Find the previous unit in the line\n            required_techs = building_upgrade.tech[\"required_techs\"].value\n            for required_tech in required_techs:\n                required_tech_id = required_tech.value\n                if required_tech_id in full_data_set.building_unlocks.keys():\n                    source_id = full_data_set.building_unlocks[required_tech_id].get_line_id()\n                    break\n\n                if required_tech_id in full_data_set.building_upgrades.keys():\n                    source_id = full_data_set.building_upgrades[required_tech_id].get_upgrade_target_id(\n                    )\n                    break\n\n            building_line = full_data_set.building_lines[line_id]\n            building_line.add_unit(unit, after=source_id)\n            full_data_set.unit_ref.update({target_id: building_line})\n\n        # Upgraded units/buildings through age ups\n        age_ups = full_data_set.age_upgrades\n        for age_up in age_ups.values():\n            effects = age_up.get_effects(effect_type=3)\n            for effect in effects:\n                source_id = effect[\"attr_a\"].value\n                target_id = effect[\"attr_b\"].value\n                unit = full_data_set.genie_units[target_id]\n\n                if source_id in full_data_set.building_lines.keys():\n                    building_line = full_data_set.building_lines[source_id]\n                    building_line.add_unit(unit, after=source_id)\n                    full_data_set.unit_ref.update({target_id: building_line})\n\n                elif source_id in full_data_set.unit_lines.keys():\n                    unit_line = full_data_set.unit_lines[source_id]\n                    unit_line.add_unit(unit, after=source_id)\n                    full_data_set.unit_ref.update({target_id: unit_line})\n\n    @staticmethod\n    def create_ambient_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create ambient groups, mostly for resources and scenery.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        ambient_ids = AMBIENT_GROUP_LOOKUPS.keys()\n        genie_units = full_data_set.genie_units\n\n        for ambient_id in ambient_ids:\n            ambient_group = RoRAmbientGroup(ambient_id, full_data_set)\n            ambient_group.add_unit(genie_units[ambient_id])\n            full_data_set.ambient_groups.update({ambient_group.get_id(): ambient_group})\n            full_data_set.unit_ref.update({ambient_id: ambient_group})\n\n    @staticmethod\n    def create_variant_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create variant groups.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        variants = VARIANT_GROUP_LOOKUPS\n\n        for group_id, variant in variants.items():\n            variant_group = RoRVariantGroup(group_id, full_data_set)\n            full_data_set.variant_groups.update({variant_group.get_id(): variant_group})\n\n            for variant_id in variant[2]:\n                variant_group.add_unit(full_data_set.genie_units[variant_id])\n                full_data_set.unit_ref.update({variant_id: variant_group})\n\n    @staticmethod\n    def create_tech_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create techs from tech connections and unit upgrades/unlocks\n        from unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        genie_techs = full_data_set.genie_techs\n\n        for tech_id, tech in genie_techs.items():\n            tech_type = tech[\"tech_type\"].value\n\n            # Test if a tech exist and skip it if it doesn't\n            required_techs = tech[\"required_techs\"].value\n            if all(required_tech.value == 0 for required_tech in required_techs):\n                # If all required techs are tech ID 0, the tech doesnt exist\n                continue\n\n            effect_bundle_id = tech[\"tech_effect_id\"].value\n\n            if effect_bundle_id == -1:\n                continue\n\n            effect_bundle = full_data_set.genie_effect_bundles[effect_bundle_id]\n\n            # Ignore techs without effects\n            if len(effect_bundle.get_effects()) == 0:\n                continue\n\n            # Town Center techs (only age ups)\n            if tech_type == 12:\n                # Age ID is set as resource value\n                setr_effects = effect_bundle.get_effects(effect_type=1)\n                for effect in setr_effects:\n                    resource_id = effect[\"attr_a\"].value\n\n                    if resource_id == 6:\n                        age_id = int(effect[\"attr_d\"].value)\n                        break\n\n                age_up = RoRAgeUpgrade(tech_id, age_id, full_data_set)\n                full_data_set.tech_groups.update({age_up.get_id(): age_up})\n                full_data_set.age_upgrades.update({age_up.get_id(): age_up})\n\n            else:\n                effects = effect_bundle.get_effects()\n                for effect in effects:\n                    # Enabling techs\n                    if effect.get_type() == 2:\n                        unit_id = effect[\"attr_a\"].value\n                        unit = full_data_set.genie_units[unit_id]\n                        unit_type = unit[\"unit_type\"].value\n\n                        if unit_type == 70:\n                            unit_unlock = RoRUnitUnlock(tech_id, unit_id, full_data_set)\n                            full_data_set.tech_groups.update(\n                                {unit_unlock.get_id(): unit_unlock}\n                            )\n                            full_data_set.unit_unlocks.update(\n                                {unit_unlock.get_id(): unit_unlock}\n                            )\n                            break\n\n                        if unit_type == 80:\n                            building_unlock = RoRBuildingUnlock(tech_id, unit_id, full_data_set)\n                            full_data_set.tech_groups.update(\n                                {building_unlock.get_id(): building_unlock}\n                            )\n                            full_data_set.building_unlocks.update(\n                                {building_unlock.get_id(): building_unlock}\n                            )\n                            break\n\n                    # Upgrades\n                    elif effect.get_type() == 3:\n                        source_unit_id = effect[\"attr_a\"].value\n                        target_unit_id = effect[\"attr_b\"].value\n                        unit = full_data_set.genie_units[source_unit_id]\n                        unit_type = unit[\"unit_type\"].value\n\n                        if unit_type == 70:\n                            unit_upgrade = RoRUnitLineUpgrade(tech_id,\n                                                              source_unit_id,\n                                                              target_unit_id,\n                                                              full_data_set)\n                            full_data_set.tech_groups.update(\n                                {unit_upgrade.get_id(): unit_upgrade}\n                            )\n                            full_data_set.unit_upgrades.update(\n                                {unit_upgrade.get_id(): unit_upgrade}\n                            )\n                            break\n\n                        if unit_type == 80:\n                            building_upgrade = RoRBuildingLineUpgrade(tech_id,\n                                                                      source_unit_id,\n                                                                      target_unit_id,\n                                                                      full_data_set)\n                            full_data_set.tech_groups.update(\n                                {building_upgrade.get_id(): building_upgrade}\n                            )\n                            full_data_set.building_upgrades.update(\n                                {building_upgrade.get_id(): building_upgrade}\n                            )\n                            break\n\n                else:\n                    # Anything else must be a stat upgrade\n                    stat_up = RoRStatUpgrade(tech_id, full_data_set)\n                    full_data_set.tech_groups.update({stat_up.get_id(): stat_up})\n                    full_data_set.stat_upgrades.update({stat_up.get_id(): stat_up})\n\n        # Initiated techs are stored with buildings\n        genie_units = full_data_set.genie_units\n\n        for genie_unit in genie_units.values():\n            if not genie_unit.has_member(\"research_id\"):\n                continue\n\n            building_id = genie_unit[\"id0\"].value\n            initiated_tech_id = genie_unit[\"research_id\"].value\n\n            if initiated_tech_id == -1:\n                continue\n\n            if building_id not in full_data_set.building_lines.keys():\n                # Skips upgraded buildings (which initiate the same techs)\n                continue\n\n            initiated_tech = InitiatedTech(initiated_tech_id, building_id, full_data_set)\n            full_data_set.tech_groups.update({initiated_tech.get_id(): initiated_tech})\n            full_data_set.initiated_techs.update({initiated_tech.get_id(): initiated_tech})\n\n    @staticmethod\n    def link_garrison(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link a garrison unit to the lines that are stored and vice versa. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        unit_lines = full_data_set.unit_lines\n\n        garrison_class_assignments = {}\n\n        for unit_line in unit_lines.values():\n            head_unit = unit_line.get_head_unit()\n\n            unit_commands = head_unit[\"unit_commands\"].value\n            for command in unit_commands:\n                command_type = command[\"type\"].value\n\n                if not command_type == 3:\n                    continue\n\n                class_id = command[\"class_id\"].value\n\n                if class_id in garrison_class_assignments:\n                    garrison_class_assignments[class_id].append(unit_line)\n\n                else:\n                    garrison_class_assignments[class_id] = [unit_line]\n\n                break\n\n        for garrison in unit_lines.values():\n            class_id = garrison.get_class_id()\n\n            if class_id in garrison_class_assignments:\n                for line in garrison_class_assignments[class_id]:\n                    garrison.garrison_entities.append(line)\n                    line.garrison_locations.append(garrison)\n\n    @staticmethod\n    def link_repairables(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Set units/buildings as repairable\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        villager_groups = full_data_set.villager_groups\n\n        repair_lines = {}\n        repair_lines.update(full_data_set.unit_lines)\n        repair_lines.update(full_data_set.building_lines)\n\n        repair_classes = []\n        for villager in villager_groups.values():\n            repair_unit = villager.get_units_with_command(106)[0]\n            unit_commands = repair_unit[\"unit_commands\"].value\n            for command in unit_commands:\n                type_id = command[\"type\"].value\n\n                if type_id != 106:\n                    continue\n\n                class_id = command[\"class_id\"].value\n                if class_id == -1:\n                    # Buildings/Siege\n                    repair_classes.append(3)\n                    repair_classes.append(13)\n\n                else:\n                    repair_classes.append(class_id)\n\n        for repair_line in repair_lines.values():\n            if repair_line.get_class_id() in repair_classes:\n                repair_line.repairable = True\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/tech_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates patches for technologies.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup, \\\n    GenieUnitLineGroup\nfrom ....service.conversion import internal_name_lookups\nfrom ..aoc.upgrade_ability_subprocessor import AoCUpgradeAbilitySubprocessor\nfrom ..aoc.upgrade_attribute_subprocessor import AoCUpgradeAttributeSubprocessor\nfrom ..aoc.upgrade_resource_subprocessor import AoCUpgradeResourceSubprocessor\nfrom .upgrade_ability_subprocessor import RoRUpgradeAbilitySubprocessor\nfrom .upgrade_attribute_subprocessor import RoRUpgradeAttributeSubprocessor\nfrom .upgrade_resource_subprocessor import RoRUpgradeResourceSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject\n    from openage.convert.value_object.conversion.forward_ref import ForwardRef\n\n\nclass RoRTechSubprocessor:\n    \"\"\"\n    Creates raw API objects and patches for techs and civ setups in RoR.\n    \"\"\"\n\n    upgrade_attribute_funcs = {\n        0: AoCUpgradeAttributeSubprocessor.hp_upgrade,\n        1: AoCUpgradeAttributeSubprocessor.los_upgrade,\n        2: AoCUpgradeAttributeSubprocessor.garrison_capacity_upgrade,\n        3: AoCUpgradeAttributeSubprocessor.unit_size_x_upgrade,\n        4: AoCUpgradeAttributeSubprocessor.unit_size_y_upgrade,\n        5: AoCUpgradeAttributeSubprocessor.move_speed_upgrade,\n        6: AoCUpgradeAttributeSubprocessor.rotation_speed_upgrade,\n        8: AoCUpgradeAttributeSubprocessor.armor_upgrade,\n        9: AoCUpgradeAttributeSubprocessor.attack_upgrade,\n        10: AoCUpgradeAttributeSubprocessor.reload_time_upgrade,\n        11: AoCUpgradeAttributeSubprocessor.accuracy_upgrade,\n        12: AoCUpgradeAttributeSubprocessor.max_range_upgrade,\n        13: AoCUpgradeAttributeSubprocessor.work_rate_upgrade,\n        14: AoCUpgradeAttributeSubprocessor.carry_capacity_upgrade,\n        16: AoCUpgradeAttributeSubprocessor.projectile_unit_upgrade,\n        17: AoCUpgradeAttributeSubprocessor.graphics_angle_upgrade,\n        18: AoCUpgradeAttributeSubprocessor.terrain_defense_upgrade,\n        19: RoRUpgradeAttributeSubprocessor.ballistics_upgrade,\n        100: AoCUpgradeAttributeSubprocessor.resource_cost_upgrade,\n        101: RoRUpgradeAttributeSubprocessor.population_upgrade,\n    }\n\n    upgrade_resource_funcs = {\n        4: AoCUpgradeResourceSubprocessor.starting_population_space_upgrade,\n        27: AoCUpgradeResourceSubprocessor.monk_conversion_upgrade,\n        28: RoRUpgradeResourceSubprocessor.building_conversion_upgrade,\n        32: AoCUpgradeResourceSubprocessor.bonus_population_upgrade,\n        35: AoCUpgradeResourceSubprocessor.faith_recharge_rate_upgrade,\n        36: AoCUpgradeResourceSubprocessor.farm_food_upgrade,\n        46: AoCUpgradeResourceSubprocessor.tribute_inefficiency_upgrade,\n        47: AoCUpgradeResourceSubprocessor.gather_gold_efficiency_upgrade,\n        50: AoCUpgradeResourceSubprocessor.reveal_ally_upgrade,\n        56: RoRUpgradeResourceSubprocessor.heal_bonus_upgrade,\n        57: RoRUpgradeResourceSubprocessor.martyrdom_upgrade,\n    }\n\n    @classmethod\n    def get_patches(cls, converter_group: ConverterObjectGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for a converter group, depending on the type\n        of its effects.\n        \"\"\"\n        patches = []\n        effects = converter_group.get_effects()\n        for effect in effects:\n            type_id = effect.get_type()\n\n            if type_id in (0, 4, 5):\n                patches.extend(cls.attribute_modify_effect(converter_group, effect))\n\n            elif type_id == 1:\n                patches.extend(cls.resource_modify_effect(converter_group, effect))\n\n            elif type_id == 2:\n                # Enabling/disabling units: Handled in creatable conditions\n                pass\n\n            elif type_id == 3:\n                patches.extend(cls.upgrade_unit_effect(converter_group, effect))\n\n        return patches\n\n    @staticmethod\n    def attribute_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying attributes of entities.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type == 0:\n            operator = MemberOperator.ASSIGN\n\n        elif effect_type == 4:\n            operator = MemberOperator.ADD\n\n        elif effect_type == 5:\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid attribute effect\")\n\n        unit_id = effect[\"attr_a\"].value\n        class_id = effect[\"attr_b\"].value\n        attribute_type = effect[\"attr_c\"].value\n        value = effect[\"attr_d\"].value\n\n        if attribute_type == -1:\n            return patches\n\n        affected_entities = []\n        if unit_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.contains_entity(unit_id):\n                    affected_entities.append(line)\n\n                elif attribute_type == 19:\n                    if line.is_projectile_shooter() and line.has_projectile(unit_id):\n                        affected_entities.append(line)\n\n        elif class_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.get_class_id() == class_id:\n                    affected_entities.append(line)\n\n        else:\n            return patches\n\n        upgrade_func = RoRTechSubprocessor.upgrade_attribute_funcs[attribute_type]\n        for affected_entity in affected_entities:\n            patches.extend(upgrade_func(converter_group, affected_entity, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def resource_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying resources.\n        \"\"\"\n        patches = []\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type == 1:\n            mode = effect[\"attr_b\"].value\n\n            if mode == 0:\n                operator = MemberOperator.ASSIGN\n\n            else:\n                operator = MemberOperator.ADD\n\n        elif effect_type == 6:\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid resource effect\")\n\n        resource_id = effect[\"attr_a\"].value\n        value = effect[\"attr_d\"].value\n\n        if resource_id in (-1, 6, 21, 30):\n            # -1 = invalid ID\n            # 6  = set current age (unused)\n            # 21 = tech count (unused)\n            # 30 = building limits (unused)\n            return patches\n\n        upgrade_func = RoRTechSubprocessor.upgrade_resource_funcs[resource_id]\n        patches.extend(upgrade_func(converter_group, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def upgrade_unit_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for upgrading entities in a line.\n        \"\"\"\n        patches = []\n        tech_id = converter_group.get_id()\n        dataset = converter_group.data\n\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        head_unit_id = effect[\"attr_a\"].value\n        upgrade_target_id = effect[\"attr_b\"].value\n\n        if head_unit_id not in dataset.unit_ref.keys() or\\\n                upgrade_target_id not in dataset.unit_ref.keys():\n            # Skip annexes or transform units\n            return patches\n\n        line = dataset.unit_ref[head_unit_id]\n        upgrade_target_pos = line.get_unit_position(upgrade_target_id)\n        upgrade_source_pos = upgrade_target_pos - 1\n\n        upgrade_source = line.line[upgrade_source_pos]\n        upgrade_target = line.line[upgrade_target_pos]\n        tech_name = tech_lookup_dict[tech_id][0]\n\n        diff = upgrade_source.diff(upgrade_target)\n\n        patches.extend(AoCUpgradeAbilitySubprocessor.death_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.despawn_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.idle_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.live_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.los_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.named_ability(\n            converter_group, line, tech_name, diff))\n        # patches.extend(AoCUpgradeAbilitySubprocessor.resistance_ability(converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.selectable_ability(\n            converter_group, line, tech_name, diff))\n        patches.extend(AoCUpgradeAbilitySubprocessor.turn_ability(\n            converter_group, line, tech_name, diff))\n\n        if line.is_projectile_shooter():\n            patches.extend(RoRUpgradeAbilitySubprocessor.shoot_projectile_ability(converter_group, line,\n                                                                                  tech_name,\n                                                                                  7, diff))\n\n        elif line.is_melee() or line.is_ranged():\n            if line.has_command(7):\n                # Attack\n                patches.extend(AoCUpgradeAbilitySubprocessor.apply_discrete_effect_ability(converter_group,\n                                                                                           line, tech_name,\n                                                                                           7,\n                                                                                           line.is_ranged(),\n                                                                                           diff))\n\n        if isinstance(line, GenieUnitLineGroup):\n            patches.extend(AoCUpgradeAbilitySubprocessor.move_ability(converter_group, line,\n                                                                      tech_name, diff))\n\n        if isinstance(line, GenieBuildingLineGroup):\n            # TODO: Damage percentages change\n            # patches.extend(AoCUpgradeAbilitySubprocessor.attribute_change_tracker_ability(converter_group, line,\n            #                                                                               tech_name, diff))\n            pass\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/upgrade_ability_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements\n# pylint: disable=too-few-public-methods,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates upgrade patches for abilities.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_unit import GenieBuildingLineGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ....value_object.read.value_members import NoDiffMember\nfrom ..aoc.upgrade_ability_subprocessor import AoCUpgradeAbilitySubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObject, \\\n        ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass RoRUpgradeAbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for ability upgrade effects in RoR.\n    \"\"\"\n\n    @staticmethod\n    def shoot_projectile_ability(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        container_obj_ref: str,\n        command_id: int,\n        diff: ConverterObject = None\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the Selectable ability of a line.\n\n        :param converter_group: Group that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param container_obj_ref: Reference of the raw API object the patch is nested in.\n        :type container_obj_ref: str\n        :param diff: A diff between two ConvertObject instances.\n        :type diff: ...dataformat.converter_object.ConverterObject\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        tech_id = converter_group.get_id()\n        dataset = line.data\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n        ability_name = command_lookup_dict[command_id][0]\n\n        data_changed = False\n        if diff:\n            diff_animation = diff[\"attack_sprite_id\"]\n            diff_comm_sound = diff[\"command_sound_id\"]\n            diff_min_range = diff[\"weapon_range_min\"]\n            diff_max_range = diff[\"weapon_range_min\"]\n            diff_reload_time = diff[\"attack_speed\"]\n            # spawn delay also depends on animation\n            diff_spawn_delay = diff[\"frame_delay\"]\n            diff_spawn_area_offsets = diff[\"weapon_offset\"]\n\n            if any(not isinstance(value, NoDiffMember) for value in (diff_min_range,\n                                                                     diff_max_range,\n                                                                     diff_reload_time,\n                                                                     diff_spawn_delay,\n                                                                     diff_spawn_area_offsets)):\n                data_changed = True\n\n        if not isinstance(diff_animation, NoDiffMember):\n            diff_animation_id = diff_animation.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, anim_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_animation_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_animation_id]\n            )\n            patches.append(anim_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if not isinstance(diff_comm_sound, NoDiffMember):\n            diff_comm_sound_id = diff_comm_sound.value\n\n            # Nyan patch\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            wrapper, sound_patch_forward_ref = AoCUpgradeAbilitySubprocessor.create_command_sound_patch(\n                converter_group,\n                line,\n                patch_target_ref,\n                nyan_patch_name,\n                container_obj_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\",\n                [diff_comm_sound_id]\n            )\n            patches.append(sound_patch_forward_ref)\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper.set_location((\"data/game_entity/generic/\"\n                                      f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n        if data_changed:\n            patch_target_ref = f\"{game_entity_name}.{ability_name}\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{ability_name}Wrapper\"\n            wrapper_ref = f\"{container_obj_ref}.{wrapper_name}\"\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            if isinstance(line, GenieBuildingLineGroup):\n                # Store building upgrades next to their game entity definition,\n                # not in the Age up techs.\n                wrapper_raw_api_object.set_location((\"data/game_entity/generic/\"\n                                                     f\"{name_lookup_dict[head_unit_id][1]}/\"))\n                wrapper_raw_api_object.set_filename(f\"{tech_lookup_dict[tech_id][1]}_upgrade\")\n\n            else:\n                wrapper_raw_api_object.set_location(ForwardRef(converter_group, container_obj_ref))\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{ability_name}\"\n            nyan_patch_ref = f\"{container_obj_ref}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            if not isinstance(diff_min_range, NoDiffMember):\n                min_range = diff_min_range.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"min_range\",\n                                                               min_range,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_max_range, NoDiffMember):\n                max_range = diff_max_range.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                               max_range,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_reload_time, NoDiffMember):\n                reload_time = diff_reload_time.value\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"reload_time\",\n                                                               reload_time,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ADD)\n\n            if not isinstance(diff_spawn_delay, NoDiffMember):\n                if not isinstance(diff_animation, NoDiffMember):\n                    attack_graphic_id = diff_animation.value\n\n                else:\n                    attack_graphic_id = diff_animation.ref.value\n\n                attack_graphic = dataset.genie_graphics[attack_graphic_id]\n                frame_rate = attack_graphic.get_frame_rate()\n                frame_delay = diff_spawn_delay.value\n                spawn_delay = frame_rate * frame_delay\n\n                nyan_patch_raw_api_object.add_raw_patch_member(\"spawn_delay\",\n                                                               spawn_delay,\n                                                               \"engine.ability.type.ShootProjectile\",\n                                                               MemberOperator.ASSIGN)\n\n            if not isinstance(diff_spawn_area_offsets, NoDiffMember):\n                diff_spawn_area_x = diff_spawn_area_offsets[0]\n                diff_spawn_area_y = diff_spawn_area_offsets[1]\n                diff_spawn_area_z = diff_spawn_area_offsets[2]\n\n                if not isinstance(diff_spawn_area_x, NoDiffMember):\n                    spawn_area_x = diff_spawn_area_x.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_x\",\n                                                                   spawn_area_x,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_spawn_area_y, NoDiffMember):\n                    spawn_area_y = diff_spawn_area_y.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_y\",\n                                                                   spawn_area_y,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n                if not isinstance(diff_spawn_area_z, NoDiffMember):\n                    spawn_area_z = diff_spawn_area_z.value\n\n                    nyan_patch_raw_api_object.add_raw_patch_member(\"spawning_area_offset_z\",\n                                                                   spawn_area_z,\n                                                                   \"engine.ability.type.ShootProjectile\",\n                                                                   MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/upgrade_attribute_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for attribute modification effects in RoR.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass RoRUpgradeAttributeSubprocessor:\n    \"\"\"\n    Creates raw API objects for attribute upgrade effects in RoR.\n    \"\"\"\n\n    @staticmethod\n    def ballistics_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ballistics modify effect (ID: 19).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        if value == 0:\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.CurrentPosition\"]\n\n        elif value == 1:\n            target_mode = dataset.nyan_api_objects[\"engine.util.target_mode.type.ExpectedPosition\"]\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        projectile_id0 = head_unit[\"projectile_id0\"].value\n        if projectile_id0 > -1:\n            patch_target_ref = f\"{game_entity_name}.ShootProjectile.Projectile0.Projectile\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}Projectile0TargetModeWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}Projectile0TargetMode\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"target_mode\",\n                                                           target_mode,\n                                                           \"engine.ability.type.Projectile\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def population_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the population effect (ID: 101).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/ror/upgrade_resource_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument\n\n\"\"\"\nCreates upgrade patches for resource modification effects in RoR.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass RoRUpgradeResourceSubprocessor:\n    \"\"\"\n    Creates raw API objects for resource upgrade effects in RoR.\n    \"\"\"\n\n    @staticmethod\n    def building_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the building conversion effect (ID: 28).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        monk_id = 125\n        dataset = converter_group.data\n        line = dataset.unit_lines[monk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[monk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Convert\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Building conversion\n\n        # Wrapper\n        wrapper_name = \"EnableBuildingConversionWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = \"EnableBuildingConversion\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # New allowed types\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()\n        ]\n        nyan_patch_raw_api_object.add_raw_patch_member(\"allowed_types\",\n                                                       allowed_types,\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.ADD)\n\n        # Blacklisted buildings\n        tc_line = dataset.building_lines[109]\n        farm_line = dataset.building_lines[50]\n        monastery_line = dataset.building_lines[104]\n        wonder_line = dataset.building_lines[276]\n\n        blacklisted_forward_refs = [ForwardRef(tc_line, \"TownCenter\"),\n                                    ForwardRef(farm_line, \"Farm\"),\n                                    ForwardRef(monastery_line, \"Temple\"),\n                                    ForwardRef(wonder_line, \"Wonder\"),\n                                    ]\n        nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                       blacklisted_forward_refs,\n                                                       \"engine.ability.type.ApplyDiscreteEffect\",\n                                                       MemberOperator.ADD)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def heal_bonus_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the AoE1 heal bonus effect (ID: 56).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def martyrdom_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the martyrdom effect (ID: 57).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tability_subprocessor.py\n\tauxiliary_subprocessor.py\n\tciv_subprocessor.py\n\tmodpack_subprocessor.py\n\tnyan_subprocessor.py\n\tpregen_subprocessor.py\n\tprocessor.py\n\ttech_subprocessor.py\n\tupgrade_attribute_subprocessor.py\n\tupgrade_resource_subprocessor.py\n)\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDrives the conversion process for Star Wars: Galactic Battlegrounds (Clone Campaigns).\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/ability_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-public-methods,too-many-lines,too-many-locals\n# pylint: disable=too-many-branches,too-many-statements,too-many-arguments\n# pylint: disable=invalid-name\n#\n# TODO:\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nDerives and adds abilities to lines. Subroutine of the\nnyan subprocessor.\n\nFor SWGB we use the functions of the AoCAbilitySubprocessor, but additionally\ncreate a diff for every civ line.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom .....util.ordered_set import OrderedSet\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieStackBuildingGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.ability_subprocessor import AoCAbilitySubprocessor\nfrom ..aoc.effect_subprocessor import AoCEffectSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n\n\nclass SWGBCCAbilitySubprocessor:\n    \"\"\"\n    Creates raw API objects for abilities in SWGB.\n    \"\"\"\n\n    @staticmethod\n    def active_transform_to_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ActiveTransformTo ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.active_transform_to_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def apply_continuous_effect_ability(\n        line: GenieGameEntityGroup,\n        command_id: int,\n        ranged: bool = False\n    ) -> ForwardRef:\n        \"\"\"\n        Adds the ApplyContinuousEffect ability to a line that is used to make entities die.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.apply_continuous_effect_ability(\n            line, command_id, ranged)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def apply_discrete_effect_ability(\n        line: GenieGameEntityGroup,\n        command_id: int,\n        ranged: bool = False,\n        projectile: int = -1\n    ) -> ForwardRef:\n        \"\"\"\n        Adds the ApplyDiscreteEffect ability to a line that is used to make entities die.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.get_units_with_command(command_id)[0]\n            current_unit_id = current_unit[\"id0\"].value\n\n        else:\n            current_unit = line.get_head_unit()\n            current_unit_id = line.get_head_unit_id()\n\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        command_lookup_dict = internal_name_lookups.get_command_lookups(dataset.game_version)\n        gset_lookup_dict = internal_name_lookups.get_graphic_set_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        ability_name = command_lookup_dict[command_id][0]\n\n        if ranged:\n            ability_parent = \"engine.ability.type.RangedDiscreteEffect\"\n\n        else:\n            ability_parent = \"engine.ability.type.ApplyDiscreteEffect\"\n\n        if projectile == -1:\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            ability_animation_id = current_unit[\"attack_sprite_id\"].value\n\n        else:\n            ability_ref = f\"{game_entity_name}.ShootProjectile.Projectile{str(projectile)}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(ability_parent)\n            ability_location = ForwardRef(line,\n                                          (f\"{game_entity_name}.ShootProjectile.\"\n                                           f\"Projectile{projectile}\"))\n            ability_raw_api_object.set_location(ability_location)\n\n            ability_animation_id = -1\n\n        # Ability properties\n        properties = {}\n\n        # Animated\n        if ability_animation_id > -1:\n            property_ref = f\"{ability_ref}.Animated\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Animated\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            animations_set = []\n            animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                line,\n                ability_animation_id,\n                property_ref,\n                ability_name,\n                f\"{command_lookup_dict[command_id][1]}_\"\n            )\n            animations_set.append(animation_forward_ref)\n            property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                   \"engine.ability.property.type.Animated\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n            })\n\n            # Create custom civ graphics\n            handled_graphics_set_ids = set()\n            for civ_group in dataset.civ_groups.values():\n                civ = civ_group.civ\n                civ_id = civ_group.get_id()\n\n                # Only proceed if the civ stores the unit in the line\n                if current_unit_id not in civ[\"units\"].value.keys():\n                    continue\n\n                civ_animation_id = civ[\"units\"][current_unit_id][\"attack_sprite_id\"].value\n\n                if civ_animation_id != ability_animation_id:\n                    # Find the corresponding graphics set\n                    graphics_set_id = -1\n                    for set_id, items in gset_lookup_dict.items():\n                        if civ_id in items[0]:\n                            graphics_set_id = set_id\n                            break\n\n                    # Check if the object for the animation has been created before\n                    obj_exists = graphics_set_id in handled_graphics_set_ids\n                    if not obj_exists:\n                        handled_graphics_set_ids.add(graphics_set_id)\n\n                    obj_prefix = f\"{gset_lookup_dict[graphics_set_id][1]}{ability_name}\"\n                    filename_prefix = (f\"{command_lookup_dict[command_id][1]}_\"\n                                       f\"{gset_lookup_dict[graphics_set_id][2]}_\")\n                    AoCAbilitySubprocessor.create_civ_animation(line,\n                                                                civ_group,\n                                                                civ_animation_id,\n                                                                property_ref,\n                                                                obj_prefix,\n                                                                filename_prefix,\n                                                                obj_exists)\n\n        # Command Sound\n        if projectile == -1:\n            ability_comm_sound_id = current_unit[\"command_sound_id\"].value\n\n        else:\n            ability_comm_sound_id = -1\n\n        if ability_comm_sound_id > -1:\n            property_ref = f\"{ability_ref}.CommandSound\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"CommandSound\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.CommandSound\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            sounds_set = []\n\n            if projectile == -1:\n                sound_obj_prefix = ability_name\n\n            else:\n                sound_obj_prefix = \"ProjectileAttack\"\n\n            sound_forward_ref = AoCAbilitySubprocessor.create_sound(line,\n                                                                    ability_comm_sound_id,\n                                                                    property_ref,\n                                                                    sound_obj_prefix,\n                                                                    \"command_\")\n            sounds_set.append(sound_forward_ref)\n            property_raw_api_object.add_raw_member(\"sounds\", sounds_set,\n                                                   \"engine.ability.property.type.CommandSound\")\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.CommandSound\"]: property_forward_ref\n            })\n\n        # Diplomacy settings\n        property_ref = f\"{ability_ref}.Diplomatic\"\n        property_raw_api_object = RawAPIObject(property_ref,\n                                               \"Diplomatic\",\n                                               dataset.nyan_api_objects)\n        property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n        property_location = ForwardRef(line, ability_ref)\n        property_raw_api_object.set_location(property_location)\n\n        line.add_raw_api_object(property_raw_api_object)\n\n        diplomatic_stances = [dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n        property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                               \"engine.ability.property.type.Diplomatic\")\n\n        property_forward_ref = ForwardRef(line, property_ref)\n        properties.update({\n            api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n        })\n\n        ability_raw_api_object.add_raw_member(\"properties\",\n                                              properties,\n                                              \"engine.ability.Ability\")\n\n        if ranged:\n            # Min range\n            min_range = current_unit[\"weapon_range_min\"].value\n            ability_raw_api_object.add_raw_member(\"min_range\",\n                                                  min_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n            # Max range\n            max_range = current_unit[\"weapon_range_max\"].value\n            ability_raw_api_object.add_raw_member(\"max_range\",\n                                                  max_range,\n                                                  \"engine.ability.type.RangedDiscreteEffect\")\n\n        # Effects\n        batch_ref = f\"{ability_ref}.Batch\"\n        batch_raw_api_object = RawAPIObject(batch_ref, \"Batch\", dataset.nyan_api_objects)\n        batch_raw_api_object.add_raw_parent(\"engine.util.effect_batch.type.UnorderedBatch\")\n        batch_location = ForwardRef(line, ability_ref)\n        batch_raw_api_object.set_location(batch_location)\n\n        line.add_raw_api_object(batch_raw_api_object)\n\n        # Effects\n        if command_id == 7:\n            # Attack\n            if projectile != 1:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref)\n\n            else:\n                effects = AoCEffectSubprocessor.get_attack_effects(line, batch_ref, projectile=1)\n\n        elif command_id == 104:\n            # Convert\n            effects = AoCEffectSubprocessor.get_convert_effects(line, batch_ref)\n\n        batch_raw_api_object.add_raw_member(\"effects\",\n                                            effects,\n                                            \"engine.util.effect_batch.EffectBatch\")\n\n        batch_forward_ref = ForwardRef(line, batch_ref)\n        ability_raw_api_object.add_raw_member(\"batches\",\n                                              [batch_forward_ref],\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Reload time\n        if projectile == -1:\n            reload_time = current_unit[\"attack_speed\"].value\n\n        else:\n            reload_time = 0\n\n        ability_raw_api_object.add_raw_member(\"reload_time\",\n                                              reload_time,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Application delay\n        if projectile == -1:\n            attack_graphic_id = current_unit[\"attack_sprite_id\"].value\n            attack_graphic = dataset.genie_graphics[attack_graphic_id]\n            frame_rate = attack_graphic.get_frame_rate()\n            frame_delay = current_unit[\"frame_delay\"].value\n            application_delay = frame_rate * frame_delay\n\n        else:\n            application_delay = 0\n\n        ability_raw_api_object.add_raw_member(\"application_delay\",\n                                              application_delay,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        # Allowed types (all buildings/units)\n        if command_id == 104:\n            # Convert\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object()]\n\n        else:\n            allowed_types = [dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(),\n                             dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object()]\n\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        if command_id == 104:\n            # Convert\n            force_master_line = dataset.unit_lines[115]\n            force_line = dataset.unit_lines[180]\n            artillery_line = dataset.unit_lines[691]\n            anti_air_line = dataset.unit_lines[702]\n            pummel_line = dataset.unit_lines[713]\n\n            blacklisted_entities = [ForwardRef(force_master_line, \"ForceMaster\"),\n                                    ForwardRef(force_line, \"ForceKnight\"),\n                                    ForwardRef(artillery_line, \"Artillery\"),\n                                    ForwardRef(anti_air_line, \"AntiAirMobile\"),\n                                    ForwardRef(pummel_line, \"Pummel\")]\n\n        else:\n            blacklisted_entities = []\n\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              blacklisted_entities,\n                                              \"engine.ability.type.ApplyDiscreteEffect\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def attribute_change_tracker_ability(line) -> ForwardRef:\n        \"\"\"\n        Adds the AttributeChangeTracker ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieStackBuildingGroup):\n            current_unit = line.get_stack_unit()\n            current_unit_id = line.get_stack_unit_id()\n\n        else:\n            current_unit = line.get_head_unit()\n            current_unit_id = line.get_head_unit_id()\n\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.AttributeChangeTracker\"\n        ability_raw_api_object = RawAPIObject(\n            ability_ref, \"AttributeChangeTracker\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.AttributeChangeTracker\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        # Attribute\n        attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n        ability_raw_api_object.add_raw_member(\"attribute\",\n                                              attribute,\n                                              \"engine.ability.type.AttributeChangeTracker\")\n\n        # Change progress\n        damage_graphics = current_unit[\"damage_graphics\"].value\n        progress_forward_refs = []\n\n        # Damage graphics are ordered ascending, so we start from 0\n        interval_left_bound = 0\n        for damage_graphic_member in damage_graphics:\n            interval_right_bound = damage_graphic_member[\"damage_percent\"].value\n            progress_ref = f\"{game_entity_name}.AttributeChangeTracker.ChangeProgress{interval_right_bound}\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   f\"ChangeProgress{interval_right_bound}\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.AttributeChange\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   interval_left_bound,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   interval_right_bound,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # AnimationOverlay property\n            # =====================================================================================\n            progress_animation_id = damage_graphic_member[\"graphic_id\"].value\n            if progress_animation_id > -1:\n                property_ref = f\"{progress_ref}.AnimationOverlay\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"AnimationOverlay\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.AnimationOverlay\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                    line,\n                    progress_animation_id,\n                    property_ref,\n                    \"Idle\",\n                    f\"idle_damage_override_{interval_right_bound}_\"\n                )\n                animations_set.append(animation_forward_ref)\n                property_raw_api_object.add_raw_member(\"overlays\",\n                                                       animations_set,\n                                                       \"engine.util.progress.property.type.AnimationOverlay\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.AnimationOverlay\"]: property_forward_ref\n                })\n\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            interval_left_bound = interval_right_bound\n\n        ability_raw_api_object.add_raw_member(\"change_progress\",\n                                              progress_forward_refs,\n                                              \"engine.ability.type.AttributeChangeTracker\")\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def constructable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Constructable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.constructable_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def death_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Death ability to a line that is used to make entities die.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.death_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def exchange_resources_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ExchangeResources ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        resource_names = [\"Food\", \"Carbon\", \"Ore\"]\n\n        abilities = []\n        for resource_name in resource_names:\n            ability_name = f\"MarketExchange{resource_name}\"\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(\"engine.ability.type.ExchangeResources\")\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            # Resource that is exchanged (resource A)\n            resource_a = dataset.pregen_nyan_objects[f\"util.resource.types.{resource_name}\"].get_nyan_object(\n            )\n            ability_raw_api_object.add_raw_member(\"resource_a\",\n                                                  resource_a,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Resource that is exchanged for (resource B)\n            resource_b = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n            ability_raw_api_object.add_raw_member(\"resource_b\",\n                                                  resource_b,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Exchange rate\n            exchange_rate_ref = f\"util.resource.market_trading.Market{resource_name}ExchangeRate\"\n            exchange_rate = dataset.pregen_nyan_objects[exchange_rate_ref].get_nyan_object()\n            ability_raw_api_object.add_raw_member(\"exchange_rate\",\n                                                  exchange_rate,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            # Exchange modes\n            buy_exchange_ref = \"util.resource.market_trading.MarketBuyExchangeMode\"\n            sell_exchange_ref = \"util.resource.market_trading.MarketSellExchangeMode\"\n            exchange_modes = [\n                dataset.pregen_nyan_objects[buy_exchange_ref].get_nyan_object(),\n                dataset.pregen_nyan_objects[sell_exchange_ref].get_nyan_object(),\n            ]\n            ability_raw_api_object.add_raw_member(\"exchange_modes\",\n                                                  exchange_modes,\n                                                  \"engine.ability.type.ExchangeResources\")\n\n            line.add_raw_api_object(ability_raw_api_object)\n            ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n            abilities.append(ability_forward_ref)\n\n        return abilities\n\n    @staticmethod\n    def gather_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Gather abilities to a line. Unlike the other methods, this\n        creates multiple abilities.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the abilities.\n        :rtype: list\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            gatherers = line.variants[0].line\n\n        else:\n            gatherers = [line.line[0]]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        abilities = []\n        for gatherer in gatherers:\n            unit_commands = gatherer[\"unit_commands\"].value\n            resource = None\n            ability_animation_id = -1\n            harvestable_class_ids = OrderedSet()\n            harvestable_unit_ids = OrderedSet()\n\n            for command in unit_commands:\n                # Find a gather ability. It doesn't matter which one because\n                # they should all produce the same resource for one genie unit.\n                type_id = command[\"type\"].value\n\n                if type_id not in (5, 110):\n                    continue\n\n                target_class_id = command[\"class_id\"].value\n                if target_class_id > -1:\n                    harvestable_class_ids.add(target_class_id)\n\n                target_unit_id = command[\"unit_id\"].value\n                if target_unit_id > -1:\n                    harvestable_unit_ids.add(target_unit_id)\n\n                resource_id = command[\"resource_out\"].value\n\n                # If resource_out is not specified, the gatherer harvests resource_in\n                if resource_id == -1:\n                    resource_id = command[\"resource_in\"].value\n\n                if resource_id == 0:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object(\n                    )\n\n                elif resource_id == 1:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object(\n                    )\n\n                elif resource_id == 2:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object(\n                    )\n\n                elif resource_id == 3:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object(\n                    )\n\n                else:\n                    continue\n\n                if type_id == 110:\n                    ability_animation_id = command[\"work_sprite_id\"].value\n\n                else:\n                    ability_animation_id = command[\"proceed_sprite_id\"].value\n\n            # Look for the harvestable groups that match the class IDs and unit IDs\n            check_groups = []\n            check_groups.extend(dataset.unit_lines.values())\n            check_groups.extend(dataset.building_lines.values())\n            check_groups.extend(dataset.ambient_groups.values())\n\n            harvestable_groups = []\n            for group in check_groups:\n                if not group.is_harvestable():\n                    continue\n\n                if group.get_class_id() in harvestable_class_ids:\n                    harvestable_groups.append(group)\n                    continue\n\n                for unit_id in harvestable_unit_ids:\n                    if group.contains_entity(unit_id):\n                        harvestable_groups.append(group)\n\n            if len(harvestable_groups) == 0:\n                # If no matching groups are found, then we don't\n                # need to create an ability.\n                continue\n\n            gatherer_unit_id = gatherer.get_id()\n            if gatherer_unit_id not in gather_lookup_dict:\n                # Skips hunting wolves\n                continue\n\n            ability_name = gather_lookup_dict[gatherer_unit_id][0]\n\n            ability_ref = f\"{game_entity_name}.{ability_name}\"\n            ability_raw_api_object = RawAPIObject(\n                ability_ref, ability_name, dataset.nyan_api_objects)\n            ability_raw_api_object.add_raw_parent(\"engine.ability.type.Gather\")\n            ability_location = ForwardRef(line, game_entity_name)\n            ability_raw_api_object.set_location(ability_location)\n\n            line.add_raw_api_object(ability_raw_api_object)\n\n            # Ability properties\n            properties = {}\n\n            # Animation\n            if ability_animation_id > -1:\n                property_ref = f\"{ability_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Animated\")\n                property_location = ForwardRef(line, ability_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(\n                    line,\n                    ability_animation_id,\n                    property_ref,\n                    ability_name,\n                    f\"{gather_lookup_dict[gatherer_unit_id][1]}_\"\n                )\n                animations_set.append(animation_forward_ref)\n                property_raw_api_object.add_raw_member(\"animations\", animations_set,\n                                                       \"engine.ability.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n                properties.update({\n                    api_objects[\"engine.ability.property.type.Animated\"]: property_forward_ref\n                })\n\n            # Diplomacy settings\n            property_ref = f\"{ability_ref}.Diplomatic\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"Diplomatic\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.ability.property.type.Diplomatic\")\n            property_location = ForwardRef(line, ability_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            diplomatic_stances = [\n                dataset.nyan_api_objects[\"engine.util.diplomatic_stance.type.Self\"]]\n            property_raw_api_object.add_raw_member(\"stances\", diplomatic_stances,\n                                                   \"engine.ability.property.type.Diplomatic\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.ability.property.type.Diplomatic\"]: property_forward_ref\n            })\n\n            ability_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.ability.Ability\")\n\n            # Auto resume\n            ability_raw_api_object.add_raw_member(\"auto_resume\",\n                                                  True,\n                                                  \"engine.ability.type.Gather\")\n\n            # search range\n            ability_raw_api_object.add_raw_member(\"resume_search_range\",\n                                                  MemberSpecialValue.NYAN_INF,\n                                                  \"engine.ability.type.Gather\")\n\n            # Gather rate\n            rate_name = f\"{game_entity_name}.{ability_name}.GatherRate\"\n            rate_raw_api_object = RawAPIObject(rate_name, \"GatherRate\", dataset.nyan_api_objects)\n            rate_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceRate\")\n            rate_location = ForwardRef(line, ability_ref)\n            rate_raw_api_object.set_location(rate_location)\n\n            rate_raw_api_object.add_raw_member(\n                \"type\", resource, \"engine.util.resource.ResourceRate\")\n\n            gather_rate = gatherer[\"work_rate\"].value\n            rate_raw_api_object.add_raw_member(\n                \"rate\", gather_rate, \"engine.util.resource.ResourceRate\")\n\n            line.add_raw_api_object(rate_raw_api_object)\n\n            rate_forward_ref = ForwardRef(line, rate_name)\n            ability_raw_api_object.add_raw_member(\"gather_rate\",\n                                                  rate_forward_ref,\n                                                  \"engine.ability.type.Gather\")\n\n            # Resource container\n            container_ref = (f\"{game_entity_name}.ResourceStorage.\"\n                             f\"{gather_lookup_dict[gatherer_unit_id][0]}Container\")\n            container_forward_ref = ForwardRef(line, container_ref)\n            ability_raw_api_object.add_raw_member(\"container\",\n                                                  container_forward_ref,\n                                                  \"engine.ability.type.Gather\")\n\n            # Targets (resource spots)\n            entity_lookups = internal_name_lookups.get_entity_lookups(dataset.game_version)\n            spot_forward_refs = []\n            for group in harvestable_groups:\n                group_id = group.get_head_unit_id()\n                group_name = entity_lookups[group_id][0]\n\n                spot_forward_ref = ForwardRef(\n                    group,\n                    f\"{group_name}.Harvestable.{group_name}ResourceSpot\"\n                )\n                spot_forward_refs.append(spot_forward_ref)\n\n            ability_raw_api_object.add_raw_member(\"targets\",\n                                                  spot_forward_refs,\n                                                  \"engine.ability.type.Gather\")\n\n            ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n            abilities.append(ability_forward_ref)\n\n        return abilities\n\n    @staticmethod\n    def harvestable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Harvestable ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Harvestable\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Harvestable\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Harvestable\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Resource spot\n        resource_storage = current_unit[\"resource_storage\"].value\n\n        for storage in resource_storage:\n            resource_id = storage[\"type\"].value\n\n            # IDs 15, 16, 17 are other types of food (meat, berries, fish)\n            if resource_id in (0, 15, 16, 17):\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object(\n                )\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object()\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n\n            else:\n                continue\n\n            spot_name = f\"{game_entity_name}.Harvestable.{game_entity_name}ResourceSpot\"\n            spot_raw_api_object = RawAPIObject(spot_name,\n                                               f\"{game_entity_name}ResourceSpot\",\n                                               dataset.nyan_api_objects)\n            spot_raw_api_object.add_raw_parent(\"engine.util.resource_spot.ResourceSpot\")\n            spot_location = ForwardRef(line, ability_ref)\n            spot_raw_api_object.set_location(spot_location)\n\n            # Type\n            spot_raw_api_object.add_raw_member(\"resource\",\n                                               resource,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Start amount (equals max amount)\n            if line.get_id() == 50:\n                # Farm food amount (hardcoded in civ)\n                starting_amount = dataset.genie_civs[1][\"resources\"][36].value\n\n            elif line.get_id() == 199:\n                # Aqua harvester food amount (hardcoded in civ)\n                starting_amount = storage[\"amount\"].value\n                starting_amount += dataset.genie_civs[1][\"resources\"][88].value\n\n            else:\n                starting_amount = storage[\"amount\"].value\n\n            spot_raw_api_object.add_raw_member(\"starting_amount\",\n                                               starting_amount,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Max amount\n            spot_raw_api_object.add_raw_member(\"max_amount\",\n                                               starting_amount,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            # Decay rate\n            decay_rate = current_unit[\"resource_decay\"].value\n            spot_raw_api_object.add_raw_member(\"decay_rate\",\n                                               decay_rate,\n                                               \"engine.util.resource_spot.ResourceSpot\")\n\n            spot_forward_ref = ForwardRef(line, spot_name)\n            ability_raw_api_object.add_raw_member(\"resources\",\n                                                  spot_forward_ref,\n                                                  \"engine.ability.type.Harvestable\")\n            line.add_raw_api_object(spot_raw_api_object)\n\n            # Only one resource spot per ability\n            break\n\n        # Harvest Progress (we don't use this for SWGB)\n        ability_raw_api_object.add_raw_member(\"harvest_progress\",\n                                              [],\n                                              \"engine.ability.type.Harvestable\")\n\n        # Restock Progress\n        progress_forward_refs = []\n        if line.get_class_id() == 7:\n            # Farms\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress33\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress33\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (0.0, 33.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   0.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction1\"\n            terrain_group = dataset.terrain_groups[29]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            init_state_ref = f\"{game_entity_name}.Constructable.InitState\"\n            init_state_forward_ref = ForwardRef(line, init_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   init_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress66\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress66\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            # Interval = (33.0, 66.0)\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   33.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction2\"\n            terrain_group = dataset.terrain_groups[30]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            construct_state_ref = f\"{game_entity_name}.Constructable.ConstructState\"\n            construct_state_forward_ref = ForwardRef(line, construct_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =====================================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n            # =====================================================================================\n            progress_ref = f\"{ability_ref}.RestockProgress100\"\n            progress_raw_api_object = RawAPIObject(progress_ref,\n                                                   \"RestockProgress100\",\n                                                   dataset.nyan_api_objects)\n            progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n            progress_location = ForwardRef(line, ability_ref)\n            progress_raw_api_object.set_location(progress_location)\n\n            line.add_raw_api_object(progress_raw_api_object)\n\n            # Type\n            progress_raw_api_object.add_raw_member(\"type\",\n                                                   api_objects[\"engine.util.progress_type.type.Restock\"],\n                                                   \"engine.util.progress.Progress\")\n\n            progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                   66.0,\n                                                   \"engine.util.progress.Progress\")\n            progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                   100.0,\n                                                   \"engine.util.progress.Progress\")\n\n            # Progress properties\n            properties = {}\n            # =====================================================================================\n            # Terrain overlay property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.TerrainOverlay\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"TerrainOverlay\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\n                \"engine.util.progress.property.type.TerrainOverlay\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # Terrain overlay\n            terrain_ref = \"FarmConstruction3\"\n            terrain_group = dataset.terrain_groups[31]\n            terrain_forward_ref = ForwardRef(terrain_group, terrain_ref)\n            property_raw_api_object.add_raw_member(\"terrain_overlay\",\n                                                   terrain_forward_ref,\n                                                   \"engine.util.progress.property.type.TerrainOverlay\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]: property_forward_ref\n            })\n            # =====================================================================================\n            # State change property\n            # =====================================================================================\n            property_ref = f\"{progress_ref}.StateChange\"\n            property_raw_api_object = RawAPIObject(property_ref,\n                                                   \"StateChange\",\n                                                   dataset.nyan_api_objects)\n            property_raw_api_object.add_raw_parent(\"engine.util.progress.property.type.StateChange\")\n            property_location = ForwardRef(line, progress_ref)\n            property_raw_api_object.set_location(property_location)\n\n            line.add_raw_api_object(property_raw_api_object)\n\n            # State change\n            construct_state_ref = f\"{game_entity_name}.Constructable.ConstructState\"\n            construct_state_forward_ref = ForwardRef(line, construct_state_ref)\n            property_raw_api_object.add_raw_member(\"state_change\",\n                                                   construct_state_forward_ref,\n                                                   \"engine.util.progress.property.type.StateChange\")\n\n            property_forward_ref = ForwardRef(line, property_ref)\n            properties.update({\n                api_objects[\"engine.util.progress.property.type.StateChange\"]: property_forward_ref\n            })\n            # =======================================================================\n            progress_raw_api_object.add_raw_member(\"properties\",\n                                                   properties,\n                                                   \"engine.util.progress.Progress\")\n\n            progress_forward_refs.append(ForwardRef(line, progress_ref))\n\n        ability_raw_api_object.add_raw_member(\"restock_progress\",\n                                              progress_forward_refs,\n                                              \"engine.ability.type.Harvestable\")\n\n        # Gatherer limit (infinite in SWGB except for farms)\n        gatherer_limit = MemberSpecialValue.NYAN_INF\n        if line.get_class_id() == 7:\n            gatherer_limit = 1\n\n        ability_raw_api_object.add_raw_member(\"gatherer_limit\",\n                                              gatherer_limit,\n                                              \"engine.ability.type.Harvestable\")\n\n        # Unit have to die before they are harvestable (except for farms)\n        harvestable_by_default = current_unit[\"hit_points\"].value == 0\n        if line.get_class_id() == 7:\n            harvestable_by_default = True\n\n        ability_raw_api_object.add_raw_member(\"harvestable_by_default\",\n                                              harvestable_by_default,\n                                              \"engine.ability.type.Harvestable\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def collision_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Collision ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.collision_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def idle_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Idle ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.idle_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def live_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Live ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.live_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def los_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the LineOfSight ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.los_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def move_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Move ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.move_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def named_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Named ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.named_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def provide_contingent_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ProvideContingent ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.provide_contingent_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def regenerate_attribute_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the RegenerateAttribute ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward references for the ability.\n        :rtype: list\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        attribute = None\n        attribute_name = \"\"\n        if current_unit_id in (115, 180):\n            # Monk; regenerates Faith\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Faith\"].get_nyan_object()\n            attribute_name = \"Faith\"\n\n        elif current_unit_id == 8:\n            # Berserk: regenerates Health\n            attribute = dataset.pregen_nyan_objects[\"util.attribute.types.Health\"].get_nyan_object()\n            attribute_name = \"Health\"\n\n        else:\n            return []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_name = f\"Regenerate{attribute_name}\"\n        ability_ref = f\"{game_entity_name}.{ability_name}\"\n        ability_raw_api_object = RawAPIObject(ability_ref, ability_name, dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.RegenerateAttribute\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Attribute rate\n        # ===============================================================================\n        rate_name = f\"{attribute_name}Rate\"\n        rate_ref = f\"{game_entity_name}.{ability_name}.{rate_name}\"\n        rate_raw_api_object = RawAPIObject(rate_ref, rate_name, dataset.nyan_api_objects)\n        rate_raw_api_object.add_raw_parent(\"engine.util.attribute.AttributeRate\")\n        rate_location = ForwardRef(line, ability_ref)\n        rate_raw_api_object.set_location(rate_location)\n\n        # Attribute\n        rate_raw_api_object.add_raw_member(\"type\",\n                                           attribute,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        # Rate\n        attribute_rate = 0\n        if current_unit_id in (115, 180):\n            # stored in civ resources\n            attribute_rate = dataset.genie_civs[0][\"resources\"][35].value\n\n        elif current_unit_id == 8:\n            # stored in civ resources\n            heal_timer = dataset.genie_civs[0][\"resources\"][96].value\n            attribute_rate = heal_timer\n\n        rate_raw_api_object.add_raw_member(\"rate\",\n                                           attribute_rate,\n                                           \"engine.util.attribute.AttributeRate\")\n\n        line.add_raw_api_object(rate_raw_api_object)\n        # ===============================================================================\n        rate_forward_ref = ForwardRef(line, rate_ref)\n        ability_raw_api_object.add_raw_member(\"rate\",\n                                              rate_forward_ref,\n                                              \"engine.ability.type.RegenerateAttribute\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return [ability_forward_ref]\n\n    @staticmethod\n    def resource_storage_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the ResourceStorage ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            gatherers = line.variants[0].line\n\n        else:\n            gatherers = [line.line[0]]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n        api_objects = dataset.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        gather_lookup_dict = internal_name_lookups.get_gather_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.ResourceStorage\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"ResourceStorage\",\n                                              dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.ResourceStorage\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Create containers\n        containers = []\n        for gatherer in gatherers:\n            unit_commands = gatherer[\"unit_commands\"].value\n            resource = None\n\n            used_command = None\n            for command in unit_commands:\n                # Find a gather ability. It doesn't matter which one because\n                # they should all produce the same resource for one genie unit.\n                type_id = command[\"type\"].value\n\n                if type_id not in (5, 110, 111):\n                    continue\n\n                resource_id = command[\"resource_out\"].value\n\n                # If resource_out is not specified, the gatherer harvests resource_in\n                if resource_id == -1:\n                    resource_id = command[\"resource_in\"].value\n\n                if resource_id == 0:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object(\n                    )\n\n                elif resource_id == 1:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object(\n                    )\n\n                elif resource_id == 2:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object(\n                    )\n\n                elif resource_id == 3:\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object(\n                    )\n\n                elif type_id == 111:\n                    target_id = command[\"unit_id\"].value\n                    if target_id not in dataset.building_lines.keys():\n                        # Skips the trade workshop trading which is never used\n                        continue\n\n                    # Trade goods --> nova\n                    resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object(\n                    )\n\n                else:\n                    continue\n\n                used_command = command\n\n            if not used_command:\n                # The unit uses no gathering command or we don't recognize it\n                continue\n\n            if line.is_gatherer():\n                gatherer_unit_id = gatherer.get_id()\n                if gatherer_unit_id not in gather_lookup_dict:\n                    # Skips hunting wolves\n                    continue\n\n                container_name = f\"{gather_lookup_dict[gatherer_unit_id][0]}Container\"\n\n            elif used_command[\"type\"].value == 111:\n                # Trading\n                container_name = \"TradeContainer\"\n\n            container_ref = f\"{ability_ref}.{container_name}\"\n            container_raw_api_object = RawAPIObject(\n                container_ref, container_name, dataset.nyan_api_objects)\n            container_raw_api_object.add_raw_parent(\"engine.util.storage.ResourceContainer\")\n            container_location = ForwardRef(line, ability_ref)\n            container_raw_api_object.set_location(container_location)\n\n            line.add_raw_api_object(container_raw_api_object)\n\n            # Resource\n            container_raw_api_object.add_raw_member(\"resource\",\n                                                    resource,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            # Carry capacity\n            carry_capacity = gatherer[\"resource_capacity\"].value\n            container_raw_api_object.add_raw_member(\"max_amount\",\n                                                    carry_capacity,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            # Carry progress\n            carry_progress = []\n            carry_move_animation_id = used_command[\"carry_sprite_id\"].value\n            if carry_move_animation_id > -1:\n                # ===========================================================================================\n                progress_ref = f\"{ability_ref}.{container_name}CarryProgress\"\n                progress_raw_api_object = RawAPIObject(progress_ref,\n                                                       f\"{container_name}CarryProgress\",\n                                                       dataset.nyan_api_objects)\n                progress_raw_api_object.add_raw_parent(\"engine.util.progress.Progress\")\n                progress_location = ForwardRef(line, container_ref)\n                progress_raw_api_object.set_location(progress_location)\n\n                line.add_raw_api_object(progress_raw_api_object)\n\n                # Type\n                progress_raw_api_object.add_raw_member(\"type\",\n                                                       api_objects[\"engine.util.progress_type.type.Carry\"],\n                                                       \"engine.util.progress.Progress\")\n\n                # Interval = (20.0, 100.0)\n                progress_raw_api_object.add_raw_member(\"left_boundary\",\n                                                       20.0,\n                                                       \"engine.util.progress.Progress\")\n                progress_raw_api_object.add_raw_member(\"right_boundary\",\n                                                       100.0,\n                                                       \"engine.util.progress.Progress\")\n\n                # Progress properties\n                properties = {}\n                # =================================================================================\n                # Animated property (animation overrides)\n                # =================================================================================\n                property_ref = f\"{progress_ref}.Animated\"\n                property_raw_api_object = RawAPIObject(property_ref,\n                                                       \"Animated\",\n                                                       dataset.nyan_api_objects)\n                property_raw_api_object.add_raw_parent(\n                    \"engine.util.progress.property.type.Animated\")\n                property_location = ForwardRef(line, progress_ref)\n                property_raw_api_object.set_location(property_location)\n\n                line.add_raw_api_object(property_raw_api_object)\n                # =================================================================================\n                overrides = []\n                # =================================================================================\n                # Move override\n                # =================================================================================\n                override_ref = f\"{property_ref}.MoveOverride\"\n                override_raw_api_object = RawAPIObject(override_ref,\n                                                       \"MoveOverride\",\n                                                       dataset.nyan_api_objects)\n                override_raw_api_object.add_raw_parent(\n                    \"engine.util.animation_override.AnimationOverride\")\n                override_location = ForwardRef(line, property_ref)\n                override_raw_api_object.set_location(override_location)\n\n                line.add_raw_api_object(override_raw_api_object)\n\n                move_forward_ref = ForwardRef(line, f\"{game_entity_name}.Move\")\n                override_raw_api_object.add_raw_member(\"ability\",\n                                                       move_forward_ref,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                # Animation\n                animations_set = []\n                animation_forward_ref = AoCAbilitySubprocessor.create_animation(line,\n                                                                                carry_move_animation_id,\n                                                                                override_ref,\n                                                                                \"Move\",\n                                                                                \"move_carry_override_\")\n\n                animations_set.append(animation_forward_ref)\n                override_raw_api_object.add_raw_member(\"animations\",\n                                                       animations_set,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_raw_api_object.add_raw_member(\"priority\",\n                                                       1,\n                                                       \"engine.util.animation_override.AnimationOverride\")\n\n                override_forward_ref = ForwardRef(line, override_ref)\n                overrides.append(override_forward_ref)\n                # =================================================================================\n                property_raw_api_object.add_raw_member(\"overrides\",\n                                                       overrides,\n                                                       \"engine.util.progress.property.type.Animated\")\n\n                property_forward_ref = ForwardRef(line, property_ref)\n\n                properties.update({\n                    api_objects[\"engine.util.progress.property.type.Animated\"]: property_forward_ref\n                })\n                # =================================================================================\n                progress_raw_api_object.add_raw_member(\"properties\",\n                                                       properties,\n                                                       \"engine.util.progress.Progress\")\n                # =================================================================================\n                progress_forward_ref = ForwardRef(line, progress_ref)\n                carry_progress.append(progress_forward_ref)\n\n            container_raw_api_object.add_raw_member(\"carry_progress\",\n                                                    carry_progress,\n                                                    \"engine.util.storage.ResourceContainer\")\n\n            container_forward_ref = ForwardRef(line, container_ref)\n            containers.append(container_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"containers\",\n                                              containers,\n                                              \"engine.ability.type.ResourceStorage\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def restock_ability(line: GenieGameEntityGroup, restock_target_id: int) -> ForwardRef:\n        \"\"\"\n        Adds the Restock ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.restock_ability(line, restock_target_id)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def selectable_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds Selectable abilities to a line. Units will get two of these,\n        one Rectangle box for the Self stance and one MatchToSprite box\n        for other stances.\n\n        :param line: Unit/Building line that gets the abilities.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the abilities.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.selectable_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def send_back_to_task_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the SendBackToTask ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        ability_ref = f\"{game_entity_name}.SendBackToTask\"\n        ability_raw_api_object = RawAPIObject(\n            ability_ref, \"SendBackToTask\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.SendBackToTask\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Only works on villagers\n        allowed_types = [\n            dataset.pregen_nyan_objects[\"util.game_entity_type.types.Worker\"].get_nyan_object()]\n        ability_raw_api_object.add_raw_member(\"allowed_types\",\n                                              allowed_types,\n                                              \"engine.ability.type.SendBackToTask\")\n        ability_raw_api_object.add_raw_member(\"blacklisted_entities\",\n                                              [],\n                                              \"engine.ability.type.SendBackToTask\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def shoot_projectile_ability(line: GenieGameEntityGroup, command_id: int) -> ForwardRef:\n        \"\"\"\n        Adds the ShootProjectile ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.shoot_projectile_ability(line, command_id)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n\n    @staticmethod\n    def trade_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Trade ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.Trade\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"Trade\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.Trade\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Trade route (use the trade route o the market)\n        trade_routes = []\n\n        unit_commands = current_unit[\"unit_commands\"].value\n        for command in unit_commands:\n            # Find the trade command and the trade post id\n            type_id = command[\"type\"].value\n\n            if type_id != 111:\n                continue\n\n            trade_post_id = command[\"unit_id\"].value\n            if trade_post_id == 530:\n                # Ignore Tattoine Spaceport\n                continue\n\n            trade_post_line = dataset.building_lines[trade_post_id]\n            trade_post_name = name_lookup_dict[trade_post_id][0]\n\n            trade_route_ref = f\"{trade_post_name}.TradePost.AoE2{trade_post_name}TradeRoute\"\n            trade_route_forward_ref = ForwardRef(trade_post_line, trade_route_ref)\n            trade_routes.append(trade_route_forward_ref)\n\n        ability_raw_api_object.add_raw_member(\"trade_routes\",\n                                              trade_routes,\n                                              \"engine.ability.type.Trade\")\n\n        # container\n        container_forward_ref = ForwardRef(\n            line, f\"{game_entity_name}.ResourceStorage.TradeContainer\")\n        ability_raw_api_object.add_raw_member(\"container\",\n                                              container_forward_ref,\n                                              \"engine.ability.type.Trade\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def trade_post_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the TradePost ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        ability_ref = f\"{game_entity_name}.TradePost\"\n        ability_raw_api_object = RawAPIObject(ability_ref, \"TradePost\", dataset.nyan_api_objects)\n        ability_raw_api_object.add_raw_parent(\"engine.ability.type.TradePost\")\n        ability_location = ForwardRef(line, game_entity_name)\n        ability_raw_api_object.set_location(ability_location)\n\n        # Trade route\n        trade_routes = []\n        # =====================================================================================\n        trade_route_name = f\"AoE2{game_entity_name}TradeRoute\"\n        trade_route_ref = f\"{game_entity_name}.TradePost.{trade_route_name}\"\n        trade_route_raw_api_object = RawAPIObject(trade_route_ref,\n                                                  trade_route_name,\n                                                  dataset.nyan_api_objects)\n        trade_route_raw_api_object.add_raw_parent(\"engine.util.trade_route.type.AoE2TradeRoute\")\n        trade_route_location = ForwardRef(line, ability_ref)\n        trade_route_raw_api_object.set_location(trade_route_location)\n\n        # Trade resource\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n        trade_route_raw_api_object.add_raw_member(\"trade_resource\",\n                                                  resource,\n                                                  \"engine.util.trade_route.TradeRoute\")\n\n        # Start- and endpoints\n        market_forward_ref = ForwardRef(line, game_entity_name)\n        trade_route_raw_api_object.add_raw_member(\"start_trade_post\",\n                                                  market_forward_ref,\n                                                  \"engine.util.trade_route.TradeRoute\")\n        trade_route_raw_api_object.add_raw_member(\"end_trade_post\",\n                                                  market_forward_ref,\n                                                  \"engine.util.trade_route.TradeRoute\")\n\n        trade_route_forward_ref = ForwardRef(line, trade_route_ref)\n        trade_routes.append(trade_route_forward_ref)\n\n        line.add_raw_api_object(trade_route_raw_api_object)\n        # =====================================================================================\n        ability_raw_api_object.add_raw_member(\"trade_routes\",\n                                              trade_routes,\n                                              \"engine.ability.type.TradePost\")\n\n        line.add_raw_api_object(ability_raw_api_object)\n\n        ability_forward_ref = ForwardRef(line, ability_raw_api_object.get_id())\n\n        return ability_forward_ref\n\n    @staticmethod\n    def turn_ability(line: GenieGameEntityGroup) -> ForwardRef:\n        \"\"\"\n        Adds the Turn ability to a line.\n\n        :param line: Unit/Building line that gets the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :returns: The forward reference for the ability.\n        :rtype: ...dataformat.forward_ref.ForwardRef\n        \"\"\"\n        ability_forward_ref = AoCAbilitySubprocessor.turn_ability(line)\n\n        # TODO: Implement diffing of civ lines\n\n        return ability_forward_ref\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/auxiliary_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches,too-many-statements\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nDerives complex auxiliary objects from unit lines, techs\nor other objects.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieBuildingLineGroup, GenieUnitLineGroup\nfrom ....entity_object.conversion.combined_sound import CombinedSound\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.auxiliary_subprocessor import AoCAuxiliarySubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n\n\nclass SWGBCCAuxiliarySubprocessor:\n    \"\"\"\n    Creates complexer auxiliary raw API objects for abilities in SWGB.\n    \"\"\"\n\n    @staticmethod\n    def get_creatable_game_entity(line: GenieGameEntityGroup) -> None:\n        \"\"\"\n        Creates the CreatableGameEntity object for a unit/building line.\n\n        :param line: Unit/Building line.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        if isinstance(line, GenieVillagerGroup):\n            current_unit = line.variants[0].line[0]\n\n        else:\n            current_unit = line.line[0]\n\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n\n        obj_ref = f\"{game_entity_name}.CreatableGameEntity\"\n        obj_name = f\"{game_entity_name}Creatable\"\n        creatable_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n        creatable_raw_api_object.add_raw_parent(\"engine.util.create.CreatableGameEntity\")\n\n        # Get train location of line\n        train_location_id = line.get_train_location_id()\n        if isinstance(line, GenieBuildingLineGroup):\n            train_location = dataset.unit_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        else:\n            train_location = dataset.building_lines[train_location_id]\n            train_location_name = name_lookup_dict[train_location_id][0]\n\n        # Location of the object depends on whether it'a a unique unit or a normal unit\n        if line.is_unique():\n            # Add object to the Civ object\n            enabling_research_id = line.get_enabling_research_id()\n            enabling_research = dataset.genie_techs[enabling_research_id]\n            enabling_civ_id = enabling_research[\"civilization_id\"].value\n\n            civ = dataset.civ_groups[enabling_civ_id]\n            civ_name = civ_lookup_dict[enabling_civ_id][0]\n\n            creatable_location = ForwardRef(civ, civ_name)\n\n        else:\n            # Add object to the train location's Create ability\n            creatable_location = ForwardRef(train_location,\n                                            f\"{train_location_name}.Create\")\n\n        creatable_raw_api_object.set_location(creatable_location)\n\n        # Game Entity\n        game_entity_forward_ref = ForwardRef(line, game_entity_name)\n        creatable_raw_api_object.add_raw_member(\"game_entity\",\n                                                game_entity_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # TODO: Variants\n        variants_set = []\n\n        creatable_raw_api_object.add_raw_member(\"variants\", variants_set,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Cost (construction)\n        cost_name = f\"{game_entity_name}.CreatableGameEntity.{game_entity_name}Cost\"\n        cost_raw_api_object = RawAPIObject(cost_name,\n                                           f\"{game_entity_name}Cost\",\n                                           dataset.nyan_api_objects)\n        cost_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n        creatable_forward_ref = ForwardRef(line, obj_ref)\n        cost_raw_api_object.set_location(creatable_forward_ref)\n\n        payment_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Advance\"]\n        cost_raw_api_object.add_raw_member(\"payment_mode\",\n                                           payment_mode,\n                                           \"engine.util.cost.Cost\")\n\n        if line.is_repairable():\n            # Cost (repair) for buildings\n            cost_repair_name = (f\"{game_entity_name}.CreatableGameEntity.\"\n                                f\"{game_entity_name}RepairCost\")\n            cost_repair_raw_api_object = RawAPIObject(cost_repair_name,\n                                                      f\"{game_entity_name}RepairCost\",\n                                                      dataset.nyan_api_objects)\n            cost_repair_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n            creatable_forward_ref = ForwardRef(line, obj_ref)\n            cost_repair_raw_api_object.set_location(creatable_forward_ref)\n\n            payment_repair_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Adaptive\"]\n            cost_repair_raw_api_object.add_raw_member(\"payment_mode\",\n                                                      payment_repair_mode,\n                                                      \"engine.util.cost.Cost\")\n            line.add_raw_api_object(cost_repair_raw_api_object)\n\n        cost_amounts = []\n        cost_repair_amounts = []\n        for resource_amount in current_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            resource = None\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object(\n                )\n                resource_name = \"Carbon\"\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object()\n                resource_name = \"Ore\"\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n                resource_name = \"Nova\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            amount = resource_amount[\"amount\"].value\n\n            cost_amount_name = f\"{cost_name}.{resource_name}Amount\"\n            cost_amount = RawAPIObject(cost_amount_name,\n                                       f\"{resource_name}Amount\",\n                                       dataset.nyan_api_objects)\n            cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            cost_forward_ref = ForwardRef(line, cost_name)\n            cost_amount.set_location(cost_forward_ref)\n\n            cost_amount.add_raw_member(\"type\",\n                                       resource,\n                                       \"engine.util.resource.ResourceAmount\")\n            cost_amount.add_raw_member(\"amount\",\n                                       amount,\n                                       \"engine.util.resource.ResourceAmount\")\n\n            cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n            cost_amounts.append(cost_amount_forward_ref)\n            line.add_raw_api_object(cost_amount)\n\n            if line.is_repairable():\n                # Cost for repairing = half of the construction cost\n                cost_amount_name = f\"{cost_repair_name}.{resource_name}Amount\"\n                cost_amount = RawAPIObject(cost_amount_name,\n                                           f\"{resource_name}Amount\",\n                                           dataset.nyan_api_objects)\n                cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n                cost_forward_ref = ForwardRef(line, cost_repair_name)\n                cost_amount.set_location(cost_forward_ref)\n\n                cost_amount.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n                cost_amount.add_raw_member(\"amount\",\n                                           amount / 2,\n                                           \"engine.util.resource.ResourceAmount\")\n\n                cost_amount_forward_ref = ForwardRef(line, cost_amount_name)\n                cost_repair_amounts.append(cost_amount_forward_ref)\n                line.add_raw_api_object(cost_amount)\n\n        cost_raw_api_object.add_raw_member(\"amount\",\n                                           cost_amounts,\n                                           \"engine.util.cost.type.ResourceCost\")\n\n        if line.is_repairable():\n            cost_repair_raw_api_object.add_raw_member(\"amount\",\n                                                      cost_repair_amounts,\n                                                      \"engine.util.cost.type.ResourceCost\")\n\n        cost_forward_ref = ForwardRef(line, cost_name)\n        creatable_raw_api_object.add_raw_member(\"cost\",\n                                                cost_forward_ref,\n                                                \"engine.util.create.CreatableGameEntity\")\n        # Creation time\n        if isinstance(line, GenieUnitLineGroup):\n            creation_time = current_unit[\"creation_time\"].value\n\n        else:\n            # Buildings are created immediately\n            creation_time = 0\n\n        creatable_raw_api_object.add_raw_member(\"creation_time\",\n                                                creation_time,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Creation sound\n        creation_sound_id = current_unit[\"train_sound_id\"].value\n\n        # Create sound object\n        obj_name = f\"{game_entity_name}.CreatableGameEntity.Sound\"\n        sound_raw_api_object = RawAPIObject(obj_name, \"CreationSound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(line, obj_ref)\n        sound_raw_api_object.set_location(sound_location)\n\n        # Search for the sound if it exists\n        creation_sounds = []\n        if creation_sound_id > -1:\n            genie_sound = dataset.genie_sounds[creation_sound_id]\n            file_ids = genie_sound.get_sounds(civ_id=-1)\n\n            if file_ids:\n                file_id = genie_sound.get_sounds(civ_id=-1)[0]\n\n                if file_id in dataset.combined_sounds:\n                    creation_sound = dataset.combined_sounds[file_id]\n                    creation_sound.add_reference(sound_raw_api_object)\n\n                else:\n                    creation_sound = CombinedSound(creation_sound_id,\n                                                   file_id,\n                                                   f\"creation_sound_{creation_sound_id}\",\n                                                   dataset)\n                    dataset.combined_sounds.update({file_id: creation_sound})\n                    creation_sound.add_reference(sound_raw_api_object)\n\n                creation_sounds.append(creation_sound)\n\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            creation_sounds,\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(line, obj_name)\n        creatable_raw_api_object.add_raw_member(\"creation_sounds\",\n                                                [sound_forward_ref],\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(sound_raw_api_object)\n\n        # Condition\n        unlock_conditions = []\n        enabling_research_id = line.get_enabling_research_id()\n        if enabling_research_id > -1:\n            unlock_conditions.extend(AoCAuxiliarySubprocessor.get_condition(line,\n                                                                            obj_ref,\n                                                                            enabling_research_id))\n\n        creatable_raw_api_object.add_raw_member(\"condition\",\n                                                unlock_conditions,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        # Placement modes\n        placement_modes = []\n        if isinstance(line, GenieBuildingLineGroup):\n            # Buildings are placed on the map\n            # Place mode\n            obj_name = f\"{game_entity_name}.CreatableGameEntity.Place\"\n            place_raw_api_object = RawAPIObject(obj_name,\n                                                \"Place\",\n                                                dataset.nyan_api_objects)\n            place_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.Place\")\n            place_location = ForwardRef(line,\n                                        f\"{game_entity_name}.CreatableGameEntity\")\n            place_raw_api_object.set_location(place_location)\n\n            # Tile snap distance (uses 1.0 for grid placement)\n            place_raw_api_object.add_raw_member(\"tile_snap_distance\",\n                                                1.0,\n                                                \"engine.util.placement_mode.type.Place\")\n            # Clearance size\n            clearance_size_x = current_unit[\"clearance_size_x\"].value\n            clearance_size_y = current_unit[\"clearance_size_y\"].value\n            place_raw_api_object.add_raw_member(\"clearance_size_x\",\n                                                clearance_size_x,\n                                                \"engine.util.placement_mode.type.Place\")\n            place_raw_api_object.add_raw_member(\"clearance_size_y\",\n                                                clearance_size_y,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Allow rotation\n            place_raw_api_object.add_raw_member(\"allow_rotation\",\n                                                True,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            # Max elevation difference\n            elevation_mode = current_unit[\"elevation_mode\"].value\n            if elevation_mode == 2:\n                max_elevation_difference = 0\n\n            elif elevation_mode == 3:\n                max_elevation_difference = 1\n\n            else:\n                max_elevation_difference = MemberSpecialValue.NYAN_INF\n\n            place_raw_api_object.add_raw_member(\"max_elevation_difference\",\n                                                max_elevation_difference,\n                                                \"engine.util.placement_mode.type.Place\")\n\n            line.add_raw_api_object(place_raw_api_object)\n\n            place_forward_ref = ForwardRef(line, obj_name)\n            placement_modes.append(place_forward_ref)\n\n            if line.get_class_id() == 39:\n                # Gates\n                obj_name = f\"{game_entity_name}.CreatableGameEntity.Replace\"\n                replace_raw_api_object = RawAPIObject(obj_name,\n                                                      \"Replace\",\n                                                      dataset.nyan_api_objects)\n                replace_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.Replace\")\n                replace_location = ForwardRef(line,\n                                              f\"{game_entity_name}.CreatableGameEntity\")\n                replace_raw_api_object.set_location(replace_location)\n\n                # Game entities (only stone wall)\n                wall_line_id = 117\n                wall_line = dataset.building_lines[wall_line_id]\n                wall_name = name_lookup_dict[117][0]\n                game_entities = [ForwardRef(wall_line, wall_name)]\n                replace_raw_api_object.add_raw_member(\"game_entities\",\n                                                      game_entities,\n                                                      \"engine.util.placement_mode.type.Replace\")\n\n                line.add_raw_api_object(replace_raw_api_object)\n\n                replace_forward_ref = ForwardRef(line, obj_name)\n                placement_modes.append(replace_forward_ref)\n\n        else:\n            placement_modes.append(\n                dataset.nyan_api_objects[\"engine.util.placement_mode.type.Eject\"])\n\n            # OwnStorage mode\n            obj_name = f\"{game_entity_name}.CreatableGameEntity.OwnStorage\"\n            own_storage_raw_api_object = RawAPIObject(obj_name, \"OwnStorage\",\n                                                      dataset.nyan_api_objects)\n            own_storage_raw_api_object.add_raw_parent(\"engine.util.placement_mode.type.OwnStorage\")\n            own_storage_location = ForwardRef(line,\n                                              f\"{game_entity_name}.CreatableGameEntity\")\n            own_storage_raw_api_object.set_location(own_storage_location)\n\n            # Container\n            container_forward_ref = ForwardRef(train_location,\n                                               (f\"{train_location_name}.Storage.\"\n                                                f\"{train_location_name}Container\"))\n            own_storage_raw_api_object.add_raw_member(\"container\",\n                                                      container_forward_ref,\n                                                      \"engine.util.placement_mode.type.OwnStorage\")\n\n            line.add_raw_api_object(own_storage_raw_api_object)\n\n            own_storage_forward_ref = ForwardRef(line, obj_name)\n            placement_modes.append(own_storage_forward_ref)\n\n        creatable_raw_api_object.add_raw_member(\"placement_modes\",\n                                                placement_modes,\n                                                \"engine.util.create.CreatableGameEntity\")\n\n        line.add_raw_api_object(creatable_raw_api_object)\n        line.add_raw_api_object(cost_raw_api_object)\n\n    @staticmethod\n    def get_researchable_tech(tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Creates the ResearchableTech object for a Tech.\n\n        :param tech_group: Tech group that is a technology.\n        :type tech_group: ...dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        dataset = tech_group.data\n        research_location_id = tech_group.get_research_location_id()\n        research_location = dataset.building_lines[research_location_id]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        research_location_name = name_lookup_dict[research_location_id][0]\n        tech_name = tech_lookup_dict[tech_group.get_id()][0]\n\n        obj_ref = f\"{tech_name}.ResearchableTech\"\n        obj_name = f\"{tech_name}Researchable\"\n        researchable_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n        researchable_raw_api_object.add_raw_parent(\"engine.util.research.ResearchableTech\")\n\n        # Location of the object depends on whether it'a a unique tech or a normal tech\n        if tech_group.is_unique():\n            # Add object to the Civ object\n            civ_id = tech_group.get_civilization()\n            civ = dataset.civ_groups[civ_id]\n            civ_name = civ_lookup_dict[civ_id][0]\n\n            researchable_location = ForwardRef(civ, civ_name)\n\n        else:\n            # Add object to the research location's Research ability\n            researchable_location = ForwardRef(research_location,\n                                               f\"{research_location_name}.Research\")\n\n        researchable_raw_api_object.set_location(researchable_location)\n\n        # Tech\n        tech_forward_ref = ForwardRef(tech_group, tech_name)\n        researchable_raw_api_object.add_raw_member(\"tech\",\n                                                   tech_forward_ref,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        # Cost\n        cost_ref = f\"{tech_name}.ResearchableTech.{tech_name}Cost\"\n        cost_raw_api_object = RawAPIObject(cost_ref,\n                                           f\"{tech_name}Cost\",\n                                           dataset.nyan_api_objects)\n        cost_raw_api_object.add_raw_parent(\"engine.util.cost.type.ResourceCost\")\n        tech_forward_ref = ForwardRef(tech_group, obj_ref)\n        cost_raw_api_object.set_location(tech_forward_ref)\n\n        payment_mode = dataset.nyan_api_objects[\"engine.util.payment_mode.type.Advance\"]\n        cost_raw_api_object.add_raw_member(\"payment_mode\",\n                                           payment_mode,\n                                           \"engine.util.cost.Cost\")\n\n        cost_amounts = []\n        for resource_amount in tech_group.tech[\"research_resource_costs\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n\n            resource = None\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object(\n                )\n                resource_name = \"Carbon\"\n\n            elif resource_id == 2:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object()\n                resource_name = \"Ore\"\n\n            elif resource_id == 3:\n                resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n                resource_name = \"Nova\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            amount = resource_amount[\"amount\"].value\n\n            cost_amount_ref = f\"{cost_ref}.{resource_name}Amount\"\n            cost_amount = RawAPIObject(cost_amount_ref,\n                                       f\"{resource_name}Amount\",\n                                       dataset.nyan_api_objects)\n            cost_amount.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n            cost_forward_ref = ForwardRef(tech_group, cost_ref)\n            cost_amount.set_location(cost_forward_ref)\n\n            cost_amount.add_raw_member(\"type\",\n                                       resource,\n                                       \"engine.util.resource.ResourceAmount\")\n            cost_amount.add_raw_member(\"amount\",\n                                       amount,\n                                       \"engine.util.resource.ResourceAmount\")\n\n            cost_amount_forward_ref = ForwardRef(tech_group, cost_amount_ref)\n            cost_amounts.append(cost_amount_forward_ref)\n            tech_group.add_raw_api_object(cost_amount)\n\n        cost_raw_api_object.add_raw_member(\"amount\",\n                                           cost_amounts,\n                                           \"engine.util.cost.type.ResourceCost\")\n\n        cost_forward_ref = ForwardRef(tech_group, cost_ref)\n        researchable_raw_api_object.add_raw_member(\"cost\",\n                                                   cost_forward_ref,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        research_time = tech_group.tech[\"research_time\"].value\n\n        researchable_raw_api_object.add_raw_member(\"research_time\",\n                                                   research_time,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        # Create sound object\n        sound_ref = f\"{tech_name}.ResearchableTech.Sound\"\n        sound_raw_api_object = RawAPIObject(sound_ref, \"ResearchSound\",\n                                            dataset.nyan_api_objects)\n        sound_raw_api_object.add_raw_parent(\"engine.util.sound.Sound\")\n        sound_location = ForwardRef(tech_group,\n                                    f\"{tech_name}.ResearchableTech\")\n        sound_raw_api_object.set_location(sound_location)\n\n        # AoE doesn't support sounds here, so this is empty\n        sound_raw_api_object.add_raw_member(\"play_delay\",\n                                            0,\n                                            \"engine.util.sound.Sound\")\n        sound_raw_api_object.add_raw_member(\"sounds\",\n                                            [],\n                                            \"engine.util.sound.Sound\")\n\n        sound_forward_ref = ForwardRef(tech_group, sound_ref)\n        researchable_raw_api_object.add_raw_member(\"research_sounds\",\n                                                   [sound_forward_ref],\n                                                   \"engine.util.research.ResearchableTech\")\n\n        tech_group.add_raw_api_object(sound_raw_api_object)\n\n        # Condition\n        unlock_conditions = []\n        if tech_group.get_id() > -1:\n            unlock_conditions.extend(AoCAuxiliarySubprocessor.get_condition(tech_group,\n                                                                            obj_ref,\n                                                                            tech_group.get_id(),\n                                                                            top_level=True))\n\n        researchable_raw_api_object.add_raw_member(\"condition\",\n                                                   unlock_conditions,\n                                                   \"engine.util.research.ResearchableTech\")\n\n        tech_group.add_raw_api_object(researchable_raw_api_object)\n        tech_group.add_raw_api_object(cost_raw_api_object)\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/civ_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements,too-many-branches\n\n\"\"\"\nCreates patches and modifiers for civs.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.civ_subprocessor import AoCCivSubprocessor\nfrom .tech_subprocessor import SWGBCCTechSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n\n\nclass SWGBCCCivSubprocessor:\n    \"\"\"\n    Creates raw API objects for civs in SWGB.\n    \"\"\"\n\n    @classmethod\n    def get_civ_setup(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for the civ setup which configures architecture sets\n        unique units, unique techs, team boni and unique stat upgrades.\n        \"\"\"\n        patches = []\n\n        patches.extend(AoCCivSubprocessor.setup_unique_units(civ_group))\n        patches.extend(AoCCivSubprocessor.setup_unique_techs(civ_group))\n        patches.extend(AoCCivSubprocessor.setup_tech_tree(civ_group))\n        patches.extend(AoCCivSubprocessor.setup_civ_bonus(civ_group))\n\n        if len(civ_group.get_team_bonus_effects()) > 0:\n            patches.extend(SWGBCCTechSubprocessor.get_patches(civ_group.team_bonus))\n\n        return patches\n\n    @classmethod\n    def get_modifiers(cls, civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns global modifiers of a civ.\n        \"\"\"\n        modifiers = []\n\n        for civ_bonus in civ_group.civ_boni.values():\n            if civ_bonus.replaces_researchable_tech():\n                # TODO: instant tech research modifier\n                pass\n\n        return modifiers\n\n    @staticmethod\n    def get_starting_resources(civ_group: GenieCivilizationGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the starting resources of a civ.\n        \"\"\"\n        resource_amounts = []\n\n        civ_id = civ_group.get_id()\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        civ_name = civ_lookup_dict[civ_id][0]\n\n        # Find starting resource amounts\n        food_amount = civ_group.civ[\"resources\"][91].value\n        carbon_amount = civ_group.civ[\"resources\"][92].value\n        nova_amount = civ_group.civ[\"resources\"][93].value\n        ore_amount = civ_group.civ[\"resources\"][94].value\n\n        # Find civ unique starting resources\n        tech_tree = civ_group.get_tech_tree_effects()\n        for effect in tech_tree:\n            type_id = effect.get_type()\n\n            if type_id != 1:\n                continue\n\n            resource_id = effect[\"attr_a\"].value\n            amount = effect[\"attr_d\"].value\n            if resource_id == 91:\n                food_amount += amount\n\n            elif resource_id == 92:\n                carbon_amount += amount\n\n            elif resource_id == 93:\n                nova_amount += amount\n\n            elif resource_id == 94:\n                ore_amount += amount\n\n        food_ref = f\"{civ_name}.FoodStartingAmount\"\n        food_raw_api_object = RawAPIObject(food_ref, \"FoodStartingAmount\",\n                                           dataset.nyan_api_objects)\n        food_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        food_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Food\"].get_nyan_object()\n        food_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_raw_api_object.add_raw_member(\"amount\",\n                                           food_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        food_forward_ref = ForwardRef(civ_group, food_ref)\n        resource_amounts.append(food_forward_ref)\n\n        carbon_ref = f\"{civ_name}.CarbonStartingAmount\"\n        carbon_raw_api_object = RawAPIObject(carbon_ref, \"CarbonStartingAmount\",\n                                             dataset.nyan_api_objects)\n        carbon_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        carbon_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Carbon\"].get_nyan_object()\n        carbon_raw_api_object.add_raw_member(\"type\",\n                                             resource,\n                                             \"engine.util.resource.ResourceAmount\")\n\n        carbon_raw_api_object.add_raw_member(\"amount\",\n                                             carbon_amount,\n                                             \"engine.util.resource.ResourceAmount\")\n\n        carbon_forward_ref = ForwardRef(civ_group, carbon_ref)\n        resource_amounts.append(carbon_forward_ref)\n\n        nova_ref = f\"{civ_name}.NovaStartingAmount\"\n        nova_raw_api_object = RawAPIObject(nova_ref, \"NovaStartingAmount\",\n                                           dataset.nyan_api_objects)\n        nova_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        nova_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Nova\"].get_nyan_object()\n        nova_raw_api_object.add_raw_member(\"type\",\n                                           resource,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        nova_raw_api_object.add_raw_member(\"amount\",\n                                           nova_amount,\n                                           \"engine.util.resource.ResourceAmount\")\n\n        nova_forward_ref = ForwardRef(civ_group, nova_ref)\n        resource_amounts.append(nova_forward_ref)\n\n        ore_ref = f\"{civ_name}.OreStartingAmount\"\n        ore_raw_api_object = RawAPIObject(ore_ref, \"OreStartingAmount\",\n                                          dataset.nyan_api_objects)\n        ore_raw_api_object.add_raw_parent(\"engine.util.resource.ResourceAmount\")\n        civ_location = ForwardRef(civ_group, civ_lookup_dict[civ_group.get_id()][0])\n        ore_raw_api_object.set_location(civ_location)\n\n        resource = dataset.pregen_nyan_objects[\"util.resource.types.Ore\"].get_nyan_object()\n        ore_raw_api_object.add_raw_member(\"type\",\n                                          resource,\n                                          \"engine.util.resource.ResourceAmount\")\n\n        ore_raw_api_object.add_raw_member(\"amount\",\n                                          ore_amount,\n                                          \"engine.util.resource.ResourceAmount\")\n\n        ore_forward_ref = ForwardRef(civ_group, ore_ref)\n        resource_amounts.append(ore_forward_ref)\n\n        civ_group.add_raw_api_object(food_raw_api_object)\n        civ_group.add_raw_api_object(carbon_raw_api_object)\n        civ_group.add_raw_api_object(nova_raw_api_object)\n        civ_group.add_raw_api_object(ore_raw_api_object)\n\n        return resource_amounts\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/modpack_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nOrganize export data (nyan objects, media, scripts, etc.)\ninto modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.modpack import Modpack\nfrom ..aoc.modpack_subprocessor import AoCModpackSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container\\\n        import GenieObjectContainer\n\n\nclass SWGBCCModpackSubprocessor:\n    \"\"\"\n    Creates the modpacks containing the nyan files and media from the SWGB conversion.\n    \"\"\"\n\n    @classmethod\n    def get_modpacks(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Return all modpacks that can be created from the gamedata.\n        \"\"\"\n        swgb_base = cls._get_swgb_base(full_data_set)\n\n        return [swgb_base]\n\n    @classmethod\n    def _get_swgb_base(cls, full_data_set: GenieObjectContainer) -> Modpack:\n        \"\"\"\n        Create the swgb-base modpack.\n        \"\"\"\n        modpack = Modpack(\"swgb_base\")\n\n        mod_def = modpack.get_info()\n\n        targetmod_info = full_data_set.game_version.edition.target_modpacks[\"swgb_base\"]\n        version = targetmod_info[\"version\"]\n        versionstr = targetmod_info[\"versionstr\"]\n        mod_def.set_info(\"swgb_base\", version, versionstr=versionstr, repo=\"openage\")\n\n        mod_def.add_include(\"data/**\")\n\n        AoCModpackSubprocessor.organize_nyan_objects(modpack, full_data_set)\n        AoCModpackSubprocessor.organize_media_objects(modpack, full_data_set)\n\n        return modpack\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/nyan_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-locals,too-many-statements,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nConvert API-like objects to nyan objects. Subroutine of the\nmain SWGB processor. Reuses functionality from the AoC subprocessor.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_tech import UnitLineUpgrade\nfrom ....entity_object.conversion.aoc.genie_unit import GenieVillagerGroup, \\\n    GenieGarrisonMode, GenieMonkGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.ability_subprocessor import AoCAbilitySubprocessor\nfrom ..aoc.nyan_subprocessor import AoCNyanSubprocessor\nfrom .ability_subprocessor import SWGBCCAbilitySubprocessor\nfrom .auxiliary_subprocessor import SWGBCCAuxiliarySubprocessor\nfrom .civ_subprocessor import SWGBCCCivSubprocessor\nfrom .tech_subprocessor import SWGBCCTechSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_civ import GenieCivilizationGroup\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n    from openage.convert.entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n        GenieBuildingLineGroup, GenieAmbientGroup\n\n\nclass SWGBCCNyanSubprocessor:\n    \"\"\"\n    Transform an SWGB dataset to nyan objects.\n    \"\"\"\n\n    @classmethod\n    def convert(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects from the given dataset.\n        \"\"\"\n        cls._process_game_entities(full_data_set)\n        cls._create_nyan_objects(full_data_set)\n        cls._create_nyan_members(full_data_set)\n\n        cls._check_objects(full_data_set)\n\n    @classmethod\n    def _check_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Check if objects are valid.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.check_readiness()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.check_readiness()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.check_readiness()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.check_readiness()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.check_readiness()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.check_readiness()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.check_readiness()\n\n    @classmethod\n    def _create_nyan_objects(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Creates nyan objects from the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_objects()\n            unit_line.execute_raw_member_pushs()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_objects()\n            building_line.execute_raw_member_pushs()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_objects()\n            ambient_group.execute_raw_member_pushs()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_objects()\n            variant_group.execute_raw_member_pushs()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_objects()\n            tech_group.execute_raw_member_pushs()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_objects()\n            terrain_group.execute_raw_member_pushs()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_objects()\n            civ_group.execute_raw_member_pushs()\n\n    @classmethod\n    def _create_nyan_members(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Fill nyan member values of the API objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            unit_line.create_nyan_members()\n\n        for building_line in full_data_set.building_lines.values():\n            building_line.create_nyan_members()\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            ambient_group.create_nyan_members()\n\n        for variant_group in full_data_set.variant_groups.values():\n            variant_group.create_nyan_members()\n\n        for tech_group in full_data_set.tech_groups.values():\n            tech_group.create_nyan_members()\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            terrain_group.create_nyan_members()\n\n        for civ_group in full_data_set.civ_groups.values():\n            civ_group.create_nyan_members()\n\n    @classmethod\n    def _process_game_entities(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create the RawAPIObject representation of the objects.\n        \"\"\"\n        for unit_line in full_data_set.unit_lines.values():\n            cls.unit_line_to_game_entity(unit_line)\n\n        for building_line in full_data_set.building_lines.values():\n            cls.building_line_to_game_entity(building_line)\n\n        for ambient_group in full_data_set.ambient_groups.values():\n            cls.ambient_group_to_game_entity(ambient_group)\n\n        for variant_group in full_data_set.variant_groups.values():\n            AoCNyanSubprocessor.variant_group_to_game_entity(variant_group)\n\n        for tech_group in full_data_set.tech_groups.values():\n            if tech_group.is_researchable():\n                cls.tech_group_to_tech(tech_group)\n\n        for terrain_group in full_data_set.terrain_groups.values():\n            AoCNyanSubprocessor.terrain_group_to_terrain(terrain_group)\n\n        for civ_group in full_data_set.civ_groups.values():\n            cls.civ_group_to_civ(civ_group)\n\n    @staticmethod\n    def unit_line_to_game_entity(unit_line: GenieUnitLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a unit line.\n\n        :param unit_line: Unit line that gets converted to a game entity.\n        :type unit_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n\n        current_unit = unit_line.get_head_unit()\n        current_unit_id = unit_line.get_head_unit_id()\n\n        dataset = unit_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_unit_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_unit_id][1])\n        unit_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a unit two types\n        #    - util.game_entity_type.types.Unit (if unit_type >= 70)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_unit[\"unit_type\"].value\n\n        if unit_type >= 70:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Unit\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(AoCAbilitySubprocessor.activity_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.death_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.idle_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.collision_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.live_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.los_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.move_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.named_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(unit_line))\n        abilities_set.extend(SWGBCCAbilitySubprocessor.selectable_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(unit_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.turn_ability(unit_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(unit_line))\n\n        # Creation\n        if len(unit_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(unit_line))\n\n        # Config\n        ability = AoCAbilitySubprocessor.use_contingent_ability(unit_line)\n        if ability:\n            abilities_set.append(ability)\n\n        if unit_line.get_head_unit_id() in (8, 115, 180):\n            # Healing/Recharging attribute points (jedi/sith/berserk)\n            abilities_set.extend(SWGBCCAbilitySubprocessor.regenerate_attribute_ability(unit_line))\n\n        # Applying effects and shooting projectiles\n        if unit_line.is_projectile_shooter():\n            abilities_set.append(SWGBCCAbilitySubprocessor.shoot_projectile_ability(unit_line, 7))\n            SWGBCCNyanSubprocessor.projectiles_from_line(unit_line)\n\n        elif unit_line.is_melee() or unit_line.is_ranged():\n            if unit_line.has_command(7):\n                # Attack\n                abilities_set.append(SWGBCCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                             7,\n                                                                                             unit_line.is_ranged()))\n\n            if unit_line.has_command(101):\n                # Build\n                abilities_set.append(SWGBCCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                               101,\n                                                                                               unit_line.is_ranged()))\n\n            if unit_line.has_command(104):\n                # Convert\n                abilities_set.append(SWGBCCAbilitySubprocessor.apply_discrete_effect_ability(unit_line,\n                                                                                             104,\n                                                                                             unit_line.is_ranged()))\n\n            if unit_line.has_command(105):\n                # Heal\n                abilities_set.append(SWGBCCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                               105,\n                                                                                               unit_line.is_ranged()))\n\n            if unit_line.has_command(106):\n                # Repair\n                abilities_set.append(SWGBCCAbilitySubprocessor.apply_continuous_effect_ability(unit_line,\n                                                                                               106,\n                                                                                               unit_line.is_ranged()))\n\n        # Formation/Stance\n        if not isinstance(unit_line, GenieVillagerGroup):\n            abilities_set.append(AoCAbilitySubprocessor.formation_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(unit_line))\n\n        # Storage abilities\n        if unit_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(unit_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(unit_line))\n\n            garrison_mode = unit_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.MONK:\n                abilities_set.append(AoCAbilitySubprocessor.collect_storage_ability(unit_line))\n\n        if len(unit_line.garrison_locations) > 0:\n            ability = AoCAbilitySubprocessor.enter_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n            ability = AoCAbilitySubprocessor.exit_container_ability(unit_line)\n            if ability:\n                abilities_set.append(ability)\n\n        if isinstance(unit_line, GenieMonkGroup):\n            abilities_set.append(AoCAbilitySubprocessor.transfer_storage_ability(unit_line))\n\n        # Resource abilities\n        if unit_line.is_gatherer():\n            abilities_set.append(AoCAbilitySubprocessor.drop_resources_ability(unit_line))\n            abilities_set.extend(SWGBCCAbilitySubprocessor.gather_ability(unit_line))\n\n        # Resource storage\n        if unit_line.is_gatherer() or unit_line.has_command(111):\n            abilities_set.append(SWGBCCAbilitySubprocessor.resource_storage_ability(unit_line))\n\n        if isinstance(unit_line, GenieVillagerGroup):\n            # Farm restocking\n            abilities_set.append(AoCAbilitySubprocessor.restock_ability(unit_line, 50))\n\n        if unit_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(unit_line))\n\n        if unit_type == 70 and unit_line.get_class_id() not in (1, 4, 5):\n            # Excludes cannons and animals\n            abilities_set.append(AoCAbilitySubprocessor.herd_ability(unit_line))\n\n        if unit_line.has_command(107):\n            abilities_set.append(AoCAbilitySubprocessor.herdable_ability(unit_line))\n\n        # Trade abilities\n        if unit_line.has_command(111):\n            abilities_set.append(SWGBCCAbilitySubprocessor.trade_ability(unit_line))\n\n        # =======================================================================\n        # TODO: Transform\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        variants_set = []\n\n        raw_api_object.add_raw_member(\"variants\", variants_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if unit_line.is_creatable():\n            SWGBCCAuxiliarySubprocessor.get_creatable_game_entity(unit_line)\n\n    @staticmethod\n    def building_line_to_game_entity(building_line: GenieBuildingLineGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a building line.\n\n        :param building_line: Building line that gets converted to a game entity.\n        :type building_line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_building = building_line.line[0]\n        current_building_id = building_line.get_head_unit_id()\n        dataset = building_line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[current_building_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[current_building_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[current_building_id][1])\n        building_line.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give a building two types\n        #    - util.game_entity_type.types.Building (if unit_type >= 80)\n        #    - util.game_entity_type.types.<Class> (depending on the class)\n        # and additionally\n        #    - util.game_entity_type.types.DropSite (only if this is used as a drop site)\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n        unit_type = current_building[\"unit_type\"].value\n\n        if unit_type >= 80:\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        unit_class = current_building[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        if building_line.is_dropsite():\n            type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.DropSite\"].get_nyan_object(\n            )\n            types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        abilities_set.append(\n            SWGBCCAbilitySubprocessor.attribute_change_tracker_ability(building_line))\n        abilities_set.append(SWGBCCAbilitySubprocessor.death_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.delete_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.despawn_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.idle_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.collision_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.live_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.los_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.named_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.resistance_ability(building_line))\n        abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.stop_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(building_line))\n        abilities_set.append(AoCAbilitySubprocessor.visibility_ability(building_line))\n\n        # Config abilities\n        # if building_line.is_creatable():\n        #     abilities_set.append(SWGBCCAbilitySubprocessor.constructable_ability(building_line))\n\n        if not building_line.is_passable():\n            abilities_set.append(AoCAbilitySubprocessor.pathable_ability(building_line))\n\n        if building_line.has_foundation():\n            if building_line.get_class_id() == 7:\n                # Use OverlayTerrain for the farm terrain\n                abilities_set.append(AoCAbilitySubprocessor.overlay_terrain_ability(building_line))\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line,\n                                                                               terrain_id=7))\n\n            else:\n                abilities_set.append(AoCAbilitySubprocessor.foundation_ability(building_line))\n\n        # Creation/Research abilities\n        if len(building_line.creates) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.create_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.production_queue_ability(building_line))\n\n        if len(building_line.researches) > 0:\n            abilities_set.append(AoCAbilitySubprocessor.research_ability(building_line))\n\n        # Effect abilities\n        if building_line.is_projectile_shooter():\n            abilities_set.append(AoCAbilitySubprocessor.shoot_projectile_ability(building_line, 7))\n            abilities_set.append(AoCAbilitySubprocessor.game_entity_stance_ability(building_line))\n            SWGBCCNyanSubprocessor.projectiles_from_line(building_line)\n\n        # Storage abilities\n        if building_line.is_garrison():\n            abilities_set.append(AoCAbilitySubprocessor.storage_ability(building_line))\n            abilities_set.append(AoCAbilitySubprocessor.remove_storage_ability(building_line))\n\n            garrison_mode = building_line.get_garrison_mode()\n\n            if garrison_mode == GenieGarrisonMode.NATURAL:\n                abilities_set.append(\n                    SWGBCCAbilitySubprocessor.send_back_to_task_ability(building_line))\n\n            if garrison_mode in (GenieGarrisonMode.NATURAL, GenieGarrisonMode.SELF_PRODUCED):\n                abilities_set.append(AoCAbilitySubprocessor.rally_point_ability(building_line))\n\n        # Resource abilities\n        if building_line.is_harvestable():\n            abilities_set.append(AoCAbilitySubprocessor.harvestable_ability(building_line))\n\n        if building_line.is_dropsite():\n            abilities_set.append(AoCAbilitySubprocessor.drop_site_ability(building_line))\n\n        ability = AoCAbilitySubprocessor.provide_contingent_ability(building_line)\n        if ability:\n            abilities_set.append(ability)\n\n        # Trade abilities\n        if building_line.is_trade_post():\n            abilities_set.append(SWGBCCAbilitySubprocessor.trade_post_ability(building_line))\n\n        if building_line.get_id() == 84:\n            # Spaceport trading\n            abilities_set.extend(\n                SWGBCCAbilitySubprocessor.exchange_resources_ability(building_line))\n\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        raw_api_object.add_raw_member(\"modifiers\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        raw_api_object.add_raw_member(\"variants\", [], \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the unit line itself, but use its values)\n        # =======================================================================\n        if building_line.is_creatable():\n            SWGBCCAuxiliarySubprocessor.get_creatable_game_entity(building_line)\n\n    @staticmethod\n    def ambient_group_to_game_entity(ambient_group: GenieAmbientGroup) -> None:\n        \"\"\"\n        Creates raw API objects for an ambient group.\n\n        :param ambient_group: Unit line that gets converted to a game entity.\n        :type ambient_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        ambient_unit = ambient_group.get_head_unit()\n        ambient_id = ambient_group.get_head_unit_id()\n\n        dataset = ambient_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        class_lookup_dict = internal_name_lookups.get_class_lookups(dataset.game_version)\n\n        # Start with the generic GameEntity\n        game_entity_name = name_lookup_dict[ambient_id][0]\n        obj_location = f\"data/game_entity/generic/{name_lookup_dict[ambient_id][1]}/\"\n        raw_api_object = RawAPIObject(game_entity_name, game_entity_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(name_lookup_dict[ambient_id][1])\n        ambient_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Game Entity Types\n        # =======================================================================\n        # we give an ambient the types\n        #    - util.game_entity_type.types.Ambient\n        # =======================================================================\n        # Create or use existing auxiliary types\n        types_set = []\n\n        type_obj = dataset.pregen_nyan_objects[\"util.game_entity_type.types.Ambient\"].get_nyan_object(\n        )\n        types_set.append(type_obj)\n\n        unit_class = ambient_unit[\"unit_class\"].value\n        class_name = class_lookup_dict[unit_class]\n        class_obj_name = f\"util.game_entity_type.types.{class_name}\"\n        type_obj = dataset.pregen_nyan_objects[class_obj_name].get_nyan_object()\n        types_set.append(type_obj)\n\n        raw_api_object.add_raw_member(\"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        abilities_set = []\n\n        interaction_mode = ambient_unit[\"interaction_mode\"].value\n\n        if interaction_mode >= 0:\n            abilities_set.append(AoCAbilitySubprocessor.death_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.collision_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.idle_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.live_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.named_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.resistance_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.terrain_requirement_ability(ambient_group))\n            abilities_set.append(AoCAbilitySubprocessor.visibility_ability(ambient_group))\n\n        if interaction_mode >= 2:\n            abilities_set.extend(AoCAbilitySubprocessor.selectable_ability(ambient_group))\n\n            if not ambient_group.is_passable():\n                abilities_set.append(AoCAbilitySubprocessor.pathable_ability(ambient_group))\n\n        if ambient_group.is_harvestable():\n            abilities_set.append(SWGBCCAbilitySubprocessor.harvestable_ability(ambient_group))\n\n        # =======================================================================\n        # Abilities\n        # =======================================================================\n        raw_api_object.add_raw_member(\"abilities\", abilities_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers_set = []\n\n        raw_api_object.add_raw_member(\"modifiers\", modifiers_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n        # =======================================================================\n        # TODO: Variants\n        # =======================================================================\n        variants_set = []\n\n        raw_api_object.add_raw_member(\"variants\", variants_set,\n                                      \"engine.util.game_entity.GameEntity\")\n\n    @staticmethod\n    def tech_group_to_tech(tech_group: GenieTechEffectBundleGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a tech group.\n\n        :param tech_group: Tech group that gets converted to a tech.\n        :type tech_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        tech_id = tech_group.get_id()\n\n        # Skip Tech Level 0 tech\n        if tech_id == 0:\n            return\n\n        dataset = tech_group.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n        tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = tech_lookup_dict[tech_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.tech.Tech\")\n\n        if isinstance(tech_group, UnitLineUpgrade):\n            unit_line = dataset.unit_lines[tech_group.get_line_id()]\n            head_unit_id = unit_line.get_head_unit_id()\n            obj_location = f\"data/game_entity/generic/{name_lookup_dict[head_unit_id][1]}/\"\n\n        else:\n            obj_location = f\"data/tech/generic/{tech_lookup_dict[tech_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(tech_lookup_dict[tech_id][1])\n        tech_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Types\n        # =======================================================================\n        raw_api_object.add_raw_member(\"types\", [], \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(tech_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(tech_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(tech_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(tech_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(tech_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(tech_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.tech.Tech\")\n        tech_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # Updates\n        # =======================================================================\n        patches = []\n        patches.extend(SWGBCCTechSubprocessor.get_patches(tech_group))\n        raw_api_object.add_raw_member(\"updates\", patches, \"engine.util.tech.Tech\")\n\n        # =======================================================================\n        # Misc (Objects that are not used by the tech group itself, but use its values)\n        # =======================================================================\n        if tech_group.is_researchable():\n            SWGBCCAuxiliarySubprocessor.get_researchable_tech(tech_group)\n\n        # TODO: Implement civ line techs\n\n    @staticmethod\n    def civ_group_to_civ(civ_group: GenieCivilizationGroup) -> None:\n        \"\"\"\n        Creates raw API objects for a civ group.\n\n        :param civ_group: Terrain group that gets converted to a tech.\n        :type civ_group: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        civ_id = civ_group.get_id()\n\n        dataset = civ_group.data\n\n        civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n\n        # Start with the Tech object\n        tech_name = civ_lookup_dict[civ_id][0]\n        raw_api_object = RawAPIObject(tech_name, tech_name,\n                                      dataset.nyan_api_objects)\n        raw_api_object.add_raw_parent(\"engine.util.setup.PlayerSetup\")\n\n        obj_location = f\"data/civ/{civ_lookup_dict[civ_id][1]}/\"\n\n        raw_api_object.set_location(obj_location)\n        raw_api_object.set_filename(civ_lookup_dict[civ_id][1])\n        civ_group.add_raw_api_object(raw_api_object)\n\n        # =======================================================================\n        # Name\n        # =======================================================================\n        name_ref = f\"{tech_name}.{tech_name}Name\"\n        name_raw_api_object = RawAPIObject(name_ref,\n                                           f\"{tech_name}Name\",\n                                           dataset.nyan_api_objects)\n        name_raw_api_object.add_raw_parent(\"engine.util.language.translated.type.TranslatedString\")\n        name_location = ForwardRef(civ_group, tech_name)\n        name_raw_api_object.set_location(name_location)\n\n        name_raw_api_object.add_raw_member(\"translations\",\n                                           [],\n                                           \"engine.util.language.translated.type.TranslatedString\")\n\n        name_forward_ref = ForwardRef(civ_group, name_ref)\n        raw_api_object.add_raw_member(\"name\", name_forward_ref, \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(name_raw_api_object)\n\n        # =======================================================================\n        # Description\n        # =======================================================================\n        description_ref = f\"{tech_name}.{tech_name}Description\"\n        description_raw_api_object = RawAPIObject(description_ref,\n                                                  f\"{tech_name}Description\",\n                                                  dataset.nyan_api_objects)\n        description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        description_location = ForwardRef(civ_group, tech_name)\n        description_raw_api_object.set_location(description_location)\n\n        description_raw_api_object.add_raw_member(\"translations\",\n                                                  [],\n                                                  \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        description_forward_ref = ForwardRef(civ_group, description_ref)\n        raw_api_object.add_raw_member(\"description\",\n                                      description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(description_raw_api_object)\n\n        # =======================================================================\n        # Long description\n        # =======================================================================\n        long_description_ref = f\"{tech_name}.{tech_name}LongDescription\"\n        long_description_raw_api_object = RawAPIObject(long_description_ref,\n                                                       f\"{tech_name}LongDescription\",\n                                                       dataset.nyan_api_objects)\n        long_description_raw_api_object.add_raw_parent(\n            \"engine.util.language.translated.type.TranslatedMarkupFile\")\n        long_description_location = ForwardRef(civ_group, tech_name)\n        long_description_raw_api_object.set_location(long_description_location)\n\n        long_description_raw_api_object.add_raw_member(\"translations\",\n                                                       [],\n                                                       \"engine.util.language.translated.type.TranslatedMarkupFile\")\n\n        long_description_forward_ref = ForwardRef(civ_group, long_description_ref)\n        raw_api_object.add_raw_member(\"long_description\",\n                                      long_description_forward_ref,\n                                      \"engine.util.setup.PlayerSetup\")\n        civ_group.add_raw_api_object(long_description_raw_api_object)\n\n        # =======================================================================\n        # TODO: Leader names\n        # =======================================================================\n        raw_api_object.add_raw_member(\"leader_names\",\n                                      [],\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Modifiers\n        # =======================================================================\n        modifiers = SWGBCCCivSubprocessor.get_modifiers(civ_group)\n        raw_api_object.add_raw_member(\"modifiers\",\n                                      modifiers,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Starting resources\n        # =======================================================================\n        resource_amounts = SWGBCCCivSubprocessor.get_starting_resources(civ_group)\n        raw_api_object.add_raw_member(\"starting_resources\",\n                                      resource_amounts,\n                                      \"engine.util.setup.PlayerSetup\")\n\n        # =======================================================================\n        # Game setup\n        # =======================================================================\n        game_setup = SWGBCCCivSubprocessor.get_civ_setup(civ_group)\n        raw_api_object.add_raw_member(\"game_setup\",\n                                      game_setup,\n                                      \"engine.util.setup.PlayerSetup\")\n\n    @staticmethod\n    def projectiles_from_line(line):\n        \"\"\"\n        Creates Projectile(GameEntity) raw API objects for a unit/building line.\n\n        :param line: Line for which the projectiles are extracted.\n        :type line: ..dataformat.converter_object.ConverterObjectGroup\n        \"\"\"\n        current_unit = line.get_head_unit()\n        current_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[current_unit_id][0]\n        game_entity_filename = name_lookup_dict[current_unit_id][1]\n\n        projectiles_location = f\"data/game_entity/generic/{game_entity_filename}/projectiles/\"\n\n        projectile_indices = []\n        projectile_primary = current_unit[\"projectile_id0\"].value\n        if projectile_primary > -1:\n            projectile_indices.append(0)\n\n        projectile_secondary = current_unit[\"projectile_id1\"].value\n        if projectile_secondary > -1:\n            projectile_indices.append(1)\n\n        for projectile_num in projectile_indices:\n            obj_ref = f\"{game_entity_name}.ShootProjectile.Projectile{projectile_num}\"\n            obj_name = f\"Projectile{str(projectile_num)}\"\n            proj_raw_api_object = RawAPIObject(obj_ref, obj_name, dataset.nyan_api_objects)\n            proj_raw_api_object.add_raw_parent(\"engine.util.game_entity.GameEntity\")\n            proj_raw_api_object.set_location(projectiles_location)\n            proj_raw_api_object.set_filename(f\"{game_entity_filename}_projectiles\")\n\n            # =======================================================================\n            # Types\n            # =======================================================================\n            types_set = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Projectile\"].get_nyan_object()]\n            proj_raw_api_object.add_raw_member(\n                \"types\", types_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Abilities\n            # =======================================================================\n            abilities_set = []\n            abilities_set.append(AoCAbilitySubprocessor.projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.move_projectile_ability(\n                line, position=projectile_num))\n            abilities_set.append(AoCAbilitySubprocessor.apply_discrete_effect_ability(\n                line, 7, False, projectile_num))\n            # TODO: Death, Despawn\n            proj_raw_api_object.add_raw_member(\n                \"abilities\", abilities_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # TODO: Modifiers\n            # =======================================================================\n            modifiers_set = []\n\n            # modifiers_set.append(AoCModifierSubprocessor.flyover_effect_modifier(line))\n            # modifiers_set.extend(AoCModifierSubprocessor.elevation_attack_modifiers(line))\n\n            proj_raw_api_object.add_raw_member(\n                \"modifiers\", modifiers_set, \"engine.util.game_entity.GameEntity\")\n\n            # =======================================================================\n            # Variants\n            # =======================================================================\n            variants_set = []\n            proj_raw_api_object.add_raw_member(\n                \"variants\", variants_set, \"engine.util.game_entity.GameEntity\")\n\n            line.add_raw_api_object(proj_raw_api_object)\n\n        # TODO: Implement diffing of civ lines\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/pregen_subprocessor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-statements\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates nyan objects for things that are hardcoded into the Genie Engine,\nbut configurable in openage. E.g. HP.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberSpecialValue\nfrom ....entity_object.conversion.converter_object import ConverterObjectGroup, \\\n    RawAPIObject\nfrom ....entity_object.conversion.swgbcc.genie_unit import SWGBUnitTransformGroup\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\nfrom ..aoc.pregen_processor import AoCPregenSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n\n\nclass SWGBCCPregenSubprocessor:\n    \"\"\"\n    Creates raw API objects for hardcoded settings in SWGB.\n    \"\"\"\n\n    @classmethod\n    def generate(cls, full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create nyan objects for hardcoded properties.\n        \"\"\"\n        # Stores pregenerated raw API objects as a container\n        pregen_converter_group = ConverterObjectGroup(\"pregen\")\n\n        AoCPregenSubprocessor.generate_activities(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_attributes(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_diplomatic_stances(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_team_property(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_entity_types(full_data_set, pregen_converter_group)\n        cls.generate_effect_types(full_data_set, pregen_converter_group)\n        cls.generate_exchange_objects(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_formation_types(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_language_objects(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_misc_effect_objects(full_data_set, pregen_converter_group)\n        # cls._generate_modifiers(gamedata, pregen_converter_group) ??\n        # cls._generate_terrain_types(gamedata, pregen_converter_group) TODO: Create terrain types\n        AoCPregenSubprocessor.generate_path_types(full_data_set, pregen_converter_group)\n        cls.generate_resources(full_data_set, pregen_converter_group)\n        AoCPregenSubprocessor.generate_death_condition(full_data_set, pregen_converter_group)\n\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        # Create nyan objects from the raw API objects\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_object()\n\n        # This has to be a separate for-loop because of possible object interdependencies\n        for pregen_object in pregen_nyan_objects.values():\n            pregen_object.create_nyan_members()\n\n            if not pregen_object.is_ready():\n                raise RuntimeError(f\"{repr(pregen_object)}: Pregenerated object is not ready \"\n                                   \"for export. Member or object not initialized.\")\n\n    @staticmethod\n    def generate_effect_types(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate types for effects and resistances.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(full_data_set.game_version)\n        armor_lookup_dict = internal_name_lookups.get_armor_class_lookups(\n            full_data_set.game_version)\n\n        # =======================================================================\n        # Armor types\n        # =======================================================================\n        type_parent = \"engine.util.attribute_change_type.AttributeChangeType\"\n        types_location = \"data/util/attribute_change_type/\"\n\n        for type_name in armor_lookup_dict.values():\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{type_name}\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               type_name, api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Heal\n        # =======================================================================\n        type_ref_in_modpack = \"util.attribute_change_type.types.Heal\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"Heal\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Repair (one for each repairable entity)\n        # =======================================================================\n        repairable_lines = []\n        repairable_lines.extend(full_data_set.building_lines.values())\n        for unit_line in full_data_set.unit_lines.values():\n            if unit_line.is_repairable():\n                repairable_lines.append(unit_line)\n\n        for repairable_line in repairable_lines:\n            if isinstance(repairable_line, SWGBUnitTransformGroup):\n                game_entity_name = name_lookup_dict[repairable_line.get_transform_unit_id()][0]\n\n            else:\n                game_entity_name = name_lookup_dict[repairable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{game_entity_name}Repair\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Repair\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # Construct (two for each constructable entity)\n        # =======================================================================\n        constructable_lines = []\n        constructable_lines.extend(full_data_set.building_lines.values())\n\n        for constructable_line in constructable_lines:\n            game_entity_name = name_lookup_dict[constructable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.attribute_change_type.types.{game_entity_name}Construct\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Construct\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        type_parent = \"engine.util.progress_type.type.Construct\"\n        types_location = \"data/util/construct_type/\"\n\n        for constructable_line in constructable_lines:\n            game_entity_name = name_lookup_dict[constructable_line.get_head_unit_id()][0]\n\n            type_ref_in_modpack = f\"util.construct_type.types.{game_entity_name}Construct\"\n            type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                               f\"{game_entity_name}Construct\",\n                                               api_objects,\n                                               types_location)\n            type_raw_api_object.set_filename(\"types\")\n            type_raw_api_object.add_raw_parent(type_parent)\n\n            pregen_converter_group.add_raw_api_object(type_raw_api_object)\n            pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # ConvertType: UnitConvert\n        # =======================================================================\n        type_parent = \"engine.util.convert_type.ConvertType\"\n        types_location = \"data/util/convert_type/\"\n\n        type_ref_in_modpack = \"util.convert_type.types.UnitConvert\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"UnitConvert\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n        # =======================================================================\n        # ConvertType: BuildingConvert\n        # =======================================================================\n        type_parent = \"engine.util.convert_type.ConvertType\"\n        types_location = \"data/util/convert_type/\"\n\n        type_ref_in_modpack = \"util.convert_type.types.BuildingConvert\"\n        type_raw_api_object = RawAPIObject(type_ref_in_modpack,\n                                           \"BuildingConvert\", api_objects,\n                                           types_location)\n        type_raw_api_object.set_filename(\"types\")\n        type_raw_api_object.add_raw_parent(type_parent)\n\n        pregen_converter_group.add_raw_api_object(type_raw_api_object)\n        pregen_nyan_objects.update({type_ref_in_modpack: type_raw_api_object})\n\n    @staticmethod\n    def generate_exchange_objects(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate objects for market trading (ExchangeResources).\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        # =======================================================================\n        # Exchange mode Buy\n        # =======================================================================\n        exchange_mode_parent = \"engine.util.exchange_mode.type.Buy\"\n        exchange_mode_location = \"data/util/resource/\"\n\n        exchange_mode_ref_in_modpack = \"util.resource.market_trading.MarketBuyExchangeMode\"\n        exchange_mode_raw_api_object = RawAPIObject(exchange_mode_ref_in_modpack,\n                                                    \"MarketBuyExchangePool\",\n                                                    api_objects,\n                                                    exchange_mode_location)\n        exchange_mode_raw_api_object.set_filename(\"market_trading\")\n        exchange_mode_raw_api_object.add_raw_parent(exchange_mode_parent)\n\n        # Fee (30% on top)\n        exchange_mode_raw_api_object.add_raw_member(\"fee_multiplier\",\n                                                    1.3,\n                                                    \"engine.util.exchange_mode.ExchangeMode\")\n\n        pregen_converter_group.add_raw_api_object(exchange_mode_raw_api_object)\n        pregen_nyan_objects.update({exchange_mode_ref_in_modpack: exchange_mode_raw_api_object})\n\n        # =======================================================================\n        # Exchange mode Sell\n        # =======================================================================\n        exchange_mode_parent = \"engine.util.exchange_mode.type.Sell\"\n        exchange_mode_location = \"data/util/resource/\"\n\n        exchange_mode_ref_in_modpack = \"util.resource.market_trading.MarketSellExchangeMode\"\n        exchange_mode_raw_api_object = RawAPIObject(exchange_mode_ref_in_modpack,\n                                                    \"MarketSellExchangeMode\",\n                                                    api_objects,\n                                                    exchange_mode_location)\n        exchange_mode_raw_api_object.set_filename(\"market_trading\")\n        exchange_mode_raw_api_object.add_raw_parent(exchange_mode_parent)\n\n        # Fee (30% reduced)\n        exchange_mode_raw_api_object.add_raw_member(\"fee_multiplier\",\n                                                    0.7,\n                                                    \"engine.util.exchange_mode.ExchangeMode\")\n\n        pregen_converter_group.add_raw_api_object(exchange_mode_raw_api_object)\n        pregen_nyan_objects.update({exchange_mode_ref_in_modpack: exchange_mode_raw_api_object})\n\n        # =======================================================================\n        # Market Food price pool\n        # =======================================================================\n        exchange_pool_parent = \"engine.util.price_pool.PricePool\"\n        exchange_pool_location = \"data/util/resource/\"\n\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketFoodPricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketFoodPricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Market Carbon price pool\n        # =======================================================================\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketCarbonPricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketCarbonPricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Market Ore price pool\n        # =======================================================================\n        exchange_pool_ref_in_modpack = \"util.resource.market_trading.MarketOrePricePool\"\n        exchange_pool_raw_api_object = RawAPIObject(exchange_pool_ref_in_modpack,\n                                                    \"MarketOrePricePool\",\n                                                    api_objects,\n                                                    exchange_pool_location)\n        exchange_pool_raw_api_object.set_filename(\"market_trading\")\n        exchange_pool_raw_api_object.add_raw_parent(exchange_pool_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_pool_raw_api_object)\n        pregen_nyan_objects.update({exchange_pool_ref_in_modpack: exchange_pool_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Food\n        # =======================================================================\n        exchange_rate_parent = \"engine.util.exchange_rate.ExchangeRate\"\n        exchange_rate_location = \"data/util/resource/\"\n\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketFoodExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketFoodExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.0,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketFoodPricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Carbon\n        # =======================================================================\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketCarbonExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketCarbonExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.0,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketCarbonPricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Exchange rate Ore\n        # =======================================================================\n        exchange_rate_ref_in_modpack = \"util.resource.market_trading.MarketOreExchangeRate\"\n        exchange_rate_raw_api_object = RawAPIObject(exchange_rate_ref_in_modpack,\n                                                    \"MarketOreExchangeRate\",\n                                                    api_objects,\n                                                    exchange_rate_location)\n        exchange_rate_raw_api_object.set_filename(\"market_trading\")\n        exchange_rate_raw_api_object.add_raw_parent(exchange_rate_parent)\n\n        # Base price\n        exchange_rate_raw_api_object.add_raw_member(\"base_price\",\n                                                    1.3,\n                                                    exchange_rate_parent)\n\n        # Price adjust methods\n        pa_buy_forward_ref = ForwardRef(pregen_converter_group,\n                                        \"util.resource.market_trading.MarketBuyPriceMode\")\n        pa_sell_forward_ref = ForwardRef(pregen_converter_group,\n                                         \"util.resource.market_trading.MarketSellPriceMode\")\n        price_adjust = {\n            api_objects[\"engine.util.exchange_mode.type.Buy\"]: pa_buy_forward_ref,\n            api_objects[\"engine.util.exchange_mode.type.Sell\"]: pa_sell_forward_ref\n        }\n        exchange_rate_raw_api_object.add_raw_member(\"price_adjust\",\n                                                    price_adjust,\n                                                    exchange_rate_parent)\n\n        # Price pool\n        pool_forward_ref = ForwardRef(pregen_converter_group,\n                                      \"util.resource.market_trading.MarketOrePricePool\")\n        exchange_rate_raw_api_object.add_raw_member(\"price_pool\",\n                                                    pool_forward_ref,\n                                                    exchange_rate_parent)\n\n        pregen_converter_group.add_raw_api_object(exchange_rate_raw_api_object)\n        pregen_nyan_objects.update({exchange_rate_ref_in_modpack: exchange_rate_raw_api_object})\n\n        # =======================================================================\n        # Buy Price mode\n        # =======================================================================\n        price_mode_parent = \"engine.util.price_mode.type.Dynamic\"\n        price_mode_location = \"data/util/resource/\"\n\n        price_mode_ref_in_modpack = \"util.resource.market_trading.MarketBuyPriceMode\"\n        price_mode_raw_api_object = RawAPIObject(price_mode_ref_in_modpack,\n                                                 \"MarketBuyPriceMode\",\n                                                 api_objects,\n                                                 price_mode_location)\n        price_mode_raw_api_object.set_filename(\"market_trading\")\n        price_mode_raw_api_object.add_raw_parent(price_mode_parent)\n\n        # Change value\n        price_mode_raw_api_object.add_raw_member(\"change_value\",\n                                                 0.03,\n                                                 price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"min_price\",\n                                                 0.3,\n                                                 price_mode_parent)\n\n        # Max price\n        price_mode_raw_api_object.add_raw_member(\"max_price\",\n                                                 99.9,\n                                                 price_mode_parent)\n\n        pregen_converter_group.add_raw_api_object(price_mode_raw_api_object)\n        pregen_nyan_objects.update({price_mode_ref_in_modpack: price_mode_raw_api_object})\n\n        # =======================================================================\n        # Sell Price mode\n        # =======================================================================\n        price_mode_parent = \"engine.util.price_mode.type.Dynamic\"\n        price_mode_location = \"data/util/resource/\"\n\n        price_mode_ref_in_modpack = \"util.resource.market_trading.MarketSellPriceMode\"\n        price_mode_raw_api_object = RawAPIObject(price_mode_ref_in_modpack,\n                                                 \"MarketSellPriceMode\",\n                                                 api_objects,\n                                                 price_mode_location)\n        price_mode_raw_api_object.set_filename(\"market_trading\")\n        price_mode_raw_api_object.add_raw_parent(price_mode_parent)\n\n        # Change value\n        price_mode_raw_api_object.add_raw_member(\"change_value\",\n                                                 -0.03,\n                                                 price_mode_parent)\n\n        # Min price\n        price_mode_raw_api_object.add_raw_member(\"min_price\",\n                                                 0.3,\n                                                 price_mode_parent)\n\n        # Max price\n        price_mode_raw_api_object.add_raw_member(\"max_price\",\n                                                 99.9,\n                                                 price_mode_parent)\n\n        pregen_converter_group.add_raw_api_object(price_mode_raw_api_object)\n        pregen_nyan_objects.update({price_mode_ref_in_modpack: price_mode_raw_api_object})\n\n    @staticmethod\n    def generate_resources(\n        full_data_set: GenieObjectContainer,\n        pregen_converter_group: ConverterObjectGroup\n    ) -> None:\n        \"\"\"\n        Generate Attribute objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        :param pregen_converter_group: GenieObjectGroup instance that stores\n                                       pregenerated API objects for referencing with\n                                       ForwardRef\n        :type pregen_converter_group: ...dataformat.aoc.genie_object_container.GenieObjectGroup\n        \"\"\"\n        pregen_nyan_objects = full_data_set.pregen_nyan_objects\n        api_objects = full_data_set.nyan_api_objects\n\n        resource_parent = \"engine.util.resource.Resource\"\n        resources_location = \"data/util/resource/\"\n\n        # =======================================================================\n        # Food\n        # =======================================================================\n        food_ref_in_modpack = \"util.resource.types.Food\"\n        food_raw_api_object = RawAPIObject(food_ref_in_modpack,\n                                           \"Food\", api_objects,\n                                           resources_location)\n        food_raw_api_object.set_filename(\"types\")\n        food_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(food_raw_api_object)\n        pregen_nyan_objects.update({food_ref_in_modpack: food_raw_api_object})\n\n        food_raw_api_object.add_raw_member(\"max_storage\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        food_name_ref_in_modpack = \"util.attribute.types.Food.FoodName\"\n        food_name_value = RawAPIObject(food_name_ref_in_modpack, \"FoodName\",\n                                       api_objects, resources_location)\n        food_name_value.set_filename(\"types\")\n        food_name_value.add_raw_parent(name_value_parent)\n        food_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      food_name_ref_in_modpack)\n        food_raw_api_object.add_raw_member(\"name\",\n                                           name_forward_ref,\n                                           resource_parent)\n\n        pregen_converter_group.add_raw_api_object(food_name_value)\n        pregen_nyan_objects.update({food_name_ref_in_modpack: food_name_value})\n\n        # =======================================================================\n        # Carbon\n        # =======================================================================\n        carbon_ref_in_modpack = \"util.resource.types.Carbon\"\n        carbon_raw_api_object = RawAPIObject(carbon_ref_in_modpack,\n                                             \"Carbon\", api_objects,\n                                             resources_location)\n        carbon_raw_api_object.set_filename(\"types\")\n        carbon_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(carbon_raw_api_object)\n        pregen_nyan_objects.update({carbon_ref_in_modpack: carbon_raw_api_object})\n\n        carbon_raw_api_object.add_raw_member(\"max_storage\",\n                                             MemberSpecialValue.NYAN_INF,\n                                             resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        carbon_name_ref_in_modpack = \"util.attribute.types.Carbon.CarbonName\"\n        carbon_name_value = RawAPIObject(carbon_name_ref_in_modpack, \"CarbonName\",\n                                         api_objects, resources_location)\n        carbon_name_value.set_filename(\"types\")\n        carbon_name_value.add_raw_parent(name_value_parent)\n        carbon_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      carbon_name_ref_in_modpack)\n        carbon_raw_api_object.add_raw_member(\"name\",\n                                             name_forward_ref,\n                                             resource_parent)\n\n        pregen_converter_group.add_raw_api_object(carbon_name_value)\n        pregen_nyan_objects.update({carbon_name_ref_in_modpack: carbon_name_value})\n\n        # =======================================================================\n        # Ore\n        # =======================================================================\n        ore_ref_in_modpack = \"util.resource.types.Ore\"\n        ore_raw_api_object = RawAPIObject(ore_ref_in_modpack,\n                                          \"Ore\", api_objects,\n                                          resources_location)\n        ore_raw_api_object.set_filename(\"types\")\n        ore_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(ore_raw_api_object)\n        pregen_nyan_objects.update({ore_ref_in_modpack: ore_raw_api_object})\n\n        ore_raw_api_object.add_raw_member(\"max_storage\",\n                                          MemberSpecialValue.NYAN_INF,\n                                          resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        ore_name_ref_in_modpack = \"util.attribute.types.Ore.OreName\"\n        ore_name_value = RawAPIObject(ore_name_ref_in_modpack, \"OreName\",\n                                      api_objects, resources_location)\n        ore_name_value.set_filename(\"types\")\n        ore_name_value.add_raw_parent(name_value_parent)\n        ore_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      ore_name_ref_in_modpack)\n        ore_raw_api_object.add_raw_member(\"name\",\n                                          name_forward_ref,\n                                          resource_parent)\n\n        pregen_converter_group.add_raw_api_object(ore_name_value)\n        pregen_nyan_objects.update({ore_name_ref_in_modpack: ore_name_value})\n\n        # =======================================================================\n        # Nova\n        # =======================================================================\n        nova_ref_in_modpack = \"util.resource.types.Nova\"\n        nova_raw_api_object = RawAPIObject(nova_ref_in_modpack,\n                                           \"Nova\", api_objects,\n                                           resources_location)\n        nova_raw_api_object.set_filename(\"types\")\n        nova_raw_api_object.add_raw_parent(resource_parent)\n\n        pregen_converter_group.add_raw_api_object(nova_raw_api_object)\n        pregen_nyan_objects.update({nova_ref_in_modpack: nova_raw_api_object})\n\n        nova_raw_api_object.add_raw_member(\"max_storage\",\n                                           MemberSpecialValue.NYAN_INF,\n                                           resource_parent)\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        nova_name_ref_in_modpack = \"util.attribute.types.Nova.NovaName\"\n        nova_name_value = RawAPIObject(nova_name_ref_in_modpack, \"NovaName\",\n                                       api_objects, resources_location)\n        nova_name_value.set_filename(\"types\")\n        nova_name_value.add_raw_parent(name_value_parent)\n        nova_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      nova_name_ref_in_modpack)\n        nova_raw_api_object.add_raw_member(\"name\",\n                                           name_forward_ref,\n                                           resource_parent)\n\n        pregen_converter_group.add_raw_api_object(nova_name_value)\n        pregen_nyan_objects.update({nova_name_ref_in_modpack: nova_name_value})\n\n        # =======================================================================\n        # Population Space\n        # =======================================================================\n        resource_contingent_parent = \"engine.util.resource.ResourceContingent\"\n\n        pop_ref_in_modpack = \"util.resource.types.PopulationSpace\"\n        pop_raw_api_object = RawAPIObject(pop_ref_in_modpack,\n                                          \"PopulationSpace\", api_objects,\n                                          resources_location)\n        pop_raw_api_object.set_filename(\"types\")\n        pop_raw_api_object.add_raw_parent(resource_contingent_parent)\n\n        pregen_converter_group.add_raw_api_object(pop_raw_api_object)\n        pregen_nyan_objects.update({pop_ref_in_modpack: pop_raw_api_object})\n\n        name_value_parent = \"engine.util.language.translated.type.TranslatedString\"\n        pop_name_ref_in_modpack = \"util.attribute.types.PopulationSpace.PopulationSpaceName\"\n        pop_name_value = RawAPIObject(pop_name_ref_in_modpack, \"PopulationSpaceName\",\n                                      api_objects, resources_location)\n        pop_name_value.set_filename(\"types\")\n        pop_name_value.add_raw_parent(name_value_parent)\n        pop_name_value.add_raw_member(\"translations\", [], name_value_parent)\n\n        name_forward_ref = ForwardRef(pregen_converter_group,\n                                      pop_name_ref_in_modpack)\n        pop_raw_api_object.add_raw_member(\"name\",\n                                          name_forward_ref,\n                                          resource_parent)\n        pop_raw_api_object.add_raw_member(\"max_storage\",\n                                          MemberSpecialValue.NYAN_INF,\n                                          resource_parent)\n        pop_raw_api_object.add_raw_member(\"min_amount\",\n                                          0,\n                                          resource_contingent_parent)\n        pop_raw_api_object.add_raw_member(\"max_amount\",\n                                          200,\n                                          resource_contingent_parent)\n\n        pregen_converter_group.add_raw_api_object(pop_name_value)\n        pregen_nyan_objects.update({pop_name_ref_in_modpack: pop_name_value})\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/processor.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-branches,too-many-statements,too-many-locals\n#\n# TODO:\n# pylint: disable=line-too-long\n\"\"\"\nConvert data from SWGB:CC to openage formats.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom openage.convert.entity_object.conversion.aoc.genie_tech import BuildingUnlock\nfrom .....log import info\nfrom ....entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\nfrom ....entity_object.conversion.aoc.genie_tech import BuildingLineUpgrade, \\\n    AgeUpgrade, StatUpgrade, InitiatedTech, CivBonus\nfrom ....entity_object.conversion.aoc.genie_unit import GenieUnitTaskGroup, \\\n    GenieVillagerGroup, GenieAmbientGroup, GenieVariantGroup, \\\n    GenieBuildingLineGroup, GenieGarrisonMode\nfrom ....entity_object.conversion.swgbcc.genie_tech import SWGBUnitUnlock, \\\n    SWGBUnitLineUpgrade\nfrom ....entity_object.conversion.swgbcc.genie_unit import SWGBUnitTransformGroup, \\\n    SWGBMonkGroup, SWGBUnitLineGroup, SWGBStackBuildingGroup\nfrom ....service.debug_info import debug_converter_objects, \\\n    debug_converter_object_groups\nfrom ....service.read.nyan_api_loader import load_api\nfrom ....value_object.conversion.swgb.internal_nyan_names import MONK_GROUP_ASSOCS, \\\n    CIV_LINE_ASSOCS, AMBIENT_GROUP_LOOKUPS, VARIANT_GROUP_LOOKUPS, \\\n    CIV_TECH_ASSOCS\nfrom ..aoc.media_subprocessor import AoCMediaSubprocessor\nfrom ..aoc.processor import AoCProcessor\nfrom .modpack_subprocessor import SWGBCCModpackSubprocessor\nfrom .nyan_subprocessor import SWGBCCNyanSubprocessor\nfrom .pregen_subprocessor import SWGBCCPregenSubprocessor\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.value_object.read.value_members import ArrayMember\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\nclass SWGBCCProcessor:\n    \"\"\"\n    Main processor for converting data from SWGB.\n    \"\"\"\n\n    @classmethod\n    def convert(\n        cls,\n        gamespec: ArrayMember,\n        args: Namespace,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> list[Modpack]:\n        \"\"\"\n        Input game specification and media here and get a set of\n        modpacks back.\n\n        :param gamespec: Gamedata from empires.dat read in by the\n                         reader functions.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :returns: A list of modpacks.\n        :rtype: list\n        \"\"\"\n\n        info(\"Starting conversion...\")\n\n        # Create a new container for the conversion process\n        dataset = cls._pre_processor(\n            gamespec,\n            args.game_version,\n            string_resources,\n            existing_graphics\n        )\n        debug_converter_objects(args.debugdir, args.debug_info, dataset)\n\n        # Create the custom openae formats (nyan, sprite, terrain)\n        dataset = cls._processor(dataset)\n        debug_converter_object_groups(args.debugdir, args.debug_info, dataset)\n\n        # Create modpack definitions\n        modpacks = cls._post_processor(dataset)\n\n        return modpacks\n\n    @classmethod\n    def _pre_processor(\n        cls,\n        gamespec: ArrayMember,\n        game_version: GameVersion,\n        string_resources: StringResource,\n        existing_graphics: list[str]\n    ) -> GenieObjectContainer:\n        \"\"\"\n        Store data from the reader in a conversion container.\n\n        :param gamespec: Gamedata from empires.dat file.\n        :type gamespec: class: ...dataformat.value_members.ArrayMember\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        dataset = GenieObjectContainer()\n\n        dataset.game_version = game_version\n        dataset.nyan_api_objects = load_api()\n        dataset.strings = string_resources\n        dataset.existing_graphics = existing_graphics\n\n        info(\"Extracting Genie data...\")\n\n        AoCProcessor.extract_genie_units(gamespec, dataset)\n        AoCProcessor.extract_genie_techs(gamespec, dataset)\n        AoCProcessor.extract_genie_effect_bundles(gamespec, dataset)\n        AoCProcessor.sanitize_effect_bundles(dataset)\n        AoCProcessor.extract_genie_civs(gamespec, dataset)\n        AoCProcessor.extract_age_connections(gamespec, dataset)\n        AoCProcessor.extract_building_connections(gamespec, dataset)\n        AoCProcessor.extract_unit_connections(gamespec, dataset)\n        AoCProcessor.extract_tech_connections(gamespec, dataset)\n        AoCProcessor.extract_genie_graphics(gamespec, dataset)\n        AoCProcessor.extract_genie_sounds(gamespec, dataset)\n        AoCProcessor.extract_genie_terrains(gamespec, dataset)\n        AoCProcessor.extract_genie_restrictions(gamespec, dataset)\n\n        return dataset\n\n    @classmethod\n    def _processor(cls, full_data_set: GenieObjectContainer) -> GenieObjectContainer:\n        \"\"\"\n        Transfer structures used in Genie games to more openage-friendly\n        Python objects.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating API-like objects...\")\n\n        cls.create_unit_lines(full_data_set)\n        cls.create_extra_unit_lines(full_data_set)\n        cls.create_building_lines(full_data_set)\n        cls.create_villager_groups(full_data_set)\n        cls.create_ambient_groups(full_data_set)\n        cls.create_variant_groups(full_data_set)\n        AoCProcessor.create_terrain_groups(full_data_set)\n        cls.create_tech_groups(full_data_set)\n        AoCProcessor.create_civ_groups(full_data_set)\n\n        info(\"Linking API-like objects...\")\n\n        AoCProcessor.link_building_upgrades(full_data_set)\n        AoCProcessor.link_creatables(full_data_set)\n        AoCProcessor.link_researchables(full_data_set)\n        AoCProcessor.link_civ_uniques(full_data_set)\n        AoCProcessor.link_gatherers_to_dropsites(full_data_set)\n        cls.link_garrison(full_data_set)\n        AoCProcessor.link_trade_posts(full_data_set)\n        cls.link_repairables(full_data_set)\n\n        info(\"Generating auxiliary objects...\")\n\n        SWGBCCPregenSubprocessor.generate(full_data_set)\n\n        return full_data_set\n\n    @classmethod\n    def _post_processor(cls, full_data_set: GenieObjectContainer) -> list[Modpack]:\n        \"\"\"\n        Convert API-like Python objects to nyan.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n\n        info(\"Creating nyan objects...\")\n\n        SWGBCCNyanSubprocessor.convert(full_data_set)\n\n        info(\"Creating requests for media export...\")\n\n        AoCMediaSubprocessor.convert(full_data_set)\n\n        return SWGBCCModpackSubprocessor.get_modpacks(full_data_set)\n\n    @staticmethod\n    def create_unit_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Sort units into lines, based on information in the unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        unit_connections = full_data_set.unit_connections\n        unit_lines = {}\n        unit_ref = {}\n\n        # First only handle the line heads (first units in a line)\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            unit = full_data_set.genie_units[unit_id]\n            line_mode = connection[\"line_mode\"].value\n\n            if line_mode != 2:\n                # It's an upgrade. Skip and handle later\n                continue\n\n            # Check for special cases first\n            if unit.has_member(\"transform_unit_id\")\\\n                    and unit[\"transform_unit_id\"].value > -1:\n                # Cannon\n                # SWGB stores the deployed cannon in the connections, but we\n                # want the undeployed cannon\n                transform_id = unit[\"transform_unit_id\"].value\n                unit_line = SWGBUnitTransformGroup(transform_id, transform_id, full_data_set)\n\n            elif unit_id in MONK_GROUP_ASSOCS:\n                # Jedi/Sith\n                # Switch to monk with relic is hardcoded :(\n                # for every civ (WTF LucasArts)\n                switch_unit_id = MONK_GROUP_ASSOCS[unit_id]\n                unit_line = SWGBMonkGroup(unit_id, unit_id, switch_unit_id, full_data_set)\n\n            elif unit.has_member(\"task_group\")\\\n                    and unit[\"task_group\"].value > 0:\n                # Villager\n                # done somewhere else because they are special^TM\n                continue\n\n            else:\n                # Normal units\n                unit_line = SWGBUnitLineGroup(unit_id, full_data_set)\n\n            unit_line.add_unit(unit)\n            unit_lines.update({unit_line.get_id(): unit_line})\n            unit_ref.update({unit_id: unit_line})\n\n        # Second, handle all upgraded units\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            unit = full_data_set.genie_units[unit_id]\n            line_mode = connection[\"line_mode\"].value\n\n            if line_mode != 3:\n                # This unit is not an upgrade and was handled in the last for-loop\n                continue\n\n            # Search other_connections for the previous unit in line\n            connected_types = connection[\"other_connections\"].value\n            for index, _ in enumerate(connected_types):\n                connected_type = connected_types[index][\"other_connection\"].value\n                if connected_type == 2:\n                    # 2 == Unit\n                    connected_index = index\n                    break\n\n            else:\n                raise ValueError(f\"Unit {unit_id} is not first in line, but no previous \"\n                                 \"unit can be found in other_connections\")\n\n            connected_ids = connection[\"other_connected_ids\"].value\n            previous_unit_id = connected_ids[connected_index].value\n\n            # Search for the first unit ID in the line recursively\n            previous_id = previous_unit_id\n            previous_connection = unit_connections[previous_unit_id]\n            while previous_connection[\"line_mode\"] != 2:\n                if previous_id in unit_ref:\n                    # Short-circuit here, if we the previous unit was already handled\n                    break\n\n                connected_types = previous_connection[\"other_connections\"].value\n                connected_index = -1\n                for index, _ in enumerate(connected_types):\n                    connected_type = connected_types[index][\"other_connection\"].value\n                    if connected_type == 2:\n                        # 2 == Unit\n                        connected_index = index\n                        break\n\n                connected_ids = previous_connection[\"other_connected_ids\"].value\n                previous_id = connected_ids[connected_index].value\n                previous_connection = unit_connections[previous_id]\n\n            unit_line = unit_ref[previous_id]\n            unit_line.add_unit(unit, after=previous_unit_id)\n\n        # Search for civ lines and attach them to their main line\n        final_unit_lines = {}\n        final_unit_lines.update(unit_lines)\n        for line in unit_lines.values():\n            if line.get_head_unit_id() not in CIV_LINE_ASSOCS:\n                for main_head_unit_id, civ_head_unit_ids in CIV_LINE_ASSOCS.items():\n                    if line.get_head_unit_id() in civ_head_unit_ids:\n                        # The line is an alternative civ line and should be stored\n                        # with the main line only.\n                        main_line = unit_lines[main_head_unit_id]\n                        main_line.add_civ_line(line)\n\n                        # Remove the line from the main reference dict, so that\n                        # it doesn't get converted to a game entity\n                        final_unit_lines.pop(line.get_id())\n\n                        # Store a reference to the main line in the unit ID refs\n                        for unit in line.line:\n                            full_data_set.unit_ref[unit.get_id()] = main_line\n\n                        break\n\n                else:\n                    # Store a reference to the line in the unit ID refs\n                    for unit in line.line:\n                        full_data_set.unit_ref[unit.get_id()] = line\n\n            else:\n                # Store a reference to the line in the unit ID refs\n                for unit in line.line:\n                    full_data_set.unit_ref[unit.get_id()] = line\n\n        # Store the remaining lines in the main reference dict\n        full_data_set.unit_lines.update(final_unit_lines)\n\n    @staticmethod\n    def create_extra_unit_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create additional units that are not in the unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        # Wildlife\n        extra_units = (48, 594, 822, 833, 1203, 1249, 1363, 1364,\n                       1365, 1366, 1367, 1469, 1471, 1473, 1475)\n\n        for unit_id in extra_units:\n            unit_line = SWGBUnitLineGroup(unit_id, full_data_set)\n            unit_line.add_unit(full_data_set.genie_units[unit_id])\n            full_data_set.unit_lines.update({unit_line.get_id(): unit_line})\n            full_data_set.unit_ref.update({unit_id: unit_line})\n\n    @staticmethod\n    def create_building_lines(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Establish building lines, based on information in the building connections.\n        Because of how Genie building lines work, this will only find the first\n        building in the line. Subsequent buildings in the line have to be determined\n        by effects in AgeUpTechs.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        building_connections = full_data_set.building_connections\n        genie_techs = full_data_set.genie_techs\n\n        # Unlocked = first in line\n        unlocked_by_tech = set()\n\n        # Upgraded = later in line\n        upgraded_by_tech = {}\n\n        # Search all techs for building upgrades. This is necessary because they are\n        # not stored in tech connections in SWGB\n        for tech_id, tech in genie_techs.items():\n            tech_effect_id = tech[\"tech_effect_id\"].value\n            if tech_effect_id < 0:\n                continue\n\n            tech_effects = full_data_set.genie_effect_bundles[tech_effect_id].get_effects()\n\n            # Search for upgrade or unlock effects\n            age_up = False\n            for effect in tech_effects:\n                effect_type = effect[\"type_id\"].value\n                unit_id_a = effect[\"attr_a\"].value\n                unit_id_b = effect[\"attr_b\"].value\n\n                if effect_type == 1 and effect[\"attr_a\"].value == 6:\n                    # if this is an age up tech, we do not need to create any additional\n                    # unlock techs\n                    age_up = True\n\n                if effect_type == 2 and unit_id_a in building_connections.keys():\n                    # Unlocks\n                    unlocked_by_tech.add(unit_id_a)\n\n                    if not age_up:\n                        # Add an unlock tech group to the data set\n                        building_unlock = BuildingUnlock(tech_id, unit_id_a, full_data_set)\n                        full_data_set.building_unlocks.update(\n                            {building_unlock.get_id(): building_unlock})\n\n                elif effect_type == 2 and unit_id_a in full_data_set.genie_units.keys():\n                    # Check if this is a stacked unit (gate or command center)\n                    # for these units, we needs the stack_unit_id\n                    building = full_data_set.genie_units[unit_id_a]\n\n                    if building.has_member(\"stack_unit_id\") and \\\n                            building[\"stack_unit_id\"].value > -1:\n                        unit_id_a = building[\"stack_unit_id\"].value\n                        unlocked_by_tech.add(unit_id_a)\n\n                        if not age_up:\n                            building_unlock = BuildingUnlock(tech_id, unit_id_a, full_data_set)\n                            full_data_set.building_unlocks.update(\n                                {building_unlock.get_id(): building_unlock})\n\n                if effect_type == 3 and unit_id_b in building_connections.keys():\n                    # Upgrades\n                    upgraded_by_tech[unit_id_b] = tech_id\n\n        # First only handle the line heads (first buildings in a line)\n        for connection in building_connections.values():\n            building_id = connection[\"id\"].value\n\n            if building_id not in unlocked_by_tech:\n                continue\n\n            building = full_data_set.genie_units[building_id]\n\n            # Check if we have to create a GenieStackBuildingGroup\n            if building.has_member(\"stack_unit_id\") and \\\n                    building[\"stack_unit_id\"].value > -1:\n                # we don't care about head units because we process\n                # them with their stack unit\n                continue\n\n            if building.has_member(\"head_unit_id\") and \\\n                    building[\"head_unit_id\"].value > -1:\n                head_unit_id = building[\"head_unit_id\"].value\n                building_line = SWGBStackBuildingGroup(building_id, head_unit_id, full_data_set)\n\n            else:\n                building_line = GenieBuildingLineGroup(building_id, full_data_set)\n\n            building_line.add_unit(building)\n            full_data_set.building_lines.update({building_line.get_id(): building_line})\n            full_data_set.unit_ref.update({building_id: building_line})\n\n        # Second, handle all upgraded buildings\n        for connection in building_connections.values():\n            building_id = connection[\"id\"].value\n\n            if building_id not in upgraded_by_tech:\n                continue\n\n            building = full_data_set.genie_units[building_id]\n\n            # Search other_connections for the previous unit in line\n            connected_types = connection[\"other_connections\"].value\n            for index, _ in enumerate(connected_types):\n                connected_type = connected_types[index][\"other_connection\"].value\n                if connected_type == 1:\n                    # 1 == Building\n                    connected_index = index\n                    break\n\n            else:\n                raise ValueError(f\"Building {building_id} is not first in line, but no previous \"\n                                 \"building can be found in other_connections\")\n\n            connected_ids = connection[\"other_connected_ids\"].value\n            previous_unit_id = connected_ids[connected_index].value\n\n            # Search for the first unit ID in the line recursively\n            previous_id = previous_unit_id\n            previous_connection = building_connections[previous_unit_id]\n            while previous_connection[\"line_mode\"] != 2:\n                if previous_id in full_data_set.unit_ref.keys():\n                    # Short-circuit here, if we the previous unit was already handled\n                    break\n\n                connected_types = previous_connection[\"other_connections\"].value\n                connected_index = -1\n                for index, _ in enumerate(connected_types):\n                    connected_type = connected_types[index][\"other_connection\"].value\n                    if connected_type == 1:\n                        # 1 == Building\n                        connected_index = index\n                        break\n\n                connected_ids = previous_connection[\"other_connected_ids\"].value\n                previous_id = connected_ids[connected_index].value\n                previous_connection = building_connections[previous_id]\n\n            building_line = full_data_set.unit_ref[previous_id]\n            building_line.add_unit(building, after=previous_unit_id)\n            full_data_set.unit_ref.update({building_id: building_line})\n\n            # Also add the building upgrade tech here\n            building_upgrade = BuildingLineUpgrade(\n                upgraded_by_tech[building_id],\n                building_line.get_id(),\n                building_id,\n                full_data_set\n            )\n            full_data_set.tech_groups.update({building_upgrade.get_id(): building_upgrade})\n            full_data_set.building_upgrades.update({building_upgrade.get_id(): building_upgrade})\n\n    @staticmethod\n    def create_villager_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create task groups and assign the relevant worker group to a\n        villager group.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        units = full_data_set.genie_units\n        task_group_ids = set()\n        unit_ids = set()\n\n        # Find task groups in the dataset\n        for unit in units.values():\n            if unit.has_member(\"task_group\"):\n                task_group_id = unit[\"task_group\"].value\n\n            else:\n                task_group_id = 0\n\n            if task_group_id == 0:\n                # no task group\n                continue\n\n            if task_group_id in task_group_ids:\n                task_group = full_data_set.task_groups[task_group_id]\n                task_group.add_unit(unit)\n\n            else:\n                if task_group_id == 1:\n                    # SWGB uses the same IDs as AoC\n                    line_id = GenieUnitTaskGroup.male_line_id\n\n                elif task_group_id == 2:\n                    # No differences to task group 1; probably unused\n                    continue\n\n                task_group = GenieUnitTaskGroup(line_id, task_group_id, full_data_set)\n                task_group.add_unit(unit)\n                full_data_set.task_groups.update({task_group_id: task_group})\n\n            task_group_ids.add(task_group_id)\n            unit_ids.add(unit[\"id0\"].value)\n\n        # Create the villager task group\n        villager = GenieVillagerGroup(118, task_group_ids, full_data_set)\n        full_data_set.unit_lines.update({villager.get_id(): villager})\n        # TODO: Find the line id elsewhere\n        full_data_set.unit_lines_vertical_ref.update({36: villager})\n        full_data_set.villager_groups.update({villager.get_id(): villager})\n        for unit_id in unit_ids:\n            full_data_set.unit_ref.update({unit_id: villager})\n\n    @staticmethod\n    def create_ambient_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create ambient groups, mostly for resources and scenery.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        ambient_ids = AMBIENT_GROUP_LOOKUPS.keys()\n        genie_units = full_data_set.genie_units\n\n        for ambient_id in ambient_ids:\n            ambient_group = GenieAmbientGroup(ambient_id, full_data_set)\n            ambient_group.add_unit(genie_units[ambient_id])\n            full_data_set.ambient_groups.update({ambient_group.get_id(): ambient_group})\n            full_data_set.unit_ref.update({ambient_id: ambient_group})\n\n    @staticmethod\n    def create_variant_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create variant groups.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        variants = VARIANT_GROUP_LOOKUPS\n\n        for group_id, variant in variants.items():\n            variant_group = GenieVariantGroup(group_id, full_data_set)\n            full_data_set.variant_groups.update({variant_group.get_id(): variant_group})\n\n            for variant_id in variant[2]:\n                variant_group.add_unit(full_data_set.genie_units[variant_id])\n                full_data_set.unit_ref.update({variant_id: variant_group})\n\n    @staticmethod\n    def create_tech_groups(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Create techs from tech connections and unit upgrades/unlocks\n        from unit connections.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        tech_connections = full_data_set.tech_connections\n\n        for connection in tech_connections.values():\n            tech_id = connection[\"id\"].value\n            tech = full_data_set.genie_techs[tech_id]\n\n            effect_id = tech[\"tech_effect_id\"].value\n            if effect_id < 0:\n                continue\n\n            tech_effects = full_data_set.genie_effect_bundles[effect_id]\n\n            # Check if the tech is an age upgrade\n            tech_found = False\n            resource_effects = tech_effects.get_effects(effect_type=1)\n            for effect in resource_effects:\n                # Resource ID 6: Current Age\n                if effect[\"attr_a\"].value != 6:\n                    continue\n\n                age_id = effect[\"attr_b\"].value\n                age_up = AgeUpgrade(tech_id, age_id, full_data_set)\n                full_data_set.tech_groups.update({age_up.get_id(): age_up})\n                full_data_set.age_upgrades.update({age_up.get_id(): age_up})\n                tech_found = True\n                break\n\n            if tech_found:\n                continue\n\n            # Building unlocks/upgrades are not in SWGB tech connections\n\n            # Create a stat upgrade for other techs\n            stat_up = StatUpgrade(tech_id, full_data_set)\n            full_data_set.tech_groups.update({stat_up.get_id(): stat_up})\n            full_data_set.stat_upgrades.update({stat_up.get_id(): stat_up})\n\n        # Unit upgrades and unlocks are stored in unit connections\n        unit_connections = full_data_set.unit_connections\n        unit_unlocks = {}\n        unit_upgrades = {}\n        for connection in unit_connections.values():\n            unit_id = connection[\"id\"].value\n            required_research_id = connection[\"required_research\"].value\n            enabling_research_id = connection[\"enabling_research\"].value\n            line_mode = connection[\"line_mode\"].value\n            line_id = full_data_set.unit_ref[unit_id].get_id()\n\n            if required_research_id == -1 and enabling_research_id == -1:\n                # Unit is unlocked from the start\n                continue\n\n            if line_mode == 2:\n                # Unit is first in line, there should be an unlock tech\n                unit_unlock = SWGBUnitUnlock(enabling_research_id, line_id, full_data_set)\n                unit_unlocks.update({unit_id: unit_unlock})\n\n            elif line_mode == 3:\n                # Units further down the line receive line upgrades\n                unit_upgrade = SWGBUnitLineUpgrade(required_research_id, line_id,\n                                                   unit_id, full_data_set)\n                unit_upgrades.update({required_research_id: unit_upgrade})\n\n        # Unit unlocks for civ lines\n        final_unit_unlocks = {}\n        for unit_unlock in unit_unlocks.values():\n            line = unit_unlock.get_unlocked_line()\n            if line.get_head_unit_id() not in CIV_LINE_ASSOCS:\n                for main_head_unit_id, civ_head_unit_ids in CIV_LINE_ASSOCS.items():\n                    if line.get_head_unit_id() in civ_head_unit_ids:\n                        if isinstance(line, SWGBUnitTransformGroup):\n                            main_head_unit_id = line.get_transform_unit_id()\n\n                        # The line is an alternative civ line so the unlock\n                        # is stored with the main unlock\n                        main_unlock = unit_unlocks[main_head_unit_id]\n                        main_unlock.add_civ_unlock(unit_unlock)\n                        break\n\n                else:\n                    # The unlock is for a line without alternative civ lines\n                    final_unit_unlocks.update({unit_unlock.get_id(): unit_unlock})\n\n            else:\n                # The unlock is for a main line\n                final_unit_unlocks.update({unit_unlock.get_id(): unit_unlock})\n\n        full_data_set.tech_groups.update(final_unit_unlocks)\n        full_data_set.unit_unlocks.update(final_unit_unlocks)\n\n        # Unit upgrades for civ lines\n        final_unit_upgrades = {}\n        for unit_upgrade in unit_upgrades.values():\n            tech_id = unit_upgrade.tech.get_id()\n            if tech_id not in CIV_TECH_ASSOCS:\n                for main_tech_id, civ_tech_ids in CIV_TECH_ASSOCS.items():\n                    if tech_id in civ_tech_ids:\n                        # The tech is upgrade for an alternative civ so the upgrade\n                        # is stored with the main upgrade\n                        main_upgrade = unit_upgrades[main_tech_id]\n                        main_upgrade.add_civ_upgrade(unit_upgrade)\n                        break\n\n                else:\n                    # The upgrade is for a line without alternative civ lines\n                    final_unit_upgrades.update({unit_upgrade.get_id(): unit_upgrade})\n\n            else:\n                # The upgrade is for a main line\n                final_unit_upgrades.update({unit_upgrade.get_id(): unit_upgrade})\n\n        full_data_set.tech_groups.update(final_unit_upgrades)\n        full_data_set.unit_upgrades.update(final_unit_upgrades)\n\n        # Initiated techs are stored with buildings\n        genie_units = full_data_set.genie_units\n        for genie_unit in genie_units.values():\n            if not genie_unit.has_member(\"research_id\"):\n                continue\n\n            building_id = genie_unit[\"id0\"].value\n            initiated_tech_id = genie_unit[\"research_id\"].value\n\n            if initiated_tech_id == -1:\n                continue\n\n            if building_id not in full_data_set.building_lines.keys():\n                # Skips upgraded buildings (which initiate the same techs)\n                continue\n\n            initiated_tech = InitiatedTech(initiated_tech_id, building_id, full_data_set)\n            full_data_set.tech_groups.update({initiated_tech.get_id(): initiated_tech})\n            full_data_set.initiated_techs.update({initiated_tech.get_id(): initiated_tech})\n\n        # Civ boni have to be aquired from techs\n        # Civ boni = ONLY passive boni (not unit unlocks, unit upgrades or team bonus)\n        genie_techs = full_data_set.genie_techs\n        for index, _ in enumerate(genie_techs):\n            tech_id = index\n\n            # Civ ID must be positive and non-zero\n            civ_id = genie_techs[index][\"civilization_id\"].value\n            if civ_id <= 0:\n                continue\n\n            # Passive boni are not researched anywhere\n            research_location_id = genie_techs[index][\"research_location_id\"].value\n            if research_location_id > 0:\n                continue\n\n            # Passive boni are not available in full tech mode\n            full_tech_mode = genie_techs[index][\"full_tech_mode\"].value\n            if full_tech_mode:\n                continue\n\n            civ_bonus = CivBonus(tech_id, civ_id, full_data_set)\n            full_data_set.tech_groups.update({civ_bonus.get_id(): civ_bonus})\n            full_data_set.civ_boni.update({civ_bonus.get_id(): civ_bonus})\n\n    @staticmethod\n    def link_garrison(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Link a garrison unit to the lines that are stored and vice versa. This is done\n        to provide quick access during conversion.\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        garrisoned_lines = {}\n        garrisoned_lines.update(full_data_set.unit_lines)\n        garrisoned_lines.update(full_data_set.ambient_groups)\n\n        garrison_lines = {}\n        garrison_lines.update(full_data_set.unit_lines)\n        garrison_lines.update(full_data_set.building_lines)\n\n        # Search through all units and look at their garrison commands\n        for unit_line in garrisoned_lines.values():\n            garrison_classes = []\n            garrison_units = []\n\n            if unit_line.has_command(3):\n                unit_commands = unit_line.get_head_unit()[\"unit_commands\"].value\n                for command in unit_commands:\n                    type_id = command[\"type\"].value\n\n                    if type_id != 3:\n                        continue\n\n                    class_id = command[\"class_id\"].value\n                    if class_id > -1:\n                        garrison_classes.append(class_id)\n\n                        if class_id == 18:\n                            # Towers because LucasArts ALSO didn't like consistent rules\n                            garrison_classes.append(10)\n\n                    unit_id = command[\"unit_id\"].value\n                    if unit_id > -1:\n                        garrison_units.append(unit_id)\n\n            for garrison_line in garrison_lines.values():\n                if not garrison_line.is_garrison():\n                    continue\n\n                # Natural garrison\n                garrison_mode = garrison_line.get_garrison_mode()\n                if garrison_mode == GenieGarrisonMode.NATURAL:\n                    if unit_line.get_head_unit().has_member(\"creatable_type\"):\n                        creatable_type = unit_line.get_head_unit()[\"creatable_type\"].value\n\n                    else:\n                        creatable_type = 0\n\n                    if garrison_line.get_head_unit().has_member(\"garrison_type\"):\n                        garrison_type = garrison_line.get_head_unit()[\"garrison_type\"].value\n\n                    else:\n                        garrison_type = 0\n\n                    if creatable_type == 1 and not garrison_type & 0x01:\n                        continue\n\n                    if creatable_type == 2 and not garrison_type & 0x02:\n                        continue\n\n                    if creatable_type == 3 and not garrison_type & 0x04:\n                        continue\n\n                    if creatable_type == 6 and not garrison_type & 0x08:\n                        continue\n\n                    if (creatable_type == 0 and unit_line.get_class_id() == 1) and not\\\n                            garrison_type & 0x10:\n                        # Bantha/Nerf\n                        continue\n\n                    if garrison_line.get_class_id() in garrison_classes:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n                        continue\n\n                    if garrison_line.get_head_unit_id() in garrison_units:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n                        continue\n\n                # Transports/ unit garrisons (no conditions)\n                elif garrison_mode in (GenieGarrisonMode.TRANSPORT,\n                                       GenieGarrisonMode.UNIT_GARRISON):\n                    if garrison_line.get_class_id() in garrison_classes:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n\n                # Self produced units (these cannot be determined from commands)\n                elif garrison_mode == GenieGarrisonMode.SELF_PRODUCED:\n                    if unit_line in garrison_line.creates:\n                        unit_line.garrison_locations.append(garrison_line)\n                        garrison_line.garrison_entities.append(unit_line)\n\n                # Jedi/Sith inventories\n                elif garrison_mode == GenieGarrisonMode.MONK:\n                    # Search for a pickup command\n                    unit_commands = garrison_line.get_head_unit()[\"unit_commands\"].value\n                    for command in unit_commands:\n                        type_id = command[\"type\"].value\n\n                        if type_id != 132:\n                            continue\n\n                        unit_id = command[\"unit_id\"].value\n                        if unit_id == unit_line.get_head_unit_id():\n                            unit_line.garrison_locations.append(garrison_line)\n                            garrison_line.garrison_entities.append(unit_line)\n\n    @staticmethod\n    def link_repairables(full_data_set: GenieObjectContainer) -> None:\n        \"\"\"\n        Set units/buildings as repairable\n\n        :param full_data_set: GenieObjectContainer instance that\n                              contains all relevant data for the conversion\n                              process.\n        :type full_data_set: class: ...dataformat.aoc.genie_object_container.GenieObjectContainer\n        \"\"\"\n        villager_groups = full_data_set.villager_groups\n\n        repair_lines = {}\n        repair_lines.update(full_data_set.unit_lines)\n        repair_lines.update(full_data_set.building_lines)\n\n        repair_classes = []\n        for villager in villager_groups.values():\n            repair_unit = villager.get_units_with_command(106)[0]\n            unit_commands = repair_unit[\"unit_commands\"].value\n            for command in unit_commands:\n                type_id = command[\"type\"].value\n\n                if type_id != 106:\n                    continue\n\n                class_id = command[\"class_id\"].value\n                if class_id == -1:\n                    # Buildings/Siege\n                    repair_classes.append(10)\n                    repair_classes.append(18)\n                    repair_classes.append(32)\n                    repair_classes.append(33)\n                    repair_classes.append(34)\n                    repair_classes.append(35)\n                    repair_classes.append(36)\n                    repair_classes.append(53)\n\n                else:\n                    repair_classes.append(class_id)\n\n        for repair_line in repair_lines.values():\n            if repair_line.get_class_id() in repair_classes:\n                repair_line.repairable = True\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/tech_subprocessor.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-branches\n#\n# TODO:\n# pylint: disable=line-too-long\n\n\"\"\"\nCreates patches for technologies.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import CivTeamBonus, CivBonus\nfrom ..aoc.tech_subprocessor import AoCTechSubprocessor\nfrom ..aoc.upgrade_attribute_subprocessor import AoCUpgradeAttributeSubprocessor\nfrom ..aoc.upgrade_resource_subprocessor import AoCUpgradeResourceSubprocessor\nfrom .upgrade_attribute_subprocessor import SWGBCCUpgradeAttributeSubprocessor\nfrom .upgrade_resource_subprocessor import SWGBCCUpgradeResourceSubprocessor\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_effect import GenieEffectObject\n    from openage.convert.value_object.conversion.forward_ref import ForwardRef\n\n\nclass SWGBCCTechSubprocessor:\n    \"\"\"\n    Creates raw API objects and patches for techs and civ setups in SWGB.\n    \"\"\"\n\n    upgrade_attribute_funcs = {\n        0: AoCUpgradeAttributeSubprocessor.hp_upgrade,\n        1: AoCUpgradeAttributeSubprocessor.los_upgrade,\n        2: AoCUpgradeAttributeSubprocessor.garrison_capacity_upgrade,\n        3: AoCUpgradeAttributeSubprocessor.unit_size_x_upgrade,\n        4: AoCUpgradeAttributeSubprocessor.unit_size_y_upgrade,\n        5: AoCUpgradeAttributeSubprocessor.move_speed_upgrade,\n        6: AoCUpgradeAttributeSubprocessor.rotation_speed_upgrade,\n        8: AoCUpgradeAttributeSubprocessor.armor_upgrade,\n        9: AoCUpgradeAttributeSubprocessor.attack_upgrade,\n        10: AoCUpgradeAttributeSubprocessor.reload_time_upgrade,\n        11: AoCUpgradeAttributeSubprocessor.accuracy_upgrade,\n        12: AoCUpgradeAttributeSubprocessor.max_range_upgrade,\n        13: AoCUpgradeAttributeSubprocessor.work_rate_upgrade,\n        14: AoCUpgradeAttributeSubprocessor.carry_capacity_upgrade,\n        16: AoCUpgradeAttributeSubprocessor.projectile_unit_upgrade,\n        17: AoCUpgradeAttributeSubprocessor.graphics_angle_upgrade,\n        18: AoCUpgradeAttributeSubprocessor.terrain_defense_upgrade,\n        19: AoCUpgradeAttributeSubprocessor.ballistics_upgrade,\n        20: AoCUpgradeAttributeSubprocessor.min_range_upgrade,\n        21: AoCUpgradeAttributeSubprocessor.resource_storage_1_upgrade,\n        22: AoCUpgradeAttributeSubprocessor.blast_radius_upgrade,\n        23: AoCUpgradeAttributeSubprocessor.search_radius_upgrade,\n        100: SWGBCCUpgradeAttributeSubprocessor.resource_cost_upgrade,\n        101: AoCUpgradeAttributeSubprocessor.creation_time_upgrade,\n        102: AoCUpgradeAttributeSubprocessor.min_projectiles_upgrade,\n        103: AoCUpgradeAttributeSubprocessor.cost_food_upgrade,\n        104: SWGBCCUpgradeAttributeSubprocessor.cost_carbon_upgrade,\n        105: SWGBCCUpgradeAttributeSubprocessor.cost_nova_upgrade,\n        106: SWGBCCUpgradeAttributeSubprocessor.cost_ore_upgrade,\n        107: AoCUpgradeAttributeSubprocessor.max_projectiles_upgrade,\n        108: AoCUpgradeAttributeSubprocessor.garrison_heal_upgrade,\n    }\n\n    upgrade_resource_funcs = {\n        4: AoCUpgradeResourceSubprocessor.starting_population_space_upgrade,\n        5: SWGBCCUpgradeResourceSubprocessor.conversion_range_upgrade,\n        10: SWGBCCUpgradeResourceSubprocessor.shield_recharge_rate_upgrade,\n        23: SWGBCCUpgradeResourceSubprocessor.submarine_detect_upgrade,\n        26: SWGBCCUpgradeResourceSubprocessor.shield_dropoff_time_upgrade,\n        27: SWGBCCUpgradeResourceSubprocessor.monk_conversion_upgrade,\n        28: SWGBCCUpgradeResourceSubprocessor.building_conversion_upgrade,\n        31: SWGBCCUpgradeResourceSubprocessor.assault_mech_anti_air_upgrade,\n        32: AoCUpgradeResourceSubprocessor.bonus_population_upgrade,\n        33: SWGBCCUpgradeResourceSubprocessor.shield_power_core_upgrade,\n        35: SWGBCCUpgradeResourceSubprocessor.faith_recharge_rate_upgrade,\n        36: AoCUpgradeResourceSubprocessor.farm_food_upgrade,\n        38: SWGBCCUpgradeResourceSubprocessor.shield_air_units_upgrade,\n        46: AoCUpgradeResourceSubprocessor.tribute_inefficiency_upgrade,\n        47: AoCUpgradeResourceSubprocessor.gather_gold_efficiency_upgrade,\n        50: AoCUpgradeResourceSubprocessor.reveal_ally_upgrade,\n        56: SWGBCCUpgradeResourceSubprocessor.cloak_upgrade,\n        58: SWGBCCUpgradeResourceSubprocessor.detect_cloak_upgrade,\n        77: AoCUpgradeResourceSubprocessor.conversion_resistance_upgrade,\n        78: AoCUpgradeResourceSubprocessor.trade_penalty_upgrade,\n        79: AoCUpgradeResourceSubprocessor.gather_stone_efficiency_upgrade,\n        84: AoCUpgradeResourceSubprocessor.starting_villagers_upgrade,\n        85: AoCUpgradeResourceSubprocessor.chinese_tech_discount_upgrade,\n        87: SWGBCCUpgradeResourceSubprocessor.concentration_upgrade,\n        89: AoCUpgradeResourceSubprocessor.heal_rate_upgrade,\n        90: SWGBCCUpgradeResourceSubprocessor.heal_range_upgrade,\n        91: AoCUpgradeResourceSubprocessor.starting_food_upgrade,\n        92: AoCUpgradeResourceSubprocessor.starting_wood_upgrade,\n        96: SWGBCCUpgradeResourceSubprocessor.berserk_heal_rate_upgrade,\n        97: AoCUpgradeResourceSubprocessor.herding_dominance_upgrade,\n        178: AoCUpgradeResourceSubprocessor.conversion_resistance_min_rounds_upgrade,\n        179: AoCUpgradeResourceSubprocessor.conversion_resistance_max_rounds_upgrade,\n        183: AoCUpgradeResourceSubprocessor.reveal_enemy_upgrade,\n        189: AoCUpgradeResourceSubprocessor.gather_wood_efficiency_upgrade,\n        190: AoCUpgradeResourceSubprocessor.gather_food_efficiency_upgrade,\n        191: AoCUpgradeResourceSubprocessor.relic_gold_bonus_upgrade,\n        192: AoCUpgradeResourceSubprocessor.heresy_upgrade,\n        193: AoCUpgradeResourceSubprocessor.theocracy_upgrade,\n        194: AoCUpgradeResourceSubprocessor.crenellations_upgrade,\n        196: AoCUpgradeResourceSubprocessor.wonder_time_increase_upgrade,\n        197: AoCUpgradeResourceSubprocessor.spies_discount_upgrade,\n    }\n\n    @classmethod\n    def get_patches(cls, converter_group: ConverterObjectGroup) -> list[ForwardRef]:\n        \"\"\"\n        Returns the patches for a converter group, depending on the type\n        of its effects.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n        team_bonus = False\n\n        if isinstance(converter_group, CivTeamBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n            team_bonus = True\n\n        elif isinstance(converter_group, CivBonus):\n            effects = converter_group.get_effects()\n\n            # Change converter group here, so that the Civ object gets the patches\n            converter_group = dataset.civ_groups[converter_group.get_civilization_id()]\n\n        else:\n            effects = converter_group.get_effects()\n\n        team_effect = False\n        for effect in effects:\n            type_id = effect.get_type()\n\n            if team_bonus or type_id in (10, 11, 12, 13, 14, 15, 16):\n                # Same effect as (type_id - 1) but gets applied to all allies\n                team_effect = True\n                type_id -= 10\n\n            if type_id in (0, 4, 5):\n                patches.extend(cls.attribute_modify_effect(converter_group,\n                                                           effect,\n                                                           team=team_effect))\n\n            elif type_id in (1, 6):\n                patches.extend(cls.resource_modify_effect(converter_group,\n                                                          effect,\n                                                          team=team_effect))\n\n            elif type_id == 2:\n                # Enabling/disabling units: Handled in creatable conditions\n                pass\n\n            elif type_id == 3:\n                patches.extend(AoCTechSubprocessor.upgrade_unit_effect(converter_group,\n                                                                       effect))\n\n            elif type_id == 7:\n                # Spawn units: This should be a script\n                pass\n\n            elif type_id == 101:\n                patches.extend(AoCTechSubprocessor.tech_cost_modify_effect(converter_group,\n                                                                           effect,\n                                                                           team=team_effect))\n\n            elif type_id == 102:\n                # Tech disable: Only used for civ tech tree\n                pass\n\n            elif type_id == 103:\n                patches.extend(AoCTechSubprocessor.tech_time_modify_effect(converter_group,\n                                                                           effect,\n                                                                           team=team_effect))\n\n            team_effect = False\n\n        return patches\n\n    @staticmethod\n    def attribute_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying attributes of entities.\n        \"\"\"\n        patches = []\n        dataset = converter_group.data\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type == 0:\n            operator = MemberOperator.ASSIGN\n\n        elif effect_type == 4:\n            operator = MemberOperator.ADD\n\n        elif effect_type == 5:\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid resource effect\")\n\n        unit_id = effect[\"attr_a\"].value\n        class_id = effect[\"attr_b\"].value\n        attribute_type = effect[\"attr_c\"].value\n        value = effect[\"attr_d\"].value\n\n        if attribute_type == -1:\n            return patches\n\n        affected_entities = []\n        if unit_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.contains_entity(unit_id):\n                    affected_entities.append(line)\n\n                elif attribute_type == 19:\n                    if line.is_projectile_shooter() and line.has_projectile(unit_id):\n                        affected_entities.append(line)\n\n        elif class_id != -1:\n            entity_lines = {}\n            entity_lines.update(dataset.unit_lines)\n            entity_lines.update(dataset.building_lines)\n            entity_lines.update(dataset.ambient_groups)\n\n            for line in entity_lines.values():\n                if line.get_class_id() == class_id:\n                    affected_entities.append(line)\n\n        else:\n            return patches\n\n        upgrade_func = SWGBCCTechSubprocessor.upgrade_attribute_funcs[attribute_type]\n        for affected_entity in affected_entities:\n            patches.extend(upgrade_func(converter_group, affected_entity, value, operator, team))\n\n        return patches\n\n    @staticmethod\n    def resource_modify_effect(\n        converter_group: ConverterObjectGroup,\n        effect: GenieEffectObject,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates the patches for modifying resources.\n        \"\"\"\n        patches = []\n\n        effect_type = effect.get_type()\n        operator = None\n        if effect_type == 1:\n            mode = effect[\"attr_b\"].value\n\n            if mode == 0:\n                operator = MemberOperator.ASSIGN\n\n            else:\n                operator = MemberOperator.ADD\n\n        elif effect_type == 6:\n            operator = MemberOperator.MULTIPLY\n\n        else:\n            raise TypeError(f\"Effect type {effect_type} is not a valid resource effect\")\n\n        resource_id = effect[\"attr_a\"].value\n        value = effect[\"attr_d\"].value\n\n        if resource_id in (-1, 6, 21):\n            # -1 = invalid ID\n            # 6  = set current age (unused)\n            # 21 = tech count (unused)\n            return patches\n\n        upgrade_func = SWGBCCTechSubprocessor.upgrade_resource_funcs[resource_id]\n        patches.extend(upgrade_func(converter_group, value, operator, team))\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/upgrade_attribute_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for attribute modification effects in SWGB.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.convert.entity_object.conversion.aoc.genie_unit import GenieGameEntityGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass SWGBCCUpgradeAttributeSubprocessor:\n    \"\"\"\n    Creates raw API objects for attribute upgrade effects in SWGB.\n    \"\"\"\n\n    @staticmethod\n    def cost_carbon_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the carbon cost modify effect (ID: 104).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.CarbonAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}CarbonCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}CarbonCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cost_nova_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the nova cost modify effect (ID: 105).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.NovaAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}NovaCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}NovaCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cost_ore_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the ore cost modify effect (ID: 106).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                            f\"{game_entity_name}Cost.OreAmount\")\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}OreCostWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}OreCost\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                       value,\n                                                       \"engine.util.resource.ResourceAmount\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def resource_cost_upgrade(\n        converter_group: ConverterObjectGroup,\n        line: GenieGameEntityGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the resource modify effect (ID: 100).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param line: Unit/Building line that has the ability.\n        :type line: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        head_unit = line.get_head_unit()\n        head_unit_id = line.get_head_unit_id()\n        dataset = line.data\n\n        patches = []\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        game_entity_name = name_lookup_dict[head_unit_id][0]\n\n        for resource_amount in head_unit[\"resource_cost\"].value:\n            resource_id = resource_amount[\"type_id\"].value\n            resource_name = \"\"\n            if resource_id == -1:\n                # Not a valid resource\n                continue\n\n            if resource_id == 0:\n                resource_name = \"Food\"\n\n            elif resource_id == 1:\n                resource_name = \"Carbon\"\n\n            elif resource_id == 2:\n                resource_name = \"Ore\"\n\n            elif resource_id == 3:\n                resource_name = \"Nova\"\n\n            else:\n                # Other resource ids are handled differently\n                continue\n\n            # Skip resources that are only expected to be there\n            if not resource_amount[\"enabled\"].value:\n                continue\n\n            patch_target_ref = (f\"{game_entity_name}.CreatableGameEntity.\"\n                                f\"{game_entity_name}Cost.\"\n                                f\"{resource_name}Amount\")\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}{resource_name}CostWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}{resource_name}Cost\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"amount\",\n                                                           value,\n                                                           \"engine.util.resource.ResourceAmount\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/conversion/swgbcc/upgrade_resource_subprocessor.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals,too-many-lines,too-many-statements,too-many-public-methods\n#\n# TODO: Remove when all methods are implemented\n# pylint: disable=unused-argument,line-too-long\n\n\"\"\"\nCreates upgrade patches for resource modification effects in SWGB.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom .....nyan.nyan_structs import MemberOperator\nfrom ....entity_object.conversion.aoc.genie_tech import GenieTechEffectBundleGroup\nfrom ....entity_object.conversion.converter_object import RawAPIObject\nfrom ....service.conversion import internal_name_lookups\nfrom ....value_object.conversion.forward_ref import ForwardRef\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup\n    from openage.nyan.nyan_structs import MemberOperator\n\n\nclass SWGBCCUpgradeResourceSubprocessor:\n    \"\"\"\n    Creates raw API objects for resource upgrade effects in SWGB.\n    \"\"\"\n\n    @staticmethod\n    def assault_mech_anti_air_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the assault mech anti air effect (ID: 31).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def berserk_heal_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the berserk heal rate modify effect (ID: 96).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        berserk_id = 8\n        dataset = converter_group.data\n        line = dataset.unit_lines[berserk_id]\n\n        patches = []\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[berserk_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.RegenerateHealth.HealthRate\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}HealthRegenerationWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}HealthRegeneration\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        # Regeneration is on a counter, so we have to invert the value\n        value = 1 / value\n        nyan_patch_raw_api_object.add_raw_patch_member(\"rate\",\n                                                       value,\n                                                       \"engine.util.attribute.AttributeRate\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def building_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the building conversion effect (ID: 28).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        force_ids = [115, 180]\n        dataset = converter_group.data\n\n        patches = []\n\n        for force_id in force_ids:\n            line = dataset.unit_lines[force_id]\n\n            name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n            obj_id = converter_group.get_id()\n            if isinstance(converter_group, GenieTechEffectBundleGroup):\n                tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n                obj_name = tech_lookup_dict[obj_id][0]\n\n            else:\n                civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n                obj_name = civ_lookup_dict[obj_id][0]\n\n            game_entity_name = name_lookup_dict[force_id][0]\n\n            patch_target_ref = f\"{game_entity_name}.Convert\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Building conversion\n\n            # Wrapper\n            wrapper_name = \"EnableBuildingConversionWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = \"EnableBuildingConversion\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            # New allowed types\n            allowed_types = [\n                dataset.pregen_nyan_objects[\"util.game_entity_type.types.Building\"].get_nyan_object(\n                )\n            ]\n            nyan_patch_raw_api_object.add_raw_patch_member(\"allowed_types\",\n                                                           allowed_types,\n                                                           \"engine.ability.type.ApplyDiscreteEffect\",\n                                                           MemberOperator.ADD)\n\n            # Blacklisted buildings\n            tc_line = dataset.building_lines[109]\n            farm_line = dataset.building_lines[50]\n            temple_line = dataset.building_lines[104]\n            wonder_line = dataset.building_lines[276]\n\n            blacklisted_forward_refs = [ForwardRef(tc_line, \"CommandCenter\"),\n                                        ForwardRef(farm_line, \"Farm\"),\n                                        ForwardRef(temple_line, \"Temple\"),\n                                        ForwardRef(wonder_line, \"Monument\"),\n                                        ]\n            nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                           blacklisted_forward_refs,\n                                                           \"engine.ability.type.ApplyDiscreteEffect\",\n                                                           MemberOperator.ADD)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def cloak_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the force cloak effect (ID: 56).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def concentration_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the concentration effect (ID: 87).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def conversion_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the conversion range modify effect (ID: 5).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def detect_cloak_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the force detect cloak effect (ID: 58).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def faith_recharge_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the faith_recharge_rate modify effect (ID: 35).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        force_ids = [115, 180]\n        dataset = converter_group.data\n\n        patches = []\n\n        for force_id in force_ids:\n            line = dataset.unit_lines[force_id]\n\n            name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n            obj_id = converter_group.get_id()\n            if isinstance(converter_group, GenieTechEffectBundleGroup):\n                tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n                obj_name = tech_lookup_dict[obj_id][0]\n\n            else:\n                civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n                obj_name = civ_lookup_dict[obj_id][0]\n\n            game_entity_name = name_lookup_dict[force_id][0]\n\n            patch_target_ref = f\"{game_entity_name}.RegenerateFaith.FaithRate\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Change{game_entity_name}FaithRegenerationWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Change{game_entity_name}FaithRegeneration\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            nyan_patch_raw_api_object.add_raw_patch_member(\"rate\",\n                                                           value,\n                                                           \"engine.util.attribute.AttributeRate\",\n                                                           operator)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def heal_range_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the heal range modify effect (ID: 90).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        medic_id = 939\n        dataset = converter_group.data\n\n        patches = []\n\n        line = dataset.unit_lines[medic_id]\n\n        name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n        obj_id = converter_group.get_id()\n        if isinstance(converter_group, GenieTechEffectBundleGroup):\n            tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n            obj_name = tech_lookup_dict[obj_id][0]\n\n        else:\n            civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n            obj_name = civ_lookup_dict[obj_id][0]\n\n        game_entity_name = name_lookup_dict[medic_id][0]\n\n        patch_target_ref = f\"{game_entity_name}.Heal\"\n        patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n        # Wrapper\n        wrapper_name = f\"Change{game_entity_name}HealRangeWrapper\"\n        wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n        wrapper_location = ForwardRef(converter_group, obj_name)\n        wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                              wrapper_name,\n                                              dataset.nyan_api_objects,\n                                              wrapper_location)\n        wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n        # Nyan patch\n        nyan_patch_name = f\"Change{game_entity_name}HealRange\"\n        nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n        nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n        nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                 nyan_patch_name,\n                                                 dataset.nyan_api_objects,\n                                                 nyan_patch_location)\n        nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n        nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n        nyan_patch_raw_api_object.add_raw_patch_member(\"max_range\",\n                                                       value,\n                                                       \"engine.ability.type.RangedContinuousEffect\",\n                                                       operator)\n\n        patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n        wrapper_raw_api_object.add_raw_member(\"patch\",\n                                              patch_forward_ref,\n                                              \"engine.util.patch.Patch\")\n\n        if team:\n            team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n            )\n            properties = {\n                dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n            }\n            wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                  properties,\n                                                  \"engine.util.patch.Patch\")\n\n        converter_group.add_raw_api_object(wrapper_raw_api_object)\n        converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n        wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n        patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def monk_conversion_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the monk conversion effect (ID: 27).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        force_ids = [115, 180]\n        dataset = converter_group.data\n\n        patches = []\n\n        for force_id in force_ids:\n            line = dataset.unit_lines[force_id]\n\n            name_lookup_dict = internal_name_lookups.get_entity_lookups(dataset.game_version)\n\n            obj_id = converter_group.get_id()\n            if isinstance(converter_group, GenieTechEffectBundleGroup):\n                tech_lookup_dict = internal_name_lookups.get_tech_lookups(dataset.game_version)\n                obj_name = tech_lookup_dict[obj_id][0]\n\n            else:\n                civ_lookup_dict = internal_name_lookups.get_civ_lookups(dataset.game_version)\n                obj_name = civ_lookup_dict[obj_id][0]\n\n            game_entity_name = name_lookup_dict[force_id][0]\n\n            patch_target_ref = f\"{game_entity_name}.Convert\"\n            patch_target_forward_ref = ForwardRef(line, patch_target_ref)\n\n            # Wrapper\n            wrapper_name = f\"Enable{game_entity_name}ConversionWrapper\"\n            wrapper_ref = f\"{obj_name}.{wrapper_name}\"\n            wrapper_location = ForwardRef(converter_group, obj_name)\n            wrapper_raw_api_object = RawAPIObject(wrapper_ref,\n                                                  wrapper_name,\n                                                  dataset.nyan_api_objects,\n                                                  wrapper_location)\n            wrapper_raw_api_object.add_raw_parent(\"engine.util.patch.Patch\")\n\n            # Nyan patch\n            nyan_patch_name = f\"Enable{game_entity_name}Conversion\"\n            nyan_patch_ref = f\"{obj_name}.{wrapper_name}.{nyan_patch_name}\"\n            nyan_patch_location = ForwardRef(converter_group, wrapper_ref)\n            nyan_patch_raw_api_object = RawAPIObject(nyan_patch_ref,\n                                                     nyan_patch_name,\n                                                     dataset.nyan_api_objects,\n                                                     nyan_patch_location)\n            nyan_patch_raw_api_object.add_raw_parent(\"engine.util.patch.NyanPatch\")\n            nyan_patch_raw_api_object.set_patch_target(patch_target_forward_ref)\n\n            monk_forward_ref = ForwardRef(line, game_entity_name)\n            nyan_patch_raw_api_object.add_raw_patch_member(\"blacklisted_entities\",\n                                                           [monk_forward_ref],\n                                                           \"engine.ability.type.ApplyDiscreteEffect\",\n                                                           MemberOperator.SUBTRACT)\n\n            patch_forward_ref = ForwardRef(converter_group, nyan_patch_ref)\n            wrapper_raw_api_object.add_raw_member(\"patch\",\n                                                  patch_forward_ref,\n                                                  \"engine.util.patch.Patch\")\n\n            if team:\n                team_property = dataset.pregen_nyan_objects[\"util.patch.property.types.Team\"].get_nyan_object(\n                )\n                properties = {\n                    dataset.nyan_api_objects[\"engine.util.patch.property.type.Diplomatic\"]: team_property\n                }\n                wrapper_raw_api_object.add_raw_member(\"properties\",\n                                                      properties,\n                                                      \"engine.util.patch.Patch\")\n\n            converter_group.add_raw_api_object(wrapper_raw_api_object)\n            converter_group.add_raw_api_object(nyan_patch_raw_api_object)\n\n            wrapper_forward_ref = ForwardRef(converter_group, wrapper_ref)\n            patches.append(wrapper_forward_ref)\n\n        return patches\n\n    @staticmethod\n    def shield_air_units_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the shield bomber/fighter effect (ID: 38).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def shield_dropoff_time_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the shield dropoff time modify effect (ID: 26).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def shield_power_core_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the shield power core effect (ID: 33).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def shield_recharge_rate_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Union[int, float],\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the shield recharge rate modify effect (ID: 10).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: int, float\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # TODO: Implement\n\n        return patches\n\n    @staticmethod\n    def submarine_detect_upgrade(\n        converter_group: ConverterObjectGroup,\n        value: typing.Any,\n        operator: MemberOperator,\n        team: bool = False\n    ) -> list[ForwardRef]:\n        \"\"\"\n        Creates a patch for the submarine detect effect (ID: 23).\n\n        :param converter_group: Tech/Civ that gets the patch.\n        :type converter_group: ...dataformat.converter_object.ConverterObjectGroup\n        :param value: Value used for patching the member.\n        :type value: Any\n        :param operator: Operator used for patching the member.\n        :type operator: MemberOperator\n        :returns: The forward references for the generated patches.\n        :rtype: list\n        \"\"\"\n        patches = []\n\n        # Unused\n\n        return patches\n"
  },
  {
    "path": "openage/convert/processor/export/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tdata_exporter.py\n\tgenerate_manifest_hashes.py\n\tmedia_exporter.py\n\tmodpack_exporter.py\n)\n\nadd_cython_modules(\n\tterrain_merge.pyx\n\ttexture_merge.pyx\n)\n"
  },
  {
    "path": "openage/convert/processor/export/__init__.pxd",
    "content": ""
  },
  {
    "path": "openage/convert/processor/export/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProcessors used for export.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/processor/export/data_exporter.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\"\"\"\nExports data formats from a modpack to files.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.directory import Directory\n    from openage.convert.entity_object.export.data_definition import DataDefinition\n\n\nclass DataExporter:\n    \"\"\"\n    Writes the contents of a created modpack into a targetdir.\n    \"\"\"\n\n    @staticmethod\n    def export(data_files: list[DataDefinition], exportdir: Directory) -> None:\n        \"\"\"\n        Exports data files.\n\n        :param data_files: Data definitions for data files.\n        :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder\n                          and target filename should be stored in the export request.\n        :type exportdir: Directory\n        :type data_files: list\n        \"\"\"\n        for data_file in data_files:\n            output_dir = exportdir.joinpath(data_file.targetdir)\n            output_content = data_file.dump()\n\n            # generate human-readable file\n            with output_dir[data_file.filename].open('wb') as outfile:\n                outfile.write(output_content.encode('utf-8'))\n"
  },
  {
    "path": "openage/convert/processor/export/generate_manifest_hashes.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\"\"\"\nProvides functions for traversing a directory and\ngenerating hash values for all the items inside.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nimport os\n\nfrom openage.util.hash import hash_file\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.path import Path\n    from openage.convert.entity_object.conversion.modpack import Modpack\n\n\ndef bfs_directory(root: Path) -> typing.Generator[Path, None, None]:\n    \"\"\"\n    Traverse the given directory with breadth-first way.\n\n    :param root: The directory to traverse.\n    :type root: ...util.fslike.path.Path\n    \"\"\"\n\n    # initiate from the root directory\n    dirs = [root]\n\n    while dirs:\n        next_level = []\n\n        for directory in dirs:\n            for item in directory.iterdir():\n                if item.is_dir():\n                    # save next subdirectories for next round\n                    next_level.append(item)\n                else:\n                    # return the path if it is a file\n                    yield item\n\n        dirs = next_level\n\n\ndef generate_hashes(\n    modpack: Modpack,\n    exportdir: Directory,\n    hash_algo: str = 'sha3_256',\n    bufsize: int = 32768\n) -> None:\n    \"\"\"\n    Generate hashes for all the items in a\n    given modpack and adds them to the manifest\n    instance.\n\n    :param modpack: The target modpack.\n    :type modpack: ..dataformats.modpack.Modpack\n    :param exportdir: Directory wheere modpacks are stored.\n    :type exportdir: ...util.fslike.path.Path\n    :param hash_algo: Hashing algorithm used.\n    :type hash_algo: str\n    :param bufsize: Buffer size for reading files.\n    :type bufsize: int\n    \"\"\"\n    # set the hashing algorithm in the manifest instance\n    modpack.manifest.set_hashing_func(hash_algo)\n\n    # traverse the directory with breadth-first way and\n    # generate hash values for the items encountered\n    for file in bfs_directory(exportdir):\n        hash_val = hash_file(file, hash_algo=hash_algo, bufsize=bufsize)\n        relative_path = os.path.relpath(str(file), str(exportdir))\n        modpack.manifest.add_hash_value(hash_val, relative_path)\n"
  },
  {
    "path": "openage/convert/processor/export/media_exporter.py",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments,too-many-locals\n\"\"\"\nConverts media requested by export requests to files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport logging\nimport os\nimport multiprocessing\nimport queue\nimport sys\n\nfrom openage.convert.entity_object.export.texture import Texture\nfrom openage.convert.service import debug_info\nfrom openage.convert.service.export.load_media_cache import load_media_cache\nfrom openage.convert.value_object.read.media.blendomatic import Blendomatic\nfrom openage.convert.value_object.read.media_types import MediaType\nfrom openage.log import dbg, info, get_loglevel\nfrom openage.util.strings import format_progress\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n    from openage.convert.entity_object.export.media_export_request import MediaExportRequest\n    from openage.convert.value_object.read.media.colortable import ColorTable\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.util.fslike.path import Path\n    from openage.util.dll import DllDirectoryManager\n\n\nclass MediaExporter:\n    \"\"\"\n    Provides functions for converting media files and writing them to a targetdir.\n\n    TODO: Avoid code duplication in the export functions.\n    \"\"\"\n\n    @staticmethod\n    def export(\n        export_requests: dict[MediaType, list[MediaExportRequest]],\n        sourcedir: Path,\n        exportdir: Path,\n        args: Namespace\n    ) -> None:\n        \"\"\"\n        Converts files requested by MediaExportRequests.\n\n        :param export_requests: Export requests for media files. This is a dict of export requests\n                                by their media type.\n        :param sourcedir: Directory where all media assets are mounted. Source subfolder and\n                          source filename should be stored in the export request.\n        :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder\n                          and target filename should be stored in the export request.\n        :param args: Converter arguments.\n        :type export_requests: dict\n        :type sourcedir: Path\n        :type exportdir: Path\n        :type args: Namespace\n        \"\"\"\n        cache_info = {}\n        if args.game_version.edition.media_cache:\n            cache_info = load_media_cache(args.game_version.edition.media_cache)\n\n        for media_type in export_requests.keys():\n            cur_export_requests = export_requests[media_type]\n\n            # Function for reading the source file data\n            read_data_func = None\n\n            # Multi-threaded function for exporting the source file data\n            export_func = None\n\n            # Optional function for handling data in the outqueue\n            handle_outqueue_func = None\n\n            kwargs = {}\n            if media_type is MediaType.BLEND:\n                read_data_func = MediaExporter._get_blend_data\n                export_func = _export_blend\n                itargs = (sourcedir, exportdir)\n                kwargs[\"blend_mode_count\"] = args.blend_mode_count\n                info(\"-- Exporting blend files...\")\n\n            elif media_type is MediaType.GRAPHICS:\n                read_data_func = MediaExporter._get_graphics_data\n                export_func = _export_texture\n                handle_outqueue_func = MediaExporter._handle_graphics_outqueue\n                itargs = (args.palettes, args.compression_level)\n                kwargs[\"cache_info\"] = cache_info\n                info(\"-- Exporting graphics files...\")\n\n            elif media_type is MediaType.SOUNDS:\n                read_data_func = MediaExporter._get_sound_data\n                export_func = _export_sound\n                itargs = tuple()\n                kwargs[\"debugdir\"] = args.debugdir\n                kwargs[\"loglevel\"] = args.debug_info\n                info(\"-- Exporting sound files...\")\n\n            elif media_type is MediaType.TERRAIN:\n                read_data_func = MediaExporter._get_terrain_data\n                export_func = _export_terrain\n                itargs = (args.palettes, args.compression_level, args.game_version)\n                info(\"-- Exporting terrain files...\")\n\n            if args.jobs == 1:\n                MediaExporter._export_singlethreaded(\n                    cur_export_requests,\n                    sourcedir,\n                    exportdir,\n                    read_data_func,\n                    export_func,\n                    handle_outqueue_func,\n                    itargs,\n                    kwargs\n                )\n\n            else:\n                MediaExporter._export_multithreaded(\n                    cur_export_requests,\n                    sourcedir,\n                    exportdir,\n                    read_data_func,\n                    export_func,\n                    handle_outqueue_func,\n                    itargs,\n                    kwargs,\n                    args.jobs,\n                    args.dll_manager,\n                )\n\n        if args.debug_info > 5:\n            cachedata = {}\n            for request in export_requests[MediaType.GRAPHICS]:\n                kwargs = {}\n                kwargs[\"palettes\"] = args.palettes\n                kwargs[\"compression_level\"] = args.compression_level\n\n                cache = MediaExporter._get_media_cache(\n                    request,\n                    sourcedir,\n                    args.palettes,\n                    compression_level=2\n                )\n\n                cachedata[request] = cache\n\n            debug_info.debug_media_cache(\n                args.debugdir,\n                args.debug_info,\n                sourcedir,\n                cachedata,\n                args.game_version\n            )\n\n    @staticmethod\n    def _export_singlethreaded(\n        requests: list[MediaExportRequest],\n        sourcedir: Path,\n        exportdir: Path,\n        read_data_func: typing.Callable,\n        export_func: typing.Callable,\n        handle_outqueue_func: typing.Callable | None,\n        itargs: tuple,\n        kwargs: dict\n    ):\n        \"\"\"\n        Export media files in a single thread.\n\n        :param requests: Export requests for media files.\n        :param sourcedir: Directory where all media assets are mounted. Source subfolder and\n                          source filename should be stored in the export request.\n        :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder\n                          and target filename should be stored in the export request.\n        :param read_data_func: Function for reading the source file data.\n        :param export_func: Function for exporting media files.\n        :param handle_outqueue_func: Optional function for handling data in the outqueue.\n        :param itargs: Arguments for the export function.\n        :param kwargs: Keyword arguments for the export function.\n        :type requests: list[MediaExportRequest]\n        :type sourcedir: Path\n        :type exportdir: Path\n        :type read_data_func: typing.Callable\n        :type export_func: typing.Callable\n        :type handle_outqueue_func: typing.Callable\n        :type itargs: tuple\n        :type kwargs: dict\n        \"\"\"\n        single_queue = queue.Queue()\n        for idx, request in enumerate(requests):\n            source_data = read_data_func(request, sourcedir, **kwargs)\n            if source_data is None:\n                continue\n\n            target_path = exportdir[request.targetdir, request.target_filename]\n\n            export_func(\n                idx,\n                source_data,\n                single_queue,\n                None,\n                request.source_filename,\n                target_path,\n                *itargs,\n                **kwargs\n            )\n\n            if get_loglevel() <= logging.DEBUG:\n                MediaExporter.log_fileinfo(\n                    sourcedir[request.get_type().value, request.source_filename],\n                    exportdir[request.targetdir, request.target_filename]\n                )\n\n            MediaExporter._show_progress(idx + 1, len(requests))\n\n        if handle_outqueue_func:\n            handle_outqueue_func(single_queue, requests)\n\n    @staticmethod\n    def _export_multithreaded(\n        requests: list[MediaExportRequest],\n        sourcedir: Path,\n        exportdir: Path,\n        read_data_func: typing.Callable,\n        export_func: typing.Callable,\n        handle_outqueue_func: typing.Callable | None,\n        itargs: tuple,\n        kwargs: dict,\n        job_count: int = None,\n        dll_manager: DllDirectoryManager = None,\n    ):\n        \"\"\"\n        Export media files in multiple threads.\n\n        :param requests: Export requests for media files.\n        :param sourcedir: Directory where all media assets are mounted. Source subfolder and\n                          source filename should be stored in the export request.\n        :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder\n                          and target filename should be stored in the export request.\n        :param read_data_func: Function for reading the source file data.\n        :param export_func: Function for exporting media files.\n        :param handle_outqueue_func: Optional function for handling data in the outqueue.\n        :param itargs: Arguments for the export function.\n        :param kwargs: Keyword arguments for the export function.\n        :param job_count: Number of worker processes to use.\n        :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only).\n        \"\"\"\n        worker_count = job_count\n        if worker_count is None:\n            # Small optimization that saves some time for small exports\n            worker_count = min(multiprocessing.cpu_count(), len(requests))\n\n        def error_callback(exception: Exception):\n            \"\"\"\n            Error callback for the worker pool.\n            \"\"\"\n            raise exception\n\n        # Create a manager for sharing data between the workers and main process\n        with multiprocessing.Manager() as manager:\n            # Workers write the image metadata to this queue\n            # so that it can be forwarded to the export requests\n            #\n            # we cannot do this in a worker process directly\n            # because the export requests cannot be pickled\n            outqueue = manager.Queue()\n\n            expected_size = len(requests)\n\n            # Create a pool of workers\n            with multiprocessing.Pool(worker_count) as pool:\n                for idx, request in enumerate(requests):\n                    # Feed the worker with the source file data (bytes) from the\n                    # main process\n                    #\n                    # This is necessary because some image files are inside an\n                    # archive and cannot be accessed asynchronously\n                    source_data = read_data_func(request, sourcedir, **kwargs)\n                    if source_data is None:\n                        expected_size -= 1\n                        continue\n\n                    target_path = exportdir[request.targetdir, request.target_filename]\n\n                    # Start an export call in a worker process\n                    # The call is asynchronous, so the next worker can be\n                    # started immediately\n                    pool.apply_async(\n                        export_func,\n                        args=(\n                            idx,\n                            source_data,\n                            outqueue,\n                            dll_manager,\n                            request.source_filename,\n                            target_path,\n                            *itargs\n                        ),\n                        kwds=kwargs,\n                        error_callback=error_callback\n                    )\n\n                    # Show progress\n                    MediaExporter._show_progress(outqueue.qsize(), expected_size)\n\n                # Close the pool since all workers have been started\n                pool.close()\n\n                # Show progress for remaining workers\n                while outqueue.qsize() < expected_size:\n                    MediaExporter._show_progress(outqueue.qsize(), expected_size)\n\n                # Wait for all workers to finish\n                pool.join()\n\n            if handle_outqueue_func:\n                handle_outqueue_func(outqueue, requests)\n\n        # Log file information\n        if get_loglevel() <= logging.DEBUG:\n            for request in requests:\n                MediaExporter.log_fileinfo(\n                    sourcedir[request.get_type().value, request.source_filename],\n                    exportdir[request.targetdir, request.target_filename]\n                )\n\n    @staticmethod\n    def _get_blend_data(\n        request: MediaExportRequest,\n        sourcedir: Path,\n        **kwargs  # pylint: disable=unused-argument\n    ) -> bytes:\n        \"\"\"\n        Get the raw file data of a blending mask.\n\n        :param request: Export request for a blending mask.\n        :param sourcedir: Directory where all media assets are mounted.\n        :type request: MediaExportRequest\n        :type sourcedir: Path\n        \"\"\"\n        source_file = sourcedir[request.get_type().value,\n                                request.source_filename]\n\n        return source_file.open(\"rb\").read()\n\n    @staticmethod\n    def _get_graphics_data(\n        request: MediaExportRequest,\n        sourcedir: Path,\n        **kwargs  # pylint: disable=unused-argument\n    ) -> bytes:\n        \"\"\"\n        Get the raw file data of a graphics file.\n\n        :param request: Export request for a graphics file.\n        :param sourcedir: Directory where all media assets are mounted.\n        :type request: MediaExportRequest\n        :type sourcedir: Path\n        \"\"\"\n        source_file = sourcedir[request.get_type().value,\n                                request.source_filename]\n        if not source_file.exists():\n            if source_file.suffix.lower() in (\".smx\", \".sld\"):\n                # Some DE2 graphics files have the wrong extension\n                # Fall back to the SMP (beta) extension\n                other_filename = request.source_filename[:-3] + \"smp\"\n                source_file = sourcedir[\n                    request.get_type().value,\n                    other_filename\n                ]\n                request.set_source_filename(other_filename)\n\n        return source_file.open(\"rb\").read()\n\n    @staticmethod\n    def _get_sound_data(\n        request: MediaExportRequest,\n        sourcedir: Path,\n        **kwargs\n    ) -> bytes | None:\n        \"\"\"\n        Get the raw file data of a sound file.\n\n        :param request: Export request for a sound file.\n        :param sourcedir: Directory where all media assets are mounted.\n        :type request: MediaExportRequest\n        :type sourcedir: Path\n        \"\"\"\n        source_file = sourcedir[request.get_type().value,\n                                request.source_filename]\n\n        if not source_file.is_file():\n            # TODO: Filter files that do not exist out sooner\n            debug_info.debug_not_found_sounds(kwargs[\"debugdir\"],\n                                              kwargs[\"loglevel\"],\n                                              source_file)\n            return None\n\n        return source_file.open(\"rb\").read()\n\n    @staticmethod\n    def _get_terrain_data(\n        request: MediaExportRequest,\n        sourcedir: Path,\n        **kwargs  # pylint: disable=unused-argument\n    ) -> bytes:\n        \"\"\"\n        Get the raw file data of a terrain graphics file.\n\n        :param request: Export request for a terrain graphics file.\n        :param sourcedir: Directory where all media assets are mounted.\n        :type request: MediaExportRequest\n        :type sourcedir: Path\n        \"\"\"\n        source_file = sourcedir[request.get_type().value,\n                                request.source_filename]\n\n        return source_file.open(\"rb\").read()\n\n    @staticmethod\n    def _handle_graphics_outqueue(\n        outqueue: multiprocessing.Queue,\n        requests: list[MediaExportRequest]\n    ):\n        \"\"\"\n        Collect the metadata from the workers and forward it to the\n        export requests.\n\n        This must be called before the manager of the queue is shutdown!\n\n        :param outqueue: Queue for passing metadata to the main process.\n        :param requests: Export requests for graphics files.\n        :type outqueue: multiprocessing.Queue\n        :type requests: list[MediaExportRequest]\n        \"\"\"\n        while not outqueue.empty():\n            idx, metadata = outqueue.get()\n            update_data = {requests[idx].target_filename: metadata}\n            requests[idx].set_changed()\n            requests[idx].notify_observers(update_data)\n            requests[idx].clear_changed()\n\n    @staticmethod\n    def _show_progress(\n        current_size: int,\n        total_size: int,\n    ):\n        \"\"\"\n        Show the progress of the export process.\n\n        :param current_size: Number of files that have been exported.\n        :param total_size: Total number of files to export.\n        :type current_size: int\n        :type total_size: int\n        \"\"\"\n        print(f\"-- Files done: {format_progress(current_size, total_size)}\",\n              end = \"\\r\", flush = True)\n\n    @staticmethod\n    def _get_media_cache(\n        export_request: MediaExportRequest,\n        sourcedir: Path,\n        palettes: dict[int, ColorTable],\n        compression_level: int\n    ) -> None:\n        \"\"\"\n        Convert a media file and return the used settings. This performs\n        a dry run, i.e. the graphics media is not saved on the filesystem.\n\n        :param export_request: Export request for a graphics file.\n        :param sourcedir: Directory where all media assets are mounted. Source subfolder and\n                          source filename should be stored in the export request.\n        :param exportdir: Directory the resulting file(s) will be exported to. Target subfolder\n                          and target filename should be stored in the export request.\n        :param palettes: Palettes used by the game.\n        :param compression_level: PNG compression level for the resulting image file.\n        :type export_request: MediaExportRequest\n        :type sourcedir: Path\n        :type exportdir: Path\n        :type palettes: dict\n        :type compression_level: int\n        \"\"\"\n        source_file = sourcedir[\n            export_request.get_type().value,\n            export_request.source_filename\n        ]\n\n        try:\n            media_file = source_file.open(\"rb\")\n\n        except FileNotFoundError:\n            if source_file.suffix.lower() == \".smx\":\n                # Rename extension to SMP and try again\n                other_filename = export_request.source_filename[:-1] + \"p\"\n                source_file = sourcedir[\n                    export_request.get_type().value,\n                    other_filename\n                ]\n\n            media_file = source_file.open(\"rb\")\n\n        if source_file.suffix.lower() == \".slp\":\n            from ...value_object.read.media.slp import SLP\n            image = SLP(media_file.read())\n\n        elif source_file.suffix.lower() == \".smp\":\n            from ...value_object.read.media.smp import SMP\n            image = SMP(media_file.read())\n\n        elif source_file.suffix.lower() == \".smx\":\n            from ...value_object.read.media.smx import SMX\n            image = SMX(media_file.read())\n\n        elif source_file.suffix.lower() == \".sld\":\n            from ...value_object.read.media.sld import SLD\n            image = SLD(media_file.read())\n\n        else:\n            raise SyntaxError(f\"Source file {source_file.name} has an unrecognized extension: \"\n                              f\"{source_file.suffix.lower()}\")\n\n        from .texture_merge import merge_frames\n        texture = Texture(image, palettes)\n        merge_frames(texture)\n        MediaExporter.save_png(\n            texture,\n            None,\n            None,\n            compression_level=compression_level,\n            cache=None,\n            dry_run=True\n        )\n\n        return texture.get_cache_params()\n\n    @staticmethod\n    def save_png(\n        texture: Texture,\n        targetdir: Path,\n        filename: str,\n        compression_level: int = 1,\n        cache: dict = None,\n        dry_run: bool = False\n    ) -> None:\n        \"\"\"\n        Store the image data into the target directory path,\n        with given filename=\"dir/out.png\".\n\n        :param texture: Texture with an image atlas.\n        :param targetdir: Directory where the image file is created.\n        :param filename: Name of the resulting image file.\n        :param compression_level: PNG compression level used for the resulting image file.\n        :param dry_run: If True, create the PNG but don't save it as a file.\n        :type texture: Texture\n        :type targetdir: Directory\n        :type filename: str\n        :type compression_level: int\n        :type dry_run: bool\n        \"\"\"\n        from ...service.export.png import png_create\n\n        compression_levels = {\n            0: png_create.CompressionMethod.COMPR_NONE,\n            1: png_create.CompressionMethod.COMPR_DEFAULT,\n            2: png_create.CompressionMethod.COMPR_OPTI,\n            3: png_create.CompressionMethod.COMPR_GREEDY,\n            4: png_create.CompressionMethod.COMPR_AGGRESSIVE,\n        }\n\n        if not dry_run:\n            _, ext = os.path.splitext(filename)\n\n            # only allow png\n            if ext != \".png\":\n                raise ValueError(\"Filename invalid, a texture must be saved\"\n                                 f\"as '*.png', not '*.{ext}'\")\n\n        compression_method = compression_levels.get(\n            compression_level,\n            png_create.CompressionMethod.COMPR_DEFAULT\n        )\n        png_data, compr_params = png_create.save(\n            texture.image_data.data,\n            compression_method,\n            cache\n        )\n\n        if not dry_run:\n            with targetdir[filename].open(\"wb\") as imagefile:\n                imagefile.write(png_data)\n\n        if compr_params:\n            texture.best_compr = (compression_level, *compr_params)\n\n    @staticmethod\n    def log_fileinfo(\n        source_file: Path,\n        target_file: Path\n    ) -> None:\n        \"\"\"\n        Log source and target file information to the shell.\n        \"\"\"\n        source_format = source_file.suffix[1:].upper()\n        target_format = target_file.suffix[1:].upper()\n\n        source_size = source_file.filesize\n        target_size = target_file.filesize\n\n        log = (\"Converted: \"\n               f\"{source_file.name} \"\n               f\"({source_format}, {source_size}B) \"\n               f\"-> {target_file.name} \"\n               f\"({target_format}, {target_size}B | \"\n               f\"{(target_size / source_size * 100) - 100:+.1f}%)\")\n\n        dbg(log)\n\n\ndef _export_blend(\n    request_id: int,\n    blendfile_data: bytes,\n    outqueue: multiprocessing.Queue,\n    dll_manager: DllDirectoryManager,\n    source_filename: str,  # pylint: disable=unused-argument\n    targetdir: Path,\n    target_filename: str,\n    blend_mode_count: int = None\n) -> None:\n    \"\"\"\n    Convert and export a blending mode.\n\n    :param request_id: ID of the export request.\n    :param blendfile_data: Raw file data of the blending mask.\n    :param outqueue: Queue for passing metadata to the main process.\n    :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only).\n    :param target_path: Path to the resulting image file.\n    :param blend_mode_count: Number of blending modes extracted from the source file.\n    \"\"\"\n    if sys.platform == \"win32\" and dll_manager is not None:\n        dll_manager.add_directories()\n\n    blend_data = Blendomatic(blendfile_data, blend_mode_count)\n\n    from .texture_merge import merge_frames\n\n    textures = blend_data.get_textures()\n    for idx, texture in enumerate(textures):\n        merge_frames(texture)\n        _save_png(\n            texture,\n            targetdir.joinpath(f\"{target_filename}_{idx}.png\")\n        )\n\n    outqueue.put(request_id)\n\n\ndef _export_sound(\n    request_id: int,\n    sound_data: bytes,\n    outqueue: multiprocessing.Queue,\n    dll_manager: DllDirectoryManager,\n    source_filename: str,  # pylint: disable=unused-argument\n    target_path: Path,\n    **kwargs  # pylint: disable=unused-argument\n) -> None:\n    \"\"\"\n    Convert and export a sound file.\n\n    :param request_id: ID of the export request.\n    :param sound_data: Raw file data of the sound file.\n    :param outqueue: Queue for passing metadata to the main process.\n    :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only).\n    :param target_path: Path to the resulting sound file.\n    \"\"\"\n    if sys.platform == \"win32\" and dll_manager is not None:\n        dll_manager.add_directories()\n\n    from ...service.export.opus.opusenc import encode\n    encoded = encode(sound_data)\n\n    if isinstance(encoded, (str, int)):\n        raise RuntimeError(f\"opusenc failed: {encoded}\")\n\n    with target_path.open(\"wb\") as outfile:\n        outfile.write(encoded)\n\n    outqueue.put(request_id)\n\n\ndef _export_terrain(\n    request_id: int,\n    graphics_data: bytes,\n    outqueue: multiprocessing.Queue,\n    dll_manager: DllDirectoryManager,\n    source_filename: str,\n    target_path: Path,\n    palettes: dict[int, ColorTable],\n    compression_level: int,\n    game_version: GameVersion\n) -> None:\n    \"\"\"\n    Convert and export a terrain graphics file.\n\n    :param request_id: ID of the export request.\n    :param graphics_data: Raw file data of the graphics file.\n    :param outqueue: Queue for passing the image metadata to the main process.\n    :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only).\n    :param source_filename: Filename of the source file.\n    :param target_path: Path to the resulting image file.\n    :param palettes: Palettes used by the game.\n    :param compression_level: PNG compression level for the resulting image file.\n    :param game_version: Game edition and expansion info.\n    \"\"\"\n    if sys.platform == \"win32\" and dll_manager is not None:\n        dll_manager.add_directories()\n\n    file_ext = source_filename.split('.')[-1].lower()\n    if file_ext == \"slp\":\n        from ...value_object.read.media.slp import SLP\n        image = SLP(graphics_data)\n\n    elif file_ext == \"dds\":\n        # TODO: Implement\n        pass\n\n    elif file_ext == \"png\":\n        with target_path.open(\"wb\") as imagefile:\n            imagefile.write(graphics_data)\n\n        outqueue.put(request_id)\n        return\n\n    else:\n        raise SyntaxError(f\"Source file {source_filename} has an unrecognized extension: \"\n                          f\"{file_ext}\")\n\n    if game_version.edition.game_id in (\"AOC\", \"SWGB\"):\n        from .terrain_merge import merge_terrain\n        texture = Texture(image, palettes)\n        merge_terrain(texture)\n\n    else:\n        from .texture_merge import merge_frames\n        texture = Texture(image, palettes)\n        merge_frames(texture)\n\n    _save_png(\n        texture,\n        target_path,\n        compression_level=compression_level\n    )\n\n    outqueue.put(request_id)\n\n\ndef _export_texture(\n    request_id: int,\n    graphics_data: bytes,\n    outqueue: multiprocessing.Queue,\n    dll_manager: DllDirectoryManager,\n    source_filename: str,\n    target_path: Path,\n    palettes: dict[int, ColorTable],\n    compression_level: int,\n    cache_info: dict = None\n) -> None:\n    \"\"\"\n    Convert and export a graphics file to a PNG texture.\n\n    :param request_id: ID of the export request.\n    :param graphics_data: Raw file data of the graphics file.\n    :param outqueue: Queue for passing the image metadata to the main process.\n    :param dll_manager: Adds DLL search paths for the subrocesses (Windows-only).\n    :param source_filename: Filename of the source file.\n    :param target_path: Path to the resulting image file.\n    :param palettes: Palettes used by the game.\n    :param compression_level: PNG compression level for the resulting image file.\n    :param cache_info: Media cache information with compression parameters from a previous run.\n    \"\"\"\n    if sys.platform == \"win32\" and dll_manager is not None:\n        dll_manager.add_directories()\n\n    file_ext = source_filename.split('.')[-1].lower()\n    if file_ext == \"slp\":\n        from ...value_object.read.media.slp import SLP\n        image = SLP(graphics_data)\n\n    elif file_ext == \"smp\":\n        from ...value_object.read.media.smp import SMP\n        image = SMP(graphics_data)\n\n    elif file_ext == \"smx\":\n        from ...value_object.read.media.smx import SMX\n        image = SMX(graphics_data)\n\n    elif file_ext == \"sld\":\n        from ...value_object.read.media.sld import SLD\n        image = SLD(graphics_data)\n\n    else:\n        raise SyntaxError(f\"Source file {source_filename} has an unrecognized extension: \"\n                          f\"{file_ext}\")\n\n    packer_cache = None\n    compr_cache = None\n    if cache_info:\n        cache_params = cache_info.get(source_filename, None)\n\n        if cache_params:\n            packer_cache = cache_params[\"packer_settings\"]\n            compression_level = cache_params[\"compr_settings\"][0]\n            compr_cache = cache_params[\"compr_settings\"][1:]\n\n    from .texture_merge import merge_frames\n\n    texture = Texture(image, palettes)\n    merge_frames(texture, cache=packer_cache)\n    _save_png(\n        texture,\n        target_path,\n        compression_level=compression_level,\n        cache=compr_cache\n    )\n    metadata = (request_id, texture.get_metadata().copy())\n    outqueue.put(metadata)\n\n\ndef _save_png(\n    texture: Texture,\n    target_path: Path,\n    compression_level: int = 1,\n    cache: dict = None,\n    dry_run: bool = False\n) -> None:\n    \"\"\"\n    Store the image data into the target directory path,\n    with given target_path=\"dir/out.png\".\n\n    :param texture: Texture with an image atlas.\n    :param target_path: Path to the resulting image file.\n    :param compression_level: PNG compression level used for the resulting image file.\n    :param dry_run: If True, create the PNG but don't save it as a file.\n    \"\"\"\n    from ...service.export.png import png_create\n\n    compression_levels = {\n        0: png_create.CompressionMethod.COMPR_NONE,\n        1: png_create.CompressionMethod.COMPR_DEFAULT,\n        2: png_create.CompressionMethod.COMPR_OPTI,\n        3: png_create.CompressionMethod.COMPR_GREEDY,\n        4: png_create.CompressionMethod.COMPR_AGGRESSIVE,\n    }\n\n    if not dry_run:\n        ext = target_path.suffix.lower()\n\n        # only allow png\n        if ext != \".png\":\n            raise ValueError(\"Filename invalid, a texture must be saved\"\n                             f\" as '*.png', not '*{ext}'\")\n\n    compression_method = compression_levels.get(\n        compression_level,\n        png_create.CompressionMethod.COMPR_DEFAULT\n    )\n    png_data, compr_params = png_create.save(\n        texture.image_data.data,\n        compression_method,\n        cache\n    )\n\n    if not dry_run:\n        with target_path.open(\"wb\") as imagefile:\n            imagefile.write(png_data)\n\n    if compr_params:\n        texture.best_compr = (compression_level, *compr_params)\n"
  },
  {
    "path": "openage/convert/processor/export/modpack_exporter.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\"\"\"\nExport data from a modpack to files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....log import info\nfrom .data_exporter import DataExporter\nfrom .generate_manifest_hashes import generate_hashes\nfrom .media_exporter import MediaExporter\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n    from openage.convert.entity_object.conversion.modpack import Modpack\n\n\nclass ModpackExporter:\n    \"\"\"\n    Writes the contents of a created modpack into a targetdir.\n    \"\"\"\n\n    @staticmethod\n    def export(modpack: Modpack, args: Namespace) -> None:\n        \"\"\"\n        Export a modpack to a directory.\n\n        :param modpack: Modpack that is going to be exported.\n        :param args: Converter arguments.\n        :type modpack: ..dataformats.modpack.Modpack\n        :type args: Namespace\n        \"\"\"\n        sourcedir = args.srcdir\n        exportdir = args.targetdir\n\n        modpack_dir = exportdir.joinpath(f\"{modpack.info.packagename}\")\n\n        info(\"Starting export...\")\n        info(\"Dumping info file...\")\n\n        # Modpack info file\n        DataExporter.export([modpack.info], modpack_dir)\n\n        info(\"Dumping data files...\")\n\n        # Data files\n        DataExporter.export(modpack.get_data_files(), modpack_dir)\n\n        if args.flag(\"no_media\"):\n            info(\"Skipping media file export...\")\n            return\n\n        info(\"Exporting media files...\")\n\n        # Media files\n        MediaExporter.export(modpack.get_media_files(), sourcedir, modpack_dir, args)\n\n        info(\"Dumping metadata files...\")\n\n        # Metadata files\n        DataExporter.export(modpack.get_metadata_files(), modpack_dir)\n\n        # Manifest file\n        generate_hashes(modpack, modpack_dir)\n        DataExporter.export([modpack.manifest], modpack_dir)\n"
  },
  {
    "path": "openage/convert/processor/export/terrain_merge.pyx",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n# pylint: disable=too-many-locals\n\"\"\"\nMerges a texture containing several terrain tiles into a single cartesian\nterrain texture.\n\"\"\"\n\nimport numpy\n\nfrom openage.convert.entity_object.export.texture import TextureImage\n\ncimport cython\ncimport numpy\n\nfrom libc.math cimport sqrt, floor, ceil\n\n\ndef merge_terrain(texture):\n    \"\"\"\n    Python wrapper for the Cython function.\n\n    :param texture: Texture containing the terrain tiles.\n    :type texture: Texture\n    \"\"\"\n    cmerge_terrain(texture)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\n@cython.cdivision(True)\ncdef void cmerge_terrain(texture):\n    \"\"\"\n    Merges tiles from an AoC terrain SLP into a single flat texture.\n\n    You can imagine every terrain tile (frame) as a puzzle piece\n    of the big merged terrain. By blending and overlapping\n    the tiles, we create a single terrain texture.\n\n    :param texture: Texture containing the terrain tiles.\n    :type texture: Texture\n    \"\"\"\n    frames = texture.frames\n    # Can be 10 (regular terrain) or 6 (farms)\n    cdef unsigned int tiles_per_row = <int>sqrt(len(frames))\n\n    # Size of one tile should be (98,49)\n    cdef unsigned int frame_width = frames[0].width\n    cdef unsigned int frame_height = frames[0].height\n\n    cdef unsigned int flat_frame_width = (frame_width // 2) + 1\n    cdef unsigned int flat_frame_height = frame_height\n\n    cdef numpy.uint8_t[:, :, ::1] csubframe_atlas\n\n    cdef unsigned int column_idx\n    cdef unsigned int row_idx\n\n    cdef unsigned int merge_atlas_width = (frame_width * tiles_per_row) - (tiles_per_row - 1)\n    cdef unsigned int merge_atlas_height = (frame_height * tiles_per_row) - (tiles_per_row - 1)\n    cdef unsigned int flat_atlas_width = (merge_atlas_width // 2) + 1\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] flat_atlas = \\\n        numpy.zeros((merge_atlas_height, flat_atlas_width, 4), dtype=numpy.uint8)\n    cdef numpy.uint8_t[:, :, ::1] cflat_atlas = flat_atlas\n\n    cdef unsigned int source_x\n    cdef unsigned int source_y\n\n    cdef unsigned int final_x\n    cdef unsigned int final_y\n\n    index = 0\n    for sub_frame in frames:\n        csubframe_atlas = sub_frame.data\n\n        # Fill each column upwards, starting with the last row\n        row_idx = (tiles_per_row - 1) - (index % tiles_per_row)\n        column_idx = index // tiles_per_row\n\n        # Does a matrix transformation using\n        # [  1 , -1  ]\n        # [ 0.5, 0.5 ]\n        # as the multipication matrix.\n        # This reverses the dimetric projection (diamond shape view)\n        # to a plan projection (bird's eye view).\n        # Reference: https://gamedev.stackexchange.com/questions/\n        #            16746/what-is-the-name-of-perspective-of-age-of-empires-ii\n        for target_x in range(flat_frame_width):\n            for target_y in range(frame_height):\n                # Find the coords of the pixel (source) that is projected\n                # to the target pixel coords\n                source_x = (target_x + flat_frame_width - 1) - target_y\n                source_y = <unsigned int>floor(0.5 * target_x + 0.5 * target_y)\n\n                if target_x + target_y < frame_height:\n                    source_y = int(ceil(0.5 * target_x + 0.5 * target_y))\n\n                # Offset the target coords for the big texture\n                final_x = target_x + (column_idx * (flat_frame_width - 1))\n                final_y = target_y + (row_idx * (flat_frame_height - 1))\n\n                cflat_atlas[final_y, final_x] = csubframe_atlas[source_y, source_x]\n\n        index += 1\n\n    # Rotate by 270 degrees to match the rotation of HD terrain textures\n    flat_atlas = numpy.ascontiguousarray(numpy.rot90(flat_atlas, 3, axes=(0, 1)))\n\n    texture.image_data = TextureImage(flat_atlas)\n"
  },
  {
    "path": "openage/convert/processor/export/texture_merge.pyx",
    "content": "# Copyright 2014-2024 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n# pylint: disable=too-many-locals\n\"\"\"\nMerges texture frames into a spritesheet or terrain tiles into\na terrain texture.\n\"\"\"\nimport numpy\nfrom enum import Enum\n\ncimport cython\ncimport numpy\n\nfrom ....log import spam\nfrom ...service.export.png.binpack cimport block\nfrom ...entity_object.export.texture import TextureImage\nfrom ...service.export.png.binpack cimport DeterministicPacker, RowPacker, ColumnPacker, BinaryTreePacker, BestPacker\nfrom ...value_object.read.media.hardcoded.texture import (MAX_TEXTURE_DIMENSION, MARGIN,\n                                                          TERRAIN_ASPECT_RATIO)\n\nclass PackerType(Enum):\n    \"\"\"\n    Packer types\n    \"\"\"\n    BEST    = 0x00\n\n    ROW     = 0x01\n    COLUMN  = 0x02\n    BINPACK = 0x03\n\n\ndef merge_frames(texture, custom_packer=PackerType.BINPACK, cache=None):\n    \"\"\"\n    Python wrapper for the Cython function.\n\n    :param texture: Texture containing animation frames.\n    :param custom_packer: Packer implementation for efficient packing of frames.\n                          Uses 2D binpacking by default.\n    :param cache: Media cache information with packer settings from a previous run.\n    :type texture: Texture\n    :type custom_packer: PackerType\n    :type cache: list\n    \"\"\"\n    cmerge_frames(texture, custom_packer, cache=cache)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\n@cython.cdivision(True)\ncdef void cmerge_frames(texture, packer_type=PackerType.BINPACK, cache=None) except *:\n    \"\"\"\n    merge all given frames in a texture into a single image atlas.\n\n    :param texture: Texture containing animation frames.\n    :param cache: Media cache information with packer settings from a previous run.\n    :type texture: Texture\n    :type cache: list\n    \"\"\"\n    cdef list frames = texture.frames\n    cdef list blocks = [block(idx, frame.width, frame.height) for idx, frame in enumerate(frames)]\n\n    if len(frames) == 0:\n        raise ValueError(\"cannot create texture with empty input frame list\")\n\n    cdef BestPacker packer\n\n    if cache:\n        packer = BestPacker([DeterministicPacker(margin=MARGIN, hints=cache)])\n\n    else:\n        if packer_type == PackerType.ROW:\n            packer = BestPacker([RowPacker(margin=MARGIN)])\n\n        elif packer_type == PackerType.COLUMN:\n            packer = BestPacker([ColumnPacker(margin=MARGIN)])\n\n        elif packer_type == PackerType.BINPACK:\n            packer = BestPacker([BinaryTreePacker(margin=MARGIN, aspect_ratio=1)])\n\n        else:\n            packer = BestPacker([BinaryTreePacker(margin=MARGIN, aspect_ratio=1),\n                                 RowPacker(margin=MARGIN),\n                                 ColumnPacker(margin=MARGIN)])\n\n    packer.pack(blocks)\n\n    cdef int width = packer.width()\n    cdef int height = packer.height()\n    assert width <= MAX_TEXTURE_DIMENSION, \"Texture width limit exceeded\"\n    assert height <= MAX_TEXTURE_DIMENSION, \"Texture height limit exceeded\"\n\n    cdef int area = sum(block.width * block.height for block in frames)\n    cdef int used_area = width * height\n    cdef int efficiency = area / used_area\n\n    spam(\"merging %d frames to %dx%d atlas, efficiency %.3f.\",\n         len(frames), width, height, efficiency)\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] atlas_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n    cdef numpy.uint8_t[:, :, ::1] catlas_data = atlas_data\n    cdef numpy.uint8_t[:, :, ::1] csub_frame\n\n    cdef int pos_x\n    cdef int pos_y\n    cdef int sub_w\n    cdef int sub_h\n\n    cdef list drawn_frames_meta = []\n    for index, sub_frame in enumerate(frames):\n        sub_w = sub_frame.width\n        sub_h = sub_frame.height\n\n        pos_x, pos_y = packer.pos(index)\n\n        spam(\"drawing frame %03d on atlas at %d x %d...\",\n             len(drawn_frames_meta), pos_x, pos_y)\n\n        # draw the subtexture on atlas_data\n        csub_frame = sub_frame.data\n        catlas_data[pos_y:pos_y + sub_h, pos_x:pos_x + sub_w] = csub_frame\n\n        hotspot_x, hotspot_y = sub_frame.hotspot\n\n        # generate subtexture meta information dict:\n        # origin x, origin y, width, height, hotspot x, hotspot y\n        drawn_frames_meta.append(\n            {\n                \"x\":  pos_x,\n                \"y\":  pos_y,\n                \"w\":  sub_w,\n                \"h\":  sub_h,\n                \"cx\": hotspot_x,\n                \"cy\": hotspot_y,\n            }\n        )\n\n    texture.image_data = TextureImage(atlas_data)\n    texture.image_metadata[\"size\"] = (width, height)\n    texture.image_metadata[\"subtex_metadata\"] = drawn_frames_meta\n\n    spam(\"successfully merged %d frames to atlas.\", len(frames))\n\n    if isinstance(packer, BestPacker):\n        # Only generate these values if no custom packer was used\n        # TODO: It might make sense to do it anyway for debugging purposes\n        texture.best_packer_hints = packer.get_mapping_hints(blocks)\n"
  },
  {
    "path": "openage/convert/service/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tdebug_info.py\n)\n\nadd_subdirectory(conversion)\nadd_subdirectory(init)\nadd_subdirectory(export)\nadd_subdirectory(read)\n"
  },
  {
    "path": "openage/convert/service/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nServices used by the converter.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/conversion/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_name_lookups.py\n)\n"
  },
  {
    "path": "openage/convert/service/conversion/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nServices used for conversion.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/conversion/internal_name_lookups.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides functions that retrieve name lookup dicts for internal nyan object\nnames or filenames.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nimport openage.convert.value_object.conversion.aoc.internal_nyan_names as aoc_internal\nimport openage.convert.value_object.conversion.de1.internal_nyan_names as de1_internal\nimport openage.convert.value_object.conversion.de2.internal_nyan_names as de2_internal\nimport openage.convert.value_object.conversion.hd.ak.internal_nyan_names as ak_internal\nimport openage.convert.value_object.conversion.hd.fgt.internal_nyan_names as fgt_internal\nimport openage.convert.value_object.conversion.hd.raj.internal_nyan_names as raj_internal\nimport openage.convert.value_object.conversion.ror.internal_nyan_names as ror_internal\nimport openage.convert.value_object.conversion.swgb.internal_nyan_names as swgb_internal\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\n@cache\ndef get_armor_class_lookups(game_version: GameVersion) -> dict[int, str]:\n    \"\"\"\n    Return the name lookup dicts for armor classes.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.ARMOR_CLASS_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.ARMOR_CLASS_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        armor_lookup_dict = {}\n        armor_lookup_dict.update(aoc_internal.ARMOR_CLASS_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return armor_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        armor_lookup_dict = {}\n        armor_lookup_dict.update(ror_internal.ARMOR_CLASS_LOOKUPS)\n        armor_lookup_dict.update(de1_internal.ARMOR_CLASS_LOOKUPS)\n\n        return armor_lookup_dict\n\n    if game_edition.game_id == \"AOE2DE\":\n        armor_lookup_dict = {}\n        armor_lookup_dict.update(aoc_internal.ARMOR_CLASS_LOOKUPS)\n        armor_lookup_dict.update(fgt_internal.ARMOR_CLASS_LOOKUPS)\n        armor_lookup_dict.update(ak_internal.ARMOR_CLASS_LOOKUPS)\n        armor_lookup_dict.update(raj_internal.ARMOR_CLASS_LOOKUPS)\n        armor_lookup_dict.update(de2_internal.ARMOR_CLASS_LOOKUPS)\n\n        return armor_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.ARMOR_CLASS_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_civ_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for civs.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.CIV_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.CIV_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        civ_lookup_dict = {}\n        civ_lookup_dict.update(aoc_internal.CIV_GROUP_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return civ_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        return ror_internal.CIV_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOE2DE\":\n        civ_lookup_dict = {}\n        civ_lookup_dict.update(aoc_internal.CIV_GROUP_LOOKUPS)\n        civ_lookup_dict.update(fgt_internal.CIV_GROUP_LOOKUPS)\n        civ_lookup_dict.update(ak_internal.CIV_GROUP_LOOKUPS)\n        civ_lookup_dict.update(raj_internal.CIV_GROUP_LOOKUPS)\n        civ_lookup_dict.update(de2_internal.CIV_GROUP_LOOKUPS)\n\n        return civ_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.CIV_GROUP_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_class_lookups(game_version: GameVersion) -> dict[int, str]:\n    \"\"\"\n    Return the name lookup dicts for unit classes.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id in (\"ROR\", \"AOE1DE\"):\n        return ror_internal.CLASS_ID_LOOKUPS\n\n    if game_edition.game_id in (\"AOC\", \"HDEDITION\", \"AOE2DE\"):\n        return aoc_internal.CLASS_ID_LOOKUPS\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.CLASS_ID_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_command_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for unit commands.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id in (\"ROR\", \"AOE1DE\"):\n        return ror_internal.COMMAND_TYPE_LOOKUPS\n\n    if game_edition.game_id in (\"AOC\", \"HDEDITION\", \"AOE2DE\"):\n        return aoc_internal.COMMAND_TYPE_LOOKUPS\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.COMMAND_TYPE_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_entity_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for game entities.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    entity_lookup_dict = {}\n\n    if game_edition.game_id == \"ROR\":\n        entity_lookup_dict.update(ror_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.VARIANT_GROUP_LOOKUPS)\n\n        return entity_lookup_dict\n\n    if game_edition.game_id == \"AOC\":\n        entity_lookup_dict.update(aoc_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.VARIANT_GROUP_LOOKUPS)\n\n        return entity_lookup_dict\n\n    if game_edition.game_id == \"HDEDITION\":\n        entity_lookup_dict.update(aoc_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.VARIANT_GROUP_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return entity_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        entity_lookup_dict.update(ror_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(ror_internal.VARIANT_GROUP_LOOKUPS)\n\n        return entity_lookup_dict\n\n    if game_edition.game_id == \"AOE2DE\":\n        entity_lookup_dict.update(aoc_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(aoc_internal.VARIANT_GROUP_LOOKUPS)\n\n        entity_lookup_dict.update(fgt_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(fgt_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(fgt_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(fgt_internal.VARIANT_GROUP_LOOKUPS)\n\n        entity_lookup_dict.update(ak_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(ak_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(ak_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(ak_internal.VARIANT_GROUP_LOOKUPS)\n\n        entity_lookup_dict.update(raj_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(raj_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(raj_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(raj_internal.VARIANT_GROUP_LOOKUPS)\n\n        entity_lookup_dict.update(de2_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(de2_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(de2_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(de2_internal.VARIANT_GROUP_LOOKUPS)\n\n        return entity_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        entity_lookup_dict.update(swgb_internal.UNIT_LINE_LOOKUPS)\n        entity_lookup_dict.update(swgb_internal.BUILDING_LINE_LOOKUPS)\n        entity_lookup_dict.update(swgb_internal.AMBIENT_GROUP_LOOKUPS)\n        entity_lookup_dict.update(swgb_internal.VARIANT_GROUP_LOOKUPS)\n\n        return entity_lookup_dict\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_gather_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for gather tasks.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id in (\"ROR\", \"AOE1DE\"):\n        return ror_internal.GATHER_TASK_LOOKUPS\n\n    if game_edition.game_id in (\"AOC\", \"HDEDITION\", \"AOE2DE\"):\n        return aoc_internal.GATHER_TASK_LOOKUPS\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.GATHER_TASK_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_graphic_set_lookups(\n    game_version: GameVersion\n) -> dict[int, tuple[tuple[int, ...], str, str]]:\n    \"\"\"\n    Return the name lookup dicts for civ graphic sets.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.GRAPHICS_SET_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.GRAPHICS_SET_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        graphic_set_lookup_dict = {}\n        graphic_set_lookup_dict.update(aoc_internal.GRAPHICS_SET_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return graphic_set_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        return ror_internal.GRAPHICS_SET_LOOKUPS\n\n    if game_edition.game_id == \"AOE2DE\":\n        graphic_set_lookup_dict = {}\n        graphic_set_lookup_dict.update(aoc_internal.GRAPHICS_SET_LOOKUPS)\n        graphic_set_lookup_dict.update(fgt_internal.GRAPHICS_SET_LOOKUPS)\n        graphic_set_lookup_dict.update(ak_internal.GRAPHICS_SET_LOOKUPS)\n        graphic_set_lookup_dict.update(raj_internal.GRAPHICS_SET_LOOKUPS)\n        graphic_set_lookup_dict.update(de2_internal.GRAPHICS_SET_LOOKUPS)\n\n        return graphic_set_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.GRAPHICS_SET_LOOKUPS\n\n    raise KeyError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_restock_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for restock targets.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return None\n\n    if game_edition.game_id in (\"AOC\", \"HDEDITION\", \"AOE2DE\"):\n        return aoc_internal.RESTOCK_TARGET_LOOKUPS\n\n    if game_edition.game_id == \"AOE1DE\":\n        # TODO: Farms\n        return None\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.RESTOCK_TARGET_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_tech_lookups(game_version: GameVersion) -> dict[int, tuple[str, str]]:\n    \"\"\"\n    Return the name lookup dicts for tech groups.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.TECH_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.TECH_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        tech_lookup_dict = {}\n        tech_lookup_dict.update(aoc_internal.TECH_GROUP_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return tech_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        return ror_internal.TECH_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOE2DE\":\n        tech_lookup_dict = {}\n        tech_lookup_dict.update(aoc_internal.TECH_GROUP_LOOKUPS)\n        tech_lookup_dict.update(fgt_internal.TECH_GROUP_LOOKUPS)\n        tech_lookup_dict.update(ak_internal.TECH_GROUP_LOOKUPS)\n        tech_lookup_dict.update(raj_internal.TECH_GROUP_LOOKUPS)\n        tech_lookup_dict.update(de2_internal.TECH_GROUP_LOOKUPS)\n\n        return tech_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.TECH_GROUP_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_terrain_lookups(\n    game_version: GameVersion\n) -> dict[int, tuple[tuple[int, ...], str, str]]:\n    \"\"\"\n    Return the name lookup dicts for terrain groups.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.TERRAIN_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.TERRAIN_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        terrain_lookup_dict = {}\n        terrain_lookup_dict.update(aoc_internal.TERRAIN_GROUP_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return terrain_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        return ror_internal.TERRAIN_GROUP_LOOKUPS\n\n    if game_edition.game_id == \"AOE2DE\":\n        terrain_lookup_dict = {}\n        terrain_lookup_dict.update(aoc_internal.TERRAIN_GROUP_LOOKUPS)\n        terrain_lookup_dict.update(fgt_internal.TERRAIN_GROUP_LOOKUPS)\n        terrain_lookup_dict.update(ak_internal.TERRAIN_GROUP_LOOKUPS)\n        terrain_lookup_dict.update(raj_internal.TERRAIN_GROUP_LOOKUPS)\n        terrain_lookup_dict.update(de2_internal.TERRAIN_GROUP_LOOKUPS)\n\n        return terrain_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.TERRAIN_GROUP_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n\n\n@cache\ndef get_terrain_type_lookups(game_version: GameVersion) -> dict[int, tuple]:\n    \"\"\"\n    Return the name lookup dicts for terrain types.\n\n    :param game_version: Game edition and expansions for which the lookups should be.\n    :type game_version: GameVersion\n    \"\"\"\n    game_edition = game_version.edition\n    # game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        return ror_internal.TERRAIN_TYPE_LOOKUPS\n\n    if game_edition.game_id == \"AOC\":\n        return aoc_internal.TERRAIN_TYPE_LOOKUPS\n\n    if game_edition.game_id == \"HDEDITION\":\n        terrain_type_lookup_dict = {}\n        terrain_type_lookup_dict.update(aoc_internal.TERRAIN_TYPE_LOOKUPS)\n\n        # TODO: Include expansion lookups\n\n        return terrain_type_lookup_dict\n\n    if game_edition.game_id == \"AOE1DE\":\n        return ror_internal.TERRAIN_TYPE_LOOKUPS\n\n    if game_edition.game_id == \"AOE2DE\":\n        terrain_type_lookup_dict = {}\n        terrain_type_lookup_dict.update(aoc_internal.TERRAIN_TYPE_LOOKUPS)\n        terrain_type_lookup_dict.update(fgt_internal.TERRAIN_TYPE_LOOKUPS)\n        terrain_type_lookup_dict.update(ak_internal.TERRAIN_TYPE_LOOKUPS)\n        terrain_type_lookup_dict.update(raj_internal.TERRAIN_TYPE_LOOKUPS)\n        terrain_type_lookup_dict.update(de2_internal.TERRAIN_TYPE_LOOKUPS)\n\n        return terrain_type_lookup_dict\n\n    if game_edition.game_id == \"SWGB\":\n        return swgb_internal.TERRAIN_TYPE_LOOKUPS\n\n    raise RuntimeError(f\"No lookup dict found for game version {game_edition.edition_name}\")\n"
  },
  {
    "path": "openage/convert/service/debug_info.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\n\"\"\"\nCreates debug output from data in a conversion run.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nfrom openage.convert.entity_object.conversion.aoc.genie_tech import AgeUpgrade, \\\n    UnitLineUpgrade, BuildingLineUpgrade, UnitUnlock, BuildingUnlock\nfrom openage.convert.entity_object.conversion.aoc.genie_unit import GenieUnitLineGroup, \\\n    GenieBuildingLineGroup, GenieStackBuildingGroup, GenieUnitTransformGroup, \\\n    GenieMonkGroup\nfrom openage.convert.entity_object.export.formats.media_cache import MediaCacheFile\nfrom openage.convert.service.conversion.internal_name_lookups import get_entity_lookups, \\\n    get_tech_lookups, get_civ_lookups, get_terrain_lookups\nfrom openage.convert.value_object.read.media.datfile.empiresdat import EmpiresDatWrapper\nfrom openage.convert.value_object.read.read_members import IncludeMembers, MultisubtypeMember\nfrom openage.util.fslike.filecollection import FileCollectionPath\nfrom openage.util.fslike.path import Path\nfrom openage.util.hash import hash_file\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n    from openage.convert.entity_object.conversion.modpack import Modpack\n    from openage.convert.entity_object.conversion.stringresource import StringResource\n    from openage.convert.entity_object.conversion.aoc.genie_object_container import GenieObjectContainer\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.util.fslike.directory import Directory\n\n\ndef debug_cli_args(debugdir: Directory, loglevel: int, args: Namespace) -> None:\n    \"\"\"\n    Create debug output for the converter CLI args.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param args: CLI arguments.\n    :type args: Namespace\n    \"\"\"\n    if loglevel < 1:\n        return\n\n    logfile = debugdir[\"args\"]\n    logtext = \"\"\n\n    # Get CLI args\n    arg_dict = {}\n    for name, arg in vars(args).items():\n        arg_dict.update({name: arg})\n\n    # Sort by name\n    arg_dict = dict(sorted(arg_dict.items(), key=lambda item: item[0]))\n\n    for name, arg in arg_dict.items():\n        logtext += f\"{name}: {arg}\\n\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_game_version(debugdir: Directory, loglevel: int, args: Namespace) -> None:\n    \"\"\"\n    Create debug output for the detected game version.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param args: CLI arguments.\n    :type args: Namespace\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    # Log game version\n    logfile = debugdir.joinpath(\"init/\")[\"game_version\"]\n    logtext = \"\"\n\n    logtext += (\n        f\"game edition:\\n\"\n        f\"    - {args.game_version.edition}\\n\"\n    )\n\n    if len(args.game_version.expansions) > 0:\n        logtext += \"game expansions:\\n\"\n        for expansion in args.game_version.expansions:\n            logtext += f\"    - {expansion}\\n\"\n\n    else:\n        logtext += \"game expansions: none detected\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_mounts(debugdir: Directory, loglevel: int, args: Namespace) -> None:\n    \"\"\"\n    Create debug output for the mounted files and folders.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param args: CLI arguments.\n    :type args: Namespace\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    # Log mounts\n    logfile = debugdir.joinpath(\"init/\")[\"mounts\"]\n    logtext = \"\"\n\n    mounts = args.srcdir.fsobj.obj.fsobj.mounts\n\n    # Sort by mounted directory name\n    mount_dict = {}\n    for mount in mounts:\n        if mount[0] in mount_dict.keys():\n            mount_dict[mount[0]].append(mount[1])\n\n        else:\n            mount_dict[mount[0]] = [mount[1]]\n\n    mount_dict = dict(sorted(mount_dict.items(), key=lambda item: item[0]))\n\n    # Format mounts\n    for mountpoint, resources in mount_dict.items():\n        if len(mountpoint) == 0:\n            logtext += \"mountpoint: ${srcdir}/\\n\"\n\n        else:\n            logtext += f\"mountpoint: ${{srcdir}}/{mountpoint[0].decode()}/\\n\"\n\n        for resource in resources:\n            resource_type = None\n            abs_path = \"\"\n            file_count = 0\n\n            if type(resource) is Path:\n                resource_type = \"dir\"\n                abs_path = resource.fsobj.path.decode()\n\n            elif type(resource) is FileCollectionPath:\n                resource_type = \"file collection\"\n                abs_path = resource.fsobj.fileobj.name.decode()\n                file_count = len(resource.fsobj.rootentries[0])\n\n            logtext += f\"    resource type: {resource_type}\\n\"\n            logtext += f\"    source path: {abs_path}\\n\"\n\n            if resource_type == \"file collection\":\n                logtext += f\"    file count: {file_count}\\n\"\n\n            logtext += \"    ----\\n\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_gamedata_format(debugdir: Directory, loglevel: int, game_version: GameVersion) -> None:\n    \"\"\"\n    Create debug output for the converted .dat format.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param game_version: Game version the .dat file comes with.\n    :type game_version: GameVersion\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    logfile = debugdir.joinpath(\"read/\")[\"data_format\"]\n    logtext = \"\"\n\n    discovered_structs = {EmpiresDatWrapper}\n    handled_structs = set()\n\n    while discovered_structs:\n        struct = discovered_structs.pop()\n\n        if struct in handled_structs:\n            continue\n\n        members = struct.get_data_format_members(game_version)\n        logtext += f\"total member count: {len(members)}\\n\"\n\n        max_name_width = 1\n        max_vmemb_width = 1\n        for member in members:\n            # Find out width of columns for table formatting\n            if len(str(member[1])) > max_name_width:\n                max_name_width = len(str(member[1]))\n\n            if len(str(member[2])) > max_vmemb_width:\n                max_vmemb_width = len(str(member[2]))\n\n            # Search for sub-structs\n            if isinstance(member[3], IncludeMembers):\n                discovered_structs.add(member[3].cls)\n\n            elif isinstance(member[3], MultisubtypeMember):\n                discovered_structs.update(member[3].class_lookup.values())\n\n        for member in members:\n            logtext += (\n                f\"{str(member[0].value):8}  \"\n                f\"{str(member[1]):{max_name_width}}  \"\n                f\"{str(member[2]):{max_vmemb_width}}  \"\n                f\"{str(member[3])}\\n\"\n            )\n\n        handled_structs.add(struct)\n        logtext += \"\\n\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_string_resources(debugdir: Directory, loglevel: int, string_resources: StringResource) -> None:\n    \"\"\"\n    Create debug output for found string resources.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param string_resources: Language and string information.\n    :type string_resources: StringResource\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    logfile = debugdir.joinpath(\"read/\")[\"string_resources\"]\n    logtext = \"\"\n\n    logtext += \"found languages: \"\n    logtext += \", \".join(string_resources.get_tables().keys())\n    logtext += \"\\n\\n\"\n\n    for language, strings in string_resources.get_tables().items():\n        logtext += f\"{language}: {len(strings)} IDs\\n\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_registered_graphics(debugdir: Directory, loglevel: int, existing_graphics: list[str]) -> None:\n    \"\"\"\n    Create debug output for found graphics files.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param existing_graphics: List of graphic ids of graphic files.\n    :type existing_graphics: list\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    logfile = debugdir.joinpath(\"read/\")[\"existing_graphics\"]\n    logtext = \"\"\n\n    logtext += f\"file count: {len(existing_graphics)}\\n\\n\"\n\n    sorted_graphics = list(sorted(existing_graphics))\n    logtext += \"\\n\".join(sorted_graphics)\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_converter_objects(debugdir: Directory, loglevel: int, dataset: GenieObjectContainer) -> None:\n    \"\"\"\n    Create debug output for ConverterObject instances from the\n    conversion preprocessor.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param dataset: Dataset containing converter objects from pre-processing.\n    :type dataset: GenieObjectContainer\n    \"\"\"\n    if loglevel < 2:\n        return\n\n    logfile = debugdir.joinpath(\"conversion/\")[\"preprocessor_objects\"]\n    logtext = \"\"\n\n    logtext += (\n        f\"unit objects count: {len(dataset.genie_units)}\\n\"\n        f\"tech objects count: {len(dataset.genie_techs)}\\n\"\n        f\"civ objects count: {len(dataset.genie_civs)}\\n\"\n        f\"effect bundles count: {len(dataset.genie_effect_bundles)}\\n\"\n        f\"age connections count: {len(dataset.age_connections)}\\n\"\n        f\"building connections count: {len(dataset.building_connections)}\\n\"\n        f\"unit connections count: {len(dataset.unit_connections)}\\n\"\n        f\"tech connections count: {len(dataset.tech_connections)}\\n\"\n        f\"graphics objects count: {len(dataset.genie_graphics)}\\n\"\n        f\"sound objects count: {len(dataset.genie_sounds)}\\n\"\n        f\"terrain objects count: {len(dataset.genie_terrains)}\\n\"\n    )\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_converter_object_groups(debugdir: Directory, loglevel: int, dataset: GenieObjectContainer) -> None:\n    \"\"\"\n    Create debug output for ConverterObjectGroup instances from the\n    conversion preprocessor.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param dataset: Dataset containing converter object groups from processing.\n    :type dataset: GenieObjectContainer\n    \"\"\"\n    if loglevel < 3:\n        return\n\n    enitity_groups = {}\n    enitity_groups.update(dataset.unit_lines)\n    enitity_groups.update(dataset.building_lines)\n    enitity_groups.update(dataset.ambient_groups)\n    enitity_groups.update(dataset.variant_groups)\n\n    entity_name_lookup_dict = get_entity_lookups(dataset.game_version)\n    tech_name_lookup_dict = get_tech_lookups(dataset.game_version)\n    civ_name_lookup_dict = get_civ_lookups(dataset.game_version)\n    terrain_name_lookup_dict = get_terrain_lookups(dataset.game_version)\n\n    # Used when a name lookup fails\n    nnn = (\"NameNotFound\", \"NameNotFound\")\n\n    for key, line in enitity_groups.items():\n        logfile = debugdir.joinpath(\"conversion/entity_groups/\")[str(key)]\n        logtext = \"\"\n\n        logtext += f\"repr: {line}\\n\"\n        logtext += (\n            f\"nyan name: \"\n            f\"{entity_name_lookup_dict.get(line.get_head_unit_id(), nnn)[0]}\\n\"\n        )\n\n        logtext += f\"is_creatable: {line.is_creatable()}\\n\"\n        logtext += f\"is_harvestable: {line.is_harvestable()}\\n\"\n        logtext += f\"is_garrison: {line.is_garrison()}\\n\"\n        logtext += f\"is_gatherer: {line.is_gatherer()}\\n\"\n        logtext += f\"is_passable: {line.is_passable()}\\n\"\n        logtext += f\"is_projectile_shooter: {line.is_projectile_shooter()}\\n\"\n        logtext += f\"is_ranged: {line.is_ranged()}\\n\"\n        logtext += f\"is_melee: {line.is_melee()}\\n\"\n        logtext += f\"is_repairable: {line.is_repairable()}\\n\"\n        logtext += f\"is_unique: {line.is_unique()}\\n\"\n\n        logtext += f\"class id: {line.get_class_id()}\\n\"\n        logtext += f\"garrison mode: {line.get_garrison_mode()}\\n\"\n        logtext += f\"head unit: {line.get_head_unit()}\\n\"\n        logtext += f\"train location id: {line.get_train_location_id()}\\n\"\n\n        logtext += \"line:\\n\"\n        for unit in line.line:\n            logtext += f\"    - {unit}\\n\"\n\n        if len(line.creates) > 0:\n            logtext += \"creates:\\n\"\n            for unit in line.creates:\n                logtext += (\n                    f\"    - {unit} \"\n                    f\"({entity_name_lookup_dict.get(unit.get_head_unit_id(), nnn)[0]})\\n\"\n                )\n\n        else:\n            logtext += \"creates: nothing\\n\"\n\n        if len(line.researches) > 0:\n            logtext += \"researches:\\n\"\n            for tech in line.researches:\n                logtext += (\n                    f\"    - {tech} \"\n                    f\"({tech_name_lookup_dict.get(tech.get_id(), nnn)[0]})\\n\"\n                )\n\n        else:\n            logtext += \"researches: nothing\\n\"\n\n        if len(line.garrison_entities) > 0:\n            logtext += \"garrisons units:\\n\"\n            for unit in line.garrison_entities:\n                logtext += (\n                    f\"    - {unit} \"\n                    f\"({entity_name_lookup_dict.get(unit.get_head_unit_id(), nnn)[0]})\\n\"\n                )\n\n        else:\n            logtext += \"garrisons units: nothing\\n\"\n\n        if len(line.garrison_locations) > 0:\n            logtext += \"garrisons in:\\n\"\n            for unit in line.garrison_locations:\n                logtext += (\n                    f\"    - {unit} \"\n                    f\"({entity_name_lookup_dict.get(unit.get_head_unit_id(), nnn)[0]})\\n\"\n                )\n\n        else:\n            logtext += \"garrisons in: nothing\\n\"\n\n        if isinstance(line, GenieUnitLineGroup):\n            logtext += \"\\n\"\n            logtext += (\n                f\"civ id: {line.get_civ_id()} \"\n                f\"({civ_name_lookup_dict.get(line.get_civ_id(), nnn)[0]})\\n\"\n            )\n            logtext += (\n                f\"enabling research id: {line.get_enabling_research_id()} \"\n                f\"({tech_name_lookup_dict.get(line.get_enabling_research_id(), nnn)[0]})\\n\"\n            )\n\n        if isinstance(line, GenieBuildingLineGroup):\n            logtext += \"\\n\"\n            logtext += f\"has_foundation: {line.has_foundation()}\\n\"\n            logtext += f\"is_dropsite: {line.is_dropsite()}\\n\"\n            logtext += f\"is_trade_post {line.is_trade_post()}\\n\"\n            logtext += (\n                f\"enabling research id: {line.get_enabling_research_id()} \"\n                f\"({tech_name_lookup_dict.get(line.get_enabling_research_id(), nnn)[0]})\\n\"\n            )\n            logtext += f\"dropoff gatherer ids: {line.get_gatherer_ids()}\\n\"\n\n        if isinstance(line, GenieStackBuildingGroup):\n            logtext += \"\\n\"\n            logtext += f\"is_gate: {line.is_gate()}\\n\"\n            logtext += f\"stack unit: {line.get_stack_unit()}\\n\"\n\n        if isinstance(line, GenieUnitTransformGroup):\n            logtext += \"\\n\"\n            logtext += f\"transform unit: {line.get_transform_unit()}\\n\"\n\n        if isinstance(line, GenieMonkGroup):\n            logtext += \"\\n\"\n            logtext += f\"switch unit: {line.get_switch_unit()}\\n\"\n\n        with logfile.open(\"w\") as log:\n            log.write(logtext)\n\n    for key, civ in dataset.civ_groups.items():\n        logfile = debugdir.joinpath(\"conversion/civ_groups/\")[str(key)]\n        logtext = \"\"\n\n        logtext += f\"repr: {civ}\\n\"\n        logtext += (\n            f\"nyan name: \"\n            f\"{civ_name_lookup_dict.get(civ.get_id(), nnn)[0]}\\n\"\n        )\n\n        logtext += f\"team bonus: {civ.team_bonus}\\n\"\n        logtext += f\"tech tree: {civ.tech_tree}\\n\"\n\n        logtext += \"civ bonus ids:\\n\"\n        for bonus in civ.civ_boni:\n            logtext += f\"    - {bonus}\\n\"\n\n        logtext += \"unique unit ids:\\n\"\n        for unit in civ.unique_entities:\n            logtext += (\n                f\"    - {unit} \"\n                f\"({entity_name_lookup_dict.get(unit, nnn)[0]})\\n\"\n            )\n\n        logtext += \"unique tech ids:\\n\"\n        for tech in civ.unique_techs:\n            logtext += (\n                f\"    - {tech} \"\n                f\"({tech_name_lookup_dict.get(tech, nnn)[0]})\\n\"\n            )\n\n        with logfile.open(\"w\") as log:\n            log.write(logtext)\n\n    for key, tech in dataset.tech_groups.items():\n        logfile = debugdir.joinpath(\"conversion/tech_groups/\")[str(key)]\n        logtext = \"\"\n\n        logtext += f\"repr: {tech}\\n\"\n        logtext += (\n            f\"nyan name: \"\n            f\"{tech_name_lookup_dict.get(tech.get_id(), nnn)[0]}\\n\"\n        )\n\n        logtext += f\"is_researchable: {tech.is_researchable()}\\n\"\n        logtext += f\"is_unique: {tech.is_unique()}\\n\"\n        logtext += (\n            f\"research location id: {tech.get_research_location_id()} \"\n            f\"({entity_name_lookup_dict.get(tech.get_research_location_id(), nnn)[0]})\\n\"\n        )\n\n        logtext += f\"required tech count: {tech.get_required_tech_count()}\\n\"\n        logtext += \"required techs:\\n\"\n        for req_tech in tech.get_required_techs():\n            logtext += (\n                f\"    - {req_tech} \"\n                f\"({tech_name_lookup_dict.get(req_tech, nnn)[0]})\\n\"\n            )\n\n        if isinstance(tech, AgeUpgrade):\n            logtext += \"\\n\"\n            logtext += f\"researched age id: {tech.age_id}\\n\"\n\n        if isinstance(tech, UnitLineUpgrade):\n            logtext += \"\\n\"\n            logtext += f\"upgraded line id: {tech.get_line_id()}\\n\"\n            logtext += (\n                f\"upgraded line: {tech.get_upgraded_line()} \"\n                f\"({entity_name_lookup_dict.get(tech.get_line_id(), nnn)[0]})\\n\"\n            )\n            logtext += f\"upgrade target id: {tech.get_upgrade_target_id()}\\n\"\n\n        if isinstance(tech, BuildingLineUpgrade):\n            logtext += \"\\n\"\n            logtext += f\"upgraded line id: {tech.get_line_id()}\\n\"\n            logtext += (\n                f\"upgraded line: {tech.get_upgraded_line()} \"\n                f\"({entity_name_lookup_dict.get(tech.get_line_id(), nnn)[0]})\\n\"\n            )\n            logtext += f\"upgrade target id: {tech.get_upgrade_target_id()}\\n\"\n\n        if isinstance(tech, UnitUnlock):\n            logtext += \"\\n\"\n            logtext += (\n                f\"unlocked line: {tech.get_unlocked_line()} \"\n                f\"({entity_name_lookup_dict.get(tech.get_unlocked_line().get_head_unit_id(), nnn)[0]})\\n\"\n            )\n\n        if isinstance(tech, BuildingUnlock):\n            logtext += \"\\n\"\n            logtext += (\n                f\"unlocked line: {tech.get_unlocked_line()} \"\n                f\"({entity_name_lookup_dict.get(tech.get_unlocked_line().get_head_unit_id(), nnn)[0]})\\n\"\n            )\n\n        with logfile.open(\"w\") as log:\n            log.write(logtext)\n\n    for key, terrain in dataset.terrain_groups.items():\n        logfile = debugdir.joinpath(\"conversion/terrain_groups/\")[str(key)]\n        logtext = \"\"\n\n        logtext += f\"repr: {terrain}\\n\"\n        logtext += (\n            f\"nyan name: \"\n            f\"{terrain_name_lookup_dict.get(terrain.get_id(), nnn)[1]}\\n\"\n        )\n\n        logtext += f\"has_subterrain: {terrain.has_subterrain()}\\n\"\n\n        with logfile.open(\"w\") as log:\n            log.write(logtext)\n\n\ndef debug_modpack(debugdir: Directory, loglevel: int, modpack: Modpack) -> None:\n    \"\"\"\n    Create debug output for a modpack.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param modpack: Modpack container.\n    :type modpack: Modpack\n    \"\"\"\n    if loglevel < 1:\n        return\n\n    # Export info and manifest file\n    logdir = debugdir.joinpath(f\"export/{modpack.name}\")\n\n    with logdir[modpack.info.filename].open('wb') as outfile:\n        outfile.write(modpack.info.dump().encode('utf-8'))\n\n    with logdir[modpack.manifest.filename].open('wb') as outfile:\n        outfile.write(modpack.manifest.dump().encode('utf-8'))\n\n    if loglevel < 2:\n        return\n\n    logfile = debugdir.joinpath(f\"export/{modpack.name}\")[\"summary\"]\n    logtext = \"\"\n\n    logtext += f\"name: {modpack.name}\\n\"\n\n    file_count = (\n        len(modpack.get_data_files()) +\n        len(modpack.get_media_files()) +\n        len(modpack.get_metadata_files())\n    )\n    logtext += f\"file count: {file_count}\\n\"\n    logtext += f\"    data: {len(modpack.get_data_files())}\\n\"\n    logtext += f\"    media: {len(modpack.get_media_files())}\\n\"\n\n    # Count the files by type\n    media_dict = {}\n    for media_type, files in modpack.get_media_files().items():\n        media_dict[media_type.value] = len(files)\n\n    # Sort by type name\n    media_dict = dict(sorted(media_dict.items(), key=lambda item: item[0]))\n\n    for media_type, file_count in media_dict.items():\n        logtext += f\"        {media_type}: {file_count}\\n\"\n\n    logtext += f\"    metadata: {len(modpack.get_metadata_files())}\\n\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_media_cache(\n    debugdir: Directory,\n    loglevel: int,\n    sourcedir: Directory,\n    cachedata: dict,\n    game_version: GameVersion\n) -> None:\n    \"\"\"\n    Create media cache data for graphics files. This allows using deterministic\n    packer and compression settings for graphics file conversion.\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param sourcedir: Sourcedir where the graphics files are mounted.\n    :type sourcedir: Directory\n    :param cachedata: Dict with cache data.\n    :type cachedata: dict\n    :param game_version: Game version.\n    :type game_version: GameVersion\n    \"\"\"\n    if loglevel < 6:\n        return\n\n    cache_file = MediaCacheFile(\"export/\", \"media_cache.toml\", game_version)\n    cache_file.set_hash_func(\"sha3_256\")\n\n    # Sort the output by filename\n    cache_data = dict(sorted(cachedata.items(), key=lambda item: item[0].source_filename))\n\n    for request, cache in cache_data.items():\n        filepath = sourcedir[\n            request.get_type().value,\n            request.source_filename\n        ]\n\n        cache_file.add_cache_data(\n            request.get_type(),\n            request.source_filename,\n            hash_file(filepath),\n            cache[1],\n            cache[0])\n\n    logfile = debugdir.joinpath(\"export/\")[\"media_cache.toml\"]\n    logtext = cache_file.dump()\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_execution_time(debugdir: Directory, loglevel: int, stages_time: dict[str, float]) -> None:\n    \"\"\"\n    Create debug output for execution time for each stage\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param stages_time: Dict with execution time for each stage.\n    :type stages_time: dict\n    \"\"\"\n    if loglevel < 1:\n        return\n\n    logfile = debugdir[\"execution_time\"]\n    logtext = \"\".join(f\"{k}: {v}\\n\" for k, v in stages_time.items())\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n\n\ndef debug_not_found_sounds(debugdir: Directory, loglevel: int, sound: Path) -> None:\n    \"\"\"\n    Create debug output for sounds not found\n\n    :param debugdir: Output directory for the debug info.\n    :type debugdir: Directory\n    :param loglevel: Determines how detailed the output is.\n    :type loglevel: int\n    :param sound: Sound object with path and name values.\n    :type sound: Path\n    \"\"\"\n    if loglevel < 6:\n        return\n\n    logfile = debugdir.joinpath(\"export/not_found_sounds\")[sound.stem]\n\n    path = [part.decode() for part in sound.parts]\n    logtext = f\"name: {sound.name}\\npath: {'/'.join(path)}\"\n\n    with logfile.open(\"w\") as log:\n        log.write(logtext)\n"
  },
  {
    "path": "openage/convert/service/export/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tload_media_cache.py\n)\n\nadd_subdirectory(interface)\nadd_subdirectory(opus)\nadd_subdirectory(png)\n"
  },
  {
    "path": "openage/convert/service/export/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nEntity objects used for export.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/export/interface/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tcutter.py\n\trename.py\n)\n\nadd_cython_modules(\n\tvisgrep.pyx\n)\n"
  },
  {
    "path": "openage/convert/service/export/interface/__init__.py",
    "content": "# Copyright 2016-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nInterface assets conversion.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/export/interface/cutter.py",
    "content": "# Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCutting some user interface assets into subtextures.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nfrom ....entity_object.export.texture import TextureImage\nfrom ....value_object.read.media.hardcoded.interface import (TOP_STRIP_PATTERN_CORNERS,\n                                                             TOP_STRIP_PATTERN_SEARCH_AREA_CORNERS,\n                                                             MID_STRIP_PATTERN_CORNERS,\n                                                             MID_STRIP_PATTERN_SEARCH_AREA_CORNERS,\n                                                             KNOWN_SUBTEX_CORNER_COORDS,\n                                                             INGAME_HUD_BACKGROUNDS,\n                                                             INGAME_HUD_BACKGROUNDS_SET)\nfrom .visgrep import visgrep, crop_array\n\nif typing.TYPE_CHECKING:\n    from numpy import ndarray\n\n\nclass InterfaceCutter:\n    \"\"\"\n    Cuts interface textures into repeatable parts.\n    \"\"\"\n\n    def __init__(self, idx: int):\n        self.idx = idx\n\n    def cut(self, image: TextureImage) -> TextureImage:\n        \"\"\"\n        Create subtextures by searching for patterns at hardcoded positions.\n        \"\"\"\n\n        if not isinstance(image, TextureImage):\n            raise ValueError(f\"we can only cut TextureImage, not '{type(image)}'\")\n\n        if is_ingame_hud_background(self.idx):\n            img_data = image.get_data()\n\n            yield self.cut_strip(img_data,\n                                 TOP_STRIP_PATTERN_CORNERS,\n                                 TOP_STRIP_PATTERN_SEARCH_AREA_CORNERS)\n\n            yield self.cut_strip(img_data,\n                                 MID_STRIP_PATTERN_CORNERS,\n                                 MID_STRIP_PATTERN_SEARCH_AREA_CORNERS)\n\n            for coords in KNOWN_SUBTEX_CORNER_COORDS:\n                yield TextureImage(crop_array(img_data, coords))\n        else:\n            yield image\n\n    def cut_strip(\n        self,\n        img_array: ndarray,\n        pattern_corners: tuple[int, int, int, int],\n        search_area_corners: tuple[int, int, int, int]\n    ) -> TextureImage:\n        \"\"\"\n        Finds a horizontally tilable piece of the strip (ex. the top of the HUD).\n\n        ||----///////////-------------///////////-------------///////////-------------///////////||\n                  ^      pattern_corners     ^                    ^  where it is found last  ^\n                  ^           this piece is tileable              ^\n\n        so, cut out a subtexture:\n                  ///////-------------///////////-------------////\n        \"\"\"\n\n        search_area = crop_array(img_array, search_area_corners)\n        pattern = crop_array(img_array, pattern_corners)\n\n        # search for patterns, with \"tolerance\"\n        matches = visgrep(search_area, pattern, 100000)\n\n        if len(matches) < 2:\n            raise RuntimeError(f\"visgrep failed to find repeating pattern in id={self.idx})\\n\")\n\n        # create the found pattern texture\n        return TextureImage(\n            crop_array(img_array, (\n                pattern_corners[0],\n                pattern_corners[1],\n                search_area_corners[0] + matches[-1].point[0],\n                pattern_corners[3]\n            ))\n        )\n\n\ndef ingame_hud_background_index(idx: int):\n    \"\"\"\n    Index in the hardcoded list of the known ingame hud backgrounds to match the civ.\n    \"\"\"\n    return INGAME_HUD_BACKGROUNDS.index(int(idx))\n\n\ndef is_ingame_hud_background(idx: int):\n    \"\"\"\n    True if in the hardcoded list of the known ingame hud backgrounds.\n    \"\"\"\n    return int(idx) in INGAME_HUD_BACKGROUNDS_SET\n"
  },
  {
    "path": "openage/convert/service/export/interface/rename.py",
    "content": "# Copyright 2016-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nRenaming interface assets and splitting into directories.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....value_object.read.media.hardcoded.interface import ASSETS\nfrom .cutter import ingame_hud_background_index\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.path import Path\n\n\ndef hud_rename(filepath: Path) -> Path:\n    \"\"\"\n    Returns a human-usable name according to the original\n    and hardcoded metadata.\n    \"\"\"\n    try:\n        return filepath.parent[\n            f\"hud{str(ingame_hud_background_index(int(filepath.stem))).zfill(4)}{filepath.suffix}\"\n        ]\n\n    except ValueError:\n        return asset_rename(filepath)\n\n\ndef asset_rename(filepath: Path) -> Path:\n    \"\"\"\n    Rename a slp asset path by the lookup map above.\n    \"\"\"\n    try:\n        return filepath.parent[ASSETS[filepath.stem] + filepath.suffix]\n\n    except (KeyError, AttributeError):\n        return filepath\n"
  },
  {
    "path": "openage/convert/service/export/interface/visgrep.pyx",
    "content": "# Copyright 2016-2023 the openage authors. See copying.md for legal info.\n\n# If you wanna boost speed even further:\n# cython: profile=False\n\n\n\"\"\" Cython version of the visgrep utility \"\"\"\n\nfrom PIL import Image\nimport argparse\nfrom collections import namedtuple\nimport itertools\nimport logging\nimport sys\n\nimport numpy\n\n\ncimport cython\ncimport numpy\n\nfrom libcpp.vector cimport vector\n\n\nTOOL_DESCRIPTION = \"\"\"Python translation of the visgrep v1.09\nvisual grep, greps for images in another image\nAuthor of the original C version: Steve Slaven - http://hoopajoo.net\"\"\"\n\nEPILOG = \"\"\"The image.png is\nscanned for detect.png starting from X,Y specified above. When detect.png\nis found, then all the match.png files are scanned at an offset of x,y as\nspecified above.  If a match is found, then visgrep prints the x,y and\nindex of the item.\n\nFor example, image.png is a screenshot and match1.png .. to match5.png are\nimages of letters a to e.  Each of these letters is enclosed in a blue box,\nso detect.png is an image of the upper left corner of the box.  This box is\nnot included in the match*.png files, so they are actually offset 5 pixels\ndown and 4 pixels to the left.  You might run it like this then:\n\n  visgrep -b -t50 -x-4 -y5 image.png match_corner.png match_a.png match_b.png ...\n\nEtc, with all matches listed.  Now suppose the screen showed 'ace' so\nvisgrep might output:\n\n0 10,10 0\n12 50,10 2\n7 90,10 4\n\nShowing that match_a.png (index 0) is at 10,10 on the screen.  If no match\nis found even though the detection image is found, the index will be -1.\n\nThe first match was 100%% accurate, while the second and third were very slightly\ninaccurate, probably due to anti-aliasing on the fonts.\n\nExit status is 0 for successful match, 1 for no match, and 2 for error.\n\nSee the examples page for use cases for different flags\"\"\"\n\n\nctypedef numpy.uint8_t pixel_t\n\ncdef struct pixel:\n    pixel_t r\n    pixel_t g\n    pixel_t b\n    pixel_t a\n\nctypedef pixel_t[:, :, :] image_t\n\nPoint = namedtuple('Point', ['x', 'y'])\nSize = namedtuple('Size', ['width', 'height'])\nFoundResult = namedtuple('FoundResult', ['badness', 'point'])\n\nGeomParams = namedtuple('GeomParams', ['off', 'src', 'sub', 'start', 'pattern_off', 'pattern'])\nGeomParams.__new__.__defaults__ = (Point(0, 0),\n                                   Size(None, None),\n                                   Size(1, 1),\n                                   Point(0, 0),\n                                   None,\n                                   None)\n\nMetricParams = namedtuple('MetricParams', ['tolerance'])\nImgParams = namedtuple('ImgParams', ['img', 'detect', 'match', 'scan_all'])\n\n\ncdef numpy.ndarray img_to_array(img):\n    \"\"\"\n    Convert a PIL image to a numpy array.\n    \"\"\"\n\n    if not isinstance(img, Image.Image):\n        raise ValueError(f\"PIL image required, not '{type(img)}'\")\n\n    return numpy.array(img)\n\n\ncpdef numpy.ndarray crop_array(array, corners):\n    \"\"\"\n    crop a numpy array by the absolute corner coordinates:\n\n    (x0, y0, x1, y1) = (left, upper, right, lower)\n\n    Those corners are all INCLUSIVE.\n\n    Array must have shape (h, w, 4), i.e. an RGBA image.\n    \"\"\"\n\n    x0, y0, x1, y1 = corners\n    return array[y0:y1, x0:x1]\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef inline pixel img_pixel_get(image_t img, Py_ssize_t x, Py_ssize_t y):\n    \"\"\"\n    Get pixel color or zero if outside bounds.\n    Totally speed boosted.\n    \"\"\"\n\n    if x < img.shape[1] and y < img.shape[0]:\n        return pixel(img[y, x, 0],\n                     img[y, x, 1],\n                     img[y, x, 2],\n                     img[y, x, 3])\n\n    return pixel(0, 0, 0, 0)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef img_subimage_find(image_t master, image_t find,\n                       start_from, float tolerance, find_next):\n    \"\"\"\n    Fuzzily find a part of an image that matches the pattern.\n    \"\"\"\n\n    cdef Py_ssize_t x_end = master.shape[1] - find.shape[1]\n    cdef Py_ssize_t y_end = master.shape[0] - find.shape[0]\n\n    if find_next:\n        start_from = Point(start_from.x + 1, start_from.y)\n\n    cdef Py_ssize_t ymax = start_from.y\n    cdef Py_ssize_t xmax = start_from.x\n    cdef Py_ssize_t y_it\n    cdef Py_ssize_t x_it\n    cdef float badness\n    # Loop the whole freakin image looking for this sub image, but not past edges\n    for y_it in range(ymax, y_end + 1):\n        for x_it in range(xmax, x_end + 1):\n            badness = img_subimage_cmp(master, find, x_it, y_it, tolerance)\n            if badness <= tolerance:\n                return FoundResult(badness, Point(x_it, y_it))\n\n    # No match\n    return FoundResult(-1, Point(-1, -1))\n\n\n#@cython.profile(False)\ncdef inline float img_pixel_cmp(const pixel &pix, const pixel &other_pix):\n    \"\"\"\n    o is the compare from pixel, assumed to be from a pattern. It's transparency\n    is the transparency used to modify the tolerance value\n      return( memcmp( &p, &o, sizeof( PIXEL ) ) );\n    make tolerance mean something\n    \"\"\"\n    cdef int difference = abs(pix.r - other_pix.r) + abs(pix.g - other_pix.g) + abs(pix.b - other_pix.b)\n    cdef int transparentness = other_pix.a\n    return difference * (transparentness / 255)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\n@cython.nonecheck(False)\ncdef float img_subimage_cmp(image_t master, image_t subimage,\n                            Py_ssize_t where_x, Py_ssize_t where_y,\n                            float tolerance):\n    \"\"\"\n    Returns 0 if subimage is inside master at where, like *cmp usually does for other stuff\n    otherwise returns an integer of how different the match is, for each color component\n    value off.  tolerance is how high to go before bailing.  set lower to avoid processing\n    lots of extra pixels, it will just ret when tolerance is met\n    \"\"\"\n\n    logging.debug(\"Comparing subimage where=%d,%d\", where_x, where_y)\n\n    # Check if subimage even fits in masterimage at POINT\n    if ((where_x + subimage.shape[1]) > master.shape[1] or\n        (where_y + subimage.shape[0]) > master.shape[0]):\n        # Superbad\n        logging.debug(\"Subimage would not fit here\")\n        return 1000\n\n    cdef float badness = 0\n\n    cdef Py_ssize_t sptx\n    cdef Py_ssize_t spty\n\n    for sptx in range(subimage.shape[1]):\n        for spty in range(subimage.shape[0]):\n            # Map U/V to X/Y.\n            # Grab pels and see if they match\n            mpx = img_pixel_get(master, sptx + where_x, spty + where_y)\n            spx = img_pixel_get(subimage, sptx, spty)\n\n            badness += abs(img_pixel_cmp(mpx, spx))\n\n            if badness > tolerance:\n                logging.debug(\"Bail out early, badness > tolerance %d > %d\", badness, tolerance)\n                # No match here, bail early\n                return badness\n\n    # Matched all of subimage\n    logging.debug(\"Image match ok, badness = %d\", badness)\n    return badness\n\n\ncdef do_output(show_badness, badness, point, idx):\n    \"\"\"\n    Print match coordinates and score.\n    \"\"\"\n    if show_badness:\n        print(f\"{badness:d} {point.x:d} {point.y:d} {idx:d}\")\n    else:\n        print(f\"{point.x:d}, {point.y:d} {idx:d}\")\n\n\ncdef advance_point(point, img, start_x):\n    \"\"\"\n    Move across the image.\n    \"\"\"\n    next_point = point(point.x + 1, point.y)\n\n    if next_point.x > img.shape[1]:\n        next_point = point(start_x, next_point.y + 1)\n        if next_point.y > img.shape[0]:\n            # done, bail\n            next_point = point(-1, -1)\n\n    return next_point\n\n\ncdef img_match_any(image_t img, patterns, off, tolerance, pt_match):\n    \"\"\"\n    Move across the image.\n    \"\"\"\n    gotmatch = None\n\n    tmp_pt_x = pt_match.point.x + off.x\n    tmp_pt_y = pt_match.point.y + off.y\n\n    for cnt, pattern in enumerate(patterns):\n        if gotmatch is None:\n            logging.info(\" Testing for pattern %d\", cnt)\n            logging.info(\" %d,%d \", tmp_pt_x, tmp_pt_y)\n            badness = img_subimage_cmp(img,\n                                       pattern,\n                                       tmp_pt_x,\n                                       tmp_pt_y,\n                                       tolerance)\n\n            if badness <= tolerance:\n                logging.info(\"  YES\")\n\n                gotmatch = (FoundResult(badness, Point(tmp_pt_x, tmp_pt_y)), cnt)\n\n                # Fall out\n                break\n            else:\n                logging.info(\"  NO\")\n\n    return gotmatch\n\n\ncpdef visgrep(image, pattern, tolerance, height=None):\n    \"\"\"\n    Return points where pattern is found in the image.\n\n    image and pattern are numpy arrays of RGBA images.\n    \"\"\"\n\n    if not (isinstance(image, numpy.ndarray) and\n            isinstance(pattern, numpy.ndarray)):\n        raise ValueError(\"image and pattern must be a numpy array\")\n\n    geom_params = GeomParams(src=Size(None, height))\n    metric_params = MetricParams(tolerance)\n\n    img_params = ImgParams(image, pattern, [pattern], False)\n\n    ret = list()\n    for find in visgrep_cli(geom_params, metric_params, img_params):\n        ret.append(find[0])\n\n    return ret\n\n\ncdef visgrep_cli(geom_params, metric_params, img_params):\n    \"\"\"\n    Perform search, return list of results.\n    \"\"\"\n\n    cdef image_t img, find\n    # cdef vector[pixel_t[:, :, :]] matches\n    cdef list matches\n\n    pt_match = FoundResult(0, Point(0, 0))\n    results = []\n    find_next = False\n\n    # we'll search in this image.\n    img = crop_array(img_params.img,\n                     (0, 0,\n                      geom_params.src.width or img_params.img.shape[1],\n                      geom_params.src.height or img_params.img.shape[0]))\n\n    if geom_params.pattern_off is not None:\n        patterns_crop = (geom_params.pattern_off.x,\n                         geom_params.pattern_off.y,\n                         geom_params.pattern.width + geom_params.pattern_off.x,\n                         geom_params.pattern.height + geom_params.pattern_off.y)\n\n        find = crop_array(img_params.detect, patterns_crop)\n\n        matches = [crop_array(match, patterns_crop)\n                   for match in img_params.match]\n    else:\n        find = img_params.detect\n        matches = img_params.match\n\n    logging.info(\"Detecting offsets...\")\n    pt_match = FoundResult(pt_match.badness, Point(geom_params.start.x, geom_params.start.y))\n    find_next = False\n    while pt_match.point.x != -1:\n        if img_params.scan_all:\n            # fake match here\n            if find_next:\n                next_point = advance_point(pt_match.point, img, geom_params.start.x)\n\n                # increment counters\n                pt_match = FoundResult(pt_match.badness, next_point)\n        else:\n            pt_match = img_subimage_find(img, find, pt_match.point,\n                                         metric_params.tolerance,\n                                         find_next)\n\n        # Not first time anymore\n        find_next = True\n\n        if pt_match.point.x != -1:\n            logging.info(\"  Found match at %d,%d\", pt_match.point.x, pt_match.point.y)\n\n            if len(img_params.match) == 1 and\\\n               numpy.array_equal(img_params.match[0], img_params.detect):\n                # Detection pattern is the single match pattern\n                results.append((pt_match, 0))\n                continue\n\n            # Try and identify what thing it is\n            gotmatch = None\n            for tmp_off_x, tmp_off_y in itertools.product(range(geom_params.sub.width),\n                                                          range(geom_params.sub.height)):\n                gotmatch = img_match_any(img,\n                                         matches,\n                                         Point(geom_params.off.x + tmp_off_x,\n                                               geom_params.off.y + tmp_off_y),\n                                         metric_params.tolerance,\n                                         pt_match)\n\n                if gotmatch is not None:\n                    results.append(gotmatch)\n                    break\n\n            # Notify of no match\n            if gotmatch is None:\n                logging.info(\" NO ITEMS MATCHED!\")\n                if not img_params.scan_all:\n                    results.append((pt_match, -1))\n\n    return results\n\n\ndef main():\n    \"\"\"\n    Visual grep, greps for images in another image.\n    \"\"\"\n    parser = argparse.ArgumentParser(description=TOOL_DESCRIPTION, epilog=EPILOG,\n                                     formatter_class=argparse.RawDescriptionHelpFormatter)\n\n    parser.add_argument(\"-x\", dest=\"off_x\", default=0, metavar=\"X_OFF\", type=int,\n                        help=\"Set x offset for detection matching\")\n    parser.add_argument(\"-y\", dest=\"off_y\", default=0, metavar=\"Y_OFF\", type=int,\n                        help=\"Set y offset for detection matching\")\n    parser.add_argument(\"-I\", dest=\"src_width\", default=0, metavar=\"WIDTH\", type=int,\n                        help=\"Set width for cropping the source image\")\n    parser.add_argument(\"-J\", dest=\"src_height\", default=0, metavar=\"HEIGHT\", type=int,\n                        help=\"Set height for cropping the source image\")\n    parser.add_argument(\"-W\", dest=\"sub_width\", default=0, metavar=\"X_OFF_WIDTH\", type=int,\n                        help=\"Set x offset width for detection matching\")\n    parser.add_argument(\"-H\", dest=\"sub_height\", default=0, metavar=\"Y_OFF_HEIGHT\", type=int,\n                        help=\"Set y offset height for detection matching\")\n    parser.add_argument(\"-X\", dest=\"start_x\", default=0, metavar=\"START_X_OFF\", type=int,\n                        help=\"Start scanning at X\")\n    parser.add_argument(\"-Y\", dest=\"start_y\", default=0, metavar=\"START_Y_OFF\", type=int,\n                        help=\"Start scanning at Y\")\n    parser.add_argument(\"-u\", dest=\"off_u\", default=0, metavar=\"U_OFF\", type=int,\n                        help=\"Set u offset for cropping patterns before use\")\n    parser.add_argument(\"-v\", dest=\"off_v\", default=0, metavar=\"V_OFF\", type=int,\n                        help=\"Set y offset for cropping patterns before use\")\n    parser.add_argument(\"-M\", dest=\"pattern_width\", metavar=\"U_WIDTH\", type=int,\n                        help=\"Set width of the cropped patterns\")\n    parser.add_argument(\"-N\", dest=\"pattern_height\", metavar=\"V_HEIGHT\", type=int,\n                        help=\"Set height of the cropped patterns\")\n    parser.add_argument(\"-a\", dest=\"scan_all\", help=\"\"\"Scan all patterns, not just after matching\n                        the detection pattern note: this method is much slower because we scan for\n                        all images at every pixel instead of just at detection points. Also, in\n                        this mode the detection image is ignored, there will be no matches\n                        for tile -1\"\"\", action=\"store_true\")\n    parser.add_argument(\"-t\", dest=\"tolerance\", default=0, metavar=\"TOLERANCE\", type=int,\n                        help=\"Set tolerance for 'fuzzy' matches, higher numbers are more tolerant\")\n    parser.add_argument(\"-b\", dest=\"show_badness\",\n                        help=\"\"\"Display 'badness' value, higher numbers mean match is less accurate,\n                        a badness value of 0 means the match is pixel-perfect\"\"\",\n                        action=\"store_true\")\n    parser.add_argument(\"-d\", metavar=\"DEBUGLEVEL\", type=int, help=\"Print debug messages\")\n    parser.add_argument(\"image\", metavar='image.png', help=\"Image to search in\")\n    parser.add_argument(\"detect\", metavar='detect.png', help=\"Pattern to search\")\n    parser.add_argument(\"match\", metavar='match.png', nargs=\"*\",\n                        help=\"Images to compare if detected\")\n\n    args = parser.parse_args()\n\n    logging.basicConfig(level=args.d)\n\n    pattern_crop_params = [v is not None for v in\n                           (args.off_u, args.off_v, args.pattern_width, args.pattern_height)]\n\n    if any(pattern_crop_params) and not all(pattern_crop_params):\n        parser.error('-u, -v, -M, -N must be given together')\n\n    image = img_to_array(Image.open(args.image).convert('RGBA'))\n    find = img_to_array(Image.open(args.detect).convert('RGBA'))\n    match = [img_to_array(Image.open(fname).convert('RGBA'))\n             for fname in args.match]\n\n    geom_params = GeomParams(Point(args.off_x, args.off_y),\n                             Size(args.src_width, args.src_height),\n                             Size(args.sub_width, args.sub_height),\n                             Point(args.start_x, args.start_y),\n                             Point(args.off_u, args.off_v),\n                             Size(args.pattern_width, args.pattern_height))\n    metric_params = MetricParams(args.tolerance)\n    img_params = ImgParams(image, find, match, args.scan_all)\n\n    results = visgrep_cli(geom_params, metric_params, img_params)\n\n    exit_status = 1\n\n    for result in results:\n        do_output(args.show_badness,\n                  result[0].badness,\n                  result[0].point,\n                  result[1])\n\n        if result[1] != -1:\n            exit_status = 0\n\n    sys.exit(exit_status)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "openage/convert/service/export/load_media_cache.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nLoad data of media cache files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport toml\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.path import Path\n\n\ndef load_media_cache(filepath: Path) -> dict[str, dict[str, tuple]]:\n    \"\"\"\n    Parses a media cache file and returns it as a dict.\n    \"\"\"\n    output_dict = {}\n\n    with filepath.open() as infile:\n        cache_file = toml.loads(infile.read())\n\n    cache_file.pop(\"file_version\")\n    cache_file.pop(\"hash_algo\")\n\n    cachedata = list(cache_file[\"graphics\"].values())\n    for cache in cachedata:\n        output_dict.update(\n            {\n                cache[\"filepath\"]: {\n                    \"compr_settings\": cache[\"compression_settings\"],\n                    \"packer_settings\": cache[\"packer_settings\"]\n                }\n            }\n        )\n\n    return output_dict\n"
  },
  {
    "path": "openage/convert/service/export/opus/CMakeLists.txt",
    "content": "find_package(Ogg REQUIRED)\nfind_package(Opusfile REQUIRED)\n\nadd_cython_modules(\n\topusenc.pyx\n)\n\npyext_link_libraries(\n\topusenc.pyx\n\t${OGG_LIBRARIES}\n\t${OPUS_LIBRARIES}\n)\n\npyext_include_directories(\n\topusenc.pyx\n\t${OGG_INCLUDE_DIRS}\n\t${OPUS_INCLUDE_DIRS}\n)\n\nadd_pxds(\n\t__init__.pxd\n\tbytearray.pxd\n\togg.pxd\n\topus.pxd\n)\n\nadd_py_modules(\n\t__init__.py\n\tdemo.py\n)\n"
  },
  {
    "path": "openage/convert/service/export/opus/__init__.pxd",
    "content": ""
  },
  {
    "path": "openage/convert/service/export/opus/__init__.py",
    "content": "# Copyright 2018-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCython module to encode opus-files using libopus.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/export/opus/bytearray.pxd",
    "content": "cdef extern from \"Python.h\":\n    char *PyByteArray_AS_STRING(object string) except NULL\n    Py_ssize_t PyByteArray_GET_SIZE(object string)\n    int PyByteArray_Resize(object string, Py_ssize_t len) except -1\n"
  },
  {
    "path": "openage/convert/service/export/opus/demo.py",
    "content": "# Copyright 2018-2022 the openage authors. See copying.md for legal info.\n\"\"\"\nDemo for the opusenc module.\n\"\"\"\n\nimport argparse\nimport time\n\nfrom . import opusenc\nfrom .....log import info, crit\n\n\ndef convert(args) -> int:\n    \"\"\" Demonstrates the usage of the opusenc module. \"\"\"\n\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"input\", metavar='file.wav',\n                     help=\"a wave file that should be converted\")\n    cli.add_argument(\"--out\", \"-o\", default=None, metavar='file.opus',\n                     help=\"the name for the resulting opus file\")\n    args = cli.parse_args(args)\n\n    wavname = args.input\n    info(\"Reading %s...\", wavname)\n\n    with open(wavname, mode='rb') as wav_file:\n        wav = wav_file.read()\n\n    info(\"Encoding...\")\n\n    tic = time.time()\n    out = opusenc.encode(wav)\n    tic = time.time() - tic\n\n    if isinstance(out, (str, int)):\n        crit(\"Encoding failed: %s\", out)\n        return 1\n\n    outname = args.out\n    if outname is None:\n        outname = wavname[:-3] + \"opus\" if wavname[-3:] == \"wav\" else wavname + \".opus\"\n\n    info(\"Writing %s.\", outname)\n    with open(outname, mode='wb') as fil:\n        fil.write(out)\n\n    info(\"Wave size: %s\", f\"{len(wav):>8}\")\n    info(\"Opus size: %s (%s%)\", f\"{len(out):>8}\",\n         f\"{100 * len(out) / len(wav):2.1f}\")\n    info(\"Encoding time: %s seconds.\", f\"{tic:.3f}\")\n    return 0\n"
  },
  {
    "path": "openage/convert/service/export/opus/ogg.pxd",
    "content": "# Copyright 2018-2020 the openage authors. See copying.md for legal info.\n\ncdef extern from \"ogg/config_types.h\":\n    ctypedef short ogg_int16_t\n    ctypedef long ogg_int64_t\n\ncdef extern from \"ogg/ogg.h\":\n    ctypedef struct ogg_stream_state:\n        pass\n    ctypedef struct ogg_page:\n        unsigned char *header\n        long header_len\n        unsigned char *body\n        long body_len\n    ctypedef struct ogg_packet:\n        unsigned char *packet\n        long bytes\n        long b_o_s\n        long e_o_s\n\n        ogg_int64_t granulepos\n        ogg_int64_t packetno\n\n    int ogg_stream_packetin(ogg_stream_state *os, ogg_packet *op)\n    int ogg_stream_pageout(ogg_stream_state *os, ogg_page *og)\n    int ogg_stream_flush(ogg_stream_state *os, ogg_page *og)\n\n    int ogg_stream_init(ogg_stream_state *os, int serialno)\n    int ogg_stream_clear(ogg_stream_state *os)\n"
  },
  {
    "path": "openage/convert/service/export/opus/opus.pxd",
    "content": "# Copyright 2018-2023 the openage authors. See copying.md for legal info.\n\ncdef extern from \"opus/opus.h\":\n    ctypedef struct OpusEncoder:\n        pass\n\n    ctypedef int opus_int32\n    ctypedef short opus_int16\n    OpusEncoder *opus_encoder_create(opus_int32 Fs, int channels,\n                                     int application, int *error)\n    void opus_encoder_destroy(OpusEncoder *st)\n    opus_int32 opus_encode(OpusEncoder *st, const opus_int16 *pcm,\n                           int frame_size, unsigned char *data,\n                           opus_int32 max_data_bytes)\n    int opus_encoder_ctl(OpusEncoder *st, int request)\n\ncdef extern from \"opus/opus_defines.h\":\n    # Errors\n    const int OPUS_OK\n    const int OPUS_ALLOC_FAIL\n    # Application\n    const int OPUS_APPLICATION_AUDIO\n    # Macros\n    int OPUS_GET_LOOKAHEAD(opus_int32 *x)\n"
  },
  {
    "path": "openage/convert/service/export/opus/opusenc.pyx",
    "content": "# Copyright 2018-2021 the openage authors. See copying.md for legal info.\n\nimport time\nfrom libc.string cimport memcpy, memset\nfrom cpython.mem cimport PyMem_Malloc, PyMem_Free\n\nfrom .....log import dbg, spam\n\nfrom .bytearray cimport (PyByteArray_AS_STRING, PyByteArray_GET_SIZE,\n                         PyByteArray_Resize)\nfrom . cimport ogg, opus\n\n\ndef encode(inputdata):\n    '''\n    Converts the wav file in the bytes object 'inputdata' to an opusfile\n    and returns it as bytes object. If allocations fail, raises a MemoryError.\n    If something else fails, a number or an error message is returned.\n    '''\n    inopt = read_wav(inputdata)\n    if not isinstance(inopt, dict):\n        return inopt\n\n    dbg(\"Wavefile\")\n    dbg(f\" Total length:   {len(inputdata)}\")\n    dbg(f\" Header length:  {inopt['header_len']}\")\n    dbg(f\" Trailer length: {inopt['trailer_len']}\")\n    if inopt['trailer_len']:\n        inputdata = inputdata[inopt['header_len']:-inopt['trailer_len']]\n    else:\n        inputdata = inputdata[inopt['header_len']:]\n\n    outdata = bytearray(1024*50)\n    cdef size_t outsize = 0  # valid bytes inside 'outdata'\n\n    cdef ogg.ogg_packet op\n    cdef ogg.ogg_page og\n    cdef ogg.ogg_stream_state os\n    cdef opus.OpusEncoder *oe\n    cdef int err\n    cdef int channels = inopt['channels']\n    cdef int wframe_len = inopt['wframe_len']\n    cdef opus.opus_int32 rate = inopt['input_rate']\n    cdef opus.opus_int32 coding_rate = 48000\n    cdef opus.opus_int32 frame_sz = 960\n    cdef opus.opus_int32 lookahead\n\n    if rate > 24000:\n        coding_rate = 48000\n    elif rate > 16000:\n        coding_rate = 24000\n    elif rate > 12000:\n        coding_rate = 16000\n    elif rate > 8000:\n        coding_rate = 12000\n    else:\n        coding_rate = 8000\n\n    frame_sz = <opus.opus_int32> (frame_sz / (48000 / coding_rate))\n\n    if coding_rate != rate:\n        dbg(\"Resampling necessary:\")\n        dbg(f\" input rate:  {rate}\")\n        dbg(f\" coding rate: {coding_rate}\")\n        try:\n            inputdata = upsample(inputdata, len(inputdata), inopt, coding_rate)\n        except MemoryError:\n            raise MemoryError(\"Upsampling failed.\") from None\n        dbg(\"Resampled data length: %s\", len(inputdata))\n\n    # initialize structs\n    if ogg.ogg_stream_init(&os, int(time.time() * 100) % (1<<31)):\n        return \"Could not initialize ogg_stream.\"\n\n    oe = opus.opus_encoder_create(coding_rate, channels, opus.OPUS_APPLICATION_AUDIO, &err)\n    if err != opus.OPUS_OK:\n        ogg.ogg_stream_clear(&os)\n        if err == opus.OPUS_ALLOC_FAIL:\n            raise MemoryError(\"Could not allocate opus encoder.\")\n        return \"Could not allocate opus encoder.\"\n\n    err = opus.opus_encoder_ctl(oe, opus.OPUS_GET_LOOKAHEAD(&lookahead))\n    if err != opus.OPUS_OK:\n        free_buffers(&os, oe, NULL, NULL)\n        return \"Getting encoder lookahead failed: {}\".format(err)\n\n    inopt['pre_skip'] = int(lookahead / (48000 / coding_rate))\n\n    try:\n        if write_opus_header(&os, &op, inopt):\n            free_buffers(&os, oe, NULL, NULL)\n            return \"Could not write opus header.\"\n\n        while ogg.ogg_stream_flush(&os, &og):\n            outsize = write_ogg_page(outsize, outdata, &og)\n    except MemoryError:\n        free_buffers(&os, oe, NULL, NULL)\n        raise MemoryError(\"Could not create opus header.\") from None\n\n    try:\n        if write_opus_comment(&os, &op):\n            free_buffers(&os, oe, NULL, NULL)\n            return \"Could not write opus comment.\"\n\n        while ogg.ogg_stream_flush(&os, &og):\n            outsize = write_ogg_page(outsize, outdata, &og)\n    except MemoryError:\n        free_buffers(&os, oe, NULL, NULL)\n        raise MemoryError(\"Could not write opus comment.\") from None\n\n    # allocate buffer for samples.\n    cdef size_t padbuf_sz = wframe_len * frame_sz\n    cdef size_t outbuf_sz = wframe_len * frame_sz  #TODO better size\n    cdef unsigned char *inbuf = NULL\n    cdef unsigned char *padbuf = <unsigned char *> PyMem_Malloc(padbuf_sz)\n    op.packet = <unsigned char *> PyMem_Malloc(outbuf_sz)\n    if padbuf == NULL or op.packet == NULL:\n        free_buffers(&os, oe, padbuf, op.packet)\n        raise MemoryError(\"Could not allocate buffers.\")\n    op.granulepos = <ogg.ogg_int64_t> inopt['pre_skip']\n\n    # Main encoding loop (one frame per iteration)\n    cdef opus.opus_int32 enc_bytes = 0  # encoded bytes in this iteration\n    cdef size_t ttl_samples = 0         # read samples\n    cdef size_t in_sz = len(inputdata)\n    cdef size_t in_pos = 0              # offset of next sample in inputdata\n    cdef size_t nb_samples              # no. of samples available this iteration\n    dbg(\"Starting encoding loop.\")\n    while not op.e_o_s:\n        # fill buffer. Read directly from input_data, if possible.\n        nb_samples = min((in_sz - in_pos) // wframe_len, frame_sz)\n        inbuf = (<unsigned char *> inputdata) + ttl_samples * wframe_len\n        if nb_samples < frame_sz:\n            op.e_o_s = 1\n            memcpy(padbuf, inbuf, in_sz - in_pos)\n            memset(padbuf + in_sz - in_pos, 0, padbuf_sz - (in_sz - in_pos))\n            inbuf = padbuf\n\n        in_pos += nb_samples * wframe_len\n        ttl_samples += nb_samples\n\n\n        # convert.\n        enc_bytes = opus.opus_encode(oe, <opus.opus_int16 *> inbuf, frame_sz, op.packet, outbuf_sz)\n        # check how much was converted/ loop or EOF ?\n        if enc_bytes < 0:\n            free_buffers(&os, oe, padbuf, op.packet)\n            return \"Encoding error in opus_encode(): {}\".format(enc_bytes)\n\n        # append converted opuspacket.\n        op.bytes = enc_bytes\n        op.granulepos += nb_samples * (48000 // coding_rate)\n        if op.e_o_s:\n            pass  # TODO. set granulepos ? for resampling decoders.\n        op.packetno += 1\n\n        err = ogg.ogg_stream_packetin(&os, &op)\n        if err:\n            free_buffers(&os, oe, padbuf, op.packet)\n            return \"ogg_stream_packetin() failed.\"\n\n        try:\n            # Try to write page or force, if end of stream is reached.\n            while ogg.ogg_stream_pageout(&os, &og)\\\n                    or (op.e_o_s and ogg.ogg_stream_flush(&os, &og)):\n                outsize = write_ogg_page(outsize, outdata, &og)\n        except MemoryError:\n                free_buffers(&os, oe, padbuf, op.packet)\n                raise MemoryError(\"Could not write opus data.\") from None\n\n\n    free_buffers(&os, oe, padbuf, op.packet)\n\n    return outdata[:outsize]\n\n\ncdef int write_opus_header(ogg.ogg_stream_state *os, ogg.ogg_packet *op, inopt):\n    '''\n    Write the Opus header.\n    Returns non-zero on failure.\n    '''\n    buf = bytearray(19)\n    buf[0:8] = b'OpusHead'\n    buf[8] = 1                # Opus encapsulating version.\n    buf[9] = inopt['channels']\n    buf[10:12] = inopt['pre_skip'].to_bytes(2, 'little')\n    buf[12:16] = inopt['input_rate'].to_bytes(4, 'little')\n    buf[16:18] = b'\\x00\\x00'  # Output gain\n    buf[18] = 0               # Channel mapping family\n\n\n    op.packet = buf\n    op.bytes = <long> 19\n    op.b_o_s = <long> 1\n    op.e_o_s = <long> 0\n    op.granulepos = <ogg.ogg_int64_t> 0\n    op.packetno = <ogg.ogg_int64_t> 0\n\n    return ogg.ogg_stream_packetin(os, op)\n\n\ncdef int write_opus_comment(ogg.ogg_stream_state *os, ogg.ogg_packet *op):\n    '''\n    Write the Opus comment header.\n    Returns non-zero on failure.\n    '''\n    vendor = b'openage asset converter'\n    buf = b''.join((b'OpusTags', len(vendor).to_bytes(4, 'little'), vendor,\n                    b'\\x00\\x00\\x00\\x00'))  # number of comments following\n\n    op.packet = buf\n    op.bytes = <long> len(buf)\n    op.b_o_s = <long> 0\n    op.e_o_s = <long> 0\n    op.granulepos = <ogg.ogg_int64_t> 0\n    op.packetno = <ogg.ogg_int64_t> 1\n\n    return ogg.ogg_stream_packetin(os, op)\n\n\ndef read_wav(wav):\n    '''\n    Reads the .wav file 'wav' and returns a dictionary with settings\n    or a string, stating an error.\n    '''\n    le = 'little'\n    inopt = {}\n    cdef pos = 0\n    chk_len = 0\n\n    if not wav or wav[0:4] != b'RIFF' or wav[8:12] != b'WAVE':\n        return \"Not a RIFF-WAVE file.\"\n\n    riff_len = int.from_bytes(wav[4:8], le)\n    # There seems to be idv3-tags or something at the end, if len(wav) - 8 > riff_len.\n    if len(wav) - 8 < riff_len:\n        err = \"Stated file-length {} in RIFF-header too high. \" +\\\n                \"It should be at most {}. Is the file maybe truncated?\"\n        return err.format(riff_len, len(wav) - 8)\n\n    pos = 12\n    # Search for 'fmt ' chunk.\n    while wav[pos:pos+4] != b'fmt ':\n        chk_len = int.from_bytes(wav[pos+4:pos+8], le)\n        pos += 8 + chk_len + (chk_len & 1)  # Pad to even byte offset.\n        if pos >= 8 + riff_len:\n            return \"Could not find 'fmt '-chunk in Wave-file.\"\n\n    chk_len = int.from_bytes(wav[pos+4:pos+8], le)\n    if chk_len < 16:\n        return \"Invalid WAVE-file: 'fmt '-chunk has invalid size.\"\n    pos += 8\n\n    if wav[pos:pos+2] != b'\\x01\\x00':\n        return \"Samples not in PCM-format.\"\n\n    inopt['channels'] = int.from_bytes(wav[pos+2:pos+4], le)\n    if inopt['channels'] not in (1, 2):\n        return \"WAVE-file has {} channels. Need 1 or 2.\"\\\n                .format(inopt['channels'])\n\n    inopt['input_rate'] = int.from_bytes(wav[pos+4:pos+8], le)\n    if inopt['input_rate'] > 48000:\n        return \"Inputrate {} too high for opus.\"\\\n                .format(inopt['input_rate'])\n\n    bytes_per_second = int.from_bytes(wav[pos+8:pos+12], le)\n    # wframe_len is the space needed to save one sample (padded to whole bytes)\n    # for each channel.\n    wframe_len = int.from_bytes(wav[pos+12:pos+14], le)\n    bits_per_sample = int.from_bytes(wav[pos+14:pos+16], le)\n    if bits_per_sample not in range(1,17):\n        return \"Samplesize is {}, but should be between 1 and 16 bits inclusive.\"\\\n                .format(bits_per_sample)\n\n    assert(wframe_len * inopt['input_rate'] == bytes_per_second)\n    assert((bits_per_sample + 7) // 8 * inopt['channels'] == wframe_len)\n\n    inopt['bits_per_sample'] = bits_per_sample\n    inopt['wframe_len'] = wframe_len\n\n    pos += chk_len + (chk_len & 1)\n    # Search for 'data' chunk.\n    while wav[pos:pos+4] != b'data':\n        chk_len = int.from_bytes(wav[pos+4:pos+8], le)\n        pos += 8 + chk_len + (chk_len & 1)  # Pad to even byte offset.\n        if pos >= 8 + riff_len:\n            return \"Could not find 'data'-chunk in Wave-file.\"\n\n    chk_len = int.from_bytes(wav[pos+4:pos+8], le)\n    pos += 8\n    if 8 + riff_len - pos < chk_len:\n        err = \"Stated data-length {} in WAVE-header too high. \" +\\\n                \"It should be at most {}. Is the file maybe truncated?\"\n        return err.format(chk_len, 8 + riff_len - pos)\n\n    inopt['header_len'] = pos\n    inopt['trailer_len'] = len(wav) - pos - chk_len\n    return inopt\n\n\ncdef int write_ogg_page(size_t offset, bytearray outdata, ogg.ogg_page *og) except? 0:\n    '''\n    Write 'og' at 'offset' into 'outdata'.\n    If succesful, return new offset. Otherwise, raise a MemoryError.\n    '''\n    if PyByteArray_GET_SIZE(outdata) < offset + og.header_len + og.body_len:\n        # TODO increase more than necessary\n        PyByteArray_Resize(outdata, offset + og.header_len + og.body_len)\n\n    cdef char *out = PyByteArray_AS_STRING(outdata) + offset\n    memcpy(out, og.header, og.header_len)\n    memcpy(out + og.header_len, og.body, og.body_len)\n    return offset + og.header_len + og.body_len\n\n\ncdef void free_buffers(ogg.ogg_stream_state *os, opus.OpusEncoder *oe,\n                       unsigned char *pymem1, unsigned char *pymem2):\n    ogg.ogg_stream_clear(os)\n    if oe != NULL:\n        opus.opus_encoder_destroy(oe)\n    if pymem1 != NULL:\n        PyMem_Free(pymem1)\n    if pymem2 != NULL:\n        PyMem_Free(pymem2)\n    return\n\n\ncdef upsample(const char *inp, size_t inp_len, dict opts, int target_rate):\n    '''\n    Upsamples the PCM-data in 'indata' to 'target_rate' using linear\n    interpolation. 'opts' is a dict containing info on 'indata'.\n    Assumptions: * 1 ≤ opts['bit_depth'] ≤ 16\n                 * opts['input_rate'] < target_rate\n                 * opts['channels'] ∈ {1, 2}\n    TODO: * a more eloquent version?\n          * a faster version?\n          * a better interpolation?\n    '''\n    cdef:\n        int wframe_len = opts['wframe_len']\n        int ch, channel = opts['channels']\n        int bit_depth = opts['bits_per_sample']\n        opus.opus_int16 mask = ~((1 << (16 - bit_depth)) - 1)\n        float alpha\n\n        size_t iin = 0, iout = 0\n        size_t ismpls, osmpls  # number of input and output samples\n        short a,b\n        int num, den, divisor\n\n        size_t outsize\n        char *out\n\n    ismpls = inp_len // wframe_len\n    if ismpls < 2:\n        # Nothing to do.\n        ret = bytearray(inp_len)\n        memcpy(PyByteArray_AS_STRING(ret), inp, inp_len)\n        return ret\n\n    divisor = gcd(target_rate, opts['input_rate'])\n    num = <int> (target_rate / divisor)\n    den = <int> (opts['input_rate'] / divisor)\n\n    # We're using linear interpolation. That means, we need for every output\n    # sample one input samples that comes before and one after the output\n    # sample. ismpls - 1 is the index of the last input sample. By multiplying\n    # with num / den we get the index of the last output sample. In general,\n    # this is not a whole number. Therefore we take only the integer part.\n    # Because indices are zero-based, add one for the number of output samples.\n    osmpls = ((ismpls - 1) * num / den).__trunc__() + 1\n    outsize = osmpls * channel * 2  # we convert to int16\n\n    ret = bytearray(outsize)\n    out = PyByteArray_AS_STRING(ret)\n\n    # iout is the next sample we want to calculate. We need one sample earlier\n    # and one later in time. Those are at indices iin and iin + 1. We need to\n    # check in every iteration, if iout is now after iin + 1 and in that case\n    # increase iin by one. Because we are _up_-sampling, the time between two\n    # output samples is shorter than between two input samples, and therefore\n    # adding one to iin suffices to keep the invariant\n    #           iin * num/den ≤ iout ≤ (iin + 1) * num/den .\n    #\n    # The following two blocks are very similar. The differences are the casts\n    # in the assignments of a and b as well as the shift in the last line\n    # When bit_depth is smaller than 9, only one byte is used per sample.\n    # Therefore inp must be casted to (char *). But we convert to a stream\n    # with 16 bits ber sample. To keep the audio volume the same, we set the\n    # higher order byte to the calculated value.\n    if bit_depth > 8:\n        for iout in range(0, osmpls):\n            if (iin + 1) * num < iout * den:\n                iin += 1\n\n            alpha = (<float> iout * den) / num - iin\n            for ch in range(0, channel):\n                a = (<short *> inp)[iin * channel + ch]\n                b = (<short *> inp)[(iin + 1) * channel + ch]\n                (<opus.opus_int16 *> out)[iout * channel + ch] = \\\n                        (<opus.opus_int16> (a + alpha * (b - a))) & mask\n    else:\n        for iout in range(0, osmpls):\n            if (iin + 1) * num < iout * den:\n                iin += 1\n\n            alpha = (<float> iout * den) / num - iin\n            for ch in range(0, channel):\n                a = (<char *> inp)[iin * channel + ch]\n                b = (<char *> inp)[(iin + 1) * channel + ch]\n                (<opus.opus_int16 *> out)[iout * channel + ch] = \\\n                        (<opus.opus_int16> (a + alpha * (b - a))) << 8 & mask\n\n    return ret\n\n\ncdef int gcd(int a, int b):\n    ''' Greatest common divisor. '''\n    cdef int m = max(a, b)\n    cdef int n = min(a, b)\n    cdef int r = -1\n    while r != 0:\n        r = m % n\n        m, n = n, r\n    return m\n"
  },
  {
    "path": "openage/convert/service/export/png/CMakeLists.txt",
    "content": "find_package(PNG REQUIRED)\n\nadd_cython_modules(\n\tbinpack.pyx\n\tpng_create.pyx\n)\n\npyext_link_libraries(\n\tpng_create.pyx\n\tPNG::PNG\n)\n\nadd_pxds(\n\t__init__.pxd\n\tlibpng.pxd\n\tpng_tmp_file.pxd\n)\n\nadd_py_modules(\n\t__init__.py\n)\n"
  },
  {
    "path": "openage/convert/service/export/png/__init__.pxd",
    "content": ""
  },
  {
    "path": "openage/convert/service/export/png/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCython module to create png files using libpng.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/export/png/binpack.pxd",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\nfrom libcpp.unordered_map cimport unordered_map\n\nctypedef (unsigned int, unsigned int, (unsigned int, unsigned int)) mapping_value\n\ncdef class Packer:\n    cdef unsigned int margin\n    cdef unordered_map[int, mapping_value] mapping\n\n    cdef void pack(self, list blocks)\n    cdef (unsigned int, unsigned int) pos(self, int index)\n    cdef unsigned int width(self)\n    cdef unsigned int height(self)\n    cdef list get_mapping_hints(self, list blocks)\n    cdef (unsigned int) get_packer_settings(self)\n\ncdef class DeterministicPacker(Packer):\n    pass\n\ncdef class BestPacker:\n    cdef list packers\n    cdef Packer current_best\n\n    cdef void pack(self, list blocks)\n    cdef Packer best_packer(self)\n    cdef (unsigned int, unsigned int) pos(self, int index)\n    cdef unsigned int width(self)\n    cdef unsigned int height(self)\n    cdef list get_mapping_hints(self, list blocks)\n    cdef (unsigned int) get_packer_settings(self)\n\ncdef class RowPacker(Packer):\n    pass\n\ncdef class ColumnPacker(Packer):\n    pass\n\ncdef class BinaryTreePacker(Packer):\n    cdef unsigned int aspect_ratio\n    cdef packer_node *root\n\n    cdef void fit(self, block block)\n    cdef (unsigned int) get_packer_settings(self)\n    cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height) noexcept\n    cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height) noexcept\n    cdef packer_node *grow_node(self, unsigned int width, unsigned int height) noexcept\n    cdef packer_node *grow_right(self, unsigned int width, unsigned int height) noexcept\n    cdef packer_node *grow_down(self, unsigned int width, unsigned int height) noexcept\n\ncdef struct packer_node:\n    unsigned int x\n    unsigned int y\n    unsigned int width\n    unsigned int height\n    bint used\n    packer_node *down\n    packer_node *right\n\ncdef struct block:\n    unsigned int index\n    unsigned int width\n    unsigned int height\n"
  },
  {
    "path": "openage/convert/service/export/png/binpack.pyx",
    "content": "# Copyright 2016-2024 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True,profile=False\n# TODO pylint: disable=C,R\n\n\"\"\"\nRoutines for 2D binpacking\n\"\"\"\n\ncimport cython\nfrom libc.math cimport sqrt\nfrom libc.stdlib cimport malloc\nfrom libcpp.unordered_map cimport unordered_map\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\n@cython.cdivision(True)\ncdef inline (unsigned int, unsigned int) factor(unsigned int n):\n    \"\"\"\n    Return two (preferable close) factors of n.\n    \"\"\"\n    cdef unsigned int a = <unsigned int>sqrt(n)\n    cdef int num\n    for num in range(a, 0, -1):\n        if n % num == 0:\n            return num, n // num\n\n\ncdef class Packer:\n    \"\"\"\n    Packs blocks.\n    \"\"\"\n    def __init__(self, int margin):\n        self.margin = margin\n\n    cdef void pack(self, list blocks):\n        \"\"\"\n        Pack all the blocks.\n\n        Each block must have a width and height attribute.\n        \"\"\"\n        raise NotImplementedError\n\n    cdef (unsigned int, unsigned int) pos(self, int index):\n        node = self.mapping[index]\n        return node[0], node[1]\n\n    cdef unsigned int width(self):\n        \"\"\"\n        Gets the total width of the packing.\n        \"\"\"\n        return max(self.pos(idx)[0] + block[2][0] for idx, block in self.mapping)\n\n    cdef unsigned int height(self):\n        \"\"\"\n        Gets the total height of the packing.\n        \"\"\"\n        return max(self.pos(idx)[1] + block[2][1] for idx, block in self.mapping)\n\n    cdef (unsigned int) get_packer_settings(self):\n        \"\"\"\n        Get the init parameters set for the packer.\n        \"\"\"\n        return (self.margin,)\n\n    cdef list get_mapping_hints(self, list blocks):\n        cdef list hints = []\n        cdef block block\n        for block in blocks:\n            hints.append(self.pos(block.index))\n\n        return hints\n\n\ncdef class DeterministicPacker(Packer):\n    \"\"\"\n    Packs blocks based on predetermined settings.\n    \"\"\"\n\n    def __init__(self, int margin, list hints):\n        super().__init__(margin)\n        self.hints = hints\n\n    cdef void pack(self, list blocks):\n        for idx, block in enumerate(blocks):\n            self.mapping[block.index] = self.hints[idx]\n\n\ncdef class BestPacker:\n    \"\"\"\n    Chooses the best result from all the given packers.\n    \"\"\"\n    def __init__(self, list packers):\n        self.packers = packers\n        self.current_best = None\n\n    cdef void pack(self, list blocks):\n        cdef Packer packer\n        for packer in self.packers:\n            packer.pack(blocks)\n\n        self.current_best = self.best_packer()\n\n    cdef Packer best_packer(self):\n        return min(self.packers, key=lambda Packer p: p.width() * p.height())\n\n    cdef (unsigned int, unsigned int) pos(self, int index):\n        return self.current_best.pos(index)\n\n    cdef unsigned int width(self):\n        return self.current_best.width()\n\n    cdef unsigned int height(self):\n        return self.current_best.height()\n\n    cdef (unsigned int) get_packer_settings(self):\n        return self.current_best.get_packer_settings()\n\n    cdef list get_mapping_hints(self, list blocks):\n        return self.current_best.get_mapping_hints(blocks)\n\n\ncdef class RowPacker(Packer):\n    \"\"\"\n    Packs blocks into rows, greedily trying to minimize the maximum width.\n    \"\"\"\n\n    cdef void pack(self, list blocks):\n        cdef unsigned int num_rows\n        cdef list rows\n\n        num_rows, _ = factor(len(blocks))\n        rows = [[] for _ in range(num_rows)]\n\n        # Put blocks into rows.\n        for block in blocks:\n            min_row = min(rows, key=lambda row: sum(b.width for b in row))\n            min_row.append(block)\n\n        # Calculate positions.\n        y = 0\n        for row in rows:\n            x = 0\n\n            for block in row:\n                self.mapping[block.index] = (x, y, (block.width, block.height))\n                x += block.width + self.margin\n\n            y += max(block.height for block in row) + self.margin\n\n\ncdef class ColumnPacker(Packer):\n    \"\"\"\n    Packs blocks into columns, greedily trying to minimize the maximum height.\n    \"\"\"\n\n    cdef void pack(self, list blocks):\n        num_columns, _ = factor(len(blocks))\n        columns = [[] for _ in range(num_columns)]\n\n        # Put blocks into columns.\n        for block in blocks:\n            min_col = min(columns, key=lambda col: sum(b.height for b in col))\n            min_col.append(block)\n\n        # Calculate positions.\n        x = 0\n        for column in columns:\n            y = 0\n\n            for block in column:\n                self.mapping[block.index] = (x, y, (block.width, block.height))\n                y += block.height + self.margin\n\n            x += max(block.width for block in column) + self.margin\n\n\ncdef inline (unsigned int, unsigned int, unsigned int, unsigned int) maxside_heuristic(block block):\n    \"\"\"\n    Heuristic: Order blocks by maximum side.\n    \"\"\"\n    return (max(block.width, block.height),\n            min(block.width, block.height),\n            block.height,\n            block.width)\n\n\ncdef class BinaryTreePacker(Packer):\n    \"\"\"\n    Binary tree bin packing strategy.\n\n    Very close to http://codeincomplete.com/posts/2011/5/7/bin_packing/.\n\n    Aditionally can target a given aspect ratio. 97/49 is optimal for terrain\n    textures.\n    \"\"\"\n\n    def __init__(self, int margin, int aspect_ratio=1):\n        # ASF: what about heuristic=max_heuristic?\n        super().__init__(margin)\n        self.aspect_ratio = aspect_ratio\n        self.root = NULL\n\n    cdef void pack(self, list blocks):\n        self.root = NULL\n\n        for block in sorted(blocks, key=maxside_heuristic, reverse=True):\n            self.fit(block)\n\n    cdef (unsigned int, unsigned int) pos(self, int index):\n        node = self.mapping[index]\n        return node[0], node[1]\n\n    cdef (unsigned int) get_packer_settings(self):\n        return (self.margin,)\n\n    cdef void fit(self, block block):\n        cdef packer_node *node\n        if self.root == NULL:\n            self.root = <packer_node *>malloc(sizeof(packer_node))\n            self.root.x = 0\n            self.root.y = 0\n            self.root.width = block.width + self.margin\n            self.root.height = block.height + self.margin\n            self.root.used = False\n            self.root.down = NULL\n            self.root.right = NULL\n\n        node = self.find_node(self.root,\n                              block.width + self.margin,\n                              block.height + self.margin)\n\n        if node != NULL:\n            node = self.split_node(node,\n                                   block.width + self.margin,\n                                   block.height + self.margin)\n        else:\n            node = self.grow_node(block.width + self.margin,\n                                  block.height + self.margin)\n\n        self.mapping[block.index] = (node.x, node.y, (block.width, block.height))\n\n    cdef packer_node *find_node(self, packer_node *root, unsigned int width, unsigned int height) noexcept:\n        if root.used:\n            return (self.find_node(root.right, width, height) or\n                    self.find_node(root.down, width, height))\n\n        elif width <= root.width and height <= root.height:\n            return root\n\n    cdef packer_node *split_node(self, packer_node *node, unsigned int width, unsigned int height) noexcept:\n        node.used = True\n\n        node.down = <packer_node *>malloc(sizeof(packer_node))\n        node.down.x = node.x\n        node.down.y = node.y + height\n        node.down.width = node.width\n        node.down.height = node.height - height\n        node.down.used = False\n        node.down.down = NULL\n        node.down.right = NULL\n\n        node.right = <packer_node *>malloc(sizeof(packer_node))\n        node.right.x = node.x + width\n        node.right.y = node.y\n        node.right.width = node.width - width\n        node.right.height = height\n        node.right.used = False\n        node.right.down = NULL\n        node.right.right = NULL\n\n        return node\n\n    @cython.cdivision(True)\n    cdef packer_node *grow_node(self, unsigned int width, unsigned int height) noexcept:\n        cdef bint can_grow_down = width <= self.root.width\n        cdef bint can_grow_right = height <= self.root.height\n        # assert can_grow_down or can_grow_right, \"Bad block ordering heuristic\"\n\n        cdef bint should_grow_right = ((self.root.height * self.aspect_ratio) >=\n                                       (self.root.width + width))\n        cdef bint should_grow_down = ((self.root.width / self.aspect_ratio) >=\n                                      (self.root.height + height))\n\n        if can_grow_right and should_grow_right:\n            return self.grow_right(width, height)\n\n        elif can_grow_down and should_grow_down:\n            return self.grow_down(width, height)\n\n        elif can_grow_right:\n            return self.grow_right(width, height)\n\n        else:\n            return self.grow_down(width, height)\n\n    cdef packer_node *grow_right(self, unsigned int width, unsigned int height) noexcept:\n        old_root = self.root\n\n        self.root = <packer_node *>malloc(sizeof(packer_node))\n        self.root.x = 0\n        self.root.y = 0\n        self.root.width = old_root.width + width\n        self.root.height = old_root.height\n        self.root.used = True\n        self.root.down = old_root\n        # self.root.right = NULL   (see below)\n\n        self.root.right = <packer_node *>malloc(sizeof(packer_node))\n        self.root.right.x = old_root.width\n        self.root.right.y = 0\n        self.root.right.width = width\n        self.root.right.height = old_root.height\n        self.root.right.used = False\n        self.root.right.down = NULL\n        self.root.right.right = NULL\n\n        node = self.find_node(self.root, width, height)\n        if node != NULL:\n            return self.split_node(node, width, height)\n\n    cdef packer_node *grow_down(self, unsigned int width, unsigned int height) noexcept:\n        old_root = self.root\n\n        self.root = <packer_node *>malloc(sizeof(packer_node))\n        self.root.x = 0\n        self.root.y = 0\n        self.root.width = old_root.width\n        self.root.height = old_root.height + height\n        self.root.used = True\n        # self.root.down = NULL  (see below)\n        self.root.right = old_root\n\n        self.root.down = <packer_node *>malloc(sizeof(packer_node))\n        self.root.down.x = 0\n        self.root.down.y = old_root.height\n        self.root.down.width = old_root.width\n        self.root.down.height = height\n        self.root.down.used = False\n        self.root.down.down = NULL\n        self.root.down.right = NULL\n\n        node = self.find_node(self.root, width, height)\n        if node != NULL:\n            return self.split_node(node, width, height)\n\n\ncdef struct packer_node:\n    unsigned int x\n    unsigned int y\n    unsigned int width\n    unsigned int height\n    bint used\n    packer_node *down\n    packer_node *right\n\ncdef struct block:\n    unsigned int index\n    unsigned int width\n    unsigned int height\n"
  },
  {
    "path": "openage/convert/service/export/png/libpng.pxd",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\nfrom libc.stdio cimport FILE\n\ncdef extern from \"png.h\":\n    const char PNG_LIBPNG_VER_STRING[]\n    const int PNG_COLOR_TYPE_RGBA\n    const int PNG_INTERLACE_NONE\n    const int PNG_COMPRESSION_TYPE_DEFAULT\n    const int PNG_FILTER_TYPE_DEFAULT\n    const int PNG_TRANSFORM_IDENTITY\n    const int PNG_IMAGE_VERSION\n    const char PNG_FORMAT_RGBA\n\n    const unsigned int PNG_FILTER_NONE\n    const unsigned int PNG_ALL_FILTERS\n\n    ctypedef unsigned char png_byte\n    ctypedef const png_byte *png_const_bytep\n    ctypedef png_byte *png_bytep\n    ctypedef png_byte **png_bytepp\n    ctypedef unsigned long int png_uint_32\n    ctypedef long int png_int_32\n\n    ctypedef struct png_struct\n    ctypedef png_struct *png_structp\n    ctypedef png_struct *png_structrp\n    ctypedef png_struct **png_structpp\n    ctypedef const png_struct *png_const_structrp\n\n    ctypedef struct png_info\n    ctypedef png_info *png_infop\n    ctypedef png_info *png_inforp\n    ctypedef png_info **png_infopp\n    ctypedef const png_info *png_const_inforp\n\n    ctypedef const char *png_const_charp\n    ctypedef void *png_voidp\n    ctypedef void *png_rw_ptr\n    ctypedef void *png_flush_ptr\n    ctypedef (png_structp, png_const_charp) *png_error_ptr\n    ctypedef FILE *png_FILE_p\n\n    ctypedef struct png_control\n    ctypedef png_control *png_controlp\n    ctypedef struct png_image:\n        png_controlp opaque\n        png_uint_32 version\n        png_uint_32 width\n        png_uint_32 height\n        png_uint_32 format\n        png_uint_32 flags\n        png_uint_32 colormap_entries\n        png_uint_32 warning_or_error\n        char message[64]\n    ctypedef png_image *png_imagep\n\n    ctypedef size_t png_alloc_size_t\n\n    png_structp png_create_write_struct(png_const_charp user_png_ver,\n                                        png_voidp error_ptr,\n                                        png_error_ptr error_fn,\n                                        png_error_ptr warn_fn)\n    png_infop png_create_info_struct(png_const_structrp png_ptr)\n\n    void png_set_IHDR(png_const_structrp png_ptr,\n                      png_inforp info_ptr,\n                      png_uint_32 width,\n                      png_uint_32 height,\n                      int bit_depth,\n                      int color_type,\n                      int interlace_method,\n                      int compression_method,\n                      int filter_method)\n    void png_init_io(png_structrp png_ptr,\n                     png_FILE_p fp)\n    void png_set_rows(png_const_structrp png_ptr,\n                      png_inforp info_ptr,\n                      png_bytepp row_pointers)\n    void png_write_png(png_structrp png_ptr,\n                       png_inforp info_ptr,\n                       int transforms,\n                       png_voidp params)\n    void png_destroy_write_struct(png_structpp png_ptr_ptr,\n                                  png_infopp info_ptr_ptr)\n\n    # PNG optimization options\n    void png_set_compression_level(png_structrp png_ptr,\n                                   int level)\n    void png_set_compression_mem_level(png_structrp png_ptr,\n                                       int mem_level)\n    void png_set_compression_strategy(png_structrp png_ptr,\n                                      int strategy)\n    void png_set_filter(png_structrp png_ptr,\n                        int method,\n                        int filters)\n\n    # Buffer writing\n    int png_image_write_to_memory(png_imagep image,\n                                  void *memory,\n                                  png_alloc_size_t *memory_bytes,\n                                  int convert_to_8_bit,\n                                  const void *buffer,\n                                  png_int_32 row_stride,\n                                  const void *colormap)\n    void png_set_write_fn(png_structp png_ptr,\n                          png_voidp io_ptr,\n                          png_rw_ptr write_data_fn,\n                          png_flush_ptr output_flush_fn)\n\n    # Should not be necessary if png_write_png() works\n    void png_write_info(png_structrp png_ptr,\n                        png_const_inforp info_ptr)\n    void png_write_row(png_structrp png_ptr,\n                       png_const_bytep row)\n    void png_write_end(png_structrp png_ptr,\n                       png_inforp info_ptr)\n"
  },
  {
    "path": "openage/convert/service/export/png/png_create.pyx",
    "content": "# Copyright 2020-2021 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n\n\"\"\"\nCreates valid PNG files as bytearrays by utilizing libpng.\n\"\"\"\n\nfrom libc.stdint cimport uint8_t\nfrom libc.stdlib cimport malloc, free\nfrom libc.string cimport memcpy, memset\n\nfrom ..opus.bytearray cimport PyByteArray_AS_STRING\nfrom . cimport libpng\nfrom . cimport png_tmp_file\nfrom enum import Enum\n\ncimport cython\nimport numpy\ncimport numpy\n\n\nclass CompressionMethod(Enum):\n    COMPR_NONE       = 0x00  # unused; no compression (for debugging)\n    COMPR_DEFAULT    = 0x01  # no optimization; default PNG compression\n    COMPR_OPTI       = 0x02  # best PNG compression; usually 50% smaller files\n    COMPR_GREEDY     = 0x03  # try several compression parameters; usually >50% smaller files\n    COMPR_AGGRESSIVE = 0x04  # unused; use zopfli for even better compression\n\ncdef struct greedy_cache_param:\n    uint8_t compr_lvl\n    uint8_t mem_lvl\n    uint8_t strat\n    uint8_t filters\n\ncdef struct process:\n    int     best_filesize\n    uint8_t best_compr_lvl\n    uint8_t best_compr_mem_lvl\n    uint8_t best_compr_strat\n    uint8_t best_filters\n\n# Running OptiPNG with optimization level 2 (-o2 flag)\ncdef int GREEDY_COMPR_LVL_MIN = 9\ncdef int GREEDY_COMPR_LVL_MAX = 9\ncdef int GREEDY_COMPR_MEM_LVL_MIN = 8\ncdef int GREEDY_COMPR_MEM_LVL_MAX = 8\ncdef int GREEDY_COMPR_STRAT_MIN = 0\ncdef int GREEDY_COMPR_STRAT_MAX = 3\ncdef int GREEDY_FILTER_0 = libpng.PNG_FILTER_NONE\ncdef int GREEDY_FILTER_5 = libpng.PNG_ALL_FILTERS\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ndef save(numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] imagedata not None,\n         compr_method=CompressionMethod.COMPR_DEFAULT, compr_settings=None):\n    \"\"\"\n    Convert an image matrix with RGBA colors to a PNG. The PNG is returned\n    as a bytearray or bytes object.\n\n    The function provides the option to reduce the resulting PNG size by\n    doing multiple compression trials.\n\n    :param imagedata: A 3-dimensional array with RGBA color values for pixels.\n    :type imagedata: numpy.ndarray\n    :param compr_method: The compression optimization method.\n    :type compr_method: CompressionMethod\n    :param compr_settings: A 4-tuple that containing compression level,\n                           memory level, strategy and filter method (in that\n                           order) used for encoding the PNG.\n    :type compr_settings: tuple\n    :returns: A bytearray containing the generated PNG file as well as the\n              settings that generate the smallest PNG, if the compression\n              method COMPR_GREEDY was chosen.\n    :rtype: tuple\n    \"\"\"\n    cdef unsigned int width = imagedata.shape[1]\n    cdef unsigned int height = imagedata.shape[0]\n    cdef numpy.uint8_t[:,:,::1] mview = imagedata\n\n    cdef greedy_cache_param cache\n\n    if compr_method is CompressionMethod.COMPR_DEFAULT:\n        outdata = optimize_default(mview, width, height)\n        best_settings = None\n\n    elif compr_method is CompressionMethod.COMPR_GREEDY:\n        if compr_settings:\n            cache.compr_lvl = compr_settings[0]\n            cache.mem_lvl = compr_settings[1]\n            cache.strat = compr_settings[2]\n            cache.filters = compr_settings[3]\n\n        else:\n            # Assign invalid values. This will trigger the optimization loop.\n            cache.compr_lvl = 0xFF\n            cache.mem_lvl = 0xFF\n            cache.strat = 0xFF\n            cache.filters = 0xFF\n\n        outdata, used_settings = optimize_greedy(mview, width, height, cache)\n        best_settings = (used_settings[\"compr_lvl\"], used_settings[\"mem_lvl\"],\n                         used_settings[\"strat\"], used_settings[\"filters\"])\n\n    elif compr_method is CompressionMethod.COMPR_OPTI:\n        cache.compr_lvl = 9\n        cache.mem_lvl = 8\n        cache.strat = 0\n        cache.filters = 8\n\n        outdata, used_settings = optimize_greedy(mview, width, height, cache)\n        best_settings = None\n\n    else:\n        raise NotImplementedError(f\"Compression method {compr_method} is not supported yet\")\n\n    return outdata, best_settings\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef bytearray optimize_default(numpy.uint8_t[:,:,::1] imagedata, int width, int height):\n    \"\"\"\n    Create an in-memory PNG with the default libpng compression level and copy it to\n    a bytearray.\n\n    :param imagedata: A memory view of a 3-dimensional array with RGBA color\n                      values for pixels. The array is expected to be C-aligned.\n    :type imagedata: uint8_t[:,:,::1]\n    :param width: Width of the image in pixels.\n    :type width: int\n    :param height: Height of the image in pixels.\n    :type height: int\n    :returns: A bytearray containing the generated PNG file.\n    :rtype: bytearray\n    \"\"\"\n    # Define basic image data\n    cdef libpng.png_image write_image\n    memset(&write_image, 0, sizeof(write_image))\n    write_image.version = libpng.PNG_IMAGE_VERSION\n    write_image.width = width\n    write_image.height = height\n    write_image.format = libpng.PNG_FORMAT_RGBA\n\n    # Get required byte size\n    cdef libpng.png_alloc_size_t write_image_size = 0\n    cdef void *rgb_data = &imagedata[0,0,0]\n    cdef int wresult = libpng.png_image_write_to_memory(&write_image,\n                                                        NULL,\n                                                        &write_image_size,\n                                                        0,\n                                                        rgb_data,\n                                                        0,\n                                                        NULL)\n\n    if not wresult:\n        raise MemoryError(\"Could not allocate memory for PNG conversion.\")\n\n    # Write in buffer\n    cdef void *outbuffer = malloc(write_image_size)\n    wresult = libpng.png_image_write_to_memory(&write_image,\n                                               outbuffer,\n                                               &write_image_size,\n                                               0,\n                                               rgb_data,\n                                               0,\n                                               NULL)\n\n    if not wresult:\n        raise MemoryError(\"Write to buffer failed for PNG conversion.\")\n\n    # Output data\n    outdata = bytearray(write_image_size)\n    cdef char *out = PyByteArray_AS_STRING(outdata)\n    memcpy(out, outbuffer, write_image_size)\n    free(outbuffer)\n\n    return outdata\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef optimize_greedy(numpy.uint8_t[:,:,::1] imagedata, int width, int height, greedy_cache_param cache):\n    \"\"\"\n    Create an in-memory PNG by greedily searching for the result with the\n    smallest file size and copying it to a bytes object.\n\n    The function provides the option to run the PNG generation with a fixed set of\n    (optimal) compression parameters that were found in a previous run. In this\n    case the search for the best parameters is skipped.\n\n    :param imagedata: A memory view of a 3-dimensional array with RGBA color\n                      values for pixels. The array is expected to be C-aligned.\n    :type imagedata: uint8_t[:,:,::1]\n    :param width: Width of the image in pixels.\n    :type width: int\n    :param height: Height of the image in pixels.\n    :type height: int\n    :param cache: A struct containing compression parameters for the PNG generation. Pass\n                   a struct with all values intialized to 0xFF to run the greedy search.\n    :type cache: greedy_cache_param\n    :returns: A bytearray containing the generated PNG file as well as the\n              settings that generate the smallest PNG.\n    :rtype: tuple\n    \"\"\"\n    if cache.compr_lvl == 0xFF:\n        cache = optimize_greedy_iterate(imagedata, width, height)\n\n    cdef png_tmp_file.tmp_file_buffer_state bufstate\n    bufstate.buffer = NULL\n    bufstate.size = 0\n\n    write_to_buffer(imagedata,\n                    &bufstate,\n                    cache.compr_lvl,\n                    cache.mem_lvl,\n                    cache.strat,\n                    cache.filters,\n                    width, height)\n\n    outbuffer = <bytes>bufstate.buffer[:bufstate.size]\n\n    return outbuffer, cache\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef greedy_cache_param optimize_greedy_iterate(numpy.uint8_t[:,:,::1] imagedata, int width, int height):\n    \"\"\"\n    Try several different compression settings and choose the settings\n    that generate the smallest PNG. The function tries 8 different\n    settings in total.\n\n    The algorithm is a reimplementation of a method used by OptiPNG.\n    Specifically, our function should be equivalent to the command\n\n    optipng -nx -o2 <filename>.png\n\n    :param imagedata: A memory view of a 3-dimensional array with RGBA color\n                      values for pixels. The array is expected to be C-aligned.\n    :type imagedata: uint8_t[:,:,::1]\n    :param width: Width of the image in pixels.\n    :type width: int\n    :param height: Height of the image in pixels.\n    :type height: int\n    :returns: Settings that generate the smallest PNG.\n    :rtype: greedy_cache_param\n    \"\"\"\n    cdef int     best_filesize = 0x7fffffff\n    cdef int     current_filesize = 0x7fffffff\n    cdef uint8_t best_compr_lvl = 0xFF\n    cdef uint8_t best_compr_mem_lvl = 0xFF\n    cdef uint8_t best_compr_strat = 0xFF\n    cdef uint8_t best_filters = 0xFF\n\n    cdef greedy_cache_param result\n\n    cdef png_tmp_file.tmp_file_buffer_state bufstate\n\n    for filters in range(GREEDY_FILTER_0, GREEDY_FILTER_5 + 1):\n        if filters != GREEDY_FILTER_0 and filters != GREEDY_FILTER_5:\n            continue\n\n        for strategy in range(GREEDY_COMPR_STRAT_MIN, GREEDY_COMPR_STRAT_MAX + 1):\n            for compr_lvl in range(GREEDY_COMPR_LVL_MIN, GREEDY_COMPR_LVL_MAX + 1):\n                for mem_lvl in range(GREEDY_COMPR_MEM_LVL_MIN, GREEDY_COMPR_MEM_LVL_MAX + 1):\n                    bufstate.buffer = NULL\n                    bufstate.size = 0\n\n                    write_to_buffer(\n                        imagedata,\n                        &bufstate,\n                        compr_lvl,\n                        mem_lvl,\n                        strategy,\n                        filters,\n                        width,\n                        height\n                    )\n\n                    current_filesize = bufstate.size\n\n                    if current_filesize < best_filesize:\n                        # Save the settings if we found a better result\n                        best_compr_lvl = compr_lvl\n                        best_compr_mem_lvl = mem_lvl\n                        best_compr_strat = strategy\n                        best_filters = filters\n                        best_filesize = current_filesize\n\n    result.compr_lvl = best_compr_lvl\n    result.mem_lvl = best_compr_mem_lvl\n    result.strat = best_compr_strat\n    result.filters = best_filters\n\n    return result\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef void write_to_file(numpy.uint8_t[:,:,::1] imagedata,\n                          libpng.png_FILE_p fp,\n                          int compression_level, int memory_level,\n                          int compression_strategy, int filters,\n                          int width, int height):\n    \"\"\"\n    Write an image matrix with RGBA color values to a file.\n\n    :param imagedata: A memory view of a 3-dimensional array with RGBA color\n                      values for pixels. The array is expected to be C-aligned.\n    :type imagedata: uint8_t[:,:,::1]\n    :param fp: Pointer to the file. For greedy compression trials it is recommended\n               to use an in-memory file stream created with posix.open_memstream()\n               to avoid costly I/O operations.\n    :type fp: libpng.png_FILE_p\n    :param compression_level: libpng compression level setting. (allowed: 1-9)\n    :type compression_level: int\n    :param memory_level: libpng compression memory level setting. (allowed: 1-9)\n    :type memory_level: int\n    :param compression_strategy: libpng compression strategy setting.  (allowed: 0-3)\n    :type compression_strategy: int\n    :param filters: libpng filter flags bitfield. (allowed: 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8)\n    :type filters: int\n    :param width: Width of the image in pixels.\n    :type width: int\n    :param height: Height of the image in pixels.\n    :type height: int\n    \"\"\"\n    write_ptr = libpng.png_create_write_struct(libpng.PNG_LIBPNG_VER_STRING,\n                                               NULL,\n                                               NULL,\n                                               NULL)\n    write_info_ptr = libpng.png_create_info_struct(write_ptr)\n\n    # Configure write settings\n    libpng.png_set_compression_level(write_ptr, compression_level)\n    libpng.png_set_compression_mem_level(write_ptr, memory_level)\n    libpng.png_set_compression_strategy(write_ptr, compression_strategy)\n    libpng.png_set_filter(write_ptr, libpng.PNG_FILTER_TYPE_DEFAULT, filters)\n\n    libpng.png_init_io(write_ptr, fp)\n    libpng.png_set_IHDR(write_ptr, write_info_ptr,\n                        width, height,\n                        8,\n                        libpng.PNG_COLOR_TYPE_RGBA,\n                        libpng.PNG_INTERLACE_NONE,\n                        libpng.PNG_COMPRESSION_TYPE_DEFAULT,\n                        libpng.PNG_FILTER_TYPE_DEFAULT)\n\n    # Write the data\n    libpng.png_write_info(write_ptr, write_info_ptr)\n\n    for row_idx in range(height):\n        libpng.png_write_row(write_ptr, &imagedata[row_idx,0,0])\n\n    libpng.png_write_end(write_ptr, write_info_ptr)\n\n    # Destroy the write struct\n    libpng.png_destroy_write_struct(&write_ptr, &write_info_ptr)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef void write_to_buffer(numpy.uint8_t[:,:,::1] imagedata,\n                          png_tmp_file.tmp_file_buffer_state *bufstate,\n                          int compression_level, int memory_level,\n                          int compression_strategy, int filters,\n                          int width, int height):\n    \"\"\"\n    Write an image matrix with RGBA color values to a given buffer.\n\n    :param imagedata: A memory view of a 3-dimensional array with RGBA color\n                      values for pixels. The array is expected to be C-aligned.\n    :type imagedata: uint8_t[:,:,::1]\n    :param bufstate: Struct containing the pointer to and the size of a buffer.\n    :type bufstate: png_tmp_file.tmp_file_buffer_state*\n    :param compression_level: libpng compression level setting. (allowed: 1-9)\n    :type compression_level: int\n    :param memory_level: libpng compression memory level setting. (allowed: 1-9)\n    :type memory_level: int\n    :param compression_strategy: libpng compression strategy setting.  (allowed: 0-3)\n    :type compression_strategy: int\n    :param filters: libpng filter flags bitfield. (allowed: 0x08, 0x10, 0x20, 0x40, 0x80, 0xF8)\n    :type filters: int\n    :param width: Width of the image in pixels.\n    :type width: int\n    :param height: Height of the image in pixels.\n    :type height: int\n    \"\"\"\n    write_ptr = libpng.png_create_write_struct(libpng.PNG_LIBPNG_VER_STRING,\n                                               NULL,\n                                               NULL,\n                                               NULL)\n    write_info_ptr = libpng.png_create_info_struct(write_ptr)\n\n    # Configure write settings\n    libpng.png_set_compression_level(write_ptr, compression_level)\n    libpng.png_set_compression_mem_level(write_ptr, memory_level)\n    libpng.png_set_compression_strategy(write_ptr, compression_strategy)\n    libpng.png_set_filter(write_ptr, libpng.PNG_FILTER_TYPE_DEFAULT, filters)\n\n    libpng.png_set_IHDR(write_ptr, write_info_ptr,\n                        width, height,\n                        8,\n                        libpng.PNG_COLOR_TYPE_RGBA,\n                        libpng.PNG_INTERLACE_NONE,\n                        libpng.PNG_COMPRESSION_TYPE_DEFAULT,\n                        libpng.PNG_FILTER_TYPE_DEFAULT)\n\n    # Set ur write function for writing to buffer\n    libpng.png_set_write_fn(write_ptr,\n                            bufstate,\n                            &png_tmp_file.tmp_file_png_write_fn,\n                            &png_tmp_file.tmp_file_flush_fn)\n\n    # Write the data\n    libpng.png_write_info(write_ptr, write_info_ptr)\n\n    for row_idx in range(height):\n        libpng.png_write_row(write_ptr, &imagedata[row_idx,0,0])\n\n    libpng.png_write_end(write_ptr, write_info_ptr)\n\n    # Destroy the write struct\n    libpng.png_destroy_write_struct(&write_ptr, &write_info_ptr)\n"
  },
  {
    "path": "openage/convert/service/export/png/png_tmp_file.pxd",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\nfrom . cimport libpng\nfrom libc.stdint cimport uint8_t\nfrom libcpp.vector cimport vector\n\ncdef extern from *:\n    \"\"\"\n    // Copyright 2021-2021 the openage authors. See copying.md for legal info.\n\n    #include \"png.h\"\n\n    #include <cstdint>\n    #include <cstdlib>\n    #include <cstring>\n\n    struct tmp_file_buffer_state {\n        png_bytep buffer;\n        size_t size;\n    };\n\n    void tmp_file_png_write_fn(png_structp png_ptr, png_bytep data, png_size_t length) {\n        // Get buffer ptr from libpng\n        tmp_file_buffer_state *state = (tmp_file_buffer_state *)png_get_io_ptr(png_ptr);\n        size_t new_size = state->size + length;\n\n        // Resize buffer to fit new data\n        if (state->buffer) {\n            state->buffer = (png_bytep)realloc(state->buffer, new_size);\n        }\n        else {\n            state->buffer = (png_bytep)malloc(new_size);\n        }\n\n        if (!state->buffer) {\n            png_error(png_ptr, \"Error allocating memory for temporary in-memory file.\");\n        }\n\n        // Append new data to buffer\n        memcpy(state->buffer + state->size, data, length);\n        state->size = new_size;\n    }\n\n    void tmp_file_flush_fn(png_structp png_ptr) {\n        // Do nothing, since changes don't need to be written to disk\n    }\n    \"\"\"\n    ctypedef struct tmp_file_buffer_state:\n        libpng.png_bytep buffer\n        size_t size\n\n    void tmp_file_png_write_fn(libpng.png_structp png_ptr,\n                               libpng.png_bytep data,\n                               libpng.png_size_t)\n    void tmp_file_flush_fn(libpng.png_structp png_ptr)\n"
  },
  {
    "path": "openage/convert/service/init/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tapi_export_required.py\n\tchangelog.py\n\tmodpack_search.py\n\tmount_asset_dirs.py\n\tversion_detect.py\n)\n"
  },
  {
    "path": "openage/convert/service/init/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nServices used during converter initialization.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/init/api_export_required.py",
    "content": "# Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTest whether the openage nyan API modpack is present.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport toml\n\nfrom ....log import info, dbg\n\nfrom .modpack_search import get_modpack_info\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.union import UnionPath\n\n\nCURRENT_API_VERSION = \"0.5.0\"\n\n\ndef api_export_required(asset_dir: UnionPath) -> bool:\n    \"\"\"\n    Returns true if the openage nyan API modpack cannot be found or is outdated.\n\n    TODO: Remove once the API modpack is generated by default.\n\n    :param asset_dir: The asset directory to search in.\n    :type asset_dir: UnionPath\n    :returns: True if the openage nyan API modpack cannot be found, else False.\n    \"\"\"\n    modpack_dir = asset_dir / \"converted\" / \"engine\"\n\n    try:\n        modpack_info = get_modpack_info(modpack_dir)\n        version = modpack_info[\"info\"][\"version\"]\n\n        if version != CURRENT_API_VERSION:\n            info(\"openage nyan API modpack is outdated\")\n            dbg(\"version is %s, expected %s\", version, CURRENT_API_VERSION)\n            return True\n\n        info(\"openage nyan API modpack is up to date\")\n        return False\n\n    except (FileNotFoundError, TypeError, toml.TomlDecodeError):\n        info(\"openage nyan API modpack not found\")\n\n    return True\n"
  },
  {
    "path": "openage/convert/service/init/changelog.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCheck for updates in the openage converter modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom itertools import chain\n\nfrom openage.util.version import SemanticVersion\n\nfrom ....log import info\n\nfrom ..init.version_detect import create_version_objects\n\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.union import UnionPath\n\n\ndef check_updates(available_modpacks: dict[str, str], game_info_dir: UnionPath):\n    \"\"\"\n    Check if there are updates available for the openage converter modpacks.\n\n    :param available_modpacks: Available modpacks and their versions. Modpack names are keys,\n                               versions are values.\n    :param game_info_dir: The directory containing the game information.\n    \"\"\"\n    game_editions, game_expansions = create_version_objects(game_info_dir)\n    for game_def in chain(game_editions, game_expansions):\n        for targetmod_name, targetmod_def in game_def.target_modpacks.items():\n            if targetmod_name in available_modpacks:\n                converter_version = SemanticVersion(targetmod_def[\"version\"])\n                try:\n                    modpack_version = SemanticVersion(available_modpacks[targetmod_name])\n\n                except ValueError:\n                    # Some really old converted modpacks don't use semantic versioning\n                    # these should always be updated\n                    modpack_version = SemanticVersion(\"0.0.0\")\n\n                if converter_version > modpack_version:\n                    info((\"Modpack %s v%s is outdated: \"\n                          \"newer version v%s is available\"),\n                         targetmod_name, modpack_version, converter_version)\n\n                else:\n                    info(\"Modpack %s v%s is up-to-date\", targetmod_name, modpack_version)\n"
  },
  {
    "path": "openage/convert/service/init/modpack_search.py",
    "content": "# Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSearch for and enumerate openage modpacks.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport toml\n\nfrom ....log import info, dbg\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.union import UnionPath\n\n\ndef enumerate_modpacks(modpacks_dir: UnionPath, exclude: set[str] = None) -> dict[str, str]:\n    \"\"\"\n    Enumerate openage modpacks in a directory.\n\n    :param asset_dir: The asset directory to search in.\n    :type asset_dir: UnionPath\n    :param exclude: Modpack names to exclude from the enumeration.\n    :type exclude: set[str]\n    :returns: Modpacks that were found. Names are keys, versions are values.\n    :rtype: dict[str, str]\n    \"\"\"\n    if not modpacks_dir.exists():\n        info(\"openage modpack directory has not been created yet\")\n        raise FileNotFoundError(\"openage modpack directory not found\")\n\n    modpacks: dict[str] = {}\n    for check_dir in modpacks_dir.iterdir():\n        if check_dir.is_dir():\n            try:\n                modpack_info = get_modpack_info(check_dir)\n                modpack_name = modpack_info[\"info\"][\"name\"]\n                modpack_version = modpack_info[\"info\"][\"version\"]\n                info(\"Found modpack %s v%s\", modpack_name, modpack_version)\n\n                if exclude is not None and modpack_name in exclude:\n                    dbg(\"Excluding modpack %s from enumeration\", modpack_name)\n                    continue\n\n                modpacks.update({modpack_name: modpack_version})\n\n            except (FileNotFoundError, TypeError, toml.TomlDecodeError):\n                dbg(\"No modpack found in directory: %s\", check_dir)\n\n    return modpacks\n\n\ndef get_modpack_info(modpack_dir: UnionPath) -> dict[str, typing.Any]:\n    \"\"\"\n    Get information about an openage modpack from its definition file.\n\n    :param modpack_dir: Modpack root directory.\n    :type modpack_dir: UnionPath\n    :returns: Modpack information.\n    :rtype: dict[str, typing.Any]\n\n    :raises FileNotFoundError: If the modpack definition file could not be found.\n    :raises TypeError: If the modpack definition file could not be parsed.\n    :raises toml.TomlDecodeError: If the modpack definition file is malformed.\n    \"\"\"\n    if not modpack_dir.exists():\n        info(\"Modpack directory %s not found\", modpack_dir.name)\n        raise FileNotFoundError(\"Modpack directory not found\")\n\n    modpack_def = modpack_dir / \"modpack.toml\"\n    dbg(\"Checking modpack definition file %s\", modpack_def)\n    try:\n        with modpack_def.open() as fileobj:\n            content = toml.loads(fileobj.read())\n\n        return content\n\n    except FileNotFoundError as err:\n        dbg(\"Modpack definition file not found; could not find %s\", modpack_def)\n        raise err\n\n    except TypeError as err:\n        dbg(\"Cannot parse modpack definition file %s; content is not a string\", modpack_def)\n        raise err\n\n    except toml.TomlDecodeError as err:\n        dbg(\"Cannot parse modpack definition file %s; content is not TOML or malformed\",\n            modpack_def)\n        raise err\n\n\ndef query_modpack(proposals: list[str]) -> str:\n    \"\"\"\n    Query interactively for a modpack from a selection of proposals.\n    \"\"\"\n    print(\"\\nPlease select a modpack before starting.\")\n    print(\"Enter the index of one of the proposals (Default = 0):\")\n\n    proposals = sorted(proposals)\n    for index, proposal in enumerate(proposals):\n        print(f\"({index}) {proposal}\")\n\n    user_selection = input(\"> \")\n    if user_selection == \"\":\n        selection = proposals[0]\n\n    else:\n        while not (user_selection.isdecimal() and int(user_selection) < len(proposals)):\n            print(f\"'{user_selection}' is not a valid index. Please try again.\")\n            user_selection = input(\"> \")\n\n        selection = proposals[int(user_selection)]\n\n    return selection\n"
  },
  {
    "path": "openage/convert/service/init/mount_asset_dirs.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-branches\n\"\"\"\nMount asset dirs of a game version into the conversion folder.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....util.fslike.union import Union\nfrom ...value_object.read.media.drs import DRS\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.util.fslike.directory import Directory\n\n\ndef mount_asset_dirs(\n    srcdir: Directory,\n    game_version: GameVersion\n) -> Union:\n    \"\"\"\n    Returns a Union path where srcdir is mounted at /,\n    and all the asset files are mounted in subfolders.\n    \"\"\"\n\n    result = Union().root\n    result.mount(srcdir)\n\n    def mount_drs(filename: str, target: str) -> None:\n        \"\"\"\n        Mounts the DRS file from srcdir's filename at result's target.\n        \"\"\"\n        drspath = srcdir[filename]\n        result[target].mount(DRS(drspath.open('rb'), game_version).root)\n\n    # Mount the media sources of the game edition\n    for media_type, media_paths in game_version.edition.media_paths.items():\n        for media_path in media_paths:\n            path_to_media = srcdir[media_path]\n            if path_to_media.is_dir():\n                # Mount folder\n                result[media_type.value].mount(path_to_media)\n\n            elif path_to_media.is_file():\n                # Mount archive\n                if path_to_media.suffix.lower() == \".drs\":\n                    mount_drs(media_path, media_type.value)\n\n            else:\n                raise FileNotFoundError(f\"Media at path {path_to_media} could not be found\")\n\n    # Mount the media sources of the game edition\n    for expansion in game_version.expansions:\n        for media_type, media_paths in expansion.media_paths.items():\n            for media_path in media_paths:\n                path_to_media = srcdir[media_path]\n                if path_to_media.is_dir():\n                    # Mount folder\n                    result[media_type.value].mount(path_to_media)\n\n                elif path_to_media.is_file():\n                    # Mount archive\n                    if path_to_media.suffix.lower() == \".drs\":\n                        mount_drs(media_path, media_type.value)\n\n                else:\n                    raise FileNotFoundError(f\"Media at path {path_to_media} could not be found\")\n\n    return result\n"
  },
  {
    "path": "openage/convert/service/init/version_detect.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments,too-many-locals,too-many-branches\n\"\"\"\nDetects the base version of the game and installed expansions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\n\nimport toml\n\n\nfrom ....log import info, warn, dbg\nfrom ....util.hash import hash_file\nfrom ...value_object.init.game_version import GameEdition, GameExpansion, GameVersion, Support\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.path import Path\n\n\ndef iterate_game_versions(\n    srcdir: Directory,\n    avail_game_eds: list[GameEdition],\n    avail_game_exps: list[GameExpansion]\n) -> GameVersion:\n    \"\"\"\n    Determine what editions and expansions of a game are installed in srcdir\n    by iterating through all versions the converter knows about.\n    \"\"\"\n    best_edition = None\n    expansions = []\n\n    for game_edition in avail_game_eds:\n        # Check for files that we know exist in the game's folder\n        for detection_hints in game_edition.game_file_versions:\n            check_paths = detection_hints.get_paths()\n\n            # Check if any of the known paths for the file exists\n            found_file = False\n            for required_path in check_paths:\n                required_file = srcdir.joinpath(required_path)\n\n                if required_file.is_file():\n                    hash_val = hash_file(required_file,\n                                         hash_algo=detection_hints.hash_algo)\n\n                    if hash_val not in detection_hints.get_hashes():\n                        dbg(f\"Found required file {required_file.resolve_native_path()} \"\n                            \"but could not determine version number\")\n\n                    else:\n                        version_no = detection_hints.get_hashes()[hash_val]\n                        dbg(f\"Found required file {required_file.resolve_native_path()} \"\n                            f\"for version {version_no}\")\n\n                    found_file = True\n                    break\n\n            if not found_file:\n                break\n\n        else:\n            # All files were found. Now check if the version is supported.\n            if game_edition.support == Support.NOPE:\n                dbg(f\"Found unsupported game edition: {game_edition}\")\n\n                if best_edition is None:\n                    best_edition = game_edition\n\n                # Continue to look for supported editions\n                continue\n\n            if game_edition.support == Support.BREAKS:\n                dbg(f\"Found broken game edition: {game_edition}\")\n\n                if best_edition is None or best_edition.support == Support.NOPE:\n                    best_edition = game_edition\n\n                # Continue to look for supported editions\n                continue\n\n            # We found a fully supported edition!\n            # No need to check for better editions\n            best_edition = game_edition\n            break\n\n    else:\n        # Either no version or an unsupported or broken was found\n        # Return the last detected edition\n        return GameVersion(edition=best_edition)\n\n    for game_expansion in best_edition.expansions:\n        for existing_game_expansion in avail_game_exps:\n            if game_expansion == existing_game_expansion.game_id:\n                game_expansion = existing_game_expansion\n\n        # Check for files that we know exist in the game expansion's folder\n        for detection_hints in game_expansion.game_file_versions:\n            check_paths = detection_hints.get_paths()\n\n            # Check if any of the known paths for the file exists\n            found_file = False\n            for required_path in check_paths:\n                required_file = srcdir.joinpath(required_path)\n\n                if required_file.is_file():\n                    found_file = True\n                    break\n\n            if not found_file:\n                break\n\n        else:\n            if game_expansion.support == Support.NOPE:\n                info(f\"Found unsupported game expansion: {game_expansion}\")\n                # Continue to look for supported expansions\n                continue\n\n            if game_expansion.support == Support.BREAKS:\n                info(f\"Found broken game expansion: {best_edition}\")\n                # Continue to look for supported expansions\n                continue\n\n            expansions.append(game_expansion)\n\n    return GameVersion(edition=best_edition, expansions=tuple(expansions))\n\n\ndef create_version_objects(srcdir: Directory) -> tuple[list[GameEdition], list[GameExpansion]]:\n    \"\"\"\n    Create GameEdition and GameExpansion objects from auxiliary\n    config files.\n    \"\"\"\n    game_expansion_list = []\n    game_edition_list = []\n\n    # initiliaze necessary paths\n    game_edition_path = srcdir.joinpath(\"game_editions.toml\")\n    game_expansion_path = srcdir.joinpath(\"game_expansions.toml\")\n\n    # load toml config files to a dictionary variable\n    with game_edition_path.open() as game_edition_toml:\n        game_editions = toml.loads(game_edition_toml.read())\n\n    with game_expansion_path.open() as game_expansion_toml:\n        game_expansions = toml.loads(game_expansion_toml.read())\n\n    # create and list GameEdition objects\n    game_editions.pop(\"file_version\")\n    for game in game_editions:\n        aux_path = srcdir[game_editions[game][\"subfolder\"]]\n        game_obj = create_game_obj(game_editions[game], aux_path)\n        game_edition_list.append(game_obj)\n\n    # create and list GameExpansion objects\n    game_expansions.pop(\"file_version\")\n    for game in game_expansions:\n        aux_path = srcdir[game_expansions[game][\"subfolder\"]]\n        game_obj = create_game_obj(game_expansions[game], aux_path, True)\n        game_expansion_list.append(game_obj)\n\n    return game_edition_list, game_expansion_list\n\n\ndef create_game_obj(\n    game_info: dict[str, str],\n    aux_path: Path,\n    expansion: bool = False\n) -> typing.Union[GameEdition, GameExpansion]:\n    \"\"\"\n    Create a GameEdition or GameExpansion object from the contents\n    of the game_info dictionary and its version hash file.\n    Use expansion parameter to decide if a GameEdition object\n    is needed to be created or GameExpansion.\n    \"\"\"\n    # initialize necessary parameters\n    game_name = game_info['name']\n    game_id = game_info['game_edition_id']\n    support = game_info['support']\n    modpacks = game_info['targetmods']\n    if not expansion:\n        expansions = game_info['expansions']\n\n    flags = {}\n\n    # add mediapaths for the game\n    game_mediapaths = []\n    for media_type in game_info['mediapaths']:\n        game_mediapaths.append(\n            (media_type, game_info['mediapaths'][media_type]))\n\n    # add install dirs for the game\n    game_installpaths = {}\n    if 'installpaths' in game_info:\n        game_installpaths = game_info['installpaths']\n\n    # add version hashes from the auxiliary file specific for the game\n    game_hash_path = aux_path[\"version_hashes.toml\"]\n    with game_hash_path.open() as game_hash_toml:\n        game_hashes = toml.loads(game_hash_toml.read())\n\n    file_version = game_hashes.pop(\"file_version\")\n    hash_algo = game_hashes.pop(\"hash_algo\")\n    if hash_algo != \"SHA3-256\":\n        warn(f\"{game_hash_path}: Unrecognized hash algorithm: {hash_algo}\")\n\n    game_version_info = []\n    for item in game_hashes.items():\n        if file_version == \"1.0\":\n            game_version_info.append((\n                [item[1]['path']],\n                item[1]['map']\n            ))\n\n        elif file_version == \"2.0\":\n            game_version_info.append((\n                item[1]['paths'],\n                item[1]['map']\n            ))\n\n        else:\n            raise SyntaxError(\n                f\"{game_hash_path}: Unrecognized file version: '{file_version}'\")\n\n    # Check if there is a media cache file and save the path if it exists\n    if aux_path[\"media_cache.toml\"].is_file():\n        flags[\"media_cache\"] = aux_path[\"media_cache.toml\"]\n\n    if expansion:\n        return GameExpansion(game_name, game_id, support, game_version_info,\n                             game_mediapaths, modpacks, **flags)\n\n    return GameEdition(game_name, game_id, support, game_version_info,\n                       game_mediapaths, game_installpaths, modpacks,\n                       expansions, **flags)\n"
  },
  {
    "path": "openage/convert/service/read/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tgamedata.py\n\tnyan_api_loader.py\n\tpalette.py\n\tregister_media.py\n\tstring_resource.py\n)\n"
  },
  {
    "path": "openage/convert/service/read/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nServices used for reading data.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/service/read/gamedata.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nModule for reading .dat files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nimport os\nimport pickle\nfrom tempfile import gettempdir\nfrom zlib import decompress\n\nfrom ....log import spam, dbg, info, warn\nfrom ...value_object.read.media.datfile.empiresdat import EmpiresDatWrapper\nfrom ...value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.read_members import ArrayMember\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.wrapper import GuardedFile\n\n\ndef get_gamespec(srcdir: Directory, game_version: GameVersion, pickle_cache: bool) -> ArrayMember:\n    \"\"\"\n    Reads empires.dat file.\n    \"\"\"\n    if game_version.edition.game_id in (\"ROR\", \"AOE1DE\", \"AOC\", \"HDEDITION\", \"AOE2DE\"):\n        filepath = srcdir.joinpath(game_version.edition.media_paths[MediaType.DATFILE][0])\n\n    elif game_version.edition.game_id == \"SWGB\":\n        if \"SWGB_CC\" in [expansion.game_id for expansion in game_version.expansions]:\n            filepath = srcdir.joinpath(game_version.expansions[0].media_paths[MediaType.DATFILE][0])\n\n        else:\n            filepath = srcdir.joinpath(game_version.edition.media_paths[MediaType.DATFILE][0])\n\n    else:\n        raise RuntimeError(\"No service found for reading data file of \"\n                           f\"version {game_version.edition.game_id}\")\n\n    cache_file = os.path.join(\n        gettempdir(), f\"{game_version.edition.game_id}_{filepath.name}.pickle\")\n\n    with filepath.open('rb') as empiresdat_file:\n        gamespec = load_gamespec(empiresdat_file,\n                                 game_version,\n                                 cache_file,\n                                 pickle_cache)\n\n    return gamespec\n\n\ndef load_gamespec(\n    fileobj: GuardedFile,\n    game_version: GameVersion,\n    cachefile_name: str = None,\n    pickle_cache: bool = False,\n    dynamic_load = False\n) -> ArrayMember:\n    \"\"\"\n    Helper method that loads the contents of a 'empires.dat' gzipped wrapper\n    file.\n\n    If cachefile_name is given, this file is consulted before performing the\n    load.\n    \"\"\"\n    # try to use the cached result from a previous run\n    if cachefile_name:\n        try:\n            with open(cachefile_name, \"rb\") as cachefile:\n                # pickle.load() can fail in many ways, we need to catch all.\n                # pylint: disable=broad-except\n                try:\n                    info(\"using cached wrapper: %s\", cachefile_name)\n                    gamespec = pickle.load(cachefile)\n                    return gamespec\n                except Exception:\n                    warn(\"could not use cached wrapper:\")\n                    import traceback\n                    traceback.print_exc()\n                    warn(\"we will just skip the cache, no worries.\")\n\n        except FileNotFoundError:\n            pass\n\n    # read the file ourselves\n\n    dbg(\"reading dat file\")\n    compressed_data = fileobj.read()\n    fileobj.close()\n\n    dbg(\"decompressing dat file\")\n    # -15: there's no header, window size is 15.\n    file_data = decompress(compressed_data, -15)\n    del compressed_data\n\n    spam(\"length of decompressed data: %d\", len(file_data))\n\n    wrapper = EmpiresDatWrapper()\n    _, gamespec = wrapper.read(file_data, 0, game_version, dynamic_load=dynamic_load)\n\n    # Remove the list sorrounding the converted data\n    gamespec = gamespec[0]\n    del wrapper\n\n    if cachefile_name and pickle_cache:\n        dbg(\"dumping dat file contents to cache file: %s\", cachefile_name)\n        with open(cachefile_name, \"wb\") as cachefile:\n            pickle.dump(gamespec, cachefile)\n\n    return gamespec\n"
  },
  {
    "path": "openage/convert/service/read/nyan_api_loader.py",
    "content": "# Copyright 2019-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long,too-many-lines,too-many-statements\n\"\"\"\nLoads the API into the converter.\n\nTODO: Implement a parser instead of hardcoded\nobject creation.\n\"\"\"\nfrom __future__ import annotations\n\nfrom ....nyan.nyan_structs import NyanMemberType\nfrom ....nyan.nyan_structs import NyanObject, NyanMember, MemberType, MemberSpecialValue, \\\n    MemberOperator\n\n# Common primitive types\n# We can use these so we don't have to create them every single time\nN_INT = NyanMemberType(\"int\")\nN_FLOAT = NyanMemberType(\"float\")\nN_TEXT = NyanMemberType(\"text\")\nN_FILE = NyanMemberType(\"file\")\nN_BOOL = NyanMemberType(\"bool\")\n\n\ndef load_api() -> dict[str, NyanObject]:\n    \"\"\"\n    Returns a dict with the API object's fqon as keys\n    and the API objects as values.\n    \"\"\"\n    api_objects = {}\n\n    api_objects = _create_objects(api_objects)\n    _insert_members(api_objects)\n\n    return api_objects\n\n\ndef _create_objects(api_objects: dict[str, NyanObject]) -> None:\n    \"\"\"\n    Creates the API objects.\n    \"\"\"\n    # engine.root\n    # engine.root.Object\n    nyan_object = NyanObject(\"Object\")\n    fqon = \"engine.root.Object\"\n    nyan_object.set_fqon(fqon)\n\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability\n    # engine.ability.Ability\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Ability\", parents)\n    fqon = \"engine.ability.Ability\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.AbilityProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AbilityProperty\", parents)\n    fqon = \"engine.ability.property.AbilityProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.Animated\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"Animated\", parents)\n    fqon = \"engine.ability.property.type.Animated\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.AnimationOverride\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"AnimationOverride\", parents)\n    fqon = \"engine.ability.property.type.AnimationOverride\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.CommandSound\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"CommandSound\", parents)\n    fqon = \"engine.ability.property.type.CommandSound\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.Diplomatic\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"Diplomatic\", parents)\n    fqon = \"engine.ability.property.type.Diplomatic\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.ExecutionSound\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"ExecutionSound\", parents)\n    fqon = \"engine.ability.property.type.ExecutionSound\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.property.type.Lock\n    parents = [api_objects[\"engine.ability.property.AbilityProperty\"]]\n    nyan_object = NyanObject(\"Lock\", parents)\n    fqon = \"engine.ability.property.type.Lock\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ActiveTransformTo\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ActiveTransformTo\", parents)\n    fqon = \"engine.ability.type.ActiveTransformTo\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Activity\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Activity\", parents)\n    fqon = \"engine.ability.type.Activity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ApplyContinuousEffect\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ApplyContinuousEffect\", parents)\n    fqon = \"engine.ability.type.ApplyContinuousEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ApplyDiscreteEffect\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ApplyDiscreteEffect\", parents)\n    fqon = \"engine.ability.type.ApplyDiscreteEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.AttributeChangeTracker\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"AttributeChangeTracker\", parents)\n    fqon = \"engine.ability.type.AttributeChangeTracker\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Cloak\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Cloak\", parents)\n    fqon = \"engine.ability.type.Cloak\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.CollectStorage\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"CollectStorage\", parents)\n    fqon = \"engine.ability.type.CollectStorage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Collision\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Collision\", parents)\n    fqon = \"engine.ability.type.Collision\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Constructable\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Constructable\", parents)\n    fqon = \"engine.ability.type.Constructable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Create\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Create\", parents)\n    fqon = \"engine.ability.type.Create\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Despawn\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Despawn\", parents)\n    fqon = \"engine.ability.type.Despawn\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.DetectCloak\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"DetectCloak\", parents)\n    fqon = \"engine.ability.type.DetectCloak\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.DropResources\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"DropResources\", parents)\n    fqon = \"engine.ability.type.DropResources\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.DropSite\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"DropSite\", parents)\n    fqon = \"engine.ability.type.DropSite\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.EnterContainer\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"EnterContainer\", parents)\n    fqon = \"engine.ability.type.EnterContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ExchangeResources\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ExchangeResources\", parents)\n    fqon = \"engine.ability.type.ExchangeResources\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ExitContainer\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ExitContainer\", parents)\n    fqon = \"engine.ability.type.ExitContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Fly\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Fly\", parents)\n    fqon = \"engine.ability.type.Fly\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Formation\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Formation\", parents)\n    fqon = \"engine.ability.type.Formation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Foundation\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Foundation\", parents)\n    fqon = \"engine.ability.type.Foundation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.GameEntityStance\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"GameEntityStance\", parents)\n    fqon = \"engine.ability.type.GameEntityStance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Gather\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Gather\", parents)\n    fqon = \"engine.ability.type.Gather\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Harvestable\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Harvestable\", parents)\n    fqon = \"engine.ability.type.Harvestable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Herd\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Herd\", parents)\n    fqon = \"engine.ability.type.Herd\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Herdable\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Herdable\", parents)\n    fqon = \"engine.ability.type.Herdable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Idle\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Idle\", parents)\n    fqon = \"engine.ability.type.Idle\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.LineOfSight\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"LineOfSight\", parents)\n    fqon = \"engine.ability.type.LineOfSight\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Live\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Live\", parents)\n    fqon = \"engine.ability.type.Live\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Lock\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Lock\", parents)\n    fqon = \"engine.ability.type.Lock\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Move\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Move\", parents)\n    fqon = \"engine.ability.type.Move\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Named\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Named\", parents)\n    fqon = \"engine.ability.type.Named\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.OverlayTerrain\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"OverlayTerrain\", parents)\n    fqon = \"engine.ability.type.OverlayTerrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.PassiveTransformTo\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"PassiveTransformTo\", parents)\n    fqon = \"engine.ability.type.PassiveTransformTo\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Pathable\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Pathable\", parents)\n    fqon = \"engine.ability.type.Pathable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ProductionQueue\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ProductionQueue\", parents)\n    fqon = \"engine.ability.type.ProductionQueue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Projectile\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Projectile\", parents)\n    fqon = \"engine.ability.type.Projectile\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ProvideContingent\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ProvideContingent\", parents)\n    fqon = \"engine.ability.type.ProvideContingent\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RallyPoint\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"RallyPoint\", parents)\n    fqon = \"engine.ability.type.RallyPoint\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RangedContinuousEffect\n    parents = [api_objects[\"engine.ability.type.ApplyContinuousEffect\"]]\n    nyan_object = NyanObject(\"RangedContinuousEffect\", parents)\n    fqon = \"engine.ability.type.RangedContinuousEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RangedDiscreteEffect\n    parents = [api_objects[\"engine.ability.type.ApplyDiscreteEffect\"]]\n    nyan_object = NyanObject(\"RangedDiscreteEffect\", parents)\n    fqon = \"engine.ability.type.RangedDiscreteEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RegenerateAttribute\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"RegenerateAttribute\", parents)\n    fqon = \"engine.ability.type.RegenerateAttribute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RegenerateResourceSpot\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"RegenerateResourceSpot\", parents)\n    fqon = \"engine.ability.type.RegenerateResourceSpot\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.RemoveStorage\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"RemoveStorage\", parents)\n    fqon = \"engine.ability.type.RemoveStorage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Research\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Research\", parents)\n    fqon = \"engine.ability.type.Research\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Resistance\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Resistance\", parents)\n    fqon = \"engine.ability.type.Resistance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ResourceStorage\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ResourceStorage\", parents)\n    fqon = \"engine.ability.type.ResourceStorage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Restock\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Restock\", parents)\n    fqon = \"engine.ability.type.Restock\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Selectable\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Selectable\", parents)\n    fqon = \"engine.ability.type.Selectable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.SendBackToTask\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"SendBackToTask\", parents)\n    fqon = \"engine.ability.type.SendBackToTask\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.ShootProjectile\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"ShootProjectile\", parents)\n    fqon = \"engine.ability.type.ShootProjectile\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Stop\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Stop\", parents)\n    fqon = \"engine.ability.type.Stop\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Storage\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Storage\", parents)\n    fqon = \"engine.ability.type.Storage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.TerrainRequirement\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"TerrainRequirement\", parents)\n    fqon = \"engine.ability.type.TerrainRequirement\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Trade\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Trade\", parents)\n    fqon = \"engine.ability.type.Trade\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.TradePost\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"TradePost\", parents)\n    fqon = \"engine.ability.type.TradePost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.TransferStorage\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"TransferStorage\", parents)\n    fqon = \"engine.ability.type.TransferStorage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Turn\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Turn\", parents)\n    fqon = \"engine.ability.type.Turn\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.UseContingent\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"UseContingent\", parents)\n    fqon = \"engine.ability.type.UseContingent\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.ability.type.Visibility\n    parents = [api_objects[\"engine.ability.Ability\"]]\n    nyan_object = NyanObject(\"Visibility\", parents)\n    fqon = \"engine.ability.type.Visibility\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util\n    # engine.util.accuracy.Accuracy\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Accuracy\", parents)\n    fqon = \"engine.util.accuracy.Accuracy\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.Activity\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Activity\", parents)\n    fqon = \"engine.util.activity.Activity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.condition.Condition\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Condition\", parents)\n    fqon = \"engine.util.activity.condition.Condition\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.condition.type.CommandInQueue\n    parents = [api_objects[\"engine.util.activity.condition.Condition\"]]\n    nyan_object = NyanObject(\"CommandInQueue\", parents)\n    fqon = \"engine.util.activity.condition.type.CommandInQueue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.condition.type.NextCommandIdle\n    parents = [api_objects[\"engine.util.activity.condition.Condition\"]]\n    nyan_object = NyanObject(\"NextCommandIdle\", parents)\n    fqon = \"engine.util.activity.condition.type.NextCommandIdle\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.condition.type.NextCommandMove\n    parents = [api_objects[\"engine.util.activity.condition.Condition\"]]\n    nyan_object = NyanObject(\"NextCommandMove\", parents)\n    fqon = \"engine.util.activity.condition.type.NextCommandMove\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.event.Event\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Event\", parents)\n    fqon = \"engine.util.activity.event.Event\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.event.type.CommandInQueue\n    parents = [api_objects[\"engine.util.activity.event.Event\"]]\n    nyan_object = NyanObject(\"CommandInQueue\", parents)\n    fqon = \"engine.util.activity.event.type.CommandInQueue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.event.type.Wait\n    parents = [api_objects[\"engine.util.activity.event.Event\"]]\n    nyan_object = NyanObject(\"Wait\", parents)\n    fqon = \"engine.util.activity.event.type.Wait\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.event.type.WaitAbility\n    parents = [api_objects[\"engine.util.activity.event.Event\"]]\n    nyan_object = NyanObject(\"WaitAbility\", parents)\n    fqon = \"engine.util.activity.event.type.WaitAbility\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.Node\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Node\", parents)\n    fqon = \"engine.util.activity.node.Node\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.type.Ability\n    parents = [api_objects[\"engine.util.activity.node.Node\"]]\n    nyan_object = NyanObject(\"Ability\", parents)\n    fqon = \"engine.util.activity.node.type.Ability\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.type.End\n    parents = [api_objects[\"engine.util.activity.node.Node\"]]\n    nyan_object = NyanObject(\"End\", parents)\n    fqon = \"engine.util.activity.node.type.End\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.type.Start\n    parents = [api_objects[\"engine.util.activity.node.Node\"]]\n    nyan_object = NyanObject(\"Start\", parents)\n    fqon = \"engine.util.activity.node.type.Start\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.type.XOREventGate\n    parents = [api_objects[\"engine.util.activity.node.Node\"]]\n    nyan_object = NyanObject(\"XOREventGate\", parents)\n    fqon = \"engine.util.activity.node.type.XOREventGate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.activity.node.type.XORGate\n    parents = [api_objects[\"engine.util.activity.node.Node\"]]\n    nyan_object = NyanObject(\"XORGate\", parents)\n    fqon = \"engine.util.activity.node.type.XORGate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.animation_override.AnimationOverride\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AnimationOverride\", parents)\n    fqon = \"engine.util.animation_override.AnimationOverride\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.animation_override.type.Reset\n    parents = [api_objects[\"engine.util.animation_override.AnimationOverride\"]]\n    nyan_object = NyanObject(\"Reset\", parents)\n    fqon = \"engine.util.animation_override.type.Reset\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute.Attribute\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Attribute\", parents)\n    fqon = \"engine.util.attribute.Attribute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute.AttributeAmount\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AttributeAmount\", parents)\n    fqon = \"engine.util.attribute.AttributeAmount\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute.AttributeRate\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AttributeRate\", parents)\n    fqon = \"engine.util.attribute.AttributeRate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute.AttributeSetting\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AttributeSetting\", parents)\n    fqon = \"engine.util.attribute.AttributeSetting\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute.ProtectingAttribute\n    parents = [api_objects[\"engine.util.attribute.Attribute\"]]\n    nyan_object = NyanObject(\"ProtectingAttribute\", parents)\n    fqon = \"engine.util.attribute.ProtectingAttribute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute_change_type.AttributeChangeType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"AttributeChangeType\", parents)\n    fqon = \"engine.util.attribute_change_type.AttributeChangeType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.attribute_change_type.type.Fallback\n    parents = [api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"]]\n    nyan_object = NyanObject(\"Fallback\", parents)\n    fqon = \"engine.util.attribute_change_type.type.Fallback\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.calculation_type.CalculationType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"CalculationType\", parents)\n    fqon = \"engine.util.calculation_type.CalculationType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.calculation_type.type.Hyperbolic\n    parents = [api_objects[\"engine.util.calculation_type.CalculationType\"]]\n    nyan_object = NyanObject(\"Hyperbolic\", parents)\n    fqon = \"engine.util.calculation_type.type.Hyperbolic\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.calculation_type.type.Linear\n    parents = [api_objects[\"engine.util.calculation_type.CalculationType\"]]\n    nyan_object = NyanObject(\"Linear\", parents)\n    fqon = \"engine.util.calculation_type.type.Linear\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.calculation_type.type.NoStack\n    parents = [api_objects[\"engine.util.calculation_type.CalculationType\"]]\n    nyan_object = NyanObject(\"NoStack\", parents)\n    fqon = \"engine.util.calculation_type.type.NoStack\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.cheat.Cheat\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Cheat\", parents)\n    fqon = \"engine.util.cheat.Cheat\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.container_type.SendToContainerType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"SendToContainerType\", parents)\n    fqon = \"engine.util.container_type.SendToContainerType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.convert_type.ConvertType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ConvertType\", parents)\n    fqon = \"engine.util.convert_type.ConvertType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.cost.Cost\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Cost\", parents)\n    fqon = \"engine.util.cost.Cost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.cost.type.AttributeCost\n    parents = [api_objects[\"engine.util.cost.Cost\"]]\n    nyan_object = NyanObject(\"AttributeCost\", parents)\n    fqon = \"engine.util.cost.type.AttributeCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.cost.type.ResourceCost\n    parents = [api_objects[\"engine.util.cost.Cost\"]]\n    nyan_object = NyanObject(\"ResourceCost\", parents)\n    fqon = \"engine.util.cost.type.ResourceCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.create.CreatableGameEntity\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"CreatableGameEntity\", parents)\n    fqon = \"engine.util.create.CreatableGameEntity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.diplomatic_stance.DiplomaticStance\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"DiplomaticStance\", parents)\n    fqon = \"engine.util.diplomatic_stance.DiplomaticStance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.diplomatic_stance.type.Any\n    parents = [api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"]]\n    nyan_object = NyanObject(\"Any\", parents)\n    fqon = \"engine.util.diplomatic_stance.type.Any\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.diplomatic_stance.type.Self\n    parents = [api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"]]\n    nyan_object = NyanObject(\"Self\", parents)\n    fqon = \"engine.util.diplomatic_stance.type.Self\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.distribution_type.DistributionType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"DistributionType\", parents)\n    fqon = \"engine.util.distribution_type.DistributionType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.distribution_type.type.Mean\n    parents = [api_objects[\"engine.util.distribution_type.DistributionType\"]]\n    nyan_object = NyanObject(\"Mean\", parents)\n    fqon = \"engine.util.distribution_type.type.Mean\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.dropoff_type.DropoffType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"DropoffType\", parents)\n    fqon = \"engine.util.dropoff_type.DropoffType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.dropoff_type.type.InverseLinear\n    parents = [api_objects[\"engine.util.dropoff_type.DropoffType\"]]\n    nyan_object = NyanObject(\"InverseLinear\", parents)\n    fqon = \"engine.util.dropoff_type.type.InverseLinear\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.dropoff_type.type.Linear\n    parents = [api_objects[\"engine.util.dropoff_type.DropoffType\"]]\n    nyan_object = NyanObject(\"Linear\", parents)\n    fqon = \"engine.util.dropoff_type.type.Linear\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.dropoff_type.type.NoDropoff\n    parents = [api_objects[\"engine.util.dropoff_type.DropoffType\"]]\n    nyan_object = NyanObject(\"NoDropoff\", parents)\n    fqon = \"engine.util.dropoff_type.type.NoDropoff\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.EffectBatch\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"EffectBatch\", parents)\n    fqon = \"engine.util.effect_batch.EffectBatch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.property.BatchProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"BatchProperty\", parents)\n    fqon = \"engine.util.effect_batch.property.BatchProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.property.type.Chance\n    parents = [api_objects[\"engine.util.effect_batch.property.BatchProperty\"]]\n    nyan_object = NyanObject(\"Chance\", parents)\n    fqon = \"engine.util.effect_batch.property.type.Chance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.property.type.Priority\n    parents = [api_objects[\"engine.util.effect_batch.property.BatchProperty\"]]\n    nyan_object = NyanObject(\"Priority\", parents)\n    fqon = \"engine.util.effect_batch.property.type.Priority\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.type.ChainedBatch\n    parents = [api_objects[\"engine.util.effect_batch.EffectBatch\"]]\n    nyan_object = NyanObject(\"ChainedBatch\", parents)\n    fqon = \"engine.util.effect_batch.type.ChainedBatch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.type.OrderedBatch\n    parents = [api_objects[\"engine.util.effect_batch.EffectBatch\"]]\n    nyan_object = NyanObject(\"OrderedBatch\", parents)\n    fqon = \"engine.util.effect_batch.type.OrderedBatch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.effect_batch.type.UnorderedBatch\n    parents = [api_objects[\"engine.util.effect_batch.EffectBatch\"]]\n    nyan_object = NyanObject(\"UnorderedBatch\", parents)\n    fqon = \"engine.util.effect_batch.type.UnorderedBatch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.exchange_mode.ExchangeMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ExchangeMode\", parents)\n    fqon = \"engine.util.exchange_mode.ExchangeMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.exchange_mode.type.Buy\n    parents = [api_objects[\"engine.util.exchange_mode.ExchangeMode\"]]\n    nyan_object = NyanObject(\"Buy\", parents)\n    fqon = \"engine.util.exchange_mode.type.Buy\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.exchange_mode.type.Sell\n    parents = [api_objects[\"engine.util.exchange_mode.ExchangeMode\"]]\n    nyan_object = NyanObject(\"Sell\", parents)\n    fqon = \"engine.util.exchange_mode.type.Sell\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.exchange_rate.ExchangeRate\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ExchangeRate\", parents)\n    fqon = \"engine.util.exchange_rate.ExchangeRate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.formation.Formation\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Formation\", parents)\n    fqon = \"engine.util.formation.Formation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.formation.Subformation\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Subformation\", parents)\n    fqon = \"engine.util.formation.Subformation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity.GameEntity\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"GameEntity\", parents)\n    fqon = \"engine.util.game_entity.GameEntity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_formation.GameEntityFormation\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"GameEntityFormation\", parents)\n    fqon = \"engine.util.game_entity_formation.GameEntityFormation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_stance.GameEntityStance\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"GameEntityStance\", parents)\n    fqon = \"engine.util.game_entity_stance.GameEntityStance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_stance.type.Aggressive\n    parents = [api_objects[\"engine.util.game_entity_stance.GameEntityStance\"]]\n    nyan_object = NyanObject(\"Aggressive\", parents)\n    fqon = \"engine.util.game_entity_stance.type.Aggressive\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_stance.type.Defensive\n    parents = [api_objects[\"engine.util.game_entity_stance.GameEntityStance\"]]\n    nyan_object = NyanObject(\"Defensive\", parents)\n    fqon = \"engine.util.game_entity_stance.type.Defensive\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_stance.type.Passive\n    parents = [api_objects[\"engine.util.game_entity_stance.GameEntityStance\"]]\n    nyan_object = NyanObject(\"Passive\", parents)\n    fqon = \"engine.util.game_entity_stance.type.Passive\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_stance.type.StandGround\n    parents = [api_objects[\"engine.util.game_entity_stance.GameEntityStance\"]]\n    nyan_object = NyanObject(\"StandGround\", parents)\n    fqon = \"engine.util.game_entity_stance.type.StandGround\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_type.GameEntityType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"GameEntityType\", parents)\n    fqon = \"engine.util.game_entity_type.GameEntityType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.game_entity_type.type.Any\n    parents = [api_objects[\"engine.util.game_entity_type.GameEntityType\"]]\n    nyan_object = NyanObject(\"Any\", parents)\n    fqon = \"engine.util.game_entity_type.type.Any\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.graphics.Animation\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Animation\", parents)\n    fqon = \"engine.util.graphics.Animation\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.graphics.Palette\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Palette\", parents)\n    fqon = \"engine.util.graphics.Palette\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.graphics.Terrain\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Terrain\", parents)\n    fqon = \"engine.util.graphics.Terrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.herdable_mode.HerdableMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"HerdableMode\", parents)\n    fqon = \"engine.util.herdable_mode.HerdableMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.herdable_mode.type.ClosestHerding\n    parents = [api_objects[\"engine.util.herdable_mode.HerdableMode\"]]\n    nyan_object = NyanObject(\"ClosestHerding\", parents)\n    fqon = \"engine.util.herdable_mode.type.ClosestHerding\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.herdable_mode.type.LongestTimeInRange\n    parents = [api_objects[\"engine.util.herdable_mode.HerdableMode\"]]\n    nyan_object = NyanObject(\"LongestTimeInRange\", parents)\n    fqon = \"engine.util.herdable_mode.type.LongestTimeInRange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.herdable_mode.type.MostHerding\n    parents = [api_objects[\"engine.util.herdable_mode.HerdableMode\"]]\n    nyan_object = NyanObject(\"MostHerding\", parents)\n    fqon = \"engine.util.herdable_mode.type.MostHerding\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.hitbox.Hitbox\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Hitbox\", parents)\n    fqon = \"engine.util.hitbox.Hitbox\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.Language\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Language\", parents)\n    fqon = \"engine.util.language.Language\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.LanguageMarkupPair\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LanguageMarkupPair\", parents)\n    fqon = \"engine.util.language.LanguageMarkupPair\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.LanguageSoundPair\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LanguageSoundPair\", parents)\n    fqon = \"engine.util.language.LanguageSoundPair\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.LanguageTextPair\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LanguageTextPair\", parents)\n    fqon = \"engine.util.language.LanguageTextPair\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.translated.TranslatedObject\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TranslatedObject\", parents)\n    fqon = \"engine.util.language.translated.TranslatedObject\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.translated.type.TranslatedMarkupFile\n    parents = [api_objects[\"engine.util.language.translated.TranslatedObject\"]]\n    nyan_object = NyanObject(\"TranslatedMarkupFile\", parents)\n    fqon = \"engine.util.language.translated.type.TranslatedMarkupFile\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.translated.type.TranslatedSound\n    parents = [api_objects[\"engine.util.language.translated.TranslatedObject\"]]\n    nyan_object = NyanObject(\"TranslatedSound\", parents)\n    fqon = \"engine.util.language.translated.type.TranslatedSound\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.language.translated.type.TranslatedString\n    parents = [api_objects[\"engine.util.language.translated.TranslatedObject\"]]\n    nyan_object = NyanObject(\"TranslatedString\", parents)\n    fqon = \"engine.util.language.translated.type.TranslatedString\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.lock.LockPool\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LockPool\", parents)\n    fqon = \"engine.util.lock.LockPool\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.LogicElement\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LogicElement\", parents)\n    fqon = \"engine.util.logic.LogicElement\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.const.False\n    parents = [api_objects[\"engine.util.logic.LogicElement\"]]\n    nyan_object = NyanObject(\"False\", parents)\n    fqon = \"engine.util.logic.const.False\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.const.True\n    parents = [api_objects[\"engine.util.logic.LogicElement\"]]\n    nyan_object = NyanObject(\"True\", parents)\n    fqon = \"engine.util.logic.const.True\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.LogicGate\n    parents = [api_objects[\"engine.util.logic.LogicElement\"]]\n    nyan_object = NyanObject(\"LogicGate\", parents)\n    fqon = \"engine.util.logic.gate.LogicGate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.AND\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"AND\", parents)\n    fqon = \"engine.util.logic.gate.type.AND\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.MULTIXOR\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"MULTIXOR\", parents)\n    fqon = \"engine.util.logic.gate.type.MULTIXOR\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.NOT\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"NOT\", parents)\n    fqon = \"engine.util.logic.gate.type.NOT\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.OR\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"OR\", parents)\n    fqon = \"engine.util.logic.gate.type.OR\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.SUBSETMAX\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"SUBSETMAX\", parents)\n    fqon = \"engine.util.logic.gate.type.SUBSETMAX\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.SUBSETMIN\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"SUBSETMIN\", parents)\n    fqon = \"engine.util.logic.gate.type.SUBSETMIN\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.gate.type.XOR\n    parents = [api_objects[\"engine.util.logic.gate.LogicGate\"]]\n    nyan_object = NyanObject(\"XOR\", parents)\n    fqon = \"engine.util.logic.gate.type.XOR\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.Literal\n    parents = [api_objects[\"engine.util.logic.LogicElement\"]]\n    nyan_object = NyanObject(\"Literal\", parents)\n    fqon = \"engine.util.logic.literal.Literal\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.AttributeAbovePercentage\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"AttributeAbovePercentage\", parents)\n    fqon = \"engine.util.logic.literal.type.AttributeAbovePercentage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.AttributeAboveValue\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"AttributeAboveValue\", parents)\n    fqon = \"engine.util.logic.literal.type.AttributeAboveValue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.AttributeBelowPercentage\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"AttributeBelowPercentage\", parents)\n    fqon = \"engine.util.logic.literal.type.AttributeBelowPercentage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.AttributeBelowValue\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"AttributeBelowValue\", parents)\n    fqon = \"engine.util.logic.literal.type.AttributeBelowValue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.GameEntityProgress\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"GameEntityProgress\", parents)\n    fqon = \"engine.util.logic.literal.type.GameEntityProgress\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.OwnsGameEntity\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"OwnsGameEntity\", parents)\n    fqon = \"engine.util.logic.literal.type.OwnsGameEntity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.ProjectileHitTerrain\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"ProjectileHitTerrain\", parents)\n    fqon = \"engine.util.logic.literal.type.ProjectileHitTerrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.ProjectilePassThrough\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"ProjectilePassThrough\", parents)\n    fqon = \"engine.util.logic.literal.type.ProjectilePassThrough\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.ResourceSpotsDepleted\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"ResourceSpotsDepleted\", parents)\n    fqon = \"engine.util.logic.literal.type.ResourceSpotsDepleted\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.StateChangeActive\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"StateChangeActive\", parents)\n    fqon = \"engine.util.logic.literal.type.StateChangeActive\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.TechResearched\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"TechResearched\", parents)\n    fqon = \"engine.util.logic.literal.type.TechResearched\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal.type.Timer\n    parents = [api_objects[\"engine.util.logic.literal.Literal\"]]\n    nyan_object = NyanObject(\"Timer\", parents)\n    fqon = \"engine.util.logic.literal.type.Timer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal_scope.LiteralScope\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LiteralScope\", parents)\n    fqon = \"engine.util.logic.literal_scope.LiteralScope\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal_scope.type.Any\n    parents = [api_objects[\"engine.util.logic.literal_scope.LiteralScope\"]]\n    nyan_object = NyanObject(\"Any\", parents)\n    fqon = \"engine.util.logic.literal_scope.type.Any\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.logic.literal_scope.type.Self\n    parents = [api_objects[\"engine.util.logic.literal_scope.LiteralScope\"]]\n    nyan_object = NyanObject(\"Self\", parents)\n    fqon = \"engine.util.logic.literal_scope.type.Self\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.lure_type.LureType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"LureType\", parents)\n    fqon = \"engine.util.lure_type.LureType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.mod.Mod\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Mod\", parents)\n    fqon = \"engine.util.mod.Mod\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.modifier_scope.ModifierScope\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ModifierScope\", parents)\n    fqon = \"engine.util.modifier_scope.ModifierScope\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.modifier_scope.type.GameEntityScope\n    parents = [api_objects[\"engine.util.modifier_scope.ModifierScope\"]]\n    nyan_object = NyanObject(\"GameEntityScope\", parents)\n    fqon = \"engine.util.modifier_scope.type.GameEntityScope\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.modifier_scope.type.Standard\n    parents = [api_objects[\"engine.util.modifier_scope.ModifierScope\"]]\n    nyan_object = NyanObject(\"Standard\", parents)\n    fqon = \"engine.util.modifier_scope.type.Standard\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.MoveMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"MoveMode\", parents)\n    fqon = \"engine.util.move_mode.MoveMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.type.AttackMove\n    parents = [api_objects[\"engine.util.move_mode.MoveMode\"]]\n    nyan_object = NyanObject(\"AttackMove\", parents)\n    fqon = \"engine.util.move_mode.type.AttackMove\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.type.Follow\n    parents = [api_objects[\"engine.util.move_mode.MoveMode\"]]\n    nyan_object = NyanObject(\"Follow\", parents)\n    fqon = \"engine.util.move_mode.type.Follow\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.type.Guard\n    parents = [api_objects[\"engine.util.move_mode.MoveMode\"]]\n    nyan_object = NyanObject(\"Guard\", parents)\n    fqon = \"engine.util.move_mode.type.Guard\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.type.Normal\n    parents = [api_objects[\"engine.util.move_mode.MoveMode\"]]\n    nyan_object = NyanObject(\"Normal\", parents)\n    fqon = \"engine.util.move_mode.type.Normal\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.move_mode.type.Patrol\n    parents = [api_objects[\"engine.util.move_mode.MoveMode\"]]\n    nyan_object = NyanObject(\"Patrol\", parents)\n    fqon = \"engine.util.move_mode.type.Patrol\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.patch.NyanPatch\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"NyanPatch\", parents)\n    fqon = \"engine.util.patch.NyanPatch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.patch.Patch\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Patch\", parents)\n    fqon = \"engine.util.patch.Patch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.patch.property.PatchProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PatchProperty\", parents)\n    fqon = \"engine.util.patch.property.PatchProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.patch.property.type.Diplomatic\n    parents = [api_objects[\"engine.util.patch.property.PatchProperty\"]]\n    nyan_object = NyanObject(\"Diplomatic\", parents)\n    fqon = \"engine.util.patch.property.type.Diplomatic\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.path_type.PathType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PathType\", parents)\n    fqon = \"engine.util.path_type.PathType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.payment_mode.PaymentMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PaymentMode\", parents)\n    fqon = \"engine.util.payment_mode.PaymentMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.payment_mode.type.Adaptive\n    parents = [api_objects[\"engine.util.payment_mode.PaymentMode\"]]\n    nyan_object = NyanObject(\"Adaptive\", parents)\n    fqon = \"engine.util.payment_mode.type.Adaptive\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.payment_mode.type.Advance\n    parents = [api_objects[\"engine.util.payment_mode.PaymentMode\"]]\n    nyan_object = NyanObject(\"Advance\", parents)\n    fqon = \"engine.util.payment_mode.type.Advance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.payment_mode.type.Arrear\n    parents = [api_objects[\"engine.util.payment_mode.PaymentMode\"]]\n    nyan_object = NyanObject(\"Arrear\", parents)\n    fqon = \"engine.util.payment_mode.type.Arrear\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.payment_mode.type.Shadow\n    parents = [api_objects[\"engine.util.payment_mode.PaymentMode\"]]\n    nyan_object = NyanObject(\"Shadow\", parents)\n    fqon = \"engine.util.payment_mode.type.Shadow\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.placement_mode.PlacementMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PlacementMode\", parents)\n    fqon = \"engine.util.placement_mode.PlacementMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.placement_mode.type.Eject\n    parents = [api_objects[\"engine.util.placement_mode.PlacementMode\"]]\n    nyan_object = NyanObject(\"Eject\", parents)\n    fqon = \"engine.util.placement_mode.type.Eject\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.placement_mode.type.OwnStorage\n    parents = [api_objects[\"engine.util.placement_mode.PlacementMode\"]]\n    nyan_object = NyanObject(\"OwnStorage\", parents)\n    fqon = \"engine.util.placement_mode.type.OwnStorage\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.placement_mode.type.Place\n    parents = [api_objects[\"engine.util.placement_mode.PlacementMode\"]]\n    nyan_object = NyanObject(\"Place\", parents)\n    fqon = \"engine.util.placement_mode.type.Place\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.placement_mode.type.Replace\n    parents = [api_objects[\"engine.util.placement_mode.PlacementMode\"]]\n    nyan_object = NyanObject(\"Replace\", parents)\n    fqon = \"engine.util.placement_mode.type.Replace\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.price_mode.PriceMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PriceMode\", parents)\n    fqon = \"engine.util.price_mode.PriceMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.price_mode.type.Dynamic\n    parents = [api_objects[\"engine.util.price_mode.PriceMode\"]]\n    nyan_object = NyanObject(\"Dynamic\", parents)\n    fqon = \"engine.util.price_mode.type.Dynamic\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.price_mode.type.Fixed\n    parents = [api_objects[\"engine.util.price_mode.PriceMode\"]]\n    nyan_object = NyanObject(\"Fixed\", parents)\n    fqon = \"engine.util.price_mode.type.Fixed\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.price_pool.PricePool\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PricePool\", parents)\n    fqon = \"engine.util.price_pool.PricePool\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.production_mode.ProductionMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ProductionMode\", parents)\n    fqon = \"engine.util.production_mode.ProductionMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.production_mode.type.Creatables\n    parents = [api_objects[\"engine.util.production_mode.ProductionMode\"]]\n    nyan_object = NyanObject(\"Creatables\", parents)\n    fqon = \"engine.util.production_mode.type.Creatables\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.production_mode.type.Researchables\n    parents = [api_objects[\"engine.util.production_mode.ProductionMode\"]]\n    nyan_object = NyanObject(\"Researchables\", parents)\n    fqon = \"engine.util.production_mode.type.Researchables\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.Progress\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Progress\", parents)\n    fqon = \"engine.util.progress.Progress\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.ProgressProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ProgressProperty\", parents)\n    fqon = \"engine.util.progress.property.ProgressProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.type.Animated\n    parents = [api_objects[\"engine.util.progress.property.ProgressProperty\"]]\n    nyan_object = NyanObject(\"Animated\", parents)\n    fqon = \"engine.util.progress.property.type.Animated\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.type.AnimationOverlay\n    parents = [api_objects[\"engine.util.progress.property.ProgressProperty\"]]\n    nyan_object = NyanObject(\"AnimationOverlay\", parents)\n    fqon = \"engine.util.progress.property.type.AnimationOverlay\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.type.StateChange\n    parents = [api_objects[\"engine.util.progress.property.ProgressProperty\"]]\n    nyan_object = NyanObject(\"StateChange\", parents)\n    fqon = \"engine.util.progress.property.type.StateChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.type.TerrainOverlay\n    parents = [api_objects[\"engine.util.progress.property.ProgressProperty\"]]\n    nyan_object = NyanObject(\"TerrainOverlay\", parents)\n    fqon = \"engine.util.progress.property.type.TerrainOverlay\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress.property.type.Terrain\n    parents = [api_objects[\"engine.util.progress.property.ProgressProperty\"]]\n    nyan_object = NyanObject(\"Terrain\", parents)\n    fqon = \"engine.util.progress.property.type.Terrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_status.ProgressStatus\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ProgressStatus\", parents)\n    fqon = \"engine.util.progress_status.ProgressStatus\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.ProgressType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ProgressType\", parents)\n    fqon = \"engine.util.progress_type.ProgressType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.AttributeChange\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"AttributeChange\", parents)\n    fqon = \"engine.util.progress_type.type.AttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.Carry\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"Carry\", parents)\n    fqon = \"engine.util.progress_type.type.Carry\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.Construct\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"Construct\", parents)\n    fqon = \"engine.util.progress_type.type.Construct\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.Harvest\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"Harvest\", parents)\n    fqon = \"engine.util.progress_type.type.Harvest\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.Restock\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"Restock\", parents)\n    fqon = \"engine.util.progress_type.type.Restock\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.progress_type.type.Transform\n    parents = [api_objects[\"engine.util.progress_type.ProgressType\"]]\n    nyan_object = NyanObject(\"Transform\", parents)\n    fqon = \"engine.util.progress_type.type.Transform\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.research.ResearchableTech\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResearchableTech\", parents)\n    fqon = \"engine.util.research.ResearchableTech\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.resource.Resource\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Resource\", parents)\n    fqon = \"engine.util.resource.Resource\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.resource.ResourceContingent\n    parents = [api_objects[\"engine.util.resource.Resource\"]]\n    nyan_object = NyanObject(\"ResourceContingent\", parents)\n    fqon = \"engine.util.resource.ResourceContingent\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.resource.ResourceAmount\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResourceAmount\", parents)\n    fqon = \"engine.util.resource.ResourceAmount\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.resource.ResourceRate\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResourceRate\", parents)\n    fqon = \"engine.util.resource.ResourceRate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.resource_spot.ResourceSpot\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResourceSpot\", parents)\n    fqon = \"engine.util.resource_spot.ResourceSpot\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.selection_box.SelectionBox\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"SelectionBox\", parents)\n    fqon = \"engine.util.selection_box.SelectionBox\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.selection_box.type.MatchToSprite\n    parents = [api_objects[\"engine.util.selection_box.SelectionBox\"]]\n    nyan_object = NyanObject(\"MatchToSprite\", parents)\n    fqon = \"engine.util.selection_box.type.MatchToSprite\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.selection_box.type.Rectangle\n    parents = [api_objects[\"engine.util.selection_box.SelectionBox\"]]\n    nyan_object = NyanObject(\"Rectangle\", parents)\n    fqon = \"engine.util.selection_box.type.Rectangle\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.setup.PlayerSetup\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"PlayerSetup\", parents)\n    fqon = \"engine.util.setup.PlayerSetup\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.sound.Sound\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Sound\", parents)\n    fqon = \"engine.util.sound.Sound\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.state_machine.StateChanger\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"StateChanger\", parents)\n    fqon = \"engine.util.state_machine.StateChanger\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.state_machine.Reset\n    parents = [api_objects[\"engine.util.state_machine.StateChanger\"]]\n    nyan_object = NyanObject(\"Reset\", parents)\n    fqon = \"engine.util.state_machine.Reset\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.storage.EntityContainer\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"EntityContainer\", parents)\n    fqon = \"engine.util.storage.EntityContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.storage.ResourceContainer\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResourceContainer\", parents)\n    fqon = \"engine.util.storage.ResourceContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.storage.resource_container.type.InternalDropSite\n    parents = [api_objects[\"engine.util.storage.ResourceContainer\"]]\n    nyan_object = NyanObject(\"InternalDropSite\", parents)\n    fqon = \"engine.util.storage.resource_container.type.InternalDropSite\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.storage.StorageElementDefinition\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"StorageElementDefinition\", parents)\n    fqon = \"engine.util.storage.StorageElementDefinition\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.target_mode.TargetMode\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TargetMode\", parents)\n    fqon = \"engine.util.target_mode.TargetMode\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.target_mode.type.CurrentPosition\n    parents = [api_objects[\"engine.util.target_mode.TargetMode\"]]\n    nyan_object = NyanObject(\"CurrentPosition\", parents)\n    fqon = \"engine.util.target_mode.type.CurrentPosition\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.target_mode.type.ExpectedPosition\n    parents = [api_objects[\"engine.util.target_mode.TargetMode\"]]\n    nyan_object = NyanObject(\"ExpectedPosition\", parents)\n    fqon = \"engine.util.target_mode.type.ExpectedPosition\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.taunt.Taunt\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Taunt\", parents)\n    fqon = \"engine.util.taunt.Taunt\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.tech.Tech\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Tech\", parents)\n    fqon = \"engine.util.tech.Tech\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.tech_type.TechType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TechType\", parents)\n    fqon = \"engine.util.tech_type.TechType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.tech_type.type.Any\n    parents = [api_objects[\"engine.util.tech_type.TechType\"]]\n    nyan_object = NyanObject(\"Any\", parents)\n    fqon = \"engine.util.tech_type.type.Any\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.terrain.Terrain\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Terrain\", parents)\n    fqon = \"engine.util.terrain.Terrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.terrain.TerrainAmbient\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TerrainAmbient\", parents)\n    fqon = \"engine.util.terrain.TerrainAmbient\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.terrain_type.TerrainType\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TerrainType\", parents)\n    fqon = \"engine.util.terrain_type.TerrainType\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.terrain_type.type.Any\n    parents = [api_objects[\"engine.util.terrain_type.TerrainType\"]]\n    nyan_object = NyanObject(\"Any\", parents)\n    fqon = \"engine.util.terrain_type.type.Any\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.trade_route.TradeRoute\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TradeRoute\", parents)\n    fqon = \"engine.util.trade_route.TradeRoute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.trade_route.type.AoE1TradeRoute\n    parents = [api_objects[\"engine.util.trade_route.TradeRoute\"]]\n    nyan_object = NyanObject(\"AoE1TradeRoute\", parents)\n    fqon = \"engine.util.trade_route.type.AoE1TradeRoute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.trade_route.type.AoE2TradeRoute\n    parents = [api_objects[\"engine.util.trade_route.TradeRoute\"]]\n    nyan_object = NyanObject(\"AoE2TradeRoute\", parents)\n    fqon = \"engine.util.trade_route.type.AoE2TradeRoute\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.transform_pool.TransformPool\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"TransformPool\", parents)\n    fqon = \"engine.util.transform_pool.TransformPool\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.variant.Variant\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Variant\", parents)\n    fqon = \"engine.util.variant.Variant\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.variant.type.AdjacentTilesVariant\n    parents = [api_objects[\"engine.util.variant.Variant\"]]\n    nyan_object = NyanObject(\"AdjacentTilesVariant\", parents)\n    fqon = \"engine.util.variant.type.AdjacentTilesVariant\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.variant.type.MiscVariant\n    parents = [api_objects[\"engine.util.variant.Variant\"]]\n    nyan_object = NyanObject(\"MiscVariant\", parents)\n    fqon = \"engine.util.variant.type.MiscVariant\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.variant.type.RandomVariant\n    parents = [api_objects[\"engine.util.variant.Variant\"]]\n    nyan_object = NyanObject(\"RandomVariant\", parents)\n    fqon = \"engine.util.variant.type.RandomVariant\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.util.variant.type.PerspectiveVariant\n    parents = [api_objects[\"engine.util.variant.Variant\"]]\n    nyan_object = NyanObject(\"PerspectiveVariant\", parents)\n    fqon = \"engine.util.variant.type.PerspectiveVariant\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect\n    # engine.effect.Effect\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Effect\", parents)\n    fqon = \"engine.effect.Effect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.property.EffectProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"EffectProperty\", parents)\n    fqon = \"engine.effect.property.EffectProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.property.type.Area\n    parents = [api_objects[\"engine.effect.property.EffectProperty\"]]\n    nyan_object = NyanObject(\"Area\", parents)\n    fqon = \"engine.effect.property.type.Area\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.property.type.Cost\n    parents = [api_objects[\"engine.effect.property.EffectProperty\"]]\n    nyan_object = NyanObject(\"Cost\", parents)\n    fqon = \"engine.effect.property.type.Cost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.property.type.Diplomatic\n    parents = [api_objects[\"engine.effect.property.EffectProperty\"]]\n    nyan_object = NyanObject(\"Diplomatic\", parents)\n    fqon = \"engine.effect.property.type.Diplomatic\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.property.type.Priority\n    parents = [api_objects[\"engine.effect.property.EffectProperty\"]]\n    nyan_object = NyanObject(\"Priority\", parents)\n    fqon = \"engine.effect.property.type.Priority\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.ContinuousEffect\n    parents = [api_objects[\"engine.effect.Effect\"]]\n    nyan_object = NyanObject(\"ContinuousEffect\", parents)\n    fqon = \"engine.effect.continuous.ContinuousEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.flat_attribute_change.FlatAttributeChange\n    parents = [api_objects[\"engine.effect.continuous.ContinuousEffect\"]]\n    nyan_object = NyanObject(\"FlatAttributeChange\", parents)\n    fqon = \"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\n    parents = [api_objects[\"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeDecrease\", parents)\n    fqon = \"engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\n    parents = [api_objects[\"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeIncrease\", parents)\n    fqon = \"engine.effect.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.type.Lure\n    parents = [api_objects[\"engine.effect.continuous.ContinuousEffect\"]]\n    nyan_object = NyanObject(\"Lure\", parents)\n    fqon = \"engine.effect.continuous.type.Lure\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\n    parents = [api_objects[\"engine.effect.continuous.ContinuousEffect\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeChange\", parents)\n    fqon = \"engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_attribute.type.TimeRelativeAttributeDecrease\n    parents = [api_objects[\"engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeDecrease\", parents)\n    fqon = \"engine.effect.continuous.time_relative_attribute.type.TimeRelativeAttributeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\n    parents = [api_objects[\"engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeIncrease\", parents)\n    fqon = \"engine.effect.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\n    parents = [api_objects[\"engine.effect.continuous.ContinuousEffect\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressChange\", parents)\n    fqon = \"engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\n    parents = [api_objects[\"engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressDecrease\", parents)\n    fqon = \"engine.effect.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\n    parents = [api_objects[\"engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressIncrease\", parents)\n    fqon = \"engine.effect.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.DiscreteEffect\n    parents = [api_objects[\"engine.effect.Effect\"]]\n    nyan_object = NyanObject(\"DiscreteEffect\", parents)\n    fqon = \"engine.effect.discrete.DiscreteEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.convert.Convert\n    parents = [api_objects[\"engine.effect.discrete.DiscreteEffect\"]]\n    nyan_object = NyanObject(\"Convert\", parents)\n    fqon = \"engine.effect.discrete.convert.Convert\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.convert.type.AoE2Convert\n    parents = [api_objects[\"engine.effect.discrete.convert.Convert\"]]\n    nyan_object = NyanObject(\"AoE2Convert\", parents)\n    fqon = \"engine.effect.discrete.convert.type.AoE2Convert\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.flat_attribute_change.FlatAttributeChange\n    parents = [api_objects[\"engine.effect.discrete.DiscreteEffect\"]]\n    nyan_object = NyanObject(\"FlatAttributeChange\", parents)\n    fqon = \"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\n    parents = [api_objects[\"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeDecrease\", parents)\n    fqon = \"engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\n    parents = [api_objects[\"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeIncrease\", parents)\n    fqon = \"engine.effect.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.type.MakeHarvestable\n    parents = [api_objects[\"engine.effect.discrete.DiscreteEffect\"]]\n    nyan_object = NyanObject(\"MakeHarvestable\", parents)\n    fqon = \"engine.effect.discrete.type.MakeHarvestable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.effect.discrete.type.SendToContainer\n    parents = [api_objects[\"engine.effect.discrete.DiscreteEffect\"]]\n    nyan_object = NyanObject(\"SendToContainer\", parents)\n    fqon = \"engine.effect.discrete.type.SendToContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance\n    # engine.resistance.Resistance\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Resistance\", parents)\n    fqon = \"engine.resistance.Resistance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.property.ResistanceProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ResistanceProperty\", parents)\n    fqon = \"engine.resistance.property.ResistanceProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.property.type.Cost\n    parents = [api_objects[\"engine.resistance.property.ResistanceProperty\"]]\n    nyan_object = NyanObject(\"Cost\", parents)\n    fqon = \"engine.resistance.property.type.Cost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.property.type.Stacked\n    parents = [api_objects[\"engine.resistance.property.ResistanceProperty\"]]\n    nyan_object = NyanObject(\"Stacked\", parents)\n    fqon = \"engine.resistance.property.type.Stacked\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.ContinuousResistance\n    parents = [api_objects[\"engine.resistance.Resistance\"]]\n    nyan_object = NyanObject(\"Resistance\", parents)\n    fqon = \"engine.resistance.continuous.ContinuousResistance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\n    parents = [api_objects[\"engine.resistance.continuous.ContinuousResistance\"]]\n    nyan_object = NyanObject(\"FlatAttributeChange\", parents)\n    fqon = \"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\n    parents = [api_objects[\"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeDecrease\", parents)\n    fqon = \"engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\n    parents = [api_objects[\"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeIncrease\", parents)\n    fqon = \"engine.resistance.continuous.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.type.Lure\n    parents = [api_objects[\"engine.resistance.continuous.ContinuousResistance\"]]\n    nyan_object = NyanObject(\"Lure\", parents)\n    fqon = \"engine.resistance.continuous.type.Lure\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\n    parents = [api_objects[\"engine.resistance.continuous.ContinuousResistance\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeChange\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_attribute.type.TimeRelativeAttributeDecrease\n    parents = [api_objects[\"engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeDecrease\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_attribute.type.TimeRelativeAttributeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\n    parents = [api_objects[\"engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeIncrease\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_attribute.type.TimeRelativeAttributeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\n    parents = [api_objects[\"engine.resistance.continuous.ContinuousResistance\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressChange\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\n    parents = [api_objects[\"engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressDecrease\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_progress.type.TimeRelativeProgressDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\n    parents = [api_objects[\"engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgressIncrease\", parents)\n    fqon = \"engine.resistance.continuous.time_relative_progress.type.TimeRelativeProgressIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.DiscreteResistance\n    parents = [api_objects[\"engine.resistance.Resistance\"]]\n    nyan_object = NyanObject(\"DiscreteResistance\", parents)\n    fqon = \"engine.resistance.discrete.DiscreteResistance\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.convert.Convert\n    parents = [api_objects[\"engine.resistance.discrete.DiscreteResistance\"]]\n    nyan_object = NyanObject(\"Convert\", parents)\n    fqon = \"engine.resistance.discrete.convert.Convert\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.convert.type.AoE2Convert\n    parents = [api_objects[\"engine.resistance.discrete.convert.Convert\"]]\n    nyan_object = NyanObject(\"AoE2Convert\", parents)\n    fqon = \"engine.resistance.discrete.convert.type.AoE2Convert\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\n    parents = [api_objects[\"engine.resistance.discrete.DiscreteResistance\"]]\n    nyan_object = NyanObject(\"FlatAttributeChange\", parents)\n    fqon = \"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\n    parents = [api_objects[\"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeDecrease\", parents)\n    fqon = \"engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeDecrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\n    parents = [api_objects[\"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"]]\n    nyan_object = NyanObject(\"FlatAttributeChangeIncrease\", parents)\n    fqon = \"engine.resistance.discrete.flat_attribute_change.type.FlatAttributeChangeIncrease\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.type.MakeHarvestable\n    parents = [api_objects[\"engine.resistance.discrete.DiscreteResistance\"]]\n    nyan_object = NyanObject(\"MakeHarvestable\", parents)\n    fqon = \"engine.resistance.discrete.type.MakeHarvestable\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.resistance.discrete.type.SendToContainer\n    parents = [api_objects[\"engine.resistance.discrete.DiscreteResistance\"]]\n    nyan_object = NyanObject(\"SendToContainer\", parents)\n    fqon = \"engine.resistance.discrete.type.SendToContainer\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier\n    # engine.modifier.Modifier\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"Modifier\", parents)\n    fqon = \"engine.modifier.Modifier\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.property.ModifierProperty\n    parents = [api_objects[\"engine.root.Object\"]]\n    nyan_object = NyanObject(\"ModifierProperty\", parents)\n    fqon = \"engine.modifier.property.ModifierProperty\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.property.type.Multiplier\n    parents = [api_objects[\"engine.modifier.property.ModifierProperty\"]]\n    nyan_object = NyanObject(\"Multiplier\", parents)\n    fqon = \"engine.modifier.property.type.Multiplier\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.property.type.Scoped\n    parents = [api_objects[\"engine.modifier.property.ModifierProperty\"]]\n    nyan_object = NyanObject(\"Scoped\", parents)\n    fqon = \"engine.modifier.property.type.Scoped\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.property.type.Stacked\n    parents = [api_objects[\"engine.modifier.property.ModifierProperty\"]]\n    nyan_object = NyanObject(\"Stacked\", parents)\n    fqon = \"engine.modifier.property.type.Stacked\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ElevationDifferenceHigh\", parents)\n    fqon = \"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ElevationDifferenceLow\", parents)\n    fqon = \"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.flat_attribute_change.type.Flyover\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Flyover\", parents)\n    fqon = \"engine.modifier.effect.flat_attribute_change.type.Flyover\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.flat_attribute_change.type.Terrain\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Terrain\", parents)\n    fqon = \"engine.modifier.effect.flat_attribute_change.type.Terrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.flat_attribute_change.type.Unconditional\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Unconditional\", parents)\n    fqon = \"engine.modifier.effect.flat_attribute_change.type.Unconditional\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.effect.type.TimeRelativeAttributeChange\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"TimeRelativeAttributeChange\", parents)\n    fqon = \"engine.modifier.effect.type.TimeRelativeAttributeChangeTime\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.multiplier.effect.type.TimeRelativeProgressChange\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"TimeRelativeProgress\", parents)\n    fqon = \"engine.modifier.multiplier.effect.type.TimeRelativeProgressChange\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceHigh\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ElevationDifferenceHigh\", parents)\n    fqon = \"engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceHigh\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceLow\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ElevationDifferenceLow\", parents)\n    fqon = \"engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceLow\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.resistance.flat_attribute_change.type.Stray\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Stray\", parents)\n    fqon = \"engine.modifier.resistance.flat_attribute_change.type.Stray\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.resistance.flat_attribute_change.type.Terrain\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Terrain\", parents)\n    fqon = \"engine.modifier.resistance.flat_attribute_change.type.Terrain\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.resistance.flat_attribute_change.type.Unconditional\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Unconditional\", parents)\n    fqon = \"engine.modifier.resistance.flat_attribute_change.type.Unconditional\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.AbsoluteProjectileAmount\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"AbsoluteProjectileAmount\", parents)\n    fqon = \"engine.modifier.type.AbsoluteProjectileAmount\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.AoE2ProjectileAmount\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"AoE2ProjectileAmount\", parents)\n    fqon = \"engine.modifier.type.AoE2ProjectileAmount\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.AttributeSettingsValue\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"AttributeSettingsValue\", parents)\n    fqon = \"engine.modifier.type.AttributeSettingsValue\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.EntityContainerCapacity\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"EntityContainerCapacity\", parents)\n    fqon = \"engine.modifier.type.EntityContainerCapacity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.ContinuousResource\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ContinuousResource\", parents)\n    fqon = \"engine.modifier.type.ContinuousResource\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.CreationAttributeCost\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"CreationAttributeCost\", parents)\n    fqon = \"engine.modifier.type.CreationAttributeCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.CreationResourceCost\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"CreationResourceCost\", parents)\n    fqon = \"engine.modifier.type.CreationResourceCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.CreationTime\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"CreationTime\", parents)\n    fqon = \"engine.modifier.type.CreationTime\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.DepositResourcesOnProgress\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"DepositResourcesOnProgress\", parents)\n    fqon = \"engine.modifier.type.DepositResourcesOnProgress\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.DiplomaticLineOfSight\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"DiplomaticLineOfSight\", parents)\n    fqon = \"engine.modifier.type.DiplomaticLineOfSight\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.GatheringEfficiency\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"GatheringEfficiency\", parents)\n    fqon = \"engine.modifier.type.GatheringEfficiency\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.GatheringRate\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"GatheringRate\", parents)\n    fqon = \"engine.modifier.type.GatheringRate\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.InContainerContinuousEffect\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"InContainerContinuousEffect\", parents)\n    fqon = \"engine.modifier.type.InContainerContinuousEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.InContainerDiscreteEffect\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"InContainerDiscreteEffect\", parents)\n    fqon = \"engine.modifier.type.InContainerDiscreteEffect\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.InstantTechResearch\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"InstantTechResearch\", parents)\n    fqon = \"engine.modifier.type.InstantTechResearch\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.MoveSpeed\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"MoveSpeed\", parents)\n    fqon = \"engine.modifier.type.MoveSpeed\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.RefundOnCondition\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"RefundOnCondition\", parents)\n    fqon = \"engine.modifier.type.RefundOnCondition\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.ReloadTime\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ReloadTime\", parents)\n    fqon = \"engine.modifier.type.ReloadTime\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.ResearchAttributeCost\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ResearchAttributeCost\", parents)\n    fqon = \"engine.modifier.type.ResearchAttributeCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.ResearchResourceCost\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ResearchResourceCost\", parents)\n    fqon = \"engine.modifier.type.ResearchResourceCost\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.ResearchTime\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"ResearchTime\", parents)\n    fqon = \"engine.modifier.type.ResearchTime\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.Reveal\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"Reveal\", parents)\n    fqon = \"engine.modifier.type.Reveal\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    # engine.modifier.type.StorageElementCapacity\n    parents = [api_objects[\"engine.modifier.Modifier\"]]\n    nyan_object = NyanObject(\"StorageElementCapacity\", parents)\n    fqon = \"engine.modifier.type.StorageElementCapacity\"\n    nyan_object.set_fqon(fqon)\n    api_objects.update({fqon: nyan_object})\n\n    return api_objects\n\n\ndef _insert_members(api_objects: dict[str, NyanObject]) -> None:\n    \"\"\"\n    Creates members for API objects.\n    \"\"\"\n    # engine.ability\n    # engine.ability.Ability\n    api_object = api_objects[\"engine.ability.Ability\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.ability.property.AbilityProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.Animated\n    api_object = api_objects[\"engine.ability.property.type.Animated\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.graphics.Animation\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"animations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.AnimationOverride\n    api_object = api_objects[\"engine.ability.property.type.AnimationOverride\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.animation_override.AnimationOverride\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"overrides\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.CommandSound\n    api_object = api_objects[\"engine.ability.property.type.CommandSound\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"sounds\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.Diplomatic\n    api_object = api_objects[\"engine.ability.property.type.Diplomatic\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.ExecutionSound\n    api_object = api_objects[\"engine.ability.property.type.ExecutionSound\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"sounds\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.property.type.Lock\n    api_object = api_objects[\"engine.ability.property.type.Lock\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.lock.LockPool\"])\n    member = NyanMember(\"lock_pool\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ActiveTransformTo\n    api_object = api_objects[\"engine.ability.type.ActiveTransformTo\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member = NyanMember(\"target_state\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"transform_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"transform_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Activity\n    api_object = api_objects[\"engine.ability.type.Activity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.Activity\"])\n    member = NyanMember(\"graph\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ApplyContinuousEffect\n    api_object = api_objects[\"engine.ability.type.ApplyContinuousEffect\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.effect.continuous.ContinuousEffect\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"effects\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"application_delay\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ApplyDiscreteEffect\n    api_object = api_objects[\"engine.ability.type.ApplyDiscreteEffect\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.effect_batch.EffectBatch\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"batches\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"reload_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"application_delay\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.AttributeChangeTracker\n    api_object = api_objects[\"engine.ability.type.AttributeChangeTracker\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"change_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Cloak\n    api_object = api_objects[\"engine.ability.type.Cloak\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"interrupted_by\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"interrupt_cooldown\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.CollectStorage\n    api_object = api_objects[\"engine.ability.type.CollectStorage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"storage_elements\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Collision\n    api_object = api_objects[\"engine.ability.type.Collision\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.hitbox.Hitbox\"])\n    member = NyanMember(\"hitbox\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Constructable\n    api_object = api_objects[\"engine.ability.type.Constructable\"]\n\n    member = NyanMember(\"starting_progress\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"construction_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Create\n    api_object = api_objects[\"engine.ability.type.Create\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.create.CreatableGameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"creatables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Despawn\n    api_object = api_objects[\"engine.ability.type.Despawn\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"activation_condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"despawn_condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"despawn_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"state_change\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.DetectCloak\n    api_object = api_objects[\"engine.ability.type.DetectCloak\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.DropResources\n    api_object = api_objects[\"engine.ability.type.DropResources\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.ResourceContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"search_range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.DropSite\n    api_object = api_objects[\"engine.ability.type.DropSite\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.ResourceContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"accepts_from\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.EnterContainer\n    api_object = api_objects[\"engine.ability.type.EnterContainer\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ExchangeResources\n    api_object = api_objects[\"engine.ability.type.ExchangeResources\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"resource_a\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"resource_b\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.exchange_rate.ExchangeRate\"])\n    member = NyanMember(\"exchange_rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.exchange_mode.ExchangeMode\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"exchange_modes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ExitContainer\n    api_object = api_objects[\"engine.ability.type.ExitContainer\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Fly\n    api_object = api_objects[\"engine.ability.type.Fly\"]\n\n    member = NyanMember(\"height\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Formation\n    api_object = api_objects[\"engine.ability.type.Formation\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity_formation.GameEntityFormation\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"formations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Foundation\n    api_object = api_objects[\"engine.ability.type.Foundation\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"foundation_terrain\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.GameEntityStance\n    api_object = api_objects[\"engine.ability.type.GameEntityStance\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity_stance.GameEntityStance\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Gather\n    api_object = api_objects[\"engine.ability.type.Gather\"]\n\n    member = NyanMember(\"auto_resume\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"resume_search_range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"targets\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceRate\"])\n    member = NyanMember(\"gather_rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.ResourceContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Harvestable\n    api_object = api_objects[\"engine.ability.type.Harvestable\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"harvest_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"restock_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"gatherer_limit\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"harvestable_by_default\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Herd\n    api_object = api_objects[\"engine.ability.type.Herd\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"strength\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Herdable\n    api_object = api_objects[\"engine.ability.type.Herdable\"]\n\n    member = NyanMember(\"adjacent_discover_range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.herdable_mode.HerdableMode\"])\n    member = NyanMember(\"mode\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.LineOfSight\n    api_object = api_objects[\"engine.ability.type.LineOfSight\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Live\n    api_object = api_objects[\"engine.ability.type.Live\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeSetting\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"attributes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Lock\n    api_object = api_objects[\"engine.ability.type.Lock\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.lock.LockPool\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"lock_pools\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Move\n    api_object = api_objects[\"engine.ability.type.Move\"]\n\n    member = NyanMember(\"speed\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.move_mode.MoveMode\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"modes\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.path_type.PathType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member = NyanMember(\"path_type\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Named\n    api_object = api_objects[\"engine.ability.type.Named\"]\n\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"description\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"long_description\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.OverlayTerrain\n    api_object = api_objects[\"engine.ability.type.OverlayTerrain\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"terrain_overlay\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.PassiveTransformTo\n    api_object = api_objects[\"engine.ability.type.PassiveTransformTo\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"transform_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member = NyanMember(\"target_state\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"transform_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Pathable\n    api_object = api_objects[\"engine.ability.type.Pathable\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.hitbox.Hitbox\"])\n    member = NyanMember(\"hitbox\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.path_type.PathType\"])\n    key_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    elem_type = N_INT\n    member_type = NyanMemberType(MemberType.DICT, (key_type, elem_type))\n    member = NyanMember(\"path_costs\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ProductionQueue\n    api_object = api_objects[\"engine.ability.type.ProductionQueue\"]\n\n    member = NyanMember(\"size\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.production_mode.ProductionMode\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"production_modes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Projectile\n    api_object = api_objects[\"engine.ability.type.Projectile\"]\n\n    member = NyanMember(\"arc\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.accuracy.Accuracy\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"accuracy\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.target_mode.TargetMode\"])\n    member = NyanMember(\"target_mode\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ignored_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"unignored_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ProvideContingent\n    api_object = api_objects[\"engine.ability.type.ProvideContingent\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"amount\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.RangedContinuousEffect\n    api_object = api_objects[\"engine.ability.type.RangedContinuousEffect\"]\n\n    member = NyanMember(\"min_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.RangedDiscreteEffect\n    api_object = api_objects[\"engine.ability.type.RangedDiscreteEffect\"]\n\n    member = NyanMember(\"min_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.RegenerateAttribute\n    api_object = api_objects[\"engine.ability.type.RegenerateAttribute\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeRate\"])\n    member = NyanMember(\"rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.RegenerateResourceSpot\n    api_object = api_objects[\"engine.ability.type.RegenerateResourceSpot\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceRate\"])\n    member = NyanMember(\"rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resource_spot\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.RemoveStorage\n    api_object = api_objects[\"engine.ability.type.RemoveStorage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"storage_elements\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Research\n    api_object = api_objects[\"engine.ability.type.Research\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.research.ResearchableTech\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"researchables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Resistance\n    api_object = api_objects[\"engine.ability.type.Resistance\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.resistance.Resistance\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"resistances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ResourceStorage\n    api_object = api_objects[\"engine.ability.type.ResourceStorage\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.ResourceContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Restock\n    api_object = api_objects[\"engine.ability.type.Restock\"]\n\n    member = NyanMember(\"auto_restock\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"target\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"restock_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"manual_cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"auto_cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Selectable\n    api_object = api_objects[\"engine.ability.type.Selectable\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.selection_box.SelectionBox\"])\n    member = NyanMember(\"selection_box\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.SendBackToTask\n    api_object = api_objects[\"engine.ability.type.SendBackToTask\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.ShootProjectile\n    api_object = api_objects[\"engine.ability.type.ShootProjectile\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"projectiles\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"min_projectiles\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_projectiles\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"min_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"reload_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawn_delay\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"projectile_delay\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"require_turning\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"manual_aiming_allowed\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_offset_x\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_offset_y\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_offset_z\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_width\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_height\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"spawning_area_randomness\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Storage\n    api_object = api_objects[\"engine.ability.type.Storage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"empty_condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.TerrainRequirement\n    api_object = api_objects[\"engine.ability.type.TerrainRequirement\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.terrain_type.TerrainType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_terrains\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Trade\n    api_object = api_objects[\"engine.ability.type.Trade\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.trade_route.TradeRoute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"trade_routes\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.ResourceContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.TradePost\n    api_object = api_objects[\"engine.ability.type.TradePost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.trade_route.TradeRoute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"trade_routes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.TransferStorage\n    api_object = api_objects[\"engine.ability.type.TransferStorage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"storage_element\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"source_container\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"target_container\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Turn\n    api_object = api_objects[\"engine.ability.type.Turn\"]\n\n    member = NyanMember(\"turn_speed\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.UseContingent\n    api_object = api_objects[\"engine.ability.type.UseContingent\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"amount\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.ability.type.Visibility\n    api_object = api_objects[\"engine.ability.type.Visibility\"]\n\n    member = NyanMember(\"visible_in_fog\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util\n    # engine.util.accuracy.Accuracy\n    api_object = api_objects[\"engine.util.accuracy.Accuracy\"]\n\n    member = NyanMember(\"accuracy\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"accuracy_dispersion\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.dropoff_type.DropoffType\"])\n    member = NyanMember(\"dispersion_dropoff\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"target_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.Activity\n    api_object = api_objects[\"engine.util.activity.Activity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.node.type.Start\"])\n    member = NyanMember(\"start\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.condition.Condition\n    api_object = api_objects[\"engine.util.activity.condition.Condition\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.node.Node\"])\n    member = NyanMember(\"next\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.event.type.Wait\n    api_object = api_objects[\"engine.util.activity.event.type.Wait\"]\n\n    member = NyanMember(\"time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.node.type.Ability\n    api_object = api_objects[\"engine.util.activity.node.type.Ability\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.node.Node\"])\n    member = NyanMember(\"next\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    member_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member = NyanMember(\"ability\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.node.type.Start\n    api_object = api_objects[\"engine.util.activity.node.type.Start\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.node.Node\"])\n    member = NyanMember(\"next\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.node.type.XOREventGate\n    api_object = api_objects[\"engine.util.activity.node.type.XOREventGate\"]\n\n    key_type = NyanMemberType(api_objects[\"engine.util.activity.event.Event\"])\n    value_type = NyanMemberType(api_objects[\"engine.util.activity.node.Node\"])\n    member_type = NyanMemberType(MemberType.DICT, (key_type, value_type))\n    member = NyanMember(\"next\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.activity.node.type.XORGate\n    api_object = api_objects[\"engine.util.activity.node.type.XORGate\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.activity.condition.Condition\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"next\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.activity.node.Node\"])\n    member = NyanMember(\"default\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.animation_override.AnimationOverride\n    api_object = api_objects[\"engine.util.animation_override.AnimationOverride\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    member_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member = NyanMember(\"ability\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.graphics.Animation\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"animations\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.animation_override.type.Reset\n    api_object = api_objects[\"engine.util.animation_override.type.Reset\"]\n    member_origin = api_objects[\"engine.util.animation_override.AnimationOverride\"]\n\n    member = api_object.get_member_by_name(\"animations\", member_origin)\n    member.set_value([], MemberOperator.ASSIGN)\n    member = api_object.get_member_by_name(\"priority\", member_origin)\n    member.set_value(0, MemberOperator.ASSIGN)\n\n    # engine.util.attribute.Attribute\n    api_object = api_objects[\"engine.util.attribute.Attribute\"]\n\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"abbreviation\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.attribute.AttributeAmount\n    api_object = api_objects[\"engine.util.attribute.AttributeAmount\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.attribute.AttributeRate\n    api_object = api_objects[\"engine.util.attribute.AttributeRate\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"rate\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.attribute.AttributeSetting\n    api_object = api_objects[\"engine.util.attribute.AttributeSetting\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"min_value\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_value\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"starting_value\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.attribute.ProtectingAttribute\n    api_object = api_objects[\"engine.util.attribute.ProtectingAttribute\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"protects\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.calculation_type.type.Hyperbolic\n    api_object = api_objects[\"engine.util.calculation_type.type.Hyperbolic\"]\n\n    member = NyanMember(\"shift_x\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"shift_y\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"scale_factor\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.calculation_type.type.Linear\n    api_object = api_objects[\"engine.util.calculation_type.type.Linear\"]\n\n    member = NyanMember(\"shift_x\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"shift_y\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"scale_factor\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.cheat.Cheat\n    api_object = api_objects[\"engine.util.cheat.Cheat\"]\n\n    member = NyanMember(\"activation_message\", N_TEXT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.Patch\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"changes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.cost.Cost\n    api_object = api_objects[\"engine.util.cost.Cost\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.payment_mode.PaymentMode\"])\n    member = NyanMember(\"payment_mode\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.cost.type.AttributeCost\n    api_object = api_objects[\"engine.util.cost.type.AttributeCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"amount\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.cost.type.ResourceCost\n    api_object = api_objects[\"engine.util.cost.type.ResourceCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"amount\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.create.CreatableGameEntity\n    api_object = api_objects[\"engine.util.create.CreatableGameEntity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"game_entity\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.variant.Variant\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"variants\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"creation_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"creation_sounds\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.placement_mode.PlacementMode\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"placement_modes\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.effect_batch.EffectBatch\n    api_object = api_objects[\"engine.util.effect_batch.EffectBatch\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.effect.discrete.DiscreteEffect\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"effects\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.effect_batch.property.BatchProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.util.effect_batch.property.type.Chance\n    api_object = api_objects[\"engine.util.effect_batch.property.type.Chance\"]\n\n    member = NyanMember(\"chance\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.effect_batch.property.type.Priority\n    api_object = api_objects[\"engine.util.effect_batch.property.type.Priority\"]\n\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.exchange_mode.ExchangeMode\n    api_object = api_objects[\"engine.util.exchange_mode.ExchangeMode\"]\n\n    member = NyanMember(\"fee_multiplier\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.exchange_rate.ExchangeRate\n    api_object = api_objects[\"engine.util.exchange_rate.ExchangeRate\"]\n\n    member = NyanMember(\"base_price\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    key_type = NyanMemberType(api_objects[\"engine.util.exchange_mode.ExchangeMode\"])\n    abstract_key = NyanMemberType(MemberType.ABSTRACT, (key_type,))\n    value_type = NyanMemberType(api_objects[\"engine.util.price_mode.PriceMode\"])\n    elem_type = NyanMemberType(MemberType.DICT, (abstract_key, value_type))\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"price_adjust\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.price_pool.PricePool\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"price_pool\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.util.formation.Formation\n    api_object = api_objects[\"engine.util.formation.Formation\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.formation.Subformation\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"subformations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.formation.Subformation\n    api_object = api_objects[\"engine.util.formation.Subformation\"]\n\n    member = NyanMember(\"ordering_priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.game_entity.GameEntity\n    api_object = api_objects[\"engine.util.game_entity.GameEntity\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"abilities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.modifier.Modifier\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"modifiers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.variant.Variant\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"variants\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.game_entity_formation.GameEntityFormation\n    api_object = api_objects[\"engine.util.game_entity_formation.GameEntityFormation\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.formation.Formation\"])\n    member = NyanMember(\"formation\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.formation.Subformation\"])\n    member = NyanMember(\"subformation\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.game_entity_stance.GameEntityStance\n    api_object = api_objects[\"engine.util.game_entity_stance.GameEntityStance\"]\n\n    member = NyanMember(\"search_range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"ability_preference\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"type_preference\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.graphics.Animation\n    api_object = api_objects[\"engine.util.graphics.Animation\"]\n\n    member = NyanMember(\"sprite\", N_FILE, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.graphics.Palette\n    api_object = api_objects[\"engine.util.graphics.Palette\"]\n\n    member = NyanMember(\"palette\", N_FILE, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.graphics.Terrain\n    api_object = api_objects[\"engine.util.graphics.Terrain\"]\n\n    member = NyanMember(\"sprite\", N_FILE, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.hitbox.hitbox\n    api_object = api_objects[\"engine.util.hitbox.Hitbox\"]\n\n    member = NyanMember(\"radius_x\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"radius_y\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"radius_z\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.Language\n    api_object = api_objects[\"engine.util.language.Language\"]\n\n    member = NyanMember(\"ietf_string\", N_TEXT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.LanguageMarkupPair\n    api_object = api_objects[\"engine.util.language.LanguageMarkupPair\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.language.Language\"])\n    member = NyanMember(\"language\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"markup_file\", N_FILE, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.LanguageSoundPair\n    api_object = api_objects[\"engine.util.language.LanguageSoundPair\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.language.Language\"])\n    member = NyanMember(\"language\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member = NyanMember(\"sound\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.LanguageTextPair\n    api_object = api_objects[\"engine.util.language.LanguageTextPair\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.language.Language\"])\n    member = NyanMember(\"language\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"string\", N_TEXT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.translated.type.TranslatedMarkupFile\n    api_object = api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.language.LanguageMarkupPair\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"translations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.translated.type.TranslatedSound\n    api_object = api_objects[\"engine.util.language.translated.type.TranslatedSound\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.language.LanguageSoundPair\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"translations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.language.translated.type.TranslatedString\n    api_object = api_objects[\"engine.util.language.translated.type.TranslatedString\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.language.LanguageTextPair\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"translations\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.LogicElement\n    api_object = api_objects[\"engine.util.lock.LockPool\"]\n\n    member = NyanMember(\"slots\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.LogicElement\n    api_object = api_objects[\"engine.util.logic.LogicElement\"]\n\n    member = NyanMember(\"only_once\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.gate.LogicGate\n    api_object = api_objects[\"engine.util.logic.gate.LogicGate\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"inputs\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.gate.type.SUBSETMAX\n    api_object = api_objects[\"engine.util.logic.gate.type.SUBSETMAX\"]\n\n    member = NyanMember(\"size\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.gate.type.SUBSETMIN\n    api_object = api_objects[\"engine.util.logic.gate.type.SUBSETMIN\"]\n\n    member = NyanMember(\"size\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.Literal\n    api_object = api_objects[\"engine.util.logic.literal.Literal\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.logic.literal_scope.LiteralScope\"])\n    member = NyanMember(\"scope\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal_scope.LiteralScope\n    api_object = api_objects[\"engine.util.logic.literal_scope.LiteralScope\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.AttributeAbovePercentage\n    api_object = api_objects[\"engine.util.logic.literal.type.AttributeAbovePercentage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"threshold\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.AttributeAboveValue\n    api_object = api_objects[\"engine.util.logic.literal.type.AttributeAboveValue\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"threshold\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.AttributeBelowPercentage\n    api_object = api_objects[\"engine.util.logic.literal.type.AttributeBelowPercentage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"threshold\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.AttributeBelowValue\n    api_object = api_objects[\"engine.util.logic.literal.type.AttributeBelowValue\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"threshold\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.GameEntityProgress\n    api_object = api_objects[\"engine.util.logic.literal.type.GameEntityProgress\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"game_entity\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.progress_status.ProgressStatus\"])\n    member = NyanMember(\"progress_status\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.OwnsGameEntity\n    api_object = api_objects[\"engine.util.logic.literal.type.OwnsGameEntity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"game_entity\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.ProjectilePassThrough\n    api_object = api_objects[\"engine.util.logic.literal.type.ProjectilePassThrough\"]\n\n    member = NyanMember(\"pass_through_range\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.ResourceSpotsDepleted\n    api_object = api_objects[\"engine.util.logic.literal.type.ResourceSpotsDepleted\"]\n\n    member = NyanMember(\"only_enabled\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.StateChangeActive\n    api_object = api_objects[\"engine.util.logic.literal.type.StateChangeActive\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member = NyanMember(\"state_change\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.TechResearched\n    api_object = api_objects[\"engine.util.logic.literal.type.TechResearched\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.tech.Tech\"])\n    member = NyanMember(\"tech\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.logic.literal.type.Timer\n    api_object = api_objects[\"engine.util.logic.literal.type.Timer\"]\n\n    member = NyanMember(\"time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.mod.Mod\n    api_object = api_objects[\"engine.util.mod.Mod\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.Patch\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"patches\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.modifier_scope.type.GameEntityScope\n    api_object = api_objects[\"engine.util.modifier_scope.type.GameEntityScope\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"affected_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.move_mode.type.Follow\n    api_object = api_objects[\"engine.util.move_mode.type.Follow\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.move_mode.type.Guard\n    api_object = api_objects[\"engine.util.move_mode.type.Guard\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.patch.Patch\n    api_object = api_objects[\"engine.util.patch.Patch\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.patch.property.PatchProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.NyanPatch\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"patch\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.patch.property.type.Diplomatic\n    api_object = api_objects[\"engine.util.patch.property.type.Diplomatic\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.placement_mode.type.OwnStorage\n    api_object = api_objects[\"engine.util.placement_mode.type.OwnStorage\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.placement_mode.type.Place\n    api_object = api_objects[\"engine.util.placement_mode.type.Place\"]\n\n    member = NyanMember(\"tile_snap_distance\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"clearance_size_x\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"clearance_size_y\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"allow_rotation\", N_BOOL, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_elevation_difference\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.placement_mode.type.Replace\n    api_object = api_objects[\"engine.util.placement_mode.type.Replace\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"game_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.price_mode.type.Dynamic\n    api_object = api_objects[\"engine.util.price_mode.type.Dynamic\"]\n\n    member = NyanMember(\"change_value\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"min_price\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_price\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.production_mode.type.Creatables\n    api_object = api_objects[\"engine.util.production_mode.type.Creatables\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.create.CreatableGameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"exclude\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.production_mode.type.Researchables\n    api_object = api_objects[\"engine.util.production_mode.type.Researchables\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.research.ResearchableTech\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"exclude\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.Progress\n    api_object = api_objects[\"engine.util.progress.Progress\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.progress.property.ProgressProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress_type.ProgressType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"left_boundary\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"right_boundary\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.property.type.Animated\n    api_object = api_objects[\"engine.util.progress.property.type.Animated\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.animation_override.AnimationOverride\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"overrides\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.property.type.AnimationOverlay\n    api_object = api_objects[\"engine.util.progress.property.type.AnimationOverlay\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.graphics.Animation\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"overlays\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.property.type.StateChange\n    api_object = api_objects[\"engine.util.progress.property.type.StateChange\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member = NyanMember(\"state_change\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.property.type.TerrainOverlay\n    api_object = api_objects[\"engine.util.progress.property.type.TerrainOverlay\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"terrain_overlay\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress.property.type.Terrain\n    api_object = api_objects[\"engine.util.progress.property.type.Terrain\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"terrain\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.progress_status.ProgressStatus\n    api_object = api_objects[\"engine.util.progress_status.ProgressStatus\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress_type.ProgressType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"progress_type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"progress\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.research.ResearchableTech\n    api_object = api_objects[\"engine.util.research.ResearchableTech\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.tech.Tech\"])\n    member = NyanMember(\"tech\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"research_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"research_sounds\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.resource.Resource\n    api_object = api_objects[\"engine.util.resource.Resource\"]\n\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_storage\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.resource.ResourceContingent\n    api_object = api_objects[\"engine.util.resource.ResourceContingent\"]\n\n    member = NyanMember(\"min_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.resource.ResourceAmount\n    api_object = api_objects[\"engine.util.resource.ResourceAmount\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.resource.ResourceRate\n    api_object = api_objects[\"engine.util.resource.ResourceRate\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"rate\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.resource_spot.ResourceSpot\n    api_object = api_objects[\"engine.util.resource_spot.ResourceSpot\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"resource\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"starting_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"decay_rate\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.selection_box.type.Rectangle\n    api_object = api_objects[\"engine.util.selection_box.type.Rectangle\"]\n\n    member = NyanMember(\"width\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"height\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.setup.PlayerSetup\n    api_object = api_objects[\"engine.util.setup.PlayerSetup\"]\n\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"description\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"long_description\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"leader_names\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.modifier.Modifier\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"modifiers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"starting_resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.Patch\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"game_setup\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.sound.Sound\n    api_object = api_objects[\"engine.util.sound.Sound\"]\n\n    member = NyanMember(\"play_delay\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = N_FILE\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"sounds\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.state_machine.StateChanger\n    api_object = api_objects[\"engine.util.state_machine.StateChanger\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    elem_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"enable_abilities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.ability.Ability\"])\n    elem_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"disable_abilities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.modifier.Modifier\"])\n    elem_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"enable_modifiers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.modifier.Modifier\"])\n    elem_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"disable_modifiers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.transform_pool.TransformPool\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"transform_pool\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.state_machine.Reset\n    api_object = api_objects[\"engine.util.state_machine.Reset\"]\n    member_origin = api_objects[\"engine.util.state_machine.StateChanger\"]\n\n    member = api_object.get_member_by_name(\"enable_abilities\", member_origin)\n    member.set_value([], MemberOperator.ASSIGN)\n    member = api_object.get_member_by_name(\"disable_abilities\", member_origin)\n    member.set_value([], MemberOperator.ASSIGN)\n    member = api_object.get_member_by_name(\"enable_modifiers\", member_origin)\n    member.set_value([], MemberOperator.ASSIGN)\n    member = api_object.get_member_by_name(\"disable_modifiers\", member_origin)\n    member.set_value([], MemberOperator.ASSIGN)\n    member = api_object.get_member_by_name(\"priority\", member_origin)\n    member.set_value(0, MemberOperator.ASSIGN)\n\n    # engine.util.storage.EntityContainer\n    api_object = api_objects[\"engine.util.storage.EntityContainer\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"allowed_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.StorageElementDefinition\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"storage_element_defs\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"slots\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"carry_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.storage.ResourceContainer\n    api_object = api_objects[\"engine.util.storage.ResourceContainer\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"resource\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress.Progress\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"carry_progress\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.storage.resource_container.type.InternalDropSite\n    api_object = api_objects[\"engine.util.storage.resource_container.type.InternalDropSite\"]\n\n    member = NyanMember(\"update_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.storage.StorageElementDefinition\n    api_object = api_objects[\"engine.util.storage.StorageElementDefinition\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"storage_element\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"elements_per_slot\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.StorageElementDefinition\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"conflicts\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.state_machine.StateChanger\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"state_change\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.util.taunt.Taunt\n    api_object = api_objects[\"engine.util.taunt.Taunt\"]\n\n    member = NyanMember(\"activation_message\", N_TEXT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"display_message\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member = NyanMember(\"sound\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.tech.Tech\n    api_object = api_objects[\"engine.util.tech.Tech\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.tech_type.TechType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"description\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedMarkupFile\"])\n    member = NyanMember(\"long_description\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.Patch\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"updates\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.terrain.Terrain\n    api_object = api_objects[\"engine.util.terrain.Terrain\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.terrain_type.TerrainType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(\n        api_objects[\"engine.util.language.translated.type.TranslatedString\"])\n    member = NyanMember(\"name\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.graphics.Terrain\"])\n    member = NyanMember(\"terrain_graphic\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.sound.Sound\"])\n    member = NyanMember(\"sound\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.terrain.TerrainAmbient\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ambience\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.path_type.PathType\"])\n    key_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    elem_type = N_INT\n    member_type = NyanMemberType(MemberType.DICT, (key_type, elem_type))\n    member = NyanMember(\"path_costs\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.terrain.TerrainAmbient\n    api_object = api_objects[\"engine.util.terrain.TerrainAmbient\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"object\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"max_density\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.trade_route.TradeRoute\n    api_object = api_objects[\"engine.util.trade_route.TradeRoute\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member = NyanMember(\"trade_resource\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"start_trade_post\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member = NyanMember(\"end_trade_post\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.trade_route.type.AoE1TradeRoute\n    api_object = api_objects[\"engine.util.trade_route.type.AoE1TradeRoute\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"exchange_resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"trade_amount\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.variant.Variant\n    api_object = api_objects[\"engine.util.variant.Variant\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.patch.Patch\"])\n    member_type = NyanMemberType(MemberType.ORDEREDSET, (elem_type,))\n    member = NyanMember(\"changes\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.variant.type.AdjacentTilesVariant\n    api_object = api_objects[\"engine.util.variant.type.AdjacentTilesVariant\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"north\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"north_east\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"east\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"south_east\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"south\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"south_west\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"west\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"north_west\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.variant.type.RandomVariant\n    api_object = api_objects[\"engine.util.variant.type.RandomVariant\"]\n\n    member = NyanMember(\"chance_share\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.util.variant.type.PerspectiveVariant\n    api_object = api_objects[\"engine.util.variant.type.PerspectiveVariant\"]\n\n    member = NyanMember(\"angle\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect\n    # engine.effect.Effect\n    api_object = api_objects[\"engine.effect.Effect\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.effect.property.EffectProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.effect.property.type.Area\n    api_object = api_objects[\"engine.effect.property.type.Area\"]\n\n    member = NyanMember(\"range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.dropoff_type.DropoffType\"])\n    member = NyanMember(\"dropoff\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.property.type.Cost\n    api_object = api_objects[\"engine.effect.property.type.Cost\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.property.type.Diplomatic\n    api_object = api_objects[\"engine.effect.property.type.Diplomatic\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.property.type.Priority\n    api_object = api_objects[\"engine.effect.property.type.Priority\"]\n\n    member = NyanMember(\"priority\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.continuous.flat_attribute_change.FlatAttributeChange\n    api_object = api_objects[\"engine.effect.continuous.flat_attribute_change.FlatAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeRate\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_change_rate\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeRate\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"max_change_rate\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeRate\"])\n    member = NyanMember(\"change_rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.ProtectingAttribute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ignore_protection\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.continuous.type.Lure\n    api_object = api_objects[\"engine.effect.continuous.type.Lure\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.lure_type.LureType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"destination\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"min_distance_to_destination\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\n    api_object = api_objects[\"engine.effect.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"total_change_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.ProtectingAttribute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ignore_protection\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\n    api_object = api_objects[\"engine.effect.continuous.time_relative_progress.TimeRelativeProgressChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress_type.ProgressType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"total_change_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.discrete.convert.Convert\n    api_object = api_objects[\"engine.effect.discrete.convert.Convert\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.convert_type.ConvertType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_chance_success\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"max_chance_success\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"chance_success\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"cost_fail\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.effect.discrete.convert.type.AoE2Convert\n    api_object = api_objects[\"engine.effect.discrete.convert.type.AoE2Convert\"]\n\n    member = NyanMember(\"skip_guaranteed_rounds\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"skip_protected_rounds\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.discrete.flat_attribute_change.FlatAttributeChange\n    api_object = api_objects[\"engine.effect.discrete.flat_attribute_change.FlatAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeAmount\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_change_value\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeAmount\"])\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"max_change_value\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeAmount\"])\n    member = NyanMember(\"change_value\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.ProtectingAttribute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ignore_protection\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.discrete.type.MakeHarvestable\n    api_object = api_objects[\"engine.effect.discrete.type.MakeHarvestable\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resource_spot\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.effect.discrete.type.SendToContainer\n    api_object = api_objects[\"engine.effect.discrete.type.SendToContainer\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.container_type.SendToContainerType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"storages\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance\n    # engine.resistance.Resistance\n    api_object = api_objects[\"engine.resistance.Resistance\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.resistance.property.ResistanceProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.property.type.Cost\n    api_object = api_objects[\"engine.resistance.property.type.Cost\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.cost.Cost\"])\n    member = NyanMember(\"cost\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.property.type.Stacked\n    api_object = api_objects[\"engine.resistance.property.type.Stacked\"]\n\n    member = NyanMember(\"stack_limit\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.calculation_type.CalculationType\"])\n    member = NyanMember(\"calculation_type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.distribution_type.DistributionType\"])\n    member = NyanMember(\"distribution_type\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\n    api_object = api_objects[\"engine.resistance.continuous.flat_attribute_change.FlatAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeRate\"])\n    member = NyanMember(\"block_rate\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.continuous.type.Lure\n    api_object = api_objects[\"engine.resistance.continuous.type.Lure\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.lure_type.LureType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\n    api_object = api_objects[\"engine.resistance.continuous.time_relative_attribute.TimeRelativeAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.continuous.time_relative_progress.TimeRelativeProgress\n    api_object = api_objects[\"engine.resistance.continuous.time_relative_progress.TimeRelativeProgressChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.progress_type.ProgressType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.discrete.convert.Convert\n    api_object = api_objects[\"engine.resistance.discrete.convert.Convert\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.convert_type.ConvertType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"chance_resist\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.discrete.convert.type.AoE2Convert\n    api_object = api_objects[\"engine.resistance.discrete.convert.type.AoE2Convert\"]\n\n    member = NyanMember(\"guaranteed_resist_rounds\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"protected_rounds\", N_INT, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"protection_round_recharge_time\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\n    api_object = api_objects[\"engine.resistance.discrete.flat_attribute_change.FlatAttributeChange\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.AttributeAmount\"])\n    member = NyanMember(\"block_value\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.discrete.type.MakeHarvestable\n    api_object = api_objects[\"engine.resistance.discrete.type.MakeHarvestable\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resource_spot\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"harvest_conditions\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.resistance.discrete.type.SendToContainer\n    api_object = api_objects[\"engine.resistance.discrete.type.SendToContainer\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.container_type.SendToContainerType\"])\n    member_type = NyanMemberType(MemberType.CHILDREN, (elem_type,))\n    member = NyanMember(\"type\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member = NyanMember(\"search_range\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"ignore_containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier\n    # engine.modifier.Modifier\n    api_object = api_objects[\"engine.modifier.Modifier\"]\n\n    subtype = NyanMemberType(api_objects[\"engine.modifier.property.ModifierProperty\"])\n    key_type = NyanMemberType(MemberType.ABSTRACT, (subtype,))\n    member_type = NyanMemberType(MemberType.DICT, (key_type, subtype))\n    member = NyanMember(\"properties\", member_type, {}, MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.property.type.Multiplier\n    api_object = api_objects[\"engine.modifier.property.type.Multiplier\"]\n\n    member = NyanMember(\"multiplier\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.property.type.Scope\n    api_object = api_objects[\"engine.modifier.property.type.Scoped\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"stances\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.property.type.Stacked\n    api_object = api_objects[\"engine.modifier.property.type.Stacked\"]\n\n    member = NyanMember(\"stack_limit\", N_INT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\n    api_object = api_objects[\"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceHigh\"]\n\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_elevation_difference\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\n    api_object = api_objects[\"engine.modifier.effect.flat_attribute_change.type.ElevationDifferenceLow\"]\n\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_elevation_difference\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.effect.flat_attribute_change.type.Flyover\n    api_object = api_objects[\"engine.modifier.effect.flat_attribute_change.type.Flyover\"]\n\n    member = NyanMember(\"relative_angle\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"flyover_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.effect.flat_attribute_change.type.Terrain\n    api_object = api_objects[\"engine.modifier.effect.flat_attribute_change.type.Terrain\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"terrain\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceHigh\n    api_object = api_objects[\"engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceHigh\"]\n\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_elevation_difference\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceLow\n    api_object = api_objects[\"engine.modifier.resistance.flat_attribute_change.type.ElevationDifferenceLow\"]\n\n    elem_type = N_FLOAT\n    member_type = NyanMemberType(MemberType.OPTIONAL, (elem_type,))\n    member = NyanMember(\"min_elevation_difference\", member_type, MemberSpecialValue.NYAN_NONE,\n                        MemberOperator.ASSIGN, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.resistance.flat_attribute_change.type.Terrain\n    api_object = api_objects[\"engine.modifier.resistance.flat_attribute_change.type.Terrain\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.terrain.Terrain\"])\n    member = NyanMember(\"terrain\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.AbsoluteProjectileAmount\n    api_object = api_objects[\"engine.modifier.type.AbsoluteProjectileAmount\"]\n\n    member = NyanMember(\"amount\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.AoE2ProjectileAmount\n    api_object = api_objects[\"engine.modifier.type.AoE2ProjectileAmount\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.ability.type.ApplyDiscreteEffect\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"provider_abilities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.ability.type.ApplyDiscreteEffect\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"receiver_abilities\", member_type, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.attribute_change_type.AttributeChangeType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"change_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.AttributeSettingsValue\n    api_object = api_objects[\"engine.modifier.type.AttributeSettingsValue\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member = NyanMember(\"attribute\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.EntityContainerCapacity\n    api_object = api_objects[\"engine.modifier.type.EntityContainerCapacity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member = NyanMember(\"container\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.ContinuousResource\n    api_object = api_objects[\"engine.modifier.type.ContinuousResource\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceRate\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"rates\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.CreationAttributeCost\n    api_object = api_objects[\"engine.modifier.type.CreationAttributeCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"attributes\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.create.CreatableGameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"creatables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.CreationResourceCost\n    api_object = api_objects[\"engine.modifier.type.CreationResourceCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.create.CreatableGameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"creatables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.CreationTime\n    api_object = api_objects[\"engine.modifier.type.CreationTime\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.create.CreatableGameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"creatables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.DepositResourcesOnProgress\n    api_object = api_objects[\"engine.modifier.type.DepositResourcesOnProgress\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.progress_status.ProgressStatus\"])\n    member = NyanMember(\"progress_status\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"affected_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.DiplomaticLineOfSight\n    api_object = api_objects[\"engine.modifier.type.DiplomaticLineOfSight\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.diplomatic_stance.DiplomaticStance\"])\n    member = NyanMember(\"diplomatic_stance\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.GatheringEfficiency\n    api_object = api_objects[\"engine.modifier.type.GatheringEfficiency\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resource_spot\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.GatheringRate\n    api_object = api_objects[\"engine.modifier.type.GatheringRate\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.resource_spot.ResourceSpot\"])\n    member = NyanMember(\"resource_spot\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.InContainerContinuousEffect\n    api_object = api_objects[\"engine.modifier.type.InContainerContinuousEffect\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.ability.type.ApplyContinuousEffect\"])\n    member = NyanMember(\"ability\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.InContainerDiscreteEffect\n    api_object = api_objects[\"engine.modifier.type.InContainerDiscreteEffect\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.storage.EntityContainer\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"containers\", member_type, None, None, 0)\n    api_object.add_member(member)\n    member_type = NyanMemberType(api_objects[\"engine.ability.type.ApplyDiscreteEffect\"])\n    member = NyanMember(\"ability\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.InstantTechResearch\n    api_object = api_objects[\"engine.modifier.type.InstantTechResearch\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.tech.Tech\"])\n    member = NyanMember(\"tech\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.logic.LogicElement\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"condition\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.RefundOnCondition\n    api_object = api_objects[\"engine.modifier.type.RefundOnCondition\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.ResourceAmount\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"refund_amount\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.ResearchAttributeCost\n    api_object = api_objects[\"engine.modifier.type.ResearchAttributeCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.attribute.Attribute\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"attributes\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.research.ResearchableTech\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"researchables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.ResearchResourceCost\n    api_object = api_objects[\"engine.modifier.type.ResearchResourceCost\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.resource.Resource\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"resources\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.research.ResearchableTech\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"researchables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.ResearchTime\n    api_object = api_objects[\"engine.modifier.type.ResearchTime\"]\n\n    elem_type = NyanMemberType(api_objects[\"engine.util.research.ResearchableTech\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"researchables\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.Reveal\n    api_object = api_objects[\"engine.modifier.type.Reveal\"]\n\n    member = NyanMember(\"line_of_sight\", N_FLOAT, None, None, 0)\n    api_object.add_member(member)\n    subtype = NyanMemberType(api_objects[\"engine.util.game_entity_type.GameEntityType\"])\n    elem_type = NyanMemberType(MemberType.CHILDREN, (subtype,))\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"affected_types\", member_type, None, None, 0)\n    api_object.add_member(member)\n    elem_type = NyanMemberType(api_objects[\"engine.util.game_entity.GameEntity\"])\n    member_type = NyanMemberType(MemberType.SET, (elem_type,))\n    member = NyanMember(\"blacklisted_entities\", member_type, None, None, 0)\n    api_object.add_member(member)\n\n    # engine.modifier.type.StorageElementCapacity\n    api_object = api_objects[\"engine.modifier.type.StorageElementCapacity\"]\n\n    member_type = NyanMemberType(api_objects[\"engine.util.storage.StorageElementDefinition\"])\n    member = NyanMember(\"storage_element\", member_type, None, None, 0)\n    api_object.add_member(member)\n"
  },
  {
    "path": "openage/convert/service/read/palette.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-branches\n\n\"\"\"\nModule for reading palette files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ...value_object.read.media.colortable import ColorTable\nfrom ...value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.util.fslike.directory import Directory\n\n\ndef get_palettes(\n    srcdir: Directory,\n    game_version: GameVersion,\n    index: int = None\n) -> dict[int, ColorTable]:\n    \"\"\"\n    Read and create the color palettes.\n    \"\"\"\n    game_edition = game_version.edition\n\n    palettes = {}\n\n    if game_edition.game_id in (\"ROR\", \"AOC\", \"SWGB\", \"HDEDITION\"):\n        if index:\n            palette_path = f\"{MediaType.PALETTES.value}/{str(index)}.bina\"\n            palette_file = srcdir[palette_path]\n            palette = ColorTable(palette_file.open(\"rb\").read())\n            palette_id = int(palette_file.stem)\n\n            palettes[palette_id] = palette\n\n        else:\n            palette_dir = srcdir[MediaType.PALETTES.value]\n            for palette_file in palette_dir.iterdir():\n                # Only 505XX.bina files are usable palettes\n                if palette_file.stem.startswith(\"505\"):\n                    palette = ColorTable(palette_file.open(\"rb\").read())\n                    palette_id = int(palette_file.stem)\n\n                    palettes[palette_id] = palette\n\n            if game_edition.game_id == \"HDEDITION\":\n                # TODO: HD edition has extra palettes in the dat folder\n                pass\n\n    elif game_edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n        # Parse palettes.conf file and save the ids/paths\n        conf_filepath = f\"{MediaType.PALETTES.value}/palettes.conf\"\n        conf_file = srcdir[conf_filepath].open('rb')\n        palette_paths = {}\n\n        for line in conf_file.read().decode('utf-8').split('\\n'):\n            line = line.strip()\n\n            # skip comments and empty lines\n            if not line or line.startswith('//'):\n                continue\n\n            palette_id, filepath = line.split(',')\n            palette_id = int(palette_id)\n            palette_paths[palette_id] = filepath\n\n        if index:\n            palette_path = f\"{MediaType.PALETTES.value}/{palette_paths[index]}\"\n            palette = ColorTable(srcdir[palette_path].open(\"rb\").read())\n\n            palettes[index] = palette\n\n        else:\n            for palette_id, filepath in palette_paths.items():\n                palette_path = f\"{MediaType.PALETTES.value}/{filepath}\"\n                palette_file = srcdir[palette_path]\n                palette = ColorTable(palette_file.open(\"rb\").read())\n\n                palettes[palette_id] = palette\n\n    else:\n        raise RuntimeError(\"no valid palette converter found for game edition\"\n                           f\"{game_edition.edition_name}\")\n\n    return palettes\n"
  },
  {
    "path": "openage/convert/service/read/register_media.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nModule for registering media files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ...value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n\ndef get_existing_graphics(args: Namespace) -> set[str]:\n    \"\"\"\n    List the graphics files that exist in the graphics file paths.\n    \"\"\"\n    filenames = set()\n    for filepath in args.srcdir[MediaType.GRAPHICS.value].iterdir():\n        filenames.add(filepath.stem)\n\n    return filenames\n"
  },
  {
    "path": "openage/convert/service/read/string_resource.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nModule for reading plaintext-based language files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nimport re\n\n\nfrom ....log import dbg\nfrom ...entity_object.conversion.stringresource import StringResource\nfrom ...value_object.read.media.langcodes import LANGCODES_DE1, LANGCODES_DE2, LANGCODES_HD\nfrom ...value_object.read.media.pefile import PEFile\nfrom ...value_object.read.media_types import MediaType\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.path import Path\n    from openage.util.fslike.wrapper import GuardedFile\n\n\ndef get_string_resources(args: Namespace) -> StringResource:\n    \"\"\" reads the (language) string resources \"\"\"\n\n    stringres = StringResource()\n\n    srcdir = args.srcdir\n    game_edition = args.game_version.edition\n\n    language_files = game_edition.media_paths[MediaType.LANGUAGE]\n\n    for language_file in language_files:\n        if game_edition.game_id in (\"ROR\", \"AOC\", \"SWGB\"):\n            # AoC/RoR use .DLL PE files for their string resources\n            pefile = PEFile(srcdir[language_file].open('rb'))\n            stringres.fill_from(pefile.resources().strings)\n\n        elif game_edition.game_id == \"HDEDITION\":\n            strings = read_hd_language_file(srcdir, language_file)\n            stringres.fill_from(strings)\n\n        elif game_edition.game_id == \"AOE1DE\":\n            strings = read_de1_language_file(srcdir, language_file)\n            stringres.fill_from(strings)\n\n        elif game_edition.game_id == \"AOE2DE\":\n            strings = read_de2_language_file(srcdir, language_file)\n            stringres.fill_from(strings)\n\n        else:\n            raise KeyError(\"No service found for parsing language files \"\n                           f\"of version {game_edition.game_id}\")\n\n        # TODO: Other game versions\n\n    # TODO: transform and cleanup the read strings:\n    #       convert formatting indicators from HTML to something sensible, etc\n\n    return stringres\n\n\ndef read_age2_hd_fe_stringresources(stringres: StringResource, path: Path) -> int:\n    \"\"\"\n    Fill the string resources from text specifications found\n    in the given path.\n\n    In age2hd forgotten those are stored in plain text files.\n\n    The data is stored in the `stringres` storage.\n    \"\"\"\n    count = 0\n\n    # multiple string files in the program source dir\n    for lang in path.list():\n        try:\n            if lang == b'_common':\n                continue\n            if lang == b'_packages':\n                continue\n            if lang.lower() == b'.ds_store'.lower():\n                continue\n\n            langfilename = [lang.decode(),\n                            \"strings\", \"key-value\",\n                            \"key-value-strings-utf8.txt\"]\n\n            with path[langfilename].open('rb') as langfile:\n                stringres.fill_from(read_hd_language_file_old(langfile, lang))\n\n            count += 1\n\n        except FileNotFoundError:\n            # that's fine, there are no language files for every language.\n            pass\n\n    return count\n\n\ndef read_age2_hd_3x_stringresources(stringres: StringResource, srcdir: Directory) -> int:\n    \"\"\"\n    HD Edition 3.x and below store language .txt files\n    in the Bin/ folder.\n    Specific language strings are in Bin/$LANG/*.txt.\n\n    The data is stored in the `stringres` storage.\n    \"\"\"\n    count = 0\n\n    for lang in srcdir[\"bin\"].list():\n        lang_path = srcdir[\"bin\", lang.decode()]\n\n        # There are some .txt files immediately in bin/, but they don't\n        # seem to contain anything useful. (Everything is overridden by\n        # files in Bin/$LANG/.)\n        if not lang_path.is_dir():\n            continue\n\n        # Sometimes we can have language DLLs in Bin/$LANG/\n        # e.g. HD Edition 2.0\n        # We do _not_ want to treat these as text files\n        # so first check explicitly\n\n        if lang_path[\"language.dll\"].is_file():\n            for name in [\"language.dll\",\n                         \"language_x1.dll\",\n                         \"language_x1_p1.dll\"]:\n\n                pefile = PEFile(lang_path[name].open('rb'))\n                stringres.fill_from(pefile.resources().strings)\n                count += 1\n\n        else:\n            for basename in lang_path.list():\n                with lang_path[basename].open('rb') as langfile:\n                    # No utf-8 :(\n                    stringres.fill_from(\n                        read_hd_language_file_old(\n                            langfile, lang, enc='iso-8859-1'))\n                count += 1\n\n    return count\n\n\ndef read_hd_language_file_old(\n    fileobj: GuardedFile,\n    langcode: str,\n    enc: str = 'utf-8'\n) -> dict[str, StringResource]:\n    \"\"\"\n    Takes a file object, and the file's language code.\n    \"\"\"\n    dbg(\"parse HD Language file %s\", langcode)\n    strings = {}\n\n    for line in fileobj.read().decode(enc).split('\\n'):\n        line = line.strip()\n\n        # skip comments & empty lines\n        if not line or line.startswith('//'):\n            continue\n\n        string_id, string = line.split(None, 1)\n\n        # strings that were added in the HD edition release have\n        # UPPERCASE_STRINGS as names, instead of the numeric ID stuff\n        # of AoC.\n        strings[string_id] = string\n\n    fileobj.close()\n\n    lang = LANGCODES_HD.get(langcode, langcode)\n\n    return {lang: strings}\n\n\ndef read_hd_language_file(\n    srcdir: Directory,\n    language_file: GuardedFile,\n    enc: str = 'utf-8'\n) -> dict[str, StringResource]:\n    \"\"\"\n    HD Edition stores language .txt files in the resources/ folder.\n    Specific language strings are in resources/$LANG/strings/key-value/*.txt.\n\n    The data is stored in the `stringres` storage.\n    \"\"\"\n    # Langcode is folder name\n    langcode = language_file.split(\"/\")[1]\n\n    dbg(\"parse HD Language file %s\", langcode)\n    strings = {}\n\n    fileobj = srcdir[language_file].open('rb')\n\n    for line in fileobj.read().decode(enc).split('\\n'):\n        line = line.strip()\n\n        # skip comments & empty lines\n        if not line or line.startswith('//'):\n            continue\n\n        string_id, string = line.split(None, 1)\n\n        # strings that were added in the HD edition release have\n        # UPPERCASE_STRINGS as names, instead of the numeric ID stuff\n        # of AoC.\n        strings[string_id] = string\n\n    fileobj.close()\n\n    lang = LANGCODES_HD.get(langcode, langcode)\n\n    return {lang: strings}\n\n\ndef read_de1_language_file(\n    srcdir: Directory,\n    language_file: GuardedFile\n) -> dict[str, StringResource]:\n    \"\"\"\n    Definitve Edition stores language .txt files in the Localization folder.\n    Specific language strings are in Data/Localization/$LANG/strings.txt.\n\n    The data is stored in the `stringres` storage.\n    \"\"\"\n    # Langcode is folder name\n    langcode = language_file.split(\"/\")[2]\n\n    dbg(\"parse DE1 Language file %s\", langcode)\n    strings = {}\n\n    fileobj = srcdir[language_file].open('rb')\n\n    for line in fileobj.read().decode('utf-8').split('\\n'):\n        line = line.strip()\n\n        # skip comments & empty lines\n        if not line or line.startswith('//'):\n            continue\n\n        # Brilliant idea to split by command AND space!!\n        string_id, string = re.split(r\",|\\s\", line, maxsplit=1)\n\n        # strings that were added in the DE2 edition release have\n        # UPPERCASE_STRINGS as names, instead of the numeric ID stuff\n        # of AoC.\n        strings[string_id] = string\n\n    fileobj.close()\n\n    lang = LANGCODES_DE1.get(langcode, langcode)\n\n    return {lang: strings}\n\n\ndef read_de2_language_file(\n    srcdir: Directory,\n    language_file: GuardedFile\n) -> dict[str, StringResource]:\n    \"\"\"\n    Definitve Edition stores language .txt files in the resources/ folder.\n    Specific language strings are in resources/$LANG/strings/key-value/*.txt.\n\n    The data is stored in the `stringres` storage.\n    \"\"\"\n    # Langcode is folder name\n    langcode = language_file.split(\"/\")[1]\n\n    dbg(\"parse DE2 Language file %s\", langcode)\n    strings = {}\n\n    fileobj = srcdir[language_file].open('rb')\n\n    for line in fileobj.read().decode('utf-8').split('\\n'):\n        line = line.strip()\n\n        # skip comments & empty lines\n        if not line or line.startswith('//'):\n            continue\n\n        string_id, string = line.split(None, 1)\n\n        # strings that were added in the DE2 edition release have\n        # UPPERCASE_STRINGS as names, instead of the numeric ID stuff\n        # of AoC.\n        strings[string_id] = string\n\n    fileobj.close()\n\n    lang = LANGCODES_DE2.get(langcode, langcode)\n\n    return {lang: strings}\n"
  },
  {
    "path": "openage/convert/tool/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n    api_export.py\n\tdriver.py\n\tinteractive.py\n\tsinglefile.py\n)\n\nadd_subdirectory(subtool)\n"
  },
  {
    "path": "openage/convert/tool/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTools used by the converter.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/tool/api_export.py",
    "content": "# Copyright 2023-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nExport tool for dumping the nyan API of the engine from the converter.\n\"\"\"\n\nfrom openage.convert.entity_object.conversion.modpack import Modpack\nfrom openage.convert.entity_object.export.formats.nyan_file import NyanFile\nfrom openage.convert.processor.export.data_exporter import DataExporter\nfrom openage.convert.processor.export.generate_manifest_hashes import generate_hashes\nfrom openage.convert.service.read.nyan_api_loader import load_api\nfrom openage.nyan.import_tree import ImportTree\nfrom openage.util.fslike.directory import Directory\nfrom openage.util.fslike.union import Union, UnionPath\nfrom openage.util.fslike.wrapper import DirectoryCreator\n\nfrom ...log import info\n\n\ndef init_subparser(cli):\n    \"\"\" Initializes the parser for convert-specific args. \"\"\"\n\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\"dir\", help=\"modpack output directory\")\n\n\ndef main(args, error):\n    \"\"\"\n    CLI entry point for API export.\n    \"\"\"\n    del error  # unused\n\n    path = Union().root\n    path.mount(Directory(args.dir).root)\n\n    export_api(path)\n\n\ndef export_api(exportdir: UnionPath) -> None:\n    \"\"\"\n    Export the nyan API of the engine to the target directory.\n\n    :param exportdir: The target directory for the modpack folder.\n    :type exportdir: Directory\n    \"\"\"\n    modpack = create_modpack()\n\n    info(\"Dumping info file...\")\n\n    targetdir = DirectoryCreator(exportdir).root\n    outdir = targetdir / \"engine\"\n\n    # Modpack info file\n    DataExporter.export([modpack.info], outdir)\n\n    info(\"Dumping data files...\")\n\n    # Data files\n    DataExporter.export(modpack.get_data_files(), outdir)\n\n    # Manifest file\n    generate_hashes(modpack, outdir)\n    DataExporter.export([modpack.manifest], outdir)\n\n\ndef create_modpack() -> Modpack:\n    \"\"\"\n    Create the nyan API as a modpack.\n\n    :return: The modpack containing the nyan API.\n    :rtype: Modpack\n    \"\"\"\n    modpack = Modpack(\"engine\")\n\n    mod_def = modpack.get_info()\n\n    mod_def.set_info(\"engine\", modpack_version=\"0.5.0\", versionstr=\"0.5.0\", repo=\"openage\")\n\n    mod_def.add_include(\"**\")\n\n    create_nyan_files(modpack)\n\n    return modpack\n\n\ndef create_nyan_files(modpack: Modpack) -> None:\n    \"\"\"\n    Create the nyan files from the API objects.\n\n    :param modpack: The modpack to add the nyan files to.\n    :type modpack: Modpack\n    \"\"\"\n    api_objects = load_api()\n    created_nyan_files: dict[str, NyanFile] = {}\n\n    for fqon, obj in api_objects.items():\n        fqon_parts = fqon.split(\".\")\n        obj_location = \"/\".join(fqon_parts[1:-2]) + \"/\"\n        obj_filename = fqon_parts[-2] + \".nyan\"\n        nyan_file_path = f\"{modpack.name}/{obj_location}{obj_filename}\"\n\n        if nyan_file_path in created_nyan_files:\n            nyan_file = created_nyan_files[nyan_file_path]\n\n        else:\n            nyan_file = NyanFile(obj_location, obj_filename, modpack.name)\n            created_nyan_files.update({nyan_file.get_relative_file_path(): nyan_file})\n            modpack.add_data_export(nyan_file)\n\n        nyan_file.add_nyan_object(obj)\n\n    import_tree = ImportTree()\n\n    for nyan_file in created_nyan_files.values():\n        import_tree.expand_from_file(nyan_file)\n\n    for nyan_file in created_nyan_files.values():\n        nyan_file.set_import_tree(import_tree)\n\n    set_static_aliases(import_tree)\n\n\ndef set_static_aliases(import_tree: ImportTree) -> None:\n    \"\"\"\n    Create the import tree for the nyan files.\n\n    :param import_tree: The import tree to add the aliases to.\n    :type import_tree: ImportTree\n    \"\"\"\n    import_tree.add_alias((\"engine\", \"root\"), \"root\")\n"
  },
  {
    "path": "openage/convert/tool/driver.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-return-statements\n\n\"\"\"\nReceives cleaned-up srcdir and targetdir objects from .main, and drives the\nactual conversion process.\n\"\"\"\nfrom __future__ import annotations\nimport typing\nimport timeit\n\n\nfrom ...log import info, dbg\nfrom ..processor.export.modpack_exporter import ModpackExporter\nfrom ..service.debug_info import debug_gamedata_format\nfrom ..service.debug_info import debug_string_resources, \\\n    debug_registered_graphics, debug_modpack, debug_execution_time\nfrom ..service.read.gamedata import get_gamespec\nfrom ..service.read.palette import get_palettes\nfrom ..service.read.register_media import get_existing_graphics\nfrom ..service.read.string_resource import get_string_resources\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\ndef convert(args: Namespace) -> None:\n    \"\"\"\n    args must hold srcdir and targetdir (FS-like objects),\n    plus any additional configuration options.\n    \"\"\"\n    convert_metadata(args)\n    # with args.targetdir[GAMESPEC_VERSION_FILENAME].open('w') as fil:\n    #     fil.write(EmpiresDat.get_hash(args.game_version))\n\n    # clean args (set by convert_metadata for convert_media)\n    del args.palettes\n\n\ndef convert_metadata(args: Namespace) -> None:\n    \"\"\"\n    Converts the metadata part.\n    \"\"\"\n    if not args.flag(\"no_metadata\"):\n        info(\"converting metadata\")\n        # data_formatter = DataFormatter()\n\n    args.converter = get_converter(args.game_version)\n\n    # required for player palette and color lookup during SLP conversion.\n    palettes = get_palettes(args.srcdir, args.game_version)\n\n    # store for use by convert_media\n    args.palettes = palettes\n\n    if args.flag(\"no_metadata\"):\n        return\n\n    gamedata_path = args.targetdir.joinpath('gamedata')\n    if gamedata_path.exists():\n        gamedata_path.removerecursive()\n\n    # Record time taken for each stage\n    stages_time = {}\n\n    # Read .dat\n    stage_start = timeit.default_timer()\n    debug_gamedata_format(args.debugdir, args.debug_info, args.game_version)\n    gamespec = get_gamespec(args.srcdir, args.game_version, not args.flag(\"no_pickle_cache\"))\n\n    # Blending mode count\n    if args.game_version.edition.game_id == \"SWGB\":\n        args.blend_mode_count = gamespec[0][\"blend_mode_count_swgb\"].value\n\n    else:\n        args.blend_mode_count = None\n\n    # Read strings\n    string_resources = get_string_resources(args)\n    debug_string_resources(args.debugdir, args.debug_info, string_resources)\n\n    # Existing graphic IDs/filenames\n    existing_graphics = get_existing_graphics(args)\n    debug_registered_graphics(args.debugdir, args.debug_info, existing_graphics)\n\n    stage_end = timeit.default_timer()\n    info(\"Finished metadata read (%.2f seconds)\", stage_end - stage_start)\n    stages_time.update({\"read\": stage_end - stage_start})\n\n    # nyan conversion\n    stage_start = timeit.default_timer()\n    modpacks = args.converter.convert(gamespec,\n                                      args,\n                                      string_resources,\n                                      existing_graphics)\n\n    stage_end = timeit.default_timer()\n    info(\"Finished data conversion (%.2f seconds)\", stage_end - stage_start)\n    stages_time.update({\"convert\": stage_end - stage_start})\n\n    # Export modpacks\n    stage_start = timeit.default_timer()\n    for modpack in modpacks:\n        mod_export_start = timeit.default_timer()\n        ModpackExporter.export(modpack, args)\n        debug_modpack(args.debugdir, args.debug_info, modpack)\n\n        mod_export_end = timeit.default_timer()\n        info(\"Finished export of modpack '%s' v%s (%.2f seconds)\",\n             modpack.info.packagename,\n             modpack.info.version,\n             mod_export_end - mod_export_start)\n\n    stage_end = timeit.default_timer()\n    info(\"Finished export (%.2f seconds)\", stage_end - stage_start)\n    stages_time.update({\"export\": stage_end - stage_start})\n\n    debug_execution_time(args.debugdir, args.debug_info, stages_time)\n\n    # TODO: player palettes\n    # player_palette = PlayerColorTable(palette)\n    # data_formatter.add_data(player_palette.dump(\"player_palette\"))\n\n    # TODO: terminal color files\n    # termcolortable = ColorTable(URXVTCOLS)\n    # data_formatter.add_data(termcolortable.dump(\"termcolors\"))\n\n    # TODO: gamespec files\n    # data_formatter.export(args.targetdir, (\"csv\",))\n\n    if args.flag('gen_extra_files'):\n        dbg(\"generating extra files for visualization\")\n        # tgt = args.targetdir\n        # with tgt['info/colortable.pal.png'].open_w() as outfile:\n        #     palette.save_visualization(outfile)\n\n        # with tgt['info/playercolortable.pal.png'].open_w() as outfile:\n        #     player_palette.save_visualization(outfile)\n\n\ndef get_converter(game_version: GameVersion):\n    \"\"\"\n    Returns the converter for the specified game version.\n    \"\"\"\n    game_edition = game_version.edition\n    game_expansions = game_version.expansions\n\n    if game_edition.game_id == \"ROR\":\n        from ..processor.conversion.ror.processor import RoRProcessor\n        return RoRProcessor\n\n    if game_edition.game_id == \"AOE1DE\":\n        from ..processor.conversion.de1.processor import DE1Processor\n        return DE1Processor\n\n    if game_edition.game_id == \"AOC\":\n        from ..processor.conversion.aoc.processor import AoCProcessor\n        return AoCProcessor\n\n    if game_edition.game_id == \"AOCDEMO\":\n        # treat the demo as AoC during conversion\n        # TODO: maybe introduce a config parameter for this purpose?\n        game_edition.game_id = \"AOC\"\n        from ..processor.conversion.aoc_demo.processor import DemoProcessor\n        return DemoProcessor\n\n    if game_edition.game_id == \"HDEDITION\":\n        from ..processor.conversion.hd.processor import HDProcessor\n        return HDProcessor\n\n    if game_edition.game_id == \"AOE2DE\":\n        from ..processor.conversion.de2.processor import DE2Processor\n        return DE2Processor\n\n    if game_edition.game_id == \"SWGB\":\n        if \"SWGB_CC\" in [expansion.game_id for expansion in game_expansions]:\n            from ..processor.conversion.swgbcc.processor import SWGBCCProcessor\n            return SWGBCCProcessor\n\n    raise RuntimeError(f\"no valid converter found for game edition {game_edition.edition_name}\")\n"
  },
  {
    "path": "openage/convert/tool/interactive.py",
    "content": "# Copyright 2020-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nInteractive browser for game asset files.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nimport os\nimport readline  # pylint: disable=unused-import\n\nfrom openage.convert.processor.export.media_exporter import MediaExporter\n\nfrom ...log import warn, info\nfrom ...util.fslike.directory import Directory\nfrom ..service.init.mount_asset_dirs import mount_asset_dirs\nfrom ..service.init.version_detect import create_version_objects\nfrom .subtool.version_select import get_game_version\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media.colortable import ColorTable\n    from openage.util.fslike.directory import Directory\n    from openage.util.fslike.path import Path\n\n\ndef interactive_browser(cfg: Path, srcdir: Directory = None) -> typing.NoReturn:\n    \"\"\"\n    launch an interactive view for browsing the original\n    archives.\n\n    TODO: Enhance functionality and fix SLP conversion.\n    \"\"\"\n\n    info(\"launching interactive data browser...\")\n\n    # the variables are actually used, in the interactive prompt.\n    # pylint: disable=possibly-unused-variable\n\n    # Initialize game versions data\n\n    auxiliary_files_dir = cfg / \"converter\" / \"games\"\n    avail_game_eds, avail_game_exps = create_version_objects(auxiliary_files_dir)\n\n    # Acquire game version info\n    game_version = get_game_version(srcdir, avail_game_eds, avail_game_exps)\n    if not game_version.edition:\n        warn(\"cannot launch browser as no valid game version was found.\")\n        return\n\n    data = mount_asset_dirs(srcdir, game_version)\n\n    if not data:\n        warn(\"cannot launch browser as no valid input assets were found.\")\n        return\n\n    def save(path: Path, target: Path) -> None:\n        \"\"\"\n        save a path to a custom target\n        \"\"\"\n        with path.open(\"rb\") as infile:\n            with open(target, \"rb\") as outfile:\n                outfile.write(infile.read())\n\n    def save_slp(path: Path, target: Path, palette: ColorTable = None) -> None:\n        \"\"\"\n        save a slp as png.\n        \"\"\"\n        from ..entity_object.export.texture import Texture\n        from ..value_object.read.media.slp import SLP\n        from ..service.read.palette import get_palettes\n\n        if not palette:\n            palette = get_palettes(data, game_version)\n\n        with path.open(\"rb\") as slpfile:\n\n            from ..processor.export.texture_merge import merge_frames\n\n            tex = Texture(SLP(slpfile.read()), palette)\n\n            merge_frames(tex)\n\n            out_path, filename = os.path.split(target)\n            MediaExporter.save_png(\n                tex,\n                Directory(out_path).root,\n                filename\n            )\n\n    def save_smx(path: Path, target: Path, palette: ColorTable = None) -> None:\n        \"\"\"\n        save a smx as png.\n        \"\"\"\n        from ..entity_object.export.texture import Texture\n        from ..value_object.read.media.smx import SMX\n        from ..service.read.palette import get_palettes\n\n        if not palette:\n            palette = get_palettes(data, game_version)\n\n        with path.open(\"rb\") as smxfile:\n\n            from ..processor.export.texture_merge import merge_frames\n\n            tex = Texture(SMX(smxfile.read()), palette)\n\n            merge_frames(tex)\n\n            out_path, filename = os.path.split(target)\n            MediaExporter.save_png(\n                tex,\n                Directory(out_path).root,\n                filename\n            )\n\n    import code\n    from pprint import pprint\n\n    import rlcompleter\n\n    completer = rlcompleter.Completer(locals())\n    readline.parse_and_bind(\"tab: complete\")\n    readline.parse_and_bind(\"set show-all-if-ambiguous on\")\n    readline.set_completer(completer.complete)\n\n    code.interact(\n        banner=(\"\\nuse `pprint` for beautiful output!\\n\"\n                \"you can access stuff by the `data` variable!\\n\"\n                \"`data` is an openage.util.fslike.path.Path!\\n\\n\"\n                \"* version detection:   pprint(game_versions)\\n\"\n                \"* list contents:       pprint(list(data['graphics'].list()))\\n\"\n                \"* dump data:           save(data['file/path'], '/tmp/outputfile')\\n\"\n                \"* save a slp as png:   save_slp(data['dir/123.slp'], '/tmp/pic.png')\\n\"\n                \"* save a smx as png:   save_smx(data['dir/123.smx'], '/tmp/pic.png')\\n\"),\n        local=locals()\n    )\n"
  },
  {
    "path": "openage/convert/tool/singlefile.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConvert a single slp/wav file from some drs archive to a png/opus file.\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\n\nfrom pathlib import Path\n\nfrom openage.convert.processor.export.media_exporter import MediaExporter\nfrom openage.convert.value_object.init.game_version import GameEdition, GameVersion\n\nfrom ...log import info\nfrom ...util.fslike.directory import Directory\nfrom ..entity_object.export.texture import Texture\nfrom ..value_object.read.media.colortable import ColorTable\nfrom ..value_object.read.media.drs import DRS\n\nAOC_GAME_VERSION = GameVersion(\n    edition=GameEdition(\"Dummy AOC\", \"AOC\", \"YES\", [], [], {}, [], [])\n)\nSWGB_GAME_VERSION = GameVersion(\n    edition=GameEdition(\"Dummy SWGB\", \"SWGB\", \"YES\", [], [], {}, [], [])\n)\n\n\ndef init_subparser(cli):\n    \"\"\" Initializes the parser for convert-specific args. \"\"\"\n    import argparse\n\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\"--palettes-path\",\n                     help=(\"path to the folder containing the palettes.conf file \"\n                           \"OR an interfac.drs archive that contains palette files\"))\n    cli.add_argument(\"--drs\", type=argparse.FileType('rb'),\n                     help=(\"drs archive filename that contains an slp or wav \"\n                           \"e.g. path ~/games/aoe/graphics.drs\"))\n    cli.add_argument(\"--mode\", choices=['drs-slp', 'drs-wav', 'sld', 'slp', 'smp', 'smx', 'wav'],\n                     help=(\"choose between drs-slp, drs-wav, sld, slp, smp, smx or wav; \"\n                           \"otherwise, this is determined by the file extension\"))\n    cli.add_argument(\"--compression-level\", type=int, default=2, choices=[0, 1, 2, 3, 4],\n                     help=\"set PNG compression level\")\n    cli.add_argument(\"--layer\", type=int, default=0, choices=[0, 1, 2, 3, 4],\n                     help=\"ID of SLD/SMP/SMX layer that should be exported to image file\")\n    cli.add_argument(\"filename\", help=(\"filename or, if inside a drs archive \"\n                                       \"given by --drs, the filename within \"\n                                       \"the drs archive\"))\n    cli.add_argument(\"output\", help=\"output path\")\n\n\ndef main(args, error):\n    \"\"\"\n    CLI entry point for single file conversions\n    \"\"\"\n    del error  # unused\n\n    file_path = Path(args.filename)\n    file_extension = file_path.suffix[1:].lower()\n\n    if sys.platform == \"win32\":\n        from openage.util.dll import DllDirectoryManager, default_paths\n        dll_manager = DllDirectoryManager(default_paths())\n        dll_manager.add_directories()\n\n    if not (args.mode in (\"sld\", \"drs-wav\", \"wav\") or file_extension in (\"sld\", \"wav\")):\n        if not args.palettes_path:\n            raise RuntimeError(\"palettes-path needs to be specified for \"\n                               f\"file type '{file_extension}'\")\n\n        palettes_path = Path(args.palettes_path)\n        palettes = read_palettes(palettes_path)\n\n    compression_level = args.compression_level\n    layer = args.layer\n    if args.mode == \"slp\" or (file_extension == \"slp\" and not args.drs):\n        read_slp_file(args.filename, args.output, palettes, compression_level)\n\n    elif args.mode == \"drs-slp\" or (file_extension == \"slp\" and args.drs):\n        read_slp_in_drs_file(args.drs, args.filename, args.output, palettes, compression_level)\n\n    elif args.mode == \"smp\" or file_extension == \"smp\":\n        read_smp_file(args.filename, args.output, palettes, compression_level, layer)\n\n    elif args.mode == \"smx\" or file_extension == \"smx\":\n        read_smx_file(args.filename, args.output, palettes, compression_level, layer)\n\n    elif args.mode == \"sld\" or file_extension == \"sld\":\n        read_sld_file(args.filename, args.output, compression_level, layer)\n\n    elif args.mode == \"wav\" or (file_extension == \"wav\" and not args.drs):\n        read_wav_file(args.filename, args.output)\n\n    elif args.mode == \"drs-wav\" or (file_extension == \"wav\" and args.drs):\n        read_wav_in_drs_file(args.drs, args.filename, args.output)\n\n    else:\n        raise SyntaxError(\"format could not be determined\")\n\n\ndef read_palettes(palettes_path: Path) -> dict[str, ColorTable]:\n    \"\"\"\n    Reads the palettes from the palettes folder/archive.\n    \"\"\"\n    palettes = {}\n\n    if palettes_path.is_dir():\n        info(\"opening palette files in directory '%s'\", palettes_path.name)\n\n        palette_dir = Directory(palettes_path)\n        conf_filepath = \"palettes.conf\"\n        conf_file = palette_dir.root[conf_filepath].open('rb')\n        palette_paths = {}\n\n        info(\"parsing palette data...\")\n        for line in conf_file.read().decode('utf-8').split('\\n'):\n            line = line.strip()\n\n            # skip comments and empty lines\n            if not line or line.startswith('//'):\n                continue\n\n            palette_id, filepath = line.split(',')\n            palette_id = int(palette_id)\n            palette_paths[palette_id] = filepath\n\n        for palette_id, filepath in palette_paths.items():\n            palette_file = palette_dir.root[filepath]\n            palette = ColorTable(palette_file.open(\"rb\").read())\n\n            palettes[palette_id] = palette\n\n    else:\n        info(\"opening palette files in drs archive '%s'\", palettes_path.name)\n\n        # open from drs archive\n        with Path(palettes_path).open(\"rb\") as palette_file:\n            game_version = AOC_GAME_VERSION\n            palette_dir = DRS(palette_file, game_version)\n\n            info(\"parsing palette data...\")\n            for palette_file in palette_dir.root.iterdir():\n                # Only 505XX.bina files are usable palettes\n                if palette_file.stem.startswith(\"505\"):\n                    palette = ColorTable(palette_file.open(\"rb\").read())\n                    palette_id = int(palette_file.stem)\n\n                    palettes[palette_id] = palette\n\n    return palettes\n\n\ndef read_slp_file(\n    slp_path: Path,\n    output_path: Path,\n    palettes: dict[str, ColorTable],\n    compression_level: int\n) -> None:\n    \"\"\"\n    Reads a single SLP file.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open the slp\n    info(\"opening slp file at '%s'\", Path(slp_path).name)\n    with Path(slp_path).open(\"rb\") as slp_file:\n        # import here to prevent that the __main__ depends on SLP\n        # just by importing this singlefile.py.\n        from ..value_object.read.media.slp import SLP\n\n        # parse the slp_path image\n        info(\"parsing slp image...\")\n        slp_image = SLP(slp_file.read())\n\n    # create texture\n    info(\"packing texture...\")\n    tex = Texture(slp_image, palettes)\n\n    from ..processor.export.texture_merge import merge_frames\n    try:\n        merge_frames(tex)\n\n    except ValueError:\n        info(\"slp contains 0 frames! aborting texture export.\")\n        return\n\n    # save as png\n    info(\"saving png file at '%s'\", output_path)\n    MediaExporter.save_png(\n        tex,\n        Directory(output_file.parent).root,\n        output_file.name,\n        compression_level\n    )\n\n\ndef read_slp_in_drs_file(\n    drs: Path,\n    slp_path: Path,\n    output_path: Path,\n    palettes: dict[str, ColorTable],\n    compression_level: int\n) -> None:\n    \"\"\"\n    Reads a SLP file from a DRS archive.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open from drs archive\n    game_version = AOC_GAME_VERSION\n    drs_file = DRS(drs, game_version)\n\n    info(\"opening slp in drs '%s:%s'...\", drs.name, slp_path)\n    with drs_file.root[slp_path].open(\"rb\") as slp_file:\n        # import here to prevent that the __main__ depends on SLP\n        # just by importing this singlefile.py.\n        from ..value_object.read.media.slp import SLP\n\n        # parse the slp image\n        info(\"parsing slp image...\")\n        slp_image = SLP(slp_file.read())\n\n    # create texture\n    info(\"packing texture...\")\n    tex = Texture(slp_image, palettes)\n\n    from ..processor.export.texture_merge import merge_frames\n    try:\n        merge_frames(tex)\n\n    except ValueError:\n        info(\"slp contains 0 frames! aborting texture export.\")\n        return\n\n    # save as png\n    info(\"saving png file at '%s'\", output_path)\n    MediaExporter.save_png(\n        tex,\n        Directory(output_file.parent).root,\n        output_file.name,\n        compression_level\n    )\n\n\ndef read_smp_file(\n    smp_path: Path,\n    output_path: Path,\n    palettes: dict[str, ColorTable],\n    compression_level: int,\n    layer: int\n) -> None:\n    \"\"\"\n    Reads a single SMP file.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open the smp\n    info(\"opening smp file at '%s'\", smp_path)\n    with Path(smp_path).open(\"rb\") as smp_file:\n        # import here to prevent that the __main__ depends on SMP\n        # just by importing this singlefile.py.\n        from ..value_object.read.media.smp import SMP\n\n        # parse the smp_path image\n        info(\"parsing smp image...\")\n        smp_image = SMP(smp_file.read())\n\n    # create texture\n    info(\"packing texture...\")\n    tex = Texture(smp_image, palettes)\n\n    from ..processor.export.texture_merge import merge_frames\n    try:\n        merge_frames(tex)\n\n    except ValueError:\n        info(\"layer %s contains 0 frames! aborting texture export.\", layer)\n        return\n\n    # save as png\n    info(\"saving png file at '%s'\", output_path)\n    MediaExporter.save_png(\n        tex,\n        Directory(output_file.parent).root,\n        output_file.name,\n        compression_level\n    )\n\n\ndef read_smx_file(\n    smx_path: Path,\n    output_path: Path,\n    palettes: dict[str, ColorTable],\n    compression_level: int,\n    layer: int\n) -> None:\n    \"\"\"\n    Reads a single SMX (compressed SMP) file.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open the smx\n    info(\"opening smx file at '%s'\", smx_path)\n    with Path(smx_path).open(\"rb\") as smx_file:\n        # import here to prevent that the __main__ depends on SMP\n        # just by importing this singlefile.py.\n        from ..value_object.read.media.smx import SMX\n\n        # parse the smx_path image\n        info(\"parsing smx image...\")\n        smx_image = SMX(smx_file.read())\n\n    # create texture\n    info(\"packing texture...\")\n    tex = Texture(smx_image, palettes, layer=layer)\n\n    from ..processor.export.texture_merge import merge_frames\n    try:\n        merge_frames(tex)\n\n    except ValueError:\n        info(\"layer %s contains 0 frames! aborting texture export.\", layer)\n        return\n\n    # save as png\n    info(\"saving png file at '%s'\", output_path)\n    MediaExporter.save_png(\n        tex,\n        Directory(output_file.parent).root,\n        output_file.name,\n        compression_level\n    )\n\n\ndef read_sld_file(\n    sld_path: Path,\n    output_path: Path,\n    compression_level: int,\n    layer: int\n) -> None:\n    \"\"\"\n    Reads a single SMX (compressed SMP) file.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open the sld\n    info(\"opening sld file at '%s'\", sld_path)\n    with Path(sld_path).open(\"rb\") as smx_file:\n        # import here to prevent that the __main__ depends on SMP\n        # just by importing this singlefile.py.\n        from ..value_object.read.media.sld import SLD\n\n        # parse the smx_path image\n        info(\"parsing sld image...\")\n        sld_image = SLD(smx_file.read())\n\n    # create texture\n    info(\"packing texture...\")\n    tex = Texture(sld_image, layer=layer)\n\n    from ..processor.export.texture_merge import merge_frames\n    try:\n        merge_frames(tex)\n\n    except ValueError:\n        info(\"layer %s contains 0 frames! aborting texture export.\", layer)\n        return\n\n    # save as png\n    info(\"saving png file at '%s'\", output_path)\n    MediaExporter.save_png(\n        tex,\n        Directory(output_file.parent).root,\n        output_file.name,\n        compression_level\n    )\n\n\ndef read_wav_file(wav_path: Path, output_path: Path) -> None:\n    \"\"\"\n    Reads a single WAV file.\n    \"\"\"\n\n    output_file = Path(output_path)\n\n    # open the wav file\n    info(\"opening wav file at '%s'\", wav_path)\n    with Path(wav_path).open(\"rb\") as wav_file:\n        # import here to prevent that the __main__ depends on opusenc\n        # just by importing this singlefile.py.\n        from ..service.export.opus.opusenc import encode\n\n        # convert wav to opus\n        info(\"converting wav to opus...\")\n        opus_data = encode(wav_file.read())\n\n    # save converted opus data to target directory\n    info(\"saving opus file...\")\n    output_file.write_bytes(opus_data)\n\n\ndef read_wav_in_drs_file(drs: Path, wav_path: Path, output_path: Path) -> None:\n    \"\"\"\n    Reads a WAV file from a DRS archive.\n    \"\"\"\n    output_file = Path(output_path)\n\n    # open from drs archive\n    game_version = AOC_GAME_VERSION\n    drs_file = DRS(drs, game_version)\n\n    info(\"opening wav in drs '%s:%s'...\", drs.name, wav_path)\n    wav_file = drs_file.root[wav_path].open(\"rb\")\n\n    # import here to prevent that the __main__ depends on opusenc\n    # just by importing this singlefile.py.\n    from ..service.export.opus.opusenc import encode\n\n    # convert wav to opus\n    info(\"converting wav to opus...\")\n    opus_data = encode(wav_file.read())\n\n    # save converted opus data to target directory\n    info(\"saving opus file...\")\n    output_file.write_bytes(opus_data)\n"
  },
  {
    "path": "openage/convert/tool/subtool/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tacquire_sourcedir.py\n\tversion_select.py\n)\n"
  },
  {
    "path": "openage/convert/tool/subtool/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSubtools for fetching user input.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/tool/subtool/acquire_sourcedir.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-branches\n\n\"\"\"\nAcquire the sourcedir for the game that is supposed to be converted.\n\"\"\"\nfrom __future__ import annotations\nimport platform\nimport typing\n\nfrom configparser import ConfigParser\nimport os\nfrom pathlib import Path\nimport subprocess\nimport sys\nfrom typing import AnyStr, Generator\n\nimport shutil\nimport tempfile\nfrom urllib.request import urlopen\n\nfrom ....log import warn, info, dbg\nfrom ....util.fslike.directory import CaseIgnoringDirectory, Directory\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameEdition\n\n\nSTANDARD_PATH_IN_32BIT_WINEPREFIX =\\\n    \"drive_c/Program Files/Microsoft Games/Age of Empires II/\"\nSTANDARD_PATH_IN_64BIT_WINEPREFIX =\\\n    \"drive_c/Program Files (x86)/Microsoft Games/Age of Empires II/\"\nSTANDARD_PATH_IN_WINEPREFIX_STEAM = \\\n    \"drive_c/Program Files (x86)/Steam/steamapps/common/Age2HD/\"\nREGISTRY_KEY = \\\n    \"HKEY_LOCAL_MACHINE\\\\Software\\\\Microsoft\\\\Microsoft Games\\\\\"\nREGISTRY_SUFFIX_AOK = \"Age of Empires\\\\2.0\"\nREGISTRY_SUFFIX_TC = \"Age of Empires II: The Conquerors Expansion\\\\1.0\"\n\nTRIAL_URL = 'https://archive.org/download/AgeOfEmpiresIiTheConquerorsDemo/Age2XTrial.exe'\n\n\ndef expand_relative_path(path: str) -> AnyStr:\n    \"\"\"Expand relative path to an absolute one, including abbreviations like\n    ~ and environment variables\"\"\"\n    return os.path.realpath(os.path.expandvars(os.path.expanduser(path)))\n\n\ndef prompt(msg: str, answer: typing.Union[bool, None] = None) -> bool:\n    \"\"\"\n    Ask the user a yes/no question.\n\n    :param msg: Message to display.\n    :param answer: Pre-determined answer (optional).\n    \"\"\"\n    while answer is None:\n        print(f\"  {msg} [Y/n]\")\n\n        user_selection = input(\"> \")\n        if user_selection.lower() in {\"yes\", \"y\", \"\"}:\n            answer = True\n\n        elif user_selection.lower() in {\"no\", \"n\"}:\n            answer = False\n\n    return answer\n\n\ndef wanna_convert(answer: typing.Union[bool, None] = None) -> bool:\n    \"\"\"\n    Ask the user if assets should be converted.\n    \"\"\"\n    return prompt(\"Do you want to convert assets?\", answer=answer)\n\n\ndef wanna_check_updates(answer: typing.Union[bool, None] = None) -> bool:\n    \"\"\"\n    Ask the user if they want to check for updates.\n    \"\"\"\n    return prompt(\"Do you want to check for updates?\", answer=answer)\n\n\ndef wanna_download_trial(answer: typing.Union[bool, None] = None) -> bool:\n    \"\"\"\n    Ask the user if the AoC trial should be downloaded.\n    \"\"\"\n    return prompt(\"Do you want to download the AoC trial version?\", answer=answer)\n\n\ndef query_source_dir(proposals: set[str]) -> AnyStr:\n    \"\"\"\n    Query interactively for a conversion source directory.\n    Lists proposals and allows selection if some were found.\n    \"\"\"\n\n    if proposals:\n        print(\"\\nPlease select an Age of Empires installation directory.\")\n        print(\"Insert the index of one of the proposals, or any path:\")\n\n        proposals = sorted(proposals)\n        for index, proposal in enumerate(proposals):\n            print(f\"({index}) {proposal}\")\n\n    else:\n        print(\"Could not find any installation directory \"\n              \"automatically.\")\n        print(\"Please enter an AOE2 install path manually.\")\n\n    while True:\n        user_selection = input(\"> \")\n        if user_selection.isdecimal() and int(user_selection) < len(proposals):\n            sourcedir = proposals[int(user_selection)]\n        else:\n            sourcedir = user_selection\n        sourcedir = expand_relative_path(sourcedir)\n        if Path(sourcedir).is_dir():\n            break\n        warn(\"No valid existing directory: %s\", sourcedir)\n\n    return sourcedir\n\n\ndef acquire_conversion_source_dir(\n    avail_game_eds: list[GameEdition],\n    prev_srcdir_paths: set[str] = None\n) -> Path:\n    \"\"\"\n    Acquires source dir for the asset conversion.\n\n    Returns a file system-like object that holds all the required files.\n    \"\"\"\n    try:\n        # TODO: use some sort of GUI for this (GTK, QtQuick, zenity?)\n        #       probably best if directly integrated into the main GUI.\n        proposals = set()\n\n        # previously used source dirs\n        if prev_srcdir_paths:\n            for prev_srcdir_path in prev_srcdir_paths:\n                if Path(prev_srcdir_path).is_dir():\n                    proposals.add(prev_srcdir_path)\n\n        # commonly used install dirs\n        current_platform = platform.system()\n        for game_edition in avail_game_eds:\n            install_paths = game_edition.install_paths\n            candidates = []\n            if current_platform == 'Linux' and 'linux' in install_paths:\n                candidates = install_paths[\"linux\"]\n\n            elif current_platform == 'Darwin' and 'macos' in install_paths:\n                candidates = install_paths[\"macos\"]\n\n            elif current_platform == 'Windows' and 'windows' in install_paths:\n                candidates = install_paths[\"windows\"]\n\n            else:\n                continue\n\n            for candidate in candidates:\n                if Path(expand_relative_path(candidate)).is_dir():\n                    proposals.add(candidate)\n\n        # TODO: Reimplement wine support\n\n        use_trial = False\n        if len(proposals) == 0:\n            print(\"\\nopenage requires a local game installation for conversion\")\n            print(\"but no local installation could be found automatically.\")\n            use_trial = wanna_download_trial()\n\n        if use_trial:\n            sourcedir = download_trial()\n\n        else:\n            sourcedir = query_source_dir(proposals)\n\n    except KeyboardInterrupt:\n        print(\"\\nInterrupted, aborting\")\n        sys.exit(0)\n    except EOFError:\n        print(\"\\nEOF, aborting\")\n        sys.exit(0)\n\n    print(f\"converting from '{sourcedir}'\")\n\n    return CaseIgnoringDirectory(sourcedir).root\n\n\ndef download_trial() -> AnyStr:\n    \"\"\"\n    Download and extract the AoC trial version.\n\n    Does not work yet. TODO: Find an exe unpack solution that works on all platforms\n    \"\"\"\n    print(f\"Downloading AoC trial version from {TRIAL_URL}\")\n    # pylint: disable=consider-using-with\n    tempdir = tempfile.mkdtemp()\n    with urlopen(TRIAL_URL) as response:\n        with tempfile.NamedTemporaryFile(delete=False) as tmp_file:\n            shutil.copyfileobj(response, tmp_file)\n\n            from ....cabextract.cab import CABFile\n\n            cab = CABFile(tmp_file, 0x65678)\n\n            sourcedir = Directory(tempdir).root\n            print(f\"Extracting game files to {sourcedir}...\")\n            dirs = [cab.root]\n\n            # Loop over all files in the CAB archive and extract them\n            # to the tempdir\n            while len(dirs) > 0:\n                cur_src_dir = dirs[0]\n                cur_tgt_dir = sourcedir\n\n                for part in cur_src_dir.parts:\n                    cur_tgt_dir = cur_tgt_dir[part]\n                cur_tgt_dir.mkdirs()\n\n                dirs.remove(cur_src_dir)\n\n                for path in cur_src_dir.iterdir():\n                    if path.is_dir():\n                        dirs.append(path)\n\n                    if path.is_file():\n                        with cur_tgt_dir[path.name].open(\"wb\") as target_file:\n                            with path.open(\"rb\") as source_file:\n                                target_file.write(source_file.read())\n\n    return tempdir\n\n\ndef wine_to_real_path(path: str) -> str:\n    \"\"\"\n    Turn a Wine file path (C:\\\\xyz) into a local filesystem path (~/.wine/xyz)\n    \"\"\"\n    return subprocess.check_output(('winepath', path)).strip().decode()\n\n\ndef unescape_winereg(value: str):\n    \"\"\"Remove quotes and escapes from a Wine registry value\"\"\"\n    return value.strip('\"').replace(r'\\\\\\\\', '\\\\')\n\n\ndef wine_srcdir_proposals() -> Generator[str, None, None]:\n    \"\"\"Yield a list of directory names where an installation might be found\"\"\"\n    if \"WINEPREFIX\" in os.environ:\n        yield \"$WINEPREFIX/\" + STANDARD_PATH_IN_32BIT_WINEPREFIX\n        yield \"$WINEPREFIX/\" + STANDARD_PATH_IN_64BIT_WINEPREFIX\n        yield \"$WINEPREFIX/\" + STANDARD_PATH_IN_WINEPREFIX_STEAM\n    yield \"~/.wine/\" + STANDARD_PATH_IN_32BIT_WINEPREFIX\n    yield \"~/.wine/\" + STANDARD_PATH_IN_64BIT_WINEPREFIX\n    yield \"~/.wine/\" + STANDARD_PATH_IN_WINEPREFIX_STEAM\n\n    try:\n        info(\"using the wine registry to query an installation location...\")\n        # get wine registry key of the age installation\n        with tempfile.NamedTemporaryFile(mode='rb') as reg_file:\n            if not subprocess.call(('wine', 'regedit', '/E', reg_file.name,\n                                    REGISTRY_KEY)):\n\n                reg_raw_data = reg_file.read()\n                try:\n                    reg_data = reg_raw_data.decode('utf-16')\n                except UnicodeDecodeError:\n                    # this is hopefully enough.\n                    # if it isn't, feel free to fight more encoding problems.\n                    reg_data = reg_raw_data.decode('utf-8', errors='replace')\n\n                # strip the REGEDIT4 header, so it becomes a valid INI\n                lines = reg_data.splitlines()\n                del lines[0:2]\n\n                reg_parser = ConfigParser()\n                reg_parser.read_string(''.join(lines))\n                for suffix in REGISTRY_SUFFIX_AOK, REGISTRY_SUFFIX_TC:\n                    reg_key = REGISTRY_KEY + suffix\n                    if reg_key in reg_parser:\n                        if '\"InstallationDirectory\"' in reg_parser[reg_key]:\n                            yield wine_to_real_path(unescape_winereg(\n                                reg_parser[reg_key]['\"InstallationDirectory\"']))\n                        if '\"EXE Path\"' in reg_parser[reg_key]:\n                            yield wine_to_real_path(unescape_winereg(\n                                reg_parser[reg_key]['\"EXE Path\"']))\n\n    except OSError as error:\n        dbg(\"wine registry extraction failed: %s\", error)\n"
  },
  {
    "path": "openage/convert/tool/subtool/version_select.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\"\"\"\nInitial version detection based on user input.\n\nTODO: Version selection.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ....log import warn, info\nfrom ...service.init.version_detect import iterate_game_versions\nfrom ...value_object.init.game_version import Support\nfrom ...value_object.init.game_version import GameVersion\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameEdition, \\\n        GameExpansion, GameVersion\n    from openage.util.fslike.directory import Directory\n\n\ndef get_game_version(\n    srcdir: Directory,\n    avail_game_eds: list[GameEdition],\n    avail_game_exps: list[GameExpansion]\n) -> GameVersion:\n    \"\"\"\n    Mount the input folders for conversion.\n    \"\"\"\n    info(\"Looking for compatible games to convert...\")\n    game_version = iterate_game_versions(\n        srcdir, avail_game_eds, avail_game_exps)\n\n    no_support = False\n    if not game_version.edition or game_version.edition.support == Support.NOPE:\n        warn(\"No valid game version(s) could not be detected \"\n             f\"in {srcdir.resolve_native_path()}\")\n\n        # no supported version was found\n        no_support = True\n\n    else:\n        # Check for broken edition\n        broken_edition = game_version.edition.support == Support.BREAKS\n\n        # a broken edition is installed\n        if broken_edition:\n            warn(\"You have installed an incompatible game edition:\")\n            warn(\" * \\x1b[31;1m%s\\x1b[m\", game_version.edition)\n            no_support = True\n\n        broken_expansions = []\n        for expansion in game_version.expansions:\n            if expansion.support == Support.BREAKS:\n                broken_expansions.append(expansion)\n\n        # a broken expansion is installed\n        if broken_expansions:\n            warn(\"You have installed incompatible game expansions:\")\n            for expansion in broken_expansions:\n                warn(\" * \\x1b[31;1m%s\\x1b[m\", expansion)\n\n    # inform about supported versions\n    if no_support:\n        warn(\"You need at least one of:\")\n        for edition in avail_game_eds:\n            if edition.support == Support.YES:\n                warn(\" * \\x1b[34m%s\\x1b[m\", edition)\n\n        return GameVersion(edition=None)\n\n    info(\"Compatible game edition detected:\")\n    info(\" * %s\", game_version.edition.edition_name)\n    if game_version.expansions:\n        info(\"Compatible expansions detected:\")\n        for expansion in game_version.expansions:\n            info(\" * %s\", expansion.expansion_name)\n\n    return game_version\n"
  },
  {
    "path": "openage/convert/value_object/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n)\n\nadd_subdirectory(conversion)\nadd_subdirectory(init)\nadd_subdirectory(read)\n"
  },
  {
    "path": "openage/convert/value_object/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nValue objects used by the converter.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tforward_ref.py\n)\n\nadd_subdirectory(aoc)\nadd_subdirectory(de1)\nadd_subdirectory(de2)\nadd_subdirectory(hd)\nadd_subdirectory(ror)\nadd_subdirectory(swgb)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nValue objects used during conversion.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/aoc/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/aoc/__init__.py",
    "content": ""
  },
  {
    "path": "openage/convert/value_object/conversion/aoc/internal_nyan_names.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\nUNIT_LINE_LOOKUPS = {\n    4: (\"Archer\", \"archer\"),\n    5: (\"HandCannoneer\", \"hand_cannoneer\"),\n    7: (\"Skirmisher\", \"skirmisher\"),\n    8: (\"Longbowman\", \"longbowman\"),\n    11: (\"Mangudai\", \"mangudai\"),\n    13: (\"FishingShip\", \"fishing_ship\"),\n    17: (\"TradeCog\", \"trade_cog\"),\n    25: (\"TeutonicKnight\", \"teutonic_knight\"),\n    35: (\"Ram\", \"ram\"),\n    36: (\"BombardCannon\", \"bombard_cannon\"),\n    38: (\"Knight\", \"knight\"),\n    39: (\"HorseArcher\", \"horse_archer\"),\n    40: (\"Cataphract\", \"cataphract\"),\n    41: (\"Huscarl\", \"huscarl\"),\n    46: (\"Janissary\", \"janissary\"),\n    48: (\"Boar\", \"boar\"),\n    65: (\"Deer\", \"deer\"),\n    73: (\"ChuKoNu\", \"chu_ko_nu\"),\n    74: (\"Militia\", \"militia\"),\n    93: (\"Spearman\", \"spearman\"),\n    118: (\"Villager\", \"villager\"),\n    125: (\"Monk\", \"monk\"),\n    128: (\"TradeCart\", \"trade_cart\"),\n    232: (\"WoadRaider\", \"woad_raider\"),\n    239: (\"WarElephant\", \"war_elephant\"),\n    250: (\"Longboat\", \"longboat\"),\n    279: (\"Scorpion\", \"scorpion\"),\n    280: (\"Mangonel\", \"mangonel\"),\n    281: (\"ThrowingAxeman\", \"throwing_axeman\"),\n    282: (\"Mameluke\", \"mameluke\"),\n    291: (\"Samurai\", \"samurai\"),\n    329: (\"CamelRider\", \"camel_rider\"),\n    331: (\"Trebuchet\", \"trebuchet\"),\n    420: (\"CannonGalleon\", \"cannon_galleon\"),\n    440: (\"Petard\", \"petard\"),\n    448: (\"ScoutCavalry\", \"scout_cavalry\"),\n    527: (\"DemolitionShip\", \"demo_ship\"),\n    529: (\"FireTrireme\", \"fire_trireme\"),\n    539: (\"Galley\", \"galley\"),\n    545: (\"TransportShip\", \"transport_ship\"),\n    594: (\"Sheep\", \"sheep\"),\n    692: (\"Berserk\", \"berserk\"),\n    725: (\"JaguarWarrior\", \"jaguar_warrior\"),\n    751: (\"EagleWarrior\", \"eagle_warrior\"),\n    755: (\"Tarkan\", \"tarkan\"),\n    763: (\"PlumedArcher\", \"plumed_archer\"),\n    771: (\"Conquistador\", \"conquistador\"),\n    775: (\"Missionary\", \"missionary\"),\n    827: (\"WarWaggon\", \"war_waggon\"),\n    831: (\"TurtleShip\", \"turtle_ship\"),\n    833: (\"Turkey\", \"turkey\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\nBUILDING_LINE_LOOKUPS = {\n    12: (\"Barracks\", \"barracks\"),\n    45: (\"Dock\", \"dock\"),\n    49: (\"SiegeWorkshop\", \"siege_workshop\"),\n    50: (\"Farm\", \"farm\"),\n    64: (\"StoneGate\", \"stone_gate\"),\n    68: (\"Mill\", \"mill\"),\n    70: (\"House\", \"house\"),\n    72: (\"PalisadeWall\", \"palisade\"),\n    79: (\"Tower\", \"tower\"),\n    82: (\"Castle\", \"castle\"),\n    84: (\"Market\", \"market\"),\n    87: (\"ArcheryRange\", \"archery_range\"),\n    101: (\"Stable\", \"stable\"),\n    103: (\"Blacksmith\", \"blacksmith\"),\n    104: (\"Monastery\", \"monastery\"),\n    109: (\"TownCenter\", \"town_center\"),\n    117: (\"StoneWall\", \"stone_wall\"),\n    199: (\"FishingTrap\", \"fishing_trap\"),\n    209: (\"University\", \"university\"),\n    236: (\"BombardTower\", \"bombard_tower\"),\n    276: (\"Wonder\", \"wonder\"),\n    562: (\"LumberCamp\", \"lumber_camp\"),\n    584: (\"MiningCamp\", \"mining_camp\"),\n    598: (\"Outpost\", \"outpost\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\nAMBIENT_GROUP_LOOKUPS = {\n    59: (\"BerryBush\", \"berry_bush\"),\n    66: (\"GoldMine\", \"gold_mine\"),\n    102: (\"StoneMine\", \"stone_mine\"),\n    285: (\"Relic\", \"relic\"),\n    348: (\"BambooForest\", \"bamboo_forest\"),\n    349: (\"OakTree\", \"oak_tree\"),\n    350: (\"Conifer\", \"conifer\"),\n    351: (\"PalmTree\", \"palm_tree\"),\n    411: (\"ForestTree\", \"forest_tree\"),\n    413: (\"SnowyConifer\", \"snowy_conifer\"),\n    414: (\"JungleTree\", \"jungle_tree\"),\n    709: (\"Cactus\", \"cactus\"),\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\nVARIANT_GROUP_LOOKUPS = {\n    69: (\"Shorefish\", \"shore_fish\", (69,), \"misc\"),\n    96: (\"Bird\", \"bird\", (96, 816), \"misc\"),\n    264: (\"Cliff\", \"cliff\", (264, 265, 266, 267, 268, 269, 270, 271, 272, 273), \"angle\"),\n    450: (\"BigOceanFish\", \"big_ocean_fish\", (450, 451), \"random\"),\n    455: (\"OceanFish\", \"ocean_fish\", (455, 456, 457, 458), \"random\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\nTECH_GROUP_LOOKUPS = {\n    2: (\"EliteTarkan\", \"elite_tarkan\"),\n    3: (\"Yeoman\", \"yeoman\"),\n    4: (\"ElDorado\", \"el_dorado\"),\n    5: (\"FurorCeltica\", \"furor_celtica\"),\n    6: (\"SiegeDrill\", \"siege_drill\"),\n    7: (\"Mahouts\", \"mahouts\"),\n    8: (\"TownWatch\", \"town_watch\"),\n    9: (\"Zealotry\", \"zealotry\"),\n    10: (\"Artillery\", \"artillery\"),\n    11: (\"Crenellations\", \"crenellations\"),\n    12: (\"CropRotation\", \"crop_rotation\"),\n    13: (\"HeavyPlow\", \"heavy_plow\"),\n    14: (\"HorseCollar\", \"horse_collar\"),\n    15: (\"Guilds\", \"guilds\"),\n    16: (\"Anarchy\", \"anarchy\"),\n    17: (\"Banking\", \"banking\"),\n    19: (\"Cartography\", \"cartography\"),\n    21: (\"Atheism\", \"atheism\"),\n    22: (\"Loom\", \"loom\"),\n    23: (\"Coinage\", \"coinage\"),\n    24: (\"GarlandWars\", \"garland_wars\"),\n    27: (\"ElitePlumedArcher\", \"elite_plumed_archer\"),\n    34: (\"WarGalley\", \"war_galley\"),\n    35: (\"Galleon\", \"galleon\"),\n    37: (\"CannonGalleon\", \"cannon_galleon\"),\n    39: (\"Husbandry\", \"husbandry\"),\n    45: (\"Faith\", \"faith\"),\n    47: (\"Chemistry\", \"chemistry\"),\n    48: (\"Caravan\", \"caravan\"),\n    49: (\"Berserkergang\", \"berserkergang\"),\n    50: (\"Masonry\", \"masonry\"),\n    51: (\"Architecture\", \"architecture\"),\n    52: (\"Rocketry\", \"rocketry\"),\n    54: (\"TreadmillCrane\", \"treadmill_crane\"),\n    55: (\"GoldMining\", \"gold_mining\"),\n    59: (\"Kataparuto\", \"kataparuto\"),\n    60: (\"EliteConquistador\", \"elite_conquistador\"),\n    61: (\"Logistica\", \"logistica\"),\n    63: (\"Keep\", \"keep\"),\n    64: (\"BombardTower\", \"bombard_tower\"),\n    67: (\"Forging\", \"forging\"),\n    68: (\"IronCasting\", \"iron_casting\"),\n    74: (\"ScaleMailArmor\", \"scale_mail_armor\"),\n    75: (\"BlastFurnace\", \"blast_furnace\"),\n    76: (\"ChainMailArmor\", \"chain_mail_armor\"),\n    77: (\"PlateMailArmor\", \"plate_mail_armor\"),\n    80: (\"PlateBardingArmor\", \"plate_barding_armor\"),\n    81: (\"ScaleBardingArmor\", \"scale_barding_armor\"),\n    82: (\"ChainBardingArmor\", \"chain_barding_armor\"),\n    83: (\"BeardedAxe\", \"bearded_axe\"),\n    90: (\"Tracking\", \"tracking\"),\n    93: (\"Ballistics\", \"ballistics\"),\n    96: (\"CappedRam\", \"capped_ram\"),\n    98: (\"EliteSkirmisher\", \"elite_skirmisher\"),\n    100: (\"Crossbowman\", \"crossbowman\"),\n    101: (\"FeudalAge\", \"feudal_age\"),\n    102: (\"CastleAge\", \"castle_age\"),\n    103: (\"ImperialAge\", \"imperial_age\"),\n    140: (\"GuardTower\", \"guard_tower\"),\n    182: (\"GoldShaftMining\", \"gold_shaft_mining\"),\n    194: (\"FortifiedWall\", \"fortified_wall\"),\n    197: (\"Pikeman\", \"pikeman\"),\n    199: (\"Fletching\", \"fletching\"),\n    200: (\"BodkinArrow\", \"bodkin_arrow\"),\n    201: (\"Bracer\", \"bracer\"),\n    202: (\"DoubleBitAxe\", \"double_bit_axe\"),\n    203: (\"BowSaw\", \"bow_saw\"),\n    207: (\"Longswordsman\", \"longswordsman\"),\n    209: (\"Chevalier\", \"chevalier\"),\n    211: (\"PaddedArcherArmor\", \"padded_archer_armor\"),\n    212: (\"LeatherArcherArmor\", \"leather_archer_armor\"),\n    213: (\"WheelBarrow\", \"wheel_barrow\"),\n    215: (\"Squires\", \"squires\"),\n    217: (\"TwoHandedSwordsman\", \"two_handed_swordsman\"),\n    218: (\"HeavyCavalryArcher\", \"heavy_cavalry_archer\"),\n    219: (\"RingArcherArmor\", \"ring_archer_armor\"),\n    221: (\"TwoManSaw\", \"two_man_saw\"),\n    222: (\"Swordsman\", \"swordsman\"),\n    230: (\"BlockPrinting\", \"block_printing\"),\n    231: (\"Sanctity\", \"sanctity\"),\n    233: (\"Illumination\", \"illumination\"),\n    236: (\"HeavyCamelRider\", \"heavy_camel_rider\"),\n    237: (\"Arbalest\", \"arbalest\"),\n    239: (\"HeavyScorpion\", \"heavy_scorpion\"),\n    244: (\"HeavyDemolitionShip\", \"heavy_demolition_ship\"),\n    246: (\"FastFireShip\", \"fast_fire_ship\"),\n    249: (\"HandCart\", \"hand_cart\"),\n    252: (\"Fervor\", \"fervor\"),\n    254: (\"LightCavalry\", \"light_cavalry\"),\n    255: (\"SiegeRam\", \"siege_ram\"),\n    257: (\"Onager\", \"onager\"),\n    264: (\"Champion\", \"champion\"),\n    265: (\"Paladin\", \"paladin\"),\n    278: (\"StoneMining\", \"stone_mining\"),\n    279: (\"StoneShaftMining\", \"stone_shaft_mining\"),\n    280: (\"TownPatrol\", \"town_patrol\"),\n    315: (\"Conscription\", \"conscription\"),\n    316: (\"Redemption\", \"redemption\"),\n    319: (\"Atonement\", \"atonement\"),\n    320: (\"SiegeOnager\", \"siege_onager\"),\n    321: (\"Sappers\", \"sappers\"),\n    322: (\"MurderHoles\", \"murder_holes\"),\n    360: (\"EliteLongbowman\", \"elite_longbowman\"),\n    361: (\"EliteCataphract\", \"elite_cataphract\"),\n    362: (\"EliteChoKoNu\", \"elite_cho_ko_nu\"),\n    363: (\"EliteThrowingAxeman\", \"elite_throwing_axeman\"),\n    364: (\"EliteTeutonicKnight\", \"elite_teutonic_knight\"),\n    365: (\"EliteHuscarl\", \"elite_huscarl\"),\n    366: (\"EliteSamurai\", \"elite_samurai\"),\n    367: (\"EliteWarElephant\", \"elite_war_elephant\"),\n    368: (\"EliteMameluke\", \"elite_mameluke\"),\n    369: (\"EliteJanissary\", \"elite_janissary\"),\n    370: (\"EliteWoadRaider\", \"elite_woad_raider\"),\n    371: (\"EliteMangudai\", \"elite_mangudai\"),\n    372: (\"EliteLongboat\", \"elite_longboat\"),\n    373: (\"Shipwright\", \"shipwright\"),\n    374: (\"Careening\", \"careening\"),\n    375: (\"DryDock\", \"dry_dock\"),\n    376: (\"EliteWarGalley\", \"elite_war_galley\"),\n    377: (\"SiegeEngineers\", \"siege_engineers\"),\n    379: (\"Hoardings\", \"hoardings\"),\n    380: (\"HeatedShot\", \"heated_shot\"),\n    398: (\"EliteBerserk\", \"elite_berserk\"),\n    408: (\"Spies\", \"spies\"),\n    428: (\"Hussar\", \"hussar\"),\n    429: (\"Helbardier\", \"helbardier\"),\n    432: (\"EliteJaguarWarrior\", \"elite_jaguar_warrior\"),\n    434: (\"EliteEagleWarrior\", \"elite_eagle_warrior\"),\n    435: (\"Bloodlines\", \"bloodlines\"),\n    436: (\"ParthianTactics\", \"parthian_tactics\"),\n    437: (\"ThumbRing\", \"thumb_ring\"),\n    438: (\"Theocracy\", \"theocracy\"),\n    439: (\"Heresy\", \"heresy\"),\n    440: (\"Supremacy\", \"supremacy\"),\n    441: (\"HerbalMedicine\", \"herbal_medicine\"),\n    445: (\"Shinkichon\", \"shinkichon\"),\n    448: (\"EliteTurtleShip\", \"elite_turtle_ship\"),\n    450: (\"EliteWarWaggon\", \"elite_war_waggon\"),\n    457: (\"Perfusion\", \"perfusion\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\nCIV_GROUP_LOOKUPS = {\n    0: (\"Gaia\", \"gaia\"),\n    1: (\"Britons\", \"britons\"),\n    2: (\"Franks\", \"franks\"),\n    3: (\"Goths\", \"goths\"),\n    4: (\"Teutons\", \"teutons\"),\n    5: (\"Japanese\", \"japanese\"),\n    6: (\"Chinese\", \"chinese\"),\n    7: (\"Byzantines\", \"byzantines\"),\n    8: (\"Persians\", \"persians\"),\n    9: (\"Saracens\", \"saracens\"),\n    10: (\"Turks\", \"turks\"),\n    11: (\"Vikings\", \"vikings\"),\n    12: (\"Mongols\", \"mongols\"),\n    13: (\"Celts\", \"celts\"),\n    14: (\"Spanish\", \"spanish\"),\n    15: (\"Aztecs\", \"aztecs\"),\n    16: (\"Mayans\", \"mayans\"),\n    17: (\"Huns\", \"huns\"),\n    18: (\"Koreans\", \"koreans\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\nGRAPHICS_SET_LOOKUPS = {\n    0: ((0, 1, 2, 13, 14), \"WesternEuropean\", \"western_european\"),\n    1: ((3, 4, 11, 17), \"CentralEuropean\", \"central_european\"),\n    2: ((5, 6, 12, 18, 31), \"EastAsian\", \"east_asian\"),\n    3: ((8, 9, 10), \"MiddleEastern\", \"middle_eastern\"),\n    4: ((7,), \"Byzantine\", \"byzantine\"),\n    5: ((15, 16, 21), \"MesoAmerican\", \"meso\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_GROUP_LOOKUPS = {\n    0:  ((0, 1, 4, 7, 8, 10, 20,), \"Grass\", \"grass\"),\n    1:  ((0, 3, 13, 15, 21,), \"Water\", \"water\"),\n    2:  ((0, 2, 3, 6, 7,), \"Beach\", \"beach\"),\n    3:  ((0, 1, 4, 7, 8, 10, 20,), \"Dirt3\", \"dirt3\"),\n    4:  ((0, 1, 3, 6, 7, 21,), \"Shallows\", \"shallows\"),\n    5:  ((0, 1, 4, 7, 8, 10, 20,), \"Leaves\", \"leaves\"),\n    6:  ((0, 1, 4, 7, 8, 10, 20,), \"Dirt\", \"dirt\"),\n    7:  ((0, 1, 4, 7, 10, 20,), \"FarmCrops\", \"farm_crops\"),\n    8:  ((0, 1, 4, 7, 10, 20,), \"FarmHarvested\", \"farm_harvested\"),\n    9:  ((0, 1, 4, 7, 8, 10, 20,), \"Grass3\", \"grass3\"),\n    10: ((0, 1, 4, 7, 8, 10, 20,), \"Forest\", \"forest\"),\n    11: ((0, 1, 4, 7, 8, 10, 20,), \"Dirt2\", \"dirt2\"),\n    12: ((0, 1, 4, 7, 8, 10, 20,), \"Grass2\", \"grass2\"),\n    13: ((0, 1, 4, 7, 8, 10, 20,), \"PalmDesert\", \"palm_desert\"),\n    14: ((0, 1, 4, 7, 8, 10, 20,), \"Desert\", \"desert\"),\n    15: ((0, 3, 13, 15, 21,), \"WaterOld\", \"water_old\"),\n    16: ((0, 1, 4, 7, 8, 10, 20,), \"GrassOld\", \"grass_old\"),\n    17: ((0, 4, 7, 8, 10, 20,), \"Jungle\", \"jungle\"),\n    18: ((0, 1, 4, 7, 8, 10, 20,), \"BambooForest\", \"bamboo_forest\"),\n    19: ((0, 1, 4, 7, 8, 10, 20,), \"PineForest\", \"pine-forest\"),\n    20: ((0, 1, 4, 7, 8, 10, 20,), \"OakForest\", \"oak_forest\"),\n    21: ((0, 1, 4, 7, 8, 10, 20,), \"SnowForest\", \"snow_forest\"),\n    22: ((0, 3, 13, 15, 21,), \"Water2\", \"water2\"),\n    23: ((0, 3, 13, 15, 21,), \"Water3\", \"water3\"),\n    24: ((0, 1, 4, 7, 8, 10, 20,), \"Road\", \"road\"),\n    25: ((0, 1, 4, 7, 8, 10, 20,), \"RoadWeathered\", \"road_weathered\"),\n    26: ((0, 4, 7, 8, 10, 20,), \"Ice\", \"ice\"),\n    27: ((0, 1, 4, 7, 8, 10, 20,), \"Foundation\", \"foundation\"),\n    28: ((0, 3, 13, 15, 21,), \"WaterBridge\", \"water_bridge\"),\n    29: ((0, 1, 4, 7, 10, 20,), \"FarmConstruction1\", \"farm_construction1\"),\n    30: ((0, 1, 4, 7, 10, 20,), \"FarmConstruction2\", \"farm_construction2\"),\n    31: ((0, 1, 4, 7, 10, 20,), \"FarmConstruction3\", \"farm_construction3\"),\n    32: ((0, 1, 4, 7, 8, 10, 20,), \"Snow\", \"snow\"),\n    33: ((0, 1, 4, 7, 8, 10, 20,), \"SnowDesert\", \"snow_desert\"),\n    34: ((0, 1, 4, 7, 8, 10, 20,), \"SnowGrass\", \"snow_grass\"),\n    35: ((0, 4, 7, 8, 10, 20,), \"Ice2\", \"ice2\"),\n    36: ((0, 1, 4, 7, 8, 10, 20,), \"SnowFoundation\", \"snow_foundation\"),\n    37: ((0, 2, 3, 6, 7,), \"IceBeach\", \"ice_beach\"),\n    38: ((0, 1, 4, 7, 8, 10, 20,), \"RoadSnow\", \"road_snow\"),\n    39: ((0, 1, 4, 7, 8, 10, 20,), \"RoadMossy\", \"road_mossy\"),\n    40: ((0, 1, 4, 7, 8, 10, 20,), \"KOH\", \"koh\"),\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_TYPE_LOOKUPS = {\n    0: ((0, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 24,\n         25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40),\n        (0, 1, 4, 7, 8, 10, 11, 12, 14, 16, 18, 20,),\n        \"Land\",),\n    1: ((1, 15, 22, 23, 28),\n        (0, 3, 6, 13, 17, 19,),\n        \"Water\",),\n    2: ((2, 37),\n        (0, 2, 7, 10, 12, 14, 16, 18, 20,),\n        \"Beach\",),\n    3: ((4,),\n        (0, 1, 7, 12, 14, 18, 21,),\n        \"Shallow\",),\n    4: ((26, 35, 37),\n        (0, 1, 4, 7, 8, 10, 11, 12, 14, 18, 19, 20,),\n        \"Ice\",),\n    5: ((7, 8, 29, 30, 31),\n        (0, 1, 4, 7, 10, 12, 14, 18, 20),\n        \"Farm\",),\n}\n\n\nCLASS_ID_LOOKUPS = {\n    0: \"Archer\",\n    1: \"Artifact\",\n    2: \"TradeBoat\",\n    3: \"BuildingMisc\",\n    4: \"Villager\",\n    5: \"OceanFish\",\n    6: \"Infantry\",\n    7: \"BerryBush\",\n    8: \"StoneMine\",\n    9: \"AnimalPrey\",\n    10: \"AnimalPredator\",\n    11: \"DeadOrProjectileOrBird\",     # do not use this as GameEntityType\n    12: \"Cavalry\",\n    13: \"SiegeWeapon\",\n    14: \"Ambient\",\n    15: \"Tree\",\n    18: \"Monk\",\n    19: \"TradecCart\",\n    20: \"TransportShip\",\n    21: \"FishingShip\",\n    22: \"Warship\",\n    23: \"Conquistador\",\n    27: \"Wall\",\n    28: \"Phalanx\",\n    29: \"DomesticAnimal\",\n    30: \"AmbientFlag\",\n    31: \"DeepSeaFish\",\n    32: \"GoldMine\",\n    33: \"ShoreFish\",\n    34: \"Cliff\",\n    35: \"Petard\",\n    36: \"CavalryArcher\",\n    37: \"Doppelgaenger\",\n    38: \"Bird\",\n    39: \"Gate\",\n    40: \"AmbientPile\",\n    41: \"AmbientResourcePile\",\n    42: \"Relic\",\n    43: \"MonkWithRelic\",            # should not be present in final modpack\n    44: \"HandConnoneer\",\n    45: \"TwoHandedSwordsman\",       # should not be present in final modpack (unused anyway)\n    46: \"Pikeman\",                  # should not be present in final modpack (unused anyway)\n    47: \"CavalryScout\",\n    48: \"OreMine\",\n    49: \"Restockable\",\n    50: \"Spearman\",\n    51: \"Trebuchet\",                # packed\n    52: \"Tower\",\n    53: \"BoardingShip\",\n    54: \"Trebuchet\",                # unpacked\n    55: \"Scorpion\",\n    56: \"Raider\",\n    57: \"CavalryRaider\",\n    58: \"Herdable\",\n    59: \"King\",\n    61: \"Horse\",\n}\n\n# key: genie unit id; value: Gather ability name\nGATHER_TASK_LOOKUPS = {\n    13: (\"Fish\", \"fish\"),  # fishing boat\n    56: (\"Fish\", \"fish\"),  # male\n    57: (\"Fish\", \"fish\"),  # female\n    120: (\"CollectBerries\", \"collect_berries\"),  # male\n    354: (\"CollectBerries\", \"collect_berries\"),  # female\n    122: (\"HarvestGame\", \"harvest_game\"),  # male\n    216: (\"HarvestGame\", \"harvest_game\"),  # female\n    123: (\"ChopWood\", \"chop_wood\"),  # male\n    218: (\"ChopWood\", \"chop_wood\"),  # female\n    124: (\"MineStone\", \"mine_stone\"),  # male\n    220: (\"MineStone\", \"mine_stone\"),  # female\n    214: (\"FarmCrops\", \"farm_crops\"),  # male\n    259: (\"FarmCrops\", \"farm_crops\"),  # female\n    579: (\"MineGold\", \"mine_gold\"),  # male\n    581: (\"MineGold\", \"mine_gold\"),  # female\n    590: (\"HarvestLivestock\", \"harvest_livestock\"),  # male\n    592: (\"HarvestLivestock\", \"harvest_livestock\"),  # female\n}\n\n# key: restock target unit id; value: Gather ability name\nRESTOCK_TARGET_LOOKUPS = {\n    50: (\"ReseedFarm\", \"reseed_farm\"),\n}\n\n# key: armor class; value: Gather ability name\nARMOR_CLASS_LOOKUPS = {\n    1: \"Infantry\",\n    2: \"TurtleShip\",\n    3: \"Pierce\",\n    4: \"Melee\",\n    5: \"WarElephant\",\n    8: \"Cavalry\",\n    11: \"BuildingNoPort\",\n    13: \"StoneDefense\",\n    15: \"Archer\",\n    16: \"ShipCamelSaboteur\",\n    17: \"Ram\",\n    18: \"Tree\",\n    19: \"UniqueUnit\",\n    20: \"SiegeWeapon\",\n    21: \"Building\",\n    22: \"Wall\",\n    24: \"Boar\",\n    25: \"Monk\",\n    26: \"Castle\",\n    27: \"Spearman\",\n    28: \"CavalryArchers\",\n    29: \"EagleWarrior\",\n}\n\n# key: command type; value: Apply*Effect ability name\nCOMMAND_TYPE_LOOKUPS = {\n    7: (\"Attack\", \"attack\"),\n    101: (\"Construct\", \"construct\"),\n    104: (\"Convert\", \"convert\"),\n    105: (\"Heal\", \"heal\"),\n    106: (\"Repair\", \"repair\"),\n    110: (\"Hunt\", \"hunt\"),\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/de1/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/de1/__init__.py",
    "content": ""
  },
  {
    "path": "openage/convert/value_object/conversion/de1/internal_nyan_names.py",
    "content": "# Copyright 2021-2021 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: armor class; value: Gather ability name\n# contains only new armor classes of DE1\nARMOR_CLASS_LOOKUPS = {\n    17: \"Wall\",\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/de2/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/de2/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II: Definitive Edition.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/de2/internal_nyan_names.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new units of DE2\nUNIT_LINE_LOOKUPS = {\n    705: (\"Cow\", \"cow\"),\n    1225: (\"KonnikCastle\", \"konnik\"),                 # Castle unit\n    1228: (\"Keshik\", \"keshik\"),\n    1231: (\"Kipchak\", \"kipchak\"),\n    1234: (\"Leitis\", \"leitis\"),\n    1239: (\"Ibex\", \"ibex\"),\n    1243: (\"Goose\", \"goose\"),\n    1245: (\"Pig\", \"pig\"),\n    1254: (\"KonnikKrepost\", \"konnik\"),                 # Krepost unit\n    1258: (\"Ram\", \"ram\"),                       # replacement for ID 35?\n    1263: (\"FlamingCamel\", \"flaming_camel\"),\n\n    # LOTW\n    1370: (\"SteppeLancer\", \"steppe_lancer\"),\n    1655: (\"Coustillier\", \"coustillier\"),\n    1658: (\"Serjeant\", \"serjeant\"),\n    1660: (\"DSerjeant\", \"dserjeant\"),           # Donjon Serjeant\n    # 1663: (\"FlemishMilitia\", \"flemish_militia\"),\n    1699: (\"FlemishMilitia\", \"flemish_militia\"),\n\n    # DOTD\n    1701: (\"Obuch\", \"obuch\"),\n    1704: (\"HussiteWagon\", \"hussite_wagon\"),\n    1709: (\"Houfnice\", \"houfnice\"),\n\n    # DOI\n    1735: (\"UrumiSwordsman\", \"urumi_swordsman\"),\n    1738: (\"MRatha\", \"mratha\"),                 # Melee Rhata\n    1741: (\"ChakramThrower\", \"chakram_thrower\"),\n    1744: (\"ArmoredElephant\", \"armored_elephant\"),\n    1747: (\"Ghulam\", \"ghulam\"),\n    1750: (\"Thirisadai\", \"thirisadai\"),\n    1751: (\"ShrivamshaRider\", \"shrivamsha_rider\"),\n    1755: (\"CamelScout\", \"camel_scout\"),        # technically in line with Camel rider\n    1759: (\"RRatha\", \"rratha\"),                 # Ranged Rhata\n\n    # ROR\n    1790: (\"Centurion\", \"centurion\"),\n    1795: (\"Dromon\", \"dromon\"),\n\n    # TMR\n    1800: (\"CompositeBowman\", \"composite_bowman\"),\n    1803: (\"Monaspa\", \"monaspa\"),\n    1811: (\"WarriorPriest\", \"warrior_priest\"),\n    1813: (\"Savar\", \"savar\"),\n    1817: (\"QizilbashWarrior\", \"qizilbash_warrior\"),\n\n    # TODO: These are upgrades\n    1737: (\"EliteUrumiSwordsman\", \"elite_urumi_swordsman\"),\n    1743: (\"EliteChakramThrower\", \"elite_chakram_thrower\"),\n    1746: (\"SiegeElephant\", \"siege_elephant\"),\n    1749: (\"EliteGhumlam\", \"elite_ghumlam\"),\n    1753: (\"EliteShrivamshaRider\", \"elite_shrivamsha_rider\"),\n    1761: (\"EliteRRatha\", \"elite_rratha\"),\n    1792: (\"EliteCenturion\", \"elite_centurion\"),\n    1793: (\"Legionary\", \"legionary\"),\n\n    1802: (\"EliteCompositeBowman\", \"elite_composite_bowman\"),\n    1805: (\"EliteMonaspa\", \"elite_monaspa\"),\n\n    # BfG\n    2101: (\"BfGUnkown_2101\", \"bfg_unkown_2101\"),\n    2102: (\"BfGUnkown_2102\", \"bfg_unkown_2102\"),\n    2104: (\"BfGUnkown_2104\", \"bfg_unkown_2104\"),\n    2105: (\"BfGUnkown_2105\", \"bfg_unkown_2105\"),\n    2107: (\"BfGUnkown_2107\", \"bfg_unkown_2107\"),\n    2108: (\"BfGUnkown_2108\", \"bfg_unkown_2108\"),\n    2110: (\"BfGUnkown_2110\", \"bfg_unkown_2110\"),\n    2111: (\"BfGUnkown_2111\", \"bfg_unkown_2111\"),\n    2123: (\"BfGUnkown_2123\", \"bfg_unkown_2123\"),\n    2124: (\"BfGUnkown_2124\", \"bfg_unkown_2124\"),\n    2125: (\"BfGUnkown_2125\", \"bfg_unkown_2125\"),\n    2126: (\"BfGUnkown_2126\", \"bfg_unkown_2126\"),\n    2127: (\"BfGUnkown_2127\", \"bfg_unkown_2127\"),\n    2128: (\"BfGUnkown_2128\", \"bfg_unkown_2128\"),\n    2129: (\"BfGUnkown_2129\", \"bfg_unkown_2129\"),\n    2130: (\"BfGUnkown_2130\", \"bfg_unkown_2130\"),\n    2131: (\"BfGUnkown_2131\", \"bfg_unkown_2131\"),\n    2132: (\"BfGUnkown_2132\", \"bfg_unkown_2132\"),\n    2133: (\"BfGUnkown_2133\", \"bfg_unkown_2133\"),\n    2134: (\"BfGUnkown_2134\", \"bfg_unkown_2134\"),\n    2135: (\"BfGUnkown_2135\", \"bfg_unkown_2135\"),\n    2138: (\"BfGUnkown_2138\", \"bfg_unkown_2138\"),\n    2139: (\"BfGUnkown_2139\", \"bfg_unkown_2139\"),\n    2140: (\"BfGUnkown_2140\", \"bfg_unkown_2140\"),\n    2148: (\"BfGUnkown_2148\", \"bfg_unkown_2148\"),\n    2149: (\"BfGUnkown_2149\", \"bfg_unkown_2149\"),\n    2150: (\"BfGUnkown_2150\", \"bfg_unkown_2150\"),\n    2151: (\"BfGUnkown_2151\", \"bfg_unkown_2151\"),\n    2162: (\"BfGUnkown_2162\", \"bfg_unkown_2162\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new buildings of DE2\nBUILDING_LINE_LOOKUPS = {\n    1251: (\"Krepost\", \"krepost\"),\n\n    # LOTW\n    1665: (\"Donjon\", \"donjon\"),\n\n    # DOTD\n    1734: (\"Folwark\", \"folwark\"),\n\n    # DOI\n    1754: (\"Caravanserai\", \"caravanserai\"),\n\n    # TMR\n    1808: (\"MuleCart\", \"mule_cart\"),\n\n    # BfG\n    2119: (\"BfGUnkown_2119\", \"bfg_unkown_2119\"),\n    2172: (\"BfGUnkown_2172\", \"bfg_unkown_2172\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\n# contains only new/changed ambience of DE2\nAMBIENT_GROUP_LOOKUPS = {\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\n# contains only new/changed variants of DE2\nVARIANT_GROUP_LOOKUPS = {\n    264: (\"Cliff\", \"cliff\", (264, 265, 266, 267, 268, 269, 270, 271, 272), \"angle\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new techs of DE2\nTECH_GROUP_LOOKUPS = {\n    46: (\"Devotion\", \"devotion\"),\n\n    488: (\"Kamandaran\", \"kamandaran\"),\n    678: (\"EliteKonnik\", \"elite_konnik\"),\n    680: (\"EliteKeshik\", \"elite_keshik\"),\n    682: (\"EliteKipchak\", \"elite_kipchak\"),\n    684: (\"EliteLeitis\", \"elite_leitis\"),\n    685: (\"Stirrups\", \"stirrups\"),\n    686: (\"Bagains\", \"bagains\"),\n    687: (\"SilkArmor\", \"silk_armor\"),\n    688: (\"TimuridSiegecraft\", \"timurid_siegecraft\"),\n    689: (\"SteppeHusbandry\", \"steppe_husbandry\"),\n    690: (\"CumanMercanaries\", \"cuman_mercinaries\"),\n    691: (\"HillForts\", \"hill_forts\"),\n    692: (\"TowerShields\", \"tower_shields\"),\n    715: (\"EliteSteppeLancer\", \"elite_steppe_lancer\"),\n    716: (\"Supplies\", \"supplies\"),\n\n    # LOTW\n    751: (\"EliteCoustillier\", \"elite_coustillier\"),\n    753: (\"EliteSerjeant\", \"elite_serjeant\"),\n    754: (\"BurgundianVineyatds\", \"burgundian_vineyards\"),\n    755: (\"FlemishRevolution\", \"flemish_revolution\"),\n    756: (\"FirstCrusade\", \"first_crusade\"),\n    757: (\"Scutage\", \"scutage\"),\n\n    # DOTD\n    779: (\"EliteObuch\", \"elite_obuch\"),\n    781: (\"EliteHussiteWagon\", \"elite_hussite_wagon\"),\n    782: (\"SzlachtaPriviledges\", \"slzachte_priviledges\"),\n    783: (\"LechiticLegacy\", \"lechitic_legacy\"),\n    784: (\"WagenburgTactics\", \"wagenburg_tactics\"),\n    785: (\"HussiteReforms\", \"hussite_reforms\"),\n    786: (\"WingedHussar\", \"winged_hussar\"),\n    787: (\"Houfnice\", \"houfnice\"),\n    793: (\"Folwark\", \"folwark\"),\n\n    # DOI\n    828: (\"EliteRatha\", \"elite_ratha\"),\n    830: (\"EliteChakramThrower\", \"elite_chakram_thrower\"),\n    831: (\"MedicalCorps\", \"medical_corps\"),\n    832: (\"WootzSteel\", \"wootz_steel\"),\n    833: (\"Paiks\", \"paiks\"),\n    834: (\"Mahayana\", \"mahayana\"),\n    835: (\"Kshatriyas\", \"kshatriyas\"),\n    836: (\"FrontierGuards\", \"frontier_guards\"),\n    838: (\"SiegeElephant\", \"siege_elephant\"),\n    840: (\"EliteGhulam\", \"elite_ghulam\"),\n    843: (\"EliteSHrivamshaRider\", \"elite_shrivamsha_rider\"),\n\n    875: (\"Gambesons\", \"gambesons\"),\n\n    # ROR\n    882: (\"EliteCenturion\", \"elite_centurion\"),\n    883: (\"Ballistas\", \"ballistas\"),\n    884: (\"Comitatensis\", \"comitatensis\"),\n    885: (\"Legionary\", \"legionary\"),\n\n    # TMR\n    918: (\"EliteCompositeBowman\", \"elite_composite_bowman\"),\n    920: (\"EliteMonaspa\", \"elite_monaspa\"),\n    921: (\"Fereters\", \"fereters\"),\n    922: (\"CilicianFleet\", \"cilician_fleet\"),\n    923: (\"SvanTowers\", \"svan_towers\"),\n    924: (\"AsnuariCavalry\", \"asnuari_cavalry\"),\n    929: (\"FortifiedChurch\", \"fortified_church\"),\n    967: (\"EliteQizilbashWarrior\", \"elite_qizilbash_warrior\"),\n\n    # BfG\n    1110: (\"BfGUnkown_1110\", \"bfg_unkown_1110\"),\n    1111: (\"BfGUnkown_1111\", \"bfg_unkown_1111\"),\n    1112: (\"BfGUnkown_1112\", \"bfg_unkown_1112\"),\n    1113: (\"BfGUnkown_1113\", \"bfg_unkown_1113\"),\n    1120: (\"BfGUnkown_1120\", \"bfg_unkown_1120\"),\n    1121: (\"BfGUnkown_1121\", \"bfg_unkown_1121\"),\n    1122: (\"BfGUnkown_1122\", \"bfg_unkown_1122\"),\n    1123: (\"BfGUnkown_1123\", \"bfg_unkown_1123\"),\n    1130: (\"BfGUnkown_1130\", \"bfg_unkown_1130\"),\n    1131: (\"BfGUnkown_1131\", \"bfg_unkown_1131\"),\n    1132: (\"BfGUnkown_1132\", \"bfg_unkown_1132\"),\n    1133: (\"BfGUnkown_1133\", \"bfg_unkown_1133\"),\n    1161: (\"BfGUnkown_1161\", \"bfg_unkown_1161\"),\n    1162: (\"BfGUnkown_1162\", \"bfg_unkown_1162\"),\n    1165: (\"BfGUnkown_1165\", \"bfg_unkown_1165\"),\n    1167: (\"BfGUnkown_1167\", \"bfg_unkown_1167\"),\n    1173: (\"BfGUnkown_1173\", \"bfg_unkown_1173\"),\n    1198: (\"BfGUnkown_1198\", \"bfg_unkown_1198\"),\n    1202: (\"BfGUnkown_1202\", \"bfg_unkown_1202\"),\n    1203: (\"BfGUnkown_1203\", \"bfg_unkown_1203\"),\n    1204: (\"BfGUnkown_1204\", \"bfg_unkown_1204\"),\n    1223: (\"BfGUnkown_1223\", \"bfg_unkown_1223\"),\n    1224: (\"BfGUnkown_1224\", \"bfg_unkown_1224\"),\n    1225: (\"BfGUnkown_1225\", \"bfg_unkown_1225\"),\n    1226: (\"BfGUnkown_1226\", \"bfg_unkown_1226\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\n# contains only new civs of DE2\nCIV_GROUP_LOOKUPS = {\n    32: (\"Bulgarians\", \"bulgarians\"),\n    33: (\"Tatars\", \"tatars\"),\n    34: (\"Cumans\", \"cumans\"),\n    35: (\"Lithuanians\", \"lithuanians\"),\n\n    # LOTW\n    36: (\"Burgundians\", \"burgundians\"),\n    37: (\"Sicilians\", \"sicilians\"),\n\n    # DOTD\n    38: (\"Poles\", \"poles\"),\n    39: (\"Bohemians\", \"bohemians\"),\n\n    # DOI\n    20: (\"Hindustanis\", \"hindustanis\"),     # Indians in HD\n    40: (\"Dravidians\", \"dravidians\"),\n    41: (\"Bengalis\", \"bengalis\"),\n    42: (\"Gurjaras\", \"gurjaras\"),\n\n    # ROR\n    43: (\"Romans\", \"romans\"),\n\n    # TMR\n    44: (\"Armenians\", \"armenians\"),\n    45: (\"Georgians\", \"georgians\"),\n\n    # BfG\n    46: (\"BfGUnkown_46\", \"bfg_unkown_46\"),\n    47: (\"BfGUnkown_47\", \"bfg_unkown_47\"),\n    48: (\"BfGUnkown_48\", \"bfg_unkown_48\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\n# contains only new/changed graphic sets of DE2\nGRAPHICS_SET_LOOKUPS = {\n    0: ((0, 1, 2, 13, 14, 36), \"WesternEuropean\", \"western_european\"),\n    4: ((7, 37), \"Byzantine\", \"byzantine\"),\n    6: ((19, 24, 43, 44, 45, 46, 47, 48), \"Mediterranean\", \"mediterranean\"),\n    7: ((20, 40, 41, 42), \"Indian\", \"indian\"),\n    8: ((22, 23, 32, 35, 38, 39), \"EasternEuropean\", \"eastern_european\"),\n    11: ((33, 34), \"CentralAsian\", \"central_asian\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrains of DE2\nTERRAIN_GROUP_LOOKUPS = {\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrain types of DE2\nTERRAIN_TYPE_LOOKUPS = {\n}\n\n# key: armor class; value: Gather ability name\n# contains only new armors of DE2\nARMOR_CLASS_LOOKUPS = {\n    0: \"Wonder\",\n    31: \"AntiLetis\",\n    32: \"DE2Condottiero\",\n    34: \"DE2FishingShip\",\n    35: \"DE2Mameluke\",\n    36: \"DE2Hero\",\n    37: \"SiegeBallista\",\n    38: \"DE2Skirmisher\",\n    39: \"DE2CamelRider\",\n    40: \"BfGUnkown_40\",\n    60: \"BfGUnkown_60\",\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/forward_ref.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nForward references point to an object that is not created yet.\nThis can be utilized to avoid cyclic dependencies like A->B\nwhile B->A during conversion. The pointer can be resolved\nonce the object has been created.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.conversion.converter_object import ConverterObjectGroup, \\\n        RawAPIObject\n    from openage.nyan.nyan_structs import NyanObject\n\n\nclass ForwardRef:\n    \"\"\"\n    Declares a forward reference to a RawAPIObject.\n    \"\"\"\n\n    __slots__ = ('group_object', 'raw_api_object_ref')\n\n    def __init__(self, converter_object_group: ConverterObjectGroup, raw_api_object_ref: str):\n        \"\"\"\n        Creates a forward reference to a RawAPIObject that will be created\n        by a converter object group.\n\n        :param converter_object_group: ConverterObjectGroup where the RawAPIObject\n                                       will be created.\n        :type converter_object_group: ConverterObjectGroup\n        :param raw_api_object_ref: Reference of the RawAPIObject in the group.\n        :type raw_api_object_ref: str\n        \"\"\"\n\n        self.group_object = converter_object_group\n        self.raw_api_object_ref = raw_api_object_ref\n\n    def resolve(self) -> NyanObject:\n        \"\"\"\n        Returns the nyan object reference for the pointer.\n        \"\"\"\n        raw_api_obj = self.group_object.get_raw_api_object(self.raw_api_object_ref)\n\n        return raw_api_obj.get_nyan_object()\n\n    def resolve_raw(self) -> RawAPIObject:\n        \"\"\"\n        Returns the raw API object reference for the pointer.\n        \"\"\"\n        return self.group_object.get_raw_api_object(self.raw_api_object_ref)\n\n    def __repr__(self):\n        return f\"ForwardRef<{self.raw_api_object_ref}>\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n)\n\nadd_subdirectory(ak)\nadd_subdirectory(fgt)\nadd_subdirectory(raj)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II: HD Edition.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/ak/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/ak/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II: HD Edition- African Kingdoms.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/ak/internal_nyan_names.py",
    "content": "# Copyright 2020-2021 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new units of AK\nUNIT_LINE_LOOKUPS = {\n    583: (\"Genitour\", \"genitour\"),\n    885: (\"SiegeTower\", \"siege_tower\"),             # old version of ID 1105 with combat\n    936: (\"Elephant\", \"elephant\"),\n    1001: (\"OrganGun\", \"organ_gun\"),\n    1004: (\"Caravel\", \"caravel\"),\n    1007: (\"CamelArcher\", \"camel_archer\"),\n    1010: (\"Genitour\", \"genitour\"),\n    1013: (\"Gbeto\", \"gbeto\"),\n    1016: (\"ShotelWarrior\", \"shotel_warrior\"),\n    1019: (\"Zebra\", \"zebra\"),\n    1026: (\"Ostrich\", \"ostrich\"),\n    1029: (\"Lion\", \"lion\"),\n    1031: (\"Crocodile\", \"crocodile\"),\n    1060: (\"Goat\", \"goat\"),\n    1103: (\"FireGalley\", \"fire_galley\"),\n    1104: (\"DemolitionRaft\", \"demolition_raft\"),\n    1105: (\"SiegeTower\", \"siege_tower\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new buildings of AK\nBUILDING_LINE_LOOKUPS = {\n    1021: (\"Feitoria\", \"feitoria\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\n# contains only new/changed ambience of AK\nAMBIENT_GROUP_LOOKUPS = {\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\n# contains only new/changed variants of AK\nVARIANT_GROUP_LOOKUPS = {\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new techs of AK\nTECH_GROUP_LOOKUPS = {\n    563: (\"EliteOrganGun\", \"elite_organ_gun\"),\n    565: (\"EliteCamelArcher\", \"elite_camel_archer\"),\n    567: (\"EliteGbeto\", \"elite_gbeto\"),\n    569: (\"EliteShotelWarrior\", \"elite_shotel_warrior\"),\n    572: (\"Carrack\", \"carrack\"),\n    573: (\"Arquebus\", \"arquebus\"),\n    574: (\"RoyalHeirs\", \"royal_heirs\"),\n    575: (\"TorsonEngines\", \"torson_engines\"),\n    576: (\"Tigui\", \"tigui\"),\n    577: (\"Farimba\", \"farimba\"),\n    578: (\"Kasbah\", \"kasbah\"),\n    579: (\"MaghrebiCamels\", \"maghrebi_camels\"),\n    597: (\"EliteCaravel\", \"elite_caravel\"),\n    599: (\"EliteGenitour\", \"elite_genitour\"),\n    602: (\"Arson\", \"arson\"),\n    608: (\"Arrowslits\", \"arrowslits\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\n# contains only new civs of AK\nCIV_GROUP_LOOKUPS = {\n    24: (\"Portugese\", \"portugese\"),\n    25: (\"Ethiopians\", \"ethiopians\"),\n    26: (\"Malians\", \"malians\"),\n    27: (\"Berbers\", \"berbers\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\n# contains only new/changed graphic sets of AK\nGRAPHICS_SET_LOOKUPS = {\n    9: ((25, 26, 27), \"African\", \"african\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrains of DE2\nTERRAIN_GROUP_LOOKUPS = {\n    41: ((0,), \"Savannah\", \"savannah\"),\n    42: ((0,), \"Dirt4\", \"dirt4\"),\n    43: ((0,), \"Road3\", \"road3\"),\n    44: ((0,), \"Moorland\", \"moorland\"),\n    45: ((0,), \"DesertCracked\", \"desert_cracked\"),\n    46: ((0,), \"DesertQuicksand\", \"desert_quicksand\"),\n    47: ((0,), \"Black\", \"black\"),\n    48: ((0,), \"ForestDragon\", \"forest_dragon\"),\n    49: ((0,), \"ForestBaobab\", \"forest_baobab\"),\n    50: ((0,), \"ForestAcacia\", \"forest_acacia\"),\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrain types of DE2\nTERRAIN_TYPE_LOOKUPS = {\n}\n\n# key: armor class; value: Gather ability name\n# contains only new armors of AK\nARMOR_CLASS_LOOKUPS = {\n    30: \"Camel\",\n    33: \"AntiGunpowder\",    # TODO: Should not be used as it is a hacky workaround for minimum damage\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/fgt/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/fgt/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II: HD Edition - The Forgotten.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/fgt/internal_nyan_names.py",
    "content": "# Copyright 2020-2021 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new units of Forgotten\nUNIT_LINE_LOOKUPS = {\n    185: (\"Slinger\", \"slinger\"),\n    305: (\"Llama\", \"llama\"),\n    486: (\"Bear\", \"bear\"),\n    639: (\"Penguin\", \"penguin\"),\n    705: (\"Cow\", \"cow\"),\n    751: (\"EagleScout\", \"eagle_scout\"),\n    866: (\"GenoeseCrossbowman\", \"genoese_crossbowman\"),\n    869: (\"MagyarHuszar\", \"magyar_huszar\"),\n    873: (\"ElephantArcher\", \"elephant_archer\"),\n    876: (\"Boyar\", \"boyar\"),\n    879: (\"Kamayuk\", \"kamayuk\"),\n    882: (\"Condottiero\", \"condottiero\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new buildings of Forgotten\nBUILDING_LINE_LOOKUPS = {\n    789: (\"PalisadeGate\", \"palisade_gate\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\n# contains only new/changed ambience of Forgotten\nAMBIENT_GROUP_LOOKUPS = {\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\n# contains only new/changed variants of Forgotten\nVARIANT_GROUP_LOOKUPS = {\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new techs of Forgotten\nTECH_GROUP_LOOKUPS = {\n    65: (\"Gillnets\", \"gillnets\"),\n    384: (\"EagleWarrior\", \"eagle_warrior\"),\n    460: (\"Atlatl\", \"atlatl\"),\n    461: (\"Warwolf\", \"warwolf\"),\n    462: (\"GreatWall\", \"great_wall\"),\n    463: (\"Chieftains\", \"chieftains\"),\n    464: (\"GreekFire\", \"greek_fire\"),\n    468: (\"EliteGenoeseCrossbowman\", \"elite_genoese_crossbowman\"),\n    472: (\"EliteMagyarHuszar\", \"elite_magyar_huszar\"),\n    481: (\"EliteElephantArcher\", \"elite_elephant_archer\"),\n    482: (\"Stronghold\", \"stronghold\"),\n    483: (\"Marauders\", \"marauders\"),\n    484: (\"Yasama\", \"yasama\"),\n    485: (\"ObsidanArrows\", \"obsidian_arrows\"),\n    486: (\"Panokseon\", \"panokseon\"),\n    487: (\"Nomads\", \"nomads\"),\n    # TODO: Boiling oil\n    489: (\"Ironclad\", \"ironclad\"),\n    490: (\"Madrasah\", \"madrasah\"),\n    491: (\"Sipahi\", \"sipahi\"),\n    492: (\"Inquisition\", \"inquisition\"),\n    493: (\"Chivalry\", \"chivalry\"),\n    494: (\"Pavise\", \"pavise\"),\n    499: (\"SilkRoad\", \"silk_road\"),\n    504: (\"EliteBoyar\", \"elite_boyar\"),\n    506: (\"Sultans\", \"sultans\"),\n    507: (\"Shatagni\", \"shatagni\"),\n    509: (\"EliteKamayuk\", \"elite_kamayuk\"),\n    512: (\"Orthodoxy\", \"orthodoxy\"),\n    513: (\"Druzhina\", \"druzhina\"),\n    514: (\"CorvinianArmy\", \"corvinian_army\"),\n    515: (\"RecurveBow\", \"recurve_bow\"),\n    516: (\"AndeanSling\", \"andean_sling\"),\n    517: (\"FabricShields\", \"fabric_shields\"),           # previously called Couriers\n    521: (\"ImperialCamelRider\", \"imperial_camel_rider\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\n# contains only new civs of Forgotten\nCIV_GROUP_LOOKUPS = {\n    19: (\"Italians\", \"italians\"),\n    20: (\"Indians\", \"indians\"),\n    21: (\"Incas\", \"incas\"),\n    22: (\"Magyars\", \"magyars\"),\n    23: (\"Slavs\", \"slavs\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\n# contains only new/changed graphic sets of Forgotten\nGRAPHICS_SET_LOOKUPS = {\n    6: ((19, 24), \"Mediterranean\", \"mediterranean\"),\n    7: ((20,), \"Indian\", \"indian\"),\n    8: ((22, 23, 32, 35), \"EasternEuropean\", \"eastern_european\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrains of DE2\nTERRAIN_GROUP_LOOKUPS = {\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrain types of DE2\nTERRAIN_TYPE_LOOKUPS = {\n}\n\n# key: armor class; value: Gather ability name\n# contains only new armors of Forgotten\nARMOR_CLASS_LOOKUPS = {\n    14: \"AnimalPredator\",\n    23: \"Gunpowder\",\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/raj/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/raj/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConversion data formats for Age of Empires II: HD Edition - Rise of the Rajas.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/conversion/hd/raj/internal_nyan_names.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new units of Rajas\nUNIT_LINE_LOOKUPS = {\n    1120: (\"BallistaElephant\", \"ballista_elephant\"),\n    1123: (\"KarambitWarrior\", \"karambit_warrior\"),\n    1126: (\"Arambai\", \"arambai\"),\n    1129: (\"RattanArcher\", \"rattan_archer\"),\n    1132: (\"BattleElephant\", \"battle_elephant\"),\n    1135: (\"KomodoDragon\", \"komodo_dragon\"),\n    1137: (\"Tiger\", \"tiger\"),\n    1139: (\"Rhinoceros\", \"rhinoceros\"),\n    1142: (\"WaterBuffalo\", \"water_buffallo\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new buildings of Rajas\nBUILDING_LINE_LOOKUPS = {\n    1189: (\"Harbor\", \"harbor\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\n# contains only new/changed ambience of Rajas\nAMBIENT_GROUP_LOOKUPS = {\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\n# contains only new/changed variants of Rajas\nVARIANT_GROUP_LOOKUPS = {\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# contains only new techs of Rajas\nTECH_GROUP_LOOKUPS = {\n    615: (\"EliteBallistaElephant\", \"elite_ballista_elephant\"),\n    617: (\"EliteKrarambitWarrior\", \"elite_karambit_warrior\"),\n    619: (\"EliteArambai\", \"elite_arambai\"),\n    621: (\"EliteRattanArcher\", \"elite_rattan_archer\"),\n    622: (\"TuskSwords\", \"tusk_swords\"),\n    623: (\"DoubleCrossbow\", \"double_crossbow\"),\n    624: (\"Thalassocracy\", \"thalassocracy\"),\n    625: (\"ForcedLevy\", \"forced_levy\"),\n    626: (\"Howdah\", \"howdah\"),\n    627: (\"ManipurCavalry\", \"manipur_cavalry\"),\n    628: (\"Chatras\", \"chatras\"),\n    629: (\"PaperMoney\", \"paper_money\"),\n    631: (\"EliteBattleElephant\", \"elite_battle_elephant\"),\n    655: (\"ImperialSkirmisher\", \"imperial_skirmisher\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\n# contains only new civs of Rajas\nCIV_GROUP_LOOKUPS = {\n    28: (\"Khmer\", \"khmer\"),\n    29: (\"Malay\", \"malay\"),\n    30: (\"Burmese\", \"burmese\"),\n    31: (\"Vietnamese\", \"vietnamese\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\n# contains only new/changed graphic sets of Rajas\nGRAPHICS_SET_LOOKUPS = {\n    10: ((28, 29, 30), \"SouthEastAsian\", \"south_east_asian\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrains of DE2\nTERRAIN_GROUP_LOOKUPS = {\n    51: ((0,), \"Beach2\", \"beach2\"),\n    52: ((0,), \"Beach3\", \"beach3\"),\n    53: ((0,), \"Beach4\", \"beach4\"),\n    54: ((0,), \"ShallowsMangroove\", \"shallows_mangroove\"),\n    55: ((0,), \"ForestMangroove\", \"forest_mangroove\"),\n    56: ((0,), \"Rainforest\", \"rainforest\"),\n    57: ((0,), \"Water4\", \"water4\"),\n    58: ((0,), \"Water5\", \"water5\"),\n    59: ((0,), \"ShallowsRainforest\", \"shallows_rainforest\"),\n    60: ((0,), \"GrassJungle\", \"grass_jungle\"),\n    61: ((0,), \"RoadJungle\", \"road_jungle\"),\n    62: ((0,), \"LeavesJungle\", \"leaves_jungle\"),\n    63: ((0,), \"RiceFarmCrops\", \"rice_farm_crops\"),\n    64: ((0,), \"RiceFarmHarvested\", \"rice_farm_harvested\"),\n    65: ((0,), \"RiceFarmConstruction1\", \"rice_farm_construction1\"),\n    66: ((0,), \"RiceFarmConstruction2\", \"rice_farm_construction2\"),\n    67: ((0,), \"RiceFarmConstruction3\", \"rice_farm_construction3\"),\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\n# contains only new/changed terrain types of DE2\nTERRAIN_TYPE_LOOKUPS = {\n}\n\n# key: armor class; value: Gather ability name\n# contains only new armors of Rajas\nARMOR_CLASS_LOOKUPS = {\n    32: \"Condottiero\",\n    34: \"FishingShip\",\n    35: \"Mameluke\",\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/ror/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/ror/__init__.py",
    "content": ""
  },
  {
    "path": "openage/convert/value_object/conversion/ror/internal_nyan_names.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n# key: head unit id; value: (nyan object name, filename prefix)\nUNIT_LINE_LOOKUPS = {\n    4: (\"Bowman\", \"bowman\"),\n    5: (\"ImprovedBowman\", \"improved_bowman\"),\n    11: (\"Ballista\", \"balista\"),\n    13: (\"FishingBoat\", \"fishing_boat\"),\n    15: (\"TradeBoat\", \"trade_boat\"),\n    17: (\"Transport\", \"transport\"),\n    19: (\"Galley\", \"galley\"),\n    25: (\"ElephantArcher\", \"elephant_archer\"),\n    35: (\"Catapult\", \"catapult\"),\n    37: (\"Cavalry\", \"cavalry\"),\n    39: (\"HorseArcher\", \"horse_archer\"),\n    40: (\"Chariot\", \"chariot\"),\n    41: (\"ChariotArcher\", \"chariot_archer\"),\n    46: (\"WarElephant\", \"war_elephant\"),\n    73: (\"Clubman\", \"clubman\"),\n    75: (\"Swordsman\", \"swordsman\"),\n    93: (\"Hoplite\", \"hoplite\"),\n    118: (\"Villager\", \"villager\"),\n    125: (\"Priest\", \"priest\"),\n    250: (\"CatapultTrireme\", \"catapult_trireme\"),\n    299: (\"Scout\", \"scout\"),\n    338: (\"CamelRider\", \"camel_rider\"),\n    347: (\"Slinger\", \"slinger\"),\n    360: (\"FireGalley\", \"fire_galley\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\nBUILDING_LINE_LOOKUPS = {\n    0: (\"Academy\", \"academy\"),\n    12: (\"Barracks\", \"barracks\"),\n    45: (\"Dock\", \"dock\"),\n    49: (\"SiegeWorkshop\", \"siege_workshop\"),\n    50: (\"Farm\", \"farm\"),\n    68: (\"Granary\", \"granary\"),\n    70: (\"House\", \"house\"),\n    72: (\"Wall\", \"wall\"),\n    79: (\"Tower\", \"tower\"),\n    82: (\"GovernmentCenter\", \"government_center\"),\n    84: (\"Market\", \"market\"),\n    87: (\"ArcheryRange\", \"archery_range\"),\n    101: (\"Stable\", \"stable\"),\n    103: (\"StoragePit\", \"storage_pit\"),\n    104: (\"Temple\", \"temple\"),\n    109: (\"TownCenter\", \"town_center\"),\n    276: (\"Wonder\", \"wonder\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\nAMBIENT_GROUP_LOOKUPS = {\n    59: (\"BerryBush\", \"berry_bush\"),\n    66: (\"GoldMine\", \"gold_mine\"),\n    102: (\"StoneMine\", \"stone_mine\"),\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\nVARIANT_GROUP_LOOKUPS = {\n    56: (\"TreeOak\", \"tree_oak\", (56, 134, 141, 144, 145, 146, 148,), \"misc\"),\n    80: (\"Shallows\", \"shallows\", (80,), \"misc\"),\n    113: (\"TreePalm\", \"tree_palm\", (113, 114, 121, 129, 148, 149, 150, 151, 152, 153), \"misc\"),\n    135: (\"TreeConifer\", \"tree_conifer\", (135, 137, 138, 139,), \"misc\"),\n    136: (\"TreeSpruce\", \"tree_spruce\", (136,), \"misc\"),\n    140: (\"TreeBeech\", \"tree_beech\", (140,), \"misc\"),\n    161: (\"TreePine\", \"tree_pine\", (161, 192, 193, 194, 197, 198, 203, 226, 391, 392,), \"misc\"),\n    164: (\"Cactus\", \"cactus\", (164, 165, 166, 169,), \"misc\"),\n    167: (\"GrassClump\", \"grass_clump\", (167, 168, 170, 171, 172, 173, 174, 175, 176, 177,), \"misc\"),\n    178: (\"DesertClump\", \"desert_clump\", (178, 179, 180,), \"misc\"),\n    181: (\"Skeleton\", \"skeleton\", (181, 182, 183,), \"misc\"),\n    186: (\"RockDirt\", \"rock_dirt\", (186, 304, 305, 306,), \"misc\"),\n    187: (\"Crack\", \"crack\", (187, 188, 189, 190, 191,), \"misc\"),\n    184: (\"RockGrass\", \"rock_grass\", (184, 307, 308, 309, 310, 311, 312, 313,), \"misc\"),\n    185: (\"RockSand\", \"rock_sand\", (185, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324,), \"misc\"),\n    332: (\"RockMud\", \"rock_mud\", (332, 333, 334,), \"misc\"),\n    343: (\"TreeDead\", \"tree_dead\", (343,), \"misc\"),\n    385: (\"RockBeach\", \"rock_beach\", (385,), \"misc\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\nTECH_GROUP_LOOKUPS = {\n    2: (\"BallistaTower\", \"ballista_tower\"),\n    4: (\"FishingShip\", \"fishing_ship\"),\n    5: (\"MediumWarship\", \"medium_warship\"),\n    6: (\"MerchantShip\", \"merchant_ship\"),\n    7: (\"Trireme\", \"trireme\"),\n    8: (\"HeavyTransport\", \"heavy_transport\"),\n    9: (\"CatapultTrireme\", \"catapult_trireme\"),\n    11: (\"StoneWall\", \"stone_wall\"),\n    12: (\"SentryTower\", \"sentry_tower\"),\n    13: (\"MediumWall\", \"medium_wall\"),\n    14: (\"Fortifications\", \"fortifications\"),\n    15: (\"GuardTower\", \"guard_tower\"),\n    16: (\"WatchTower\", \"watch_tower\"),\n    18: (\"Afterlife\", \"afterlife\"),\n    19: (\"Monotheism\", \"monotheism\"),\n    20: (\"Fanatism\", \"fanatism\"),\n    21: (\"Mysticism\", \"mysticism\"),\n    22: (\"Astrology\", \"astrology\"),\n    23: (\"HolyWar\", \"holy_war\"),\n    24: (\"Polytheism\", \"polytheism\"),\n    25: (\"HeavyWarship\", \"heavy_warship\"),\n    27: (\"Helepolis\", \"helepolis\"),\n    28: (\"Wheel\", \"wheel\"),\n    30: (\"Coinage\", \"coinage\"),\n    31: (\"Plow\", \"plow\"),\n    32: (\"Artisanship\", \"artisanship\"),\n    34: (\"Nobility\", \"nobility\"),\n    35: (\"Engineering\", \"engineering\"),\n    36: (\"MassiveCatapult\", \"massive_catapult\"),\n    37: (\"Alchemy\", \"alchemy\"),\n    38: (\"HeavyHorseArcher\", \"heavy_horse_archer\"),\n    40: (\"LeatherArmorInfantry\", \"leather_armor_infantry\"),\n    41: (\"LeatherArmorArcher\", \"leather_armor_archer\"),\n    42: (\"LeatherArmorCavalry\", \"leather_armor_cavalry\"),\n    43: (\"ScaleArmorInfantry\", \"scale_armor_infantry\"),\n    44: (\"ScaleArmorArcher\", \"scale_armor_archer\"),\n    45: (\"ScaleArmorCavalry\", \"scale_armor_cavalry\"),\n    46: (\"ToolWorking\", \"tool_working\"),\n    47: (\"BronzeShield\", \"bronze_shield\"),\n    48: (\"ChainMailInfantry\", \"chain_mail_infantry\"),\n    49: (\"ChainMailArcher\", \"chain_mail_archer\"),\n    50: (\"ChainMailCavalry\", \"chain_mail_cavalry\"),\n    51: (\"MetalWorking\", \"metal_working\"),\n    52: (\"Metalurgy\", \"metalurgy\"),\n    54: (\"HeavyCatapult\", \"heavy_catapult\"),\n    56: (\"ImprovedBow\", \"improved_bow\"),\n    57: (\"CompositeBowman\", \"composite_bowman\"),\n    63: (\"Axe\", \"axe\"),\n    64: (\"ShortSword\", \"short_sword\"),\n    65: (\"Broadswordsman\", \"broadswordsman\"),\n    66: (\"Longswordsman\", \"longswordsman\"),\n    71: (\"HeavyCavalry\", \"heavy_cavalry\"),\n    73: (\"Phalanx\", \"phalanx\"),\n    77: (\"Legion\", \"legion\"),\n    78: (\"Cataphract\", \"cataphract\"),\n    79: (\"Centurion\", \"centurion\"),\n    80: (\"Irrigation\", \"irrigation\"),\n    81: (\"Domestication\", \"domestication\"),\n    100: (\"StoneAge\", \"stone_age\"),\n    101: (\"ToolAge\", \"tool_age\"),\n    102: (\"BronzeAge\", \"bronze_age\"),\n    103: (\"IronAge\", \"iron_age\"),\n    106: (\"Ballistics\", \"ballistics\"),\n    107: (\"WoodWorking\", \"wood_working\"),\n    108: (\"GoldMining\", \"gold_mining\"),\n    109: (\"StoneMining\", \"stone_mining\"),\n    110: (\"Craftmanship\", \"craftmanship\"),\n    111: (\"SiegeCraft\", \"siege_craft\"),\n    112: (\"Architecture\", \"architecture\"),\n    113: (\"Aristocracy\", \"aristocracy\"),\n    114: (\"Writing\", \"writing\"),\n    117: (\"IronShield\", \"iron_shield\"),\n    119: (\"Medicine\", \"medicine\"),\n    120: (\"Martyrdom\", \"martyrdom\"),\n    121: (\"Logistics\", \"logistics\"),\n    122: (\"TowerShield\", \"tower_shield\"),\n    125: (\"ArmoredElephant\", \"armored_elephant\"),\n    126: (\"HeavyChariot\", \"heavy_chariot\"),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\nCIV_GROUP_LOOKUPS = {\n    0: (\"Gaia\", \"gaia\"),\n    1: (\"Egyptians\", \"egyptians\"),\n    2: (\"Greek\", \"greek\"),\n    3: (\"Babylonians\", \"babylonians\"),\n    4: (\"Assyrians\", \"assyrians\"),\n    5: (\"Minoans\", \"minoans\"),\n    6: (\"Hittite\", \"hittite\"),\n    7: (\"Phoenicians\", \"phoenicians\"),\n    8: (\"Sumerians\", \"sumerians\"),\n    9: (\"Persians\", \"persians\"),\n    10: (\"Shang\", \"shang\"),\n    11: (\"Yamato\", \"yanato\"),\n    12: (\"Choson\", \"choson\"),\n    13: (\"Romans\", \"romans\"),\n    14: (\"Carthage\", \"carthage\"),\n    15: (\"Palmyra\", \"palmyra\"),\n    16: (\"Macedonians\", \"macedonians\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\nGRAPHICS_SET_LOOKUPS = {\n    0: ((1, 4, 8), \"MiddleEastern\", \"middle_eastern\"),\n    1: ((2, 5, 7), \"Mediterranean\", \"mediterranean\"),\n    2: ((3, 6, 9), \"CentralAsian\", \"central_asian\"),\n    3: ((0, 10, 11, 12), \"EastAsian\", \"east_asian\"),\n    4: ((13, 14, 15, 16), \"Roman\", \"roman\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_GROUP_LOOKUPS = {\n    0:  ((0, 1, 4, 7, 8, 9, 10,), \"Grass\", \"grass\"),\n    1:  ((0, 3, 6,), \"Water\", \"water\"),\n    2:  ((0, 2, 6, 7, 10,), \"Beach\", \"beach\"),\n    3:  (((),), \"ThinRiver\", \"thin_river\"),\n    4:  ((0, 1, 3, 6, 7,), \"Shallows\", \"shallows\"),\n    5:  ((0, 1, 4, 7, 9, 10,), \"JungleEdge\", \"jungle_edge\"),\n    6:  ((0, 1, 4, 7, 8, 9, 10,), \"Desert\", \"desert\"),\n    7:  (((),), \"Crop\", \"crop\"),\n    8:  (((),), \"Rows\", \"rows\"),\n    9:  (((),), \"Wheat\", \"wheat\"),\n    10: ((0, 1, 4, 7, 9, 10,), \"Forest\", \"forest\"),\n    11: ((0, 1, 4, 10,), \"Dirt\", \"dirt\"),\n    12: (((),), \"Grass2\", \"grass2\"),\n    13: ((0, 1, 4, 7, 9, 10,), \"DesertPalm\", \"desert_palm\"),\n    14: (((),), \"DesertImpassable\", \"desert_impassable\"),\n    15: (((),), \"WaterImpassable\", \"water_impassable\"),\n    16: (((),), \"GrassImpassable\", \"grass_impassable\"),\n    17: (((),), \"Fog\", \"fog\"),\n    18: ((0, 1, 4, 7, 9, 10,), \"ForestEdge\", \"forest_edge\"),\n    19: ((0, 1, 4, 7, 9, 10,), \"PineForest\", \"pine_forest\"),\n    20: ((0, 1, 4, 7, 9, 10,), \"Jungle\", \"jungle\"),\n    21: ((0, 1, 4, 7, 9, 10,), \"PineForestEdge\", \"pine_forest_edge\"),\n    22: ((0, 3, 6,), \"WaterDark\", \"water_dark\"),\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_TYPE_LOOKUPS = {\n    0: ((0, 4, 5, 6, 10, 11, 13, 18, 19, 20, 21,),\n        (1, 4, 7, 8, 9, 10),\n        \"Land\",),\n    1: ((1, 22,),\n        (3, 6,),\n        \"Water\",),\n    2: ((2, 4,),\n        (2, 6, ),\n        \"Shallow\",),\n}\n\nCLASS_ID_LOOKUPS = {\n    0: \"Archer\",\n    1: \"Artifact\",\n    2: \"TradeBoat\",\n    3: \"BuildingMisc\",\n    4: \"Villager\",\n    5: \"OceanFish\",\n    6: \"Infantry\",\n    7: \"BerryBush\",\n    8: \"StoneMine\",\n    9: \"AnimalPrey\",\n    10: \"AnimalPredator\",\n    11: \"DeadOrProjectileOrBird\",     # do not use this as GameEntityType\n    12: \"Cavalry\",\n    13: \"SiegeWeapon\",\n    14: \"Ambient\",\n    15: \"Tree\",\n    18: \"Monk\",\n    19: \"TradecCart\",\n    20: \"TransportShip\",\n    21: \"FishingShip\",\n    22: \"Warship\",\n    23: \"ChariotArcher\",\n    24: \"WarElephant\",\n    25: \"Hero\",\n    26: \"ElephantArcher\",\n    27: \"Wall\",\n    28: \"Phalanx\",\n    29: \"DomesticAnimal\",\n    30: \"AmbientFlag\",\n    31: \"DeepSeaFish\",\n    32: \"GoldMine\",\n    33: \"ShoreFish\",\n    34: \"Cliff\",\n    35: \"Chariot\",\n    36: \"CavalryArcher\",\n    37: \"Doppelgaenger\",\n    38: \"Bird\",\n    39: \"Slinger\",\n}\n\n# key: genie unit id; value: Gather ability name\nGATHER_TASK_LOOKUPS = {\n    13: (\"Fish\", \"fish\"),  # fishing boat\n    119: (\"Fish\", \"fish\"),\n    120: (\"CollectBerries\", \"collect_berries\"),\n    122: (\"HarvestGame\", \"harvest_game\"),\n    123: (\"ChopWood\", \"chop_wood\"),\n    124: (\"MineStone\", \"mine_stone\"),\n    251: (\"MineGold\", \"mine_gold\"),\n    259: (\"FarmCrops\", \"farm_crops\"),\n}\n\n# key: armor class; value: Gather ability name\nARMOR_CLASS_LOOKUPS = {\n    0: \"FireGalley\",\n    1: \"Archer\",\n    3: \"Pierce\",\n    4: \"Melee\",\n    6: \"Building\",\n    7: \"Priest\",\n    8: \"Cavalry\",\n    9: \"Infantry\",\n    10: \"StoneDefense\",\n    12: \"Civilian\",\n}\n\n# key: command type; value: Apply*Effect ability name\nCOMMAND_TYPE_LOOKUPS = {\n    7: (\"Attack\", \"attack\"),\n    101: (\"Construct\", \"construct\"),\n    104: (\"Convert\", \"convert\"),\n    105: (\"Heal\", \"heal\"),\n    106: (\"Repair\", \"repair\"),\n    110: (\"Hunt\", \"hunt\"),\n}\n"
  },
  {
    "path": "openage/convert/value_object/conversion/swgb/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinternal_nyan_names.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/conversion/swgb/__init__.py",
    "content": ""
  },
  {
    "path": "openage/convert/value_object/conversion/swgb/internal_nyan_names.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=line-too-long\n\n\"\"\"\nAge of Empires games do not necessarily come with an english\ntranslation. Therefore, we use the strings in this file to\nfigure out the names for a nyan object.\n\"\"\"\n\n\n# key: head unit id; value: (nyan object name, filename prefix)\n# For unit lines with different graphics per civ only the unit line of\n# the first civ (Empire) is stored\nUNIT_LINE_LOOKUPS = {\n    2: (\"FambaaShieldGenerator\", \"fambaa_shield_generator\"),\n    4: (\"RoyalCrusader\", \"royal_crusader\"),\n    6: (\"Airspeeder\", \"airspeeder\"),\n    8: (\"Berserker\", \"berserker\"),\n    13: (\"UtilityTrawler\", \"utility_trawler\"),\n    48: (\"WildFambaa\", \"wild_fambaa\"),\n    115: (\"ForceMaster\", \"force_master\"),\n    118: (\"Worker\", \"worker\"),\n    174: (\"DestroyerDroid\", \"destroyer_droid\"),\n    180: (\"ForceKnight\", \"force_knight\"),\n    307: (\"Trooper\", \"trooper\"),\n    359: (\"MountedTrooper\", \"mounted_trooper\"),\n    381: (\"GrenadeTrooper\", \"grenade_trooper\"),\n    438: (\"AntiAirTrooper\", \"anti_air_trooper\"),\n    469: (\"StrikeMech\", \"strike_mech\"),\n    481: (\"Ackley\", \"ackeley\"),\n    485: (\"MechDestroyer\", \"mech_destroyer\"),\n    500: (\"AssaultMech\", \"assault_mech\"),\n    550: (\"Scout\", \"scout\"),\n    594: (\"Nerf\", \"nerf\"),\n    625: (\"AirCruiser\", \"air_cruiser\"),\n    641: (\"JediStarfighter\", \"jedi_starfighter\"),\n    642: (\"GeonosianWarrior\", \"geonosian_warrior\"),\n    691: (\"Artillery\", \"artillery\"),\n    702: (\"AntiAirMobile\", \"anti_air_mobile\"),\n    713: (\"Pummel\", \"pummel\"),\n    762: (\"Bomber\", \"bomber\"),\n    773: (\"Fighter\", \"fighter\"),\n    815: (\"Cruiser\", \"cruiser\"),\n    822: (\"Falumpaset\", \"faumpaset\"),\n    833: (\"Bantha\", \"bantha\"),\n    838: (\"TransportShip\", \"transport_ship\"),\n    860: (\"Nexu\", \"nexu\"),\n    868: (\"Frigate\", \"frigate\"),\n    898: (\"Destroyer\", \"destroyer\"),\n    918: (\"AntiAirDestroyer\", \"anti_air_destroyer\"),\n    921: (\"Reek\", \"reek\"),\n    931: (\"CargoTrader\", \"cargo_trader\"),\n    939: (\"Medic\", \"medic\"),\n    951: (\"Cannon\", \"cannon\"),\n    961: (\"BountyHunter\", \"bounty_hunter\"),\n    983: (\"Cannon\", \"cannon\"),\n    993: (\"DarkTrooper\", \"dark_trooper\"),\n    1009: (\"PowerDroid\", \"power_droid\"),\n    1034: (\"Probot\", \"probot\"),\n    1036: (\"AirTransport\", \"air_transport\"),\n    1203: (\"Mynock\", \"mynock\"),\n    1249: (\"Dewback\", \"dewback\"),\n    1363: (\"Kaadu\", \"kaadu\"),\n    1364: (\"Ronto\", \"ronto\"),\n    1365: (\"Eopie\", \"eopie\"),\n    1366: (\"Tauntaun\", \"tauntaun\"),\n    1367: (\"CuPa\", \"cu_pa\"),\n    1469: (\"Massiff\", \"massiff\"),\n    1471: (\"Orray\", \"orray\"),\n    1473: (\"Shaak\", \"shaak\"),\n    1475: (\"WompRat\", \"womp_rat\"),\n    1582: (\"AWing\", \"a_wing\"),\n}\n\n\n# key: head unit id; value: (head units of civ lines)\nCIV_LINE_ASSOCS = {\n    115: (89, 115, 125, 134, 136, 140, 643, 645),\n    180: (52, 180, 183, 204, 232, 239, 647, 652),\n    307: (238, 280, 282, 286, 307, 309, 1548, 1552),\n    359: (289, 359, 361, 370, 376, 378, 1043, 1047),\n    381: (292, 381, 383, 385, 387, 389, 1478, 1482),\n    438: (305, 438, 440, 442, 444, 446, 1540, 1544),\n    469: (244, 469, 471, 473, 475, 479, 903, 907),\n    485: (246, 485, 489, 493, 495, 497, 911, 915),\n    500: (249, 500, 502, 508, 514, 517, 919, 923),\n    550: (258, 550, 552, 554, 556, 558, 927, 932),\n    625: (619, 620, 625, 628, 630, 632, 634, 636),\n    691: (203, 691, 693, 695, 698, 700, 1551, 1553),\n    702: (207, 702, 704, 706, 708, 711, 1555, 1557),\n    713: (231, 713, 715, 717, 719, 721, 1559, 1561),\n    762: (127, 762, 764, 766, 769, 771, 1523, 1524),\n    773: (158, 773, 775, 777, 779, 781, 1525, 1526),\n    815: (190, 815, 818, 820, 823, 825, 1509, 1511),\n    838: (191, 838, 839, 840, 841, 842, 1513, 1515),\n    868: (187, 868, 870, 872, 874, 876, 1497, 1499),\n    898: (186, 898, 900, 902, 904, 906, 1493, 1495),\n    918: (188, 918, 920, 922, 924, 926, 1501, 1503),\n    939: (167, 939, 941, 943, 945, 947, 996, 1005),\n    961: (170, 961, 963, 965, 967, 969, 1533, 1535),\n    951: (169, 951, 952, 953, 954, 955, 1531, 1532),\n    1009: (1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014),\n    1036: (1036, 1038, 1040, 1042, 1044, 1046, 1521, 1522),\n}\n\n# There is one jedi/sith for every civ\n# key: jedi/sith unit id; value: switch unit id\nMONK_GROUP_ASSOCS = {\n    # Jedi/Sith Knight\n    52: 15,\n    180: 151,\n    183: 152,\n    204: 154,\n    232: 157,\n    239: 178,\n    647: 648,\n    652: 649,\n\n    # Jedi/sith Master\n    89: 33,\n    115: 98,\n    125: 100,\n    134: 107,\n    136: 111,\n    140: 114,\n    643: 1564,\n    645: 1566,\n}\n\n\n# key: head unit id; value: (nyan object name, filename prefix)\nBUILDING_LINE_LOOKUPS = {\n    12: (\"PowerCore\", \"power_core\"),\n    45: (\"Shipyard\", \"shipyard\"),\n    49: (\"HeavyWeaponsFactory\", \"heavy_weapons_factory\"),\n    50: (\"Farm\", \"farm\"),\n    68: (\"FoodProcCenter\", \"food_proc_center\"),\n    70: (\"PrefabShelter\", \"prefab_shelter\"),\n    72: (\"LightWall\", \"light_wall\"),\n    79: (\"LaserTurret\", \"laser_turret\"),\n    82: (\"Fortress\", \"fortress\"),\n    84: (\"Spaceport\", \"spaceport\"),\n    87: (\"TroopCenter\", \"troop_center\"),\n    101: (\"MechFactory\", \"mech_factory\"),\n    103: (\"WarCenter\", \"war_center\"),\n    104: (\"Temple\", \"temple\"),\n    109: (\"CommandCenter\", \"command_center\"),\n    117: (\"Wall\", \"wall\"),\n    199: (\"AquaHarvester\", \"aqua_harvester\"),\n    209: (\"ResearchCenter\", \"research_center\"),\n    236: (\"AntiAirTurret\", \"anti_air_turret\"),\n    276: (\"Monument\", \"monument\"),\n    317: (\"Airbase\", \"airbase\"),\n    319: (\"AnimalNursery\", \"animal_nursery\"),\n    323: (\"NovaProcCenter\", \"nova_proc_center\"),\n    335: (\"ShieldGenerator\", \"shield_generator\"),\n    487: (\"Gate\", \"gate\"),\n    562: (\"CarbonProcCenter\", \"carbon_proc_center\"),\n    584: (\"OreProcCenter\", \"ore_proc_center\"),\n    598: (\"SentryPost\", \"sentry_post\"),\n    1576: (\"SensorBuoy\", \"sensor_buoy\"),\n}\n\n# key: (head) unit id; value: (nyan object name, filename prefix)\nAMBIENT_GROUP_LOOKUPS = {\n    59: (\"BerryBush\", \"berry_bush\"),\n    66: (\"NovaMine\", \"nova_mine\"),\n    102: (\"OreMine\", \"ore_mine\"),\n    217: (\"CarbonRockRed\", \"carbon_rock_red\"),\n    285: (\"Holocron\", \"holocron\"),\n    348: (\"OakTree\", \"oak_tree\"),\n    349: (\"PalmTree\", \"palm_tree\"),\n    350: (\"ScrewedTree\", \"screwed_tree\"),\n    351: (\"LeaflessTree\", \"leafless_tree\"),\n    411: (\"TimberTree\", \"timber_tree\"),\n    413: (\"PineTree\", \"pine_tree\"),\n    414: (\"JungleTree\", \"jungle_tree\"),\n    1341: (\"CarbonRock\", \"carbon_rock\"),\n}\n\n# key: index; value: (nyan object name, filename prefix, units belonging to group, variant type)\nVARIANT_GROUP_LOOKUPS = {\n    69: (\"Shorefish\", \"shore_fish\", (69,), \"misc\"),\n    96: (\"Bird\", \"bird\", (96, 816), \"misc\"),\n    264: (\"Cliff\", \"cliff\", (264, 265, 266, 267, 268, 269, 270, 271, 272, 273), \"angle\"),\n    450: (\"BigOceanFish\", \"big_ocean_fish\", (450, 451), \"random\"),\n    455: (\"OceanFish\", \"ocean_fish\", (455, 456, 457, 458), \"random\"),\n}\n\n# key: head unit id; value: (nyan object name, filename prefix)\nTECH_GROUP_LOOKUPS = {\n    1: (\"TechLevel2\", \"tech_level_2\"),\n    2: (\"TechLevel3\", \"tech_level_3\"),\n    3: (\"TechLevel4\", \"tech_level_4\"),\n    9: (\"AWing\", \"a_wing\"),\n    33: (\"BasicTraining\", \"basic_training\"),\n    61: (\"HolonetTranceiver\", \"holonet_traceiver\"),\n    62: (\"BothanSpynet\", \"bothan_spynet\"),\n    63: (\"ElevationTracking\", \"elevation_tracking\"),\n    64: (\"LightPlating\", \"light_plating\"),\n    65: (\"BasicArmor\", \"basic_armor\"),\n    66: (\"PrimaryFocusingCoils\", \"primary_focusing_coils\"),\n    67: (\"HeavyArmor\", \"heavy_armor\"),\n    68: (\"HeavyPlating\", \"heavy_plating\"),\n    69: (\"TargetingSensors\", \"targeting_sensors\"),\n    70: (\"EnlargedBombHold\", \"enlarged_bomb_hold\"),\n    71: (\"FlightSchool\", \"flight_school\"),\n    72: (\"EfficientManufactering\", \"efficient_manufacturing\"),\n    73: (\"ShieldModifications\", \"shield_modifications\"),\n    74: (\"CoolingSleeves\", \"cooling_sleeves\"),\n    75: (\"ExternalSensorPod\", \"external_sensor_pod\"),\n    76: (\"AdvancedFlightSchool\", \"advanced_flight_school\"),\n    77: (\"ArmoredPlates\", \"armored_plates\"),\n    78: (\"AdvancedEngines\", \"advanced_engines\"),\n    79: (\"LightArmor\", \"light_armor\"),\n    80: (\"MediumPlating\", \"medium_plating\"),\n    81: (\"AdvancedPowerPack\", \"advanced_power_pack\"),\n    82: (\"Stimulants\", \"stimulants\"),\n    83: (\"Genetics\", \"genetics\"),\n    126: (\"SensorBeacon\", \"sensor_beacon\"),\n    127: (\"Cloning\", \"cloning\"),\n    128: (\"UpgradedMotivator\", \"upgraded_motivator\"),\n    129: (\"OptimizedMotivator\", \"optimized_motivator\"),\n    130: (\"SensorArray\", \"sensor_array\"),\n    132: (\"FusionExtractor\", \"fusion_extractor\"),\n    133: (\"SelfRegeneraration\", \"self_regeneration\"),\n    134: (\"Irrigation\", \"irrigation\"),\n    135: (\"HarvestingProgram\", \"harvesting_program\"),\n    136: (\"AdvancedHarvestingProgram\", \"advanced_harvesting_program\"),\n    137: (\"BattleArmor\", \"battle_armor\"),\n    138: (\"Taxation\", \"taxation\"),\n    139: (\"AttackProgramming\", \"attack_programming\"),\n    141: (\"Presidium\", \"presidium\"),\n    143: (\"BerserkerJetPacks\", \"berserker_jet_packs\"),\n    144: (\"StrengthenedFrame\", \"strengthened_frame\"),\n    145: (\"CreatureTraining\", \"creature_training\"),\n    146: (\"ReinforcedFrame\", \"reinforced_frame\"),\n    147: (\"Mechanics\", \"mechanics\"),\n    148: (\"ForestVision\", \"forest_vision\"),\n    149: (\"NovaBeamdrillMining\", \"beamdrill_mining_nova\"),\n    150: (\"HeavyNovaBeamdrill\", \"heavy_beamdrill_nova\"),\n    151: (\"ForceAgility\", \"force_agility\"),\n    152: (\"ForceConcentration\", \"force_concentration\"),\n    153: (\"ForceRegeneration\", \"force_regeneration\"),\n    154: (\"ForceStrength\", \"force_strength\"),\n    155: (\"FaithInTheForce\", \"faith_in_the_force\"),\n    156: (\"ForceInfluence\", \"force_influence\"),\n    157: (\"ForceStrong\", \"force_strong\"),\n    158: (\"ForcePerception\", \"force_perception\"),\n    159: (\"JediMindTrick\", \"jedi_mind_trick\"),\n    160: (\"HandheldCarbonExtractor\", \"handheld_carbon_extractor\"),\n    161: (\"EnhancedCarbonExtractor\", \"enhanced_carbon_extractor\"),\n    162: (\"HeavyCarbonExtractor\", \"heavy_carbon_extractor\"),\n    163: (\"UpgradedGenerator\", \"upgraded_generator\"),\n    164: (\"WalkerResearch\", \"walker_research\"),\n    166: (\"AdvancedGenerator\", \"advanced_generator\"),\n    167: (\"GunganCreatureArmor\", \"gungan_creature_armor\"),\n    168: (\"AdvancedRedesign\", \"advanced_redesign\"),\n    169: (\"WookieIngenuity\", \"wookie_ingenuity\"),\n    170: (\"Technicians\", \"technicians\"),\n    171: (\"OreBeamdrillMining\", \"beamdrill_mining_ore\"),\n    172: (\"HeavyOreBeamdrill\", \"heavy_beamdrill_ore\"),\n    173: (\"Durasteel\", \"durasteel\"),\n    174: (\"IonAccelerator\", \"ion_accelerator\"),\n    175: (\"PowerCalibrator\", \"power_calibrator\"),\n    176: (\"RotationBearings\", \"rotation_bearings\"),\n    177: (\"TrackingComputer\", \"tracking_computer\"),\n    178: (\"HomingSensors\", \"homing_sensors\"),\n    179: (\"HeavyWeaponsEngineers\", \"heavy_weapons_engineers\"),\n    180: (\"PermacitePlating\", \"permacite_plating\"),\n    183: (\"RedesignedSpecs\", \"redesigned_specs\"),\n    184: (\"AdvancedPropulsion\", \"advanced_propulsion\"),\n    185: (\"RedoubledEfforts\", \"redoubled_efforts\"),\n    186: (\"AdvancedScanning\", \"advanced_scanning\"),\n    187: (\"FasterGrowthChambers\", \"faster_growth_chambers\"),\n    188: (\"HuttEndorsement\", \"hutt_endorsement\"),\n    189: (\"NeimoidianEndorsement\", \"neimoidian_endorsement\"),\n    190: (\"GalacticBanking\", \"galactic_banking\"),\n    192: (\"InsiderTrading\", \"insider_trading\"),\n    193: (\"GalacticTradeComission\", \"galactic_trade_comission\"),\n    194: (\"AlteredBargains\", \"altered_bargains\"),\n    195: (\"MarketControl\", \"market_control\"),\n    196: (\"DroidAssistents\", \"droid_assistents\"),\n    197: (\"MacroBinoculars\", \"macro_binoculars\"),\n    198: (\"LighterArmor\", \"lighter_armor\"),\n    199: (\"PortableScanner\", \"portable_scanner\"),\n    201: (\"FarSeeInBinoculars\", \"far_see_in_binoculars\"),\n    202: (\"IncreasedExplosiveYields\", \"increased_explosive_yields\"),\n    203: (\"IntegratedRangefinder\", \"integrated_rangefinder\"),\n    204: (\"TougherArmor\", \"tougher_armor\"),\n    205: (\"Dexterity\", \"dexterity\"),\n    206: (\"AutomatedProcesses\", \"automated_processes\"),\n    500: (\"ForceMeditation\", \"force_meditation\"),\n    501: (\"BactaTanks\", \"bacta_tanks\"),\n    535: (\"GrenadierTraining\", \"grenadier_training\"),\n    568: (\"AntiAirRetrofit\", \"anti_air_retrofit\"),\n    569: (\"StrenghtenedSuperstructure\", \"streghtened_superstructure\"),\n    570: (\"SuperconductingShields\", \"superconducting_shields\"),\n    571: (\"EfficientBuildings\", \"efficient_buildings\"),\n    572: (\"PowerCoreShielding\", \"power_core_shielding\"),\n    573: (\"StregthenedAssembly\", \"strengthened_assembly\"),\n    574: (\"GeonosianDiligence\", \"geonosian_diligence\"),\n    575: (\"ConfederacyAlliance\", \"confederacy_alliance\"),\n    576: (\"GeonosianEngineers\", \"geonosian_engineers\"),\n    577: (\"DroidUpgrades\", \"droid_upgrades\"),\n    578: (\"UpgradedMedicDroids\", \"upgraded_medic_droids\"),\n    579: (\"KaminoanCloners\", \"kaminoan_cloners\"),\n    580: (\"GalacticSenateHub\", \"galactic_senate_hub\"),\n    581: (\"SightBeyondSight\", \"sight_beyond_sight\"),\n    582: (\"KaminoanRefit\", \"kaminoan_refit\"),\n    583: (\"AirCruiserBoost\", \"air_cruiser_boost\"),\n\n    # Unit/Building upgrades\n    113: (\"HeavyDestroyerDroid\", \"heavy_destroyer_droid\"),\n    222: (\"ForceKnightUpgrade\", \"force_knight_upgrade\"),\n    236: (\"AdvancedMountedTrooper\", \"advanced_mounted_trooper\"),\n    241: (\"HeavyMountedTrooper\", \"heavy_mounted_trooper\"),\n    246: (\"HeavyAntiAirTrooper\", \"heavy_anti_air_trooper\"),\n    251: (\"HeavyTrooper\", \"heavy_trooper\"),\n    256: (\"RepeaterTrooper\", \"repeater_trooper\"),\n    261: (\"TrooperUpgrade\", \"trooper_upgrade\"),\n    301: (\"HeavyStrikeMech\", \"heavy_strike_mech\"),\n    306: (\"HeavyMechDestroyer\", \"heavy_mech_destroyer\"),\n    311: (\"HeavyAssaultMech\", \"heavy_assault_mech\"),\n    331: (\"HeavyArtillery\", \"heavy_artillery\"),\n    341: (\"HeavyAntiAirMobile\", \"heavy_anti_air_mobile\"),\n    346: (\"HeavyPummel\", \"heavy_pummel\"),\n    366: (\"AdvancedBomber\", \"advanced_bomber\"),\n    371: (\"AdvancedFighter\", \"advanced_fighter\"),\n    376: (\"EnhancedBomber\", \"enhanced_bomber\"),\n    381: (\"FastFighter\", \"fast_fighter\"),\n    416: (\"AdvancedFrigate\", \"advanced_frigate\"),\n    421: (\"AdvancedCruiser\", \"advanced_cruiser\"),\n    426: (\"HeavyDestroyer\", \"heavy_destroyer\"),\n    431: (\"HeavyAntiAirDestroyer\", \"heavy_anti_air_destroyer\"),\n    441: (\"FrigateUpgrade\", \"frigate_upgrade\"),\n    447: (\"AdvancedAntiAirTurret\", \"advanced_anti_air_turret\"),\n    472: (\"DarkTrooperPhase2\", \"dark_trooper_phase2\"),\n    474: (\"HeavyFambaaShieldGenerator\", \"heavy_fambaa_shield_generator\"),\n    475: (\"EliteRoyalCrusader\", \"elite_royal_crusader\"),\n    476: (\"ArmoredAirspeeder\", \"armored_airspeeder\"),\n    477: (\"AdvancedBerserker\", \"advanced_berserker\"),\n    480: (\"MediumTurret\", \"medium_turret\"),\n    481: (\"AdvancedTurret\", \"advanced_turret\"),\n    483: (\"HeavyWall\", \"heavy_wall\"),\n    484: (\"ShieldWall\", \"shield_wall\"),\n    533: (\"AdvancedJediStarfighter\", \"advanced_jedi_starfighter\"),\n    534: (\"EliteGeonosianWarrior\", \"elite_geonosian_warrior\"),\n}\n\n# key: tech id; value: (tech ids of civ line unlocks/upgrades)\nCIV_TECH_ASSOCS = {\n    222: (93, 222, 223, 224, 225, 226, 544, 545),\n    236: (86, 236, 237, 238, 239, 240, 556, 557),\n    241: (55, 241, 242, 243, 244, 245, 558, 559),\n    246: (45, 246, 247, 248, 249, 250, 560, 561),\n    251: (44, 251, 252, 253, 254, 255, 562, 563),\n    256: (90, 256, 257, 258, 259, 260, 564, 565),\n    261: (43, 261, 262, 263, 264, 265, 566, 567),\n    301: (42, 301, 302, 303, 304, 305, 550, 551),\n    306: (40, 306, 307, 308, 309, 310, 552, 553),\n    311: (99, 311, 312, 313, 314, 315, 554, 555),\n    331: (57, 331, 332, 333, 334, 335, 538, 539),\n    341: (56, 341, 342, 343, 344, 345, 540, 541),\n    346: (59, 346, 347, 348, 349, 350, 542, 543),\n    366: (108, 366, 367, 368, 369, 370, 517, 518),\n    371: (110, 371, 372, 373, 374, 375, 519, 520),\n    376: (107, 376, 377, 378, 379, 380, 521, 522),\n    381: (109, 381, 382, 383, 384, 385, 523, 524),\n    416: (53, 416, 417, 418, 419, 420, 508, 537),\n    421: (51, 421, 422, 423, 424, 425, 509, 510),\n    426: (50, 426, 427, 428, 429, 430, 511, 512),\n    431: (49, 431, 432, 433, 434, 435, 513, 514),\n    441: (52, 441, 442, 443, 444, 445, 515, 516),\n}\n\n# key: civ index; value: (nyan object name, filename prefix)\nCIV_GROUP_LOOKUPS = {\n    0: (\"Nature\", \"nature\"),\n    1: (\"Empire\", \"empire\"),\n    2: (\"Gungans\", \"gungans\"),\n    3: (\"Rebels\", \"rebels\"),\n    4: (\"Naboo\", \"naboo\"),\n    5: (\"Federation\", \"federation\"),\n    6: (\"Wookiees\", \"wokiees\"),\n    7: (\"Republic\", \"republic\"),\n    8: (\"Confederacy\", \"confederacy\"),\n}\n\n# key: civ index; value: (civ ids, nyan object name, filename prefix)\nGRAPHICS_SET_LOOKUPS = {\n    0: ((0, 1), \"Empire\", \"empire\"),\n    1: ((2,), \"Gungan\", \"gungan\"),\n    2: ((3,), \"Rebel\", \"rebel\"),\n    3: ((4,), \"Naboo\", \"naboo\"),\n    4: ((5,), \"Federation\", \"federate\"),\n    5: ((6,), \"Wookiee\", \"wookiee\"),\n    6: ((7,), \"Republican\", \"reppublican\"),\n    7: ((8,), \"Confederate\", \"confederate\"),\n}\n\n# key: terrain index; value: (unit terrain restrictions (manual), nyan object name, filename prefix)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_GROUP_LOOKUPS = {\n    0:  ((0,), \"Grass0\", \"grass0\"),\n    1:  ((0,), \"Water0\", \"water0\"),\n    2:  ((0,), \"Shore\", \"shore\"),\n    3:  ((0,), \"Dirt0\", \"dirt0\"),\n    4:  ((0,), \"Swamp\", \"swamp\"),\n    5:  ((0,), \"Leaves\", \"leaves\"),\n    6:  ((0,), \"Dirt1\", \"dirt1\"),\n    7:  ((0,), \"FarmCrops\", \"farm_crops\"),\n    8:  ((0,), \"FarmHarvested\", \"farm_harvested\"),\n    9:  ((0,), \"Grass1\", \"grass1\"),\n    10: ((0,), \"Forest0\", \"forest0\"),\n    11: ((0,), \"Dirt2\", \"dirt2\"),\n    12: ((0,), \"Grass2\", \"grass2\"),\n    13: ((0,), \"Forest1\", \"forest1\"),\n    14: ((0,), \"Sand\", \"sand\"),\n    15: ((0,), \"Clouds\", \"clouds\"),\n    16: ((0,), \"Space\", \"space\"),\n    17: ((0,), \"Forest2\", \"forest2\"),\n    18: ((0,), \"Forest3\", \"forest3\"),\n    19: ((0,), \"Forest4\", \"forest4\"),\n    20: ((0,), \"Forest5\", \"forest5\"),\n    21: ((0,), \"Forest6\", \"forest6\"),\n    22: ((0,), \"Water1\", \"water1\"),\n    23: ((0,), \"Water2\", \"water2\"),\n    24: ((0,), \"Path0\", \"path0\"),\n    25: ((0,), \"Path1\", \"path1\"),\n    26: ((0,), \"Desert0\", \"desert0\"),\n    27: ((0,), \"Foundation\", \"foundation\"),\n    28: ((0,), \"Water3\", \"water3\"),\n    29: ((0,), \"FarmConstruction3\", \"farm_construction3\"),\n    30: ((0,), \"FarmConstruction2\", \"farm_construction2\"),\n    31: ((0,), \"FarmConstruction1\", \"farm_construction1\"),\n    32: ((0,), \"Snow0\", \"snow0\"),\n    33: ((0,), \"Snow1\", \"snow1\"),\n    34: ((0,), \"Snow2\", \"snow2\"),\n    35: ((0,), \"Ice\", \"ice\"),\n    36: ((0,), \"FoundationSnow\", \"foundation_snow\"),\n    37: ((0,), \"ShoreIce\", \"shore_ice\"),\n    38: ((0,), \"Path2\", \"path2\"),\n    39: ((0,), \"Path3\", \"path3\"),\n    40: ((0,), \"Path4\", \"path4\"),\n    41: ((0,), \"Grass3\", \"grass3\"),\n    42: ((0,), \"Rock0\", \"rock0\"),\n    43: ((0,), \"Metal\", \"metal\"),\n    44: ((0,), \"Rock1\", \"rock1\"),\n    45: ((0,), \"Desert1\", \"desert1\"),\n    46: ((0,), \"Desert2\", \"desert2\"),\n    47: ((0,), \"Snow3\", \"snow3\"),\n    48: ((0,), \"FarmGreen\", \"farm_green\"),\n    49: ((0,), \"MetalCarb\", \"metal_carb\"),\n    50: ((0,), \"Rock2\", \"rock2\"),\n    51: ((0,), \"Lava\", \"lava\"),\n    52: ((0,), \"Rock3\", \"rock3\"),\n}\n\n# key: not relevant; value: (terrain indices, unit terrain restrictions (manual), nyan object name)\n# TODO: Use terrain restrictions from .dat\nTERRAIN_TYPE_LOOKUPS = {\n}\n\n\nCLASS_ID_LOOKUPS = {\n    1: \"Bantha\",\n    2: \"Fambaa\",\n    4: \"Animal\",\n    5: \"Monster\",\n    6: \"Wall\",\n    7: \"Farm\",\n    8: \"Gate\",\n    9: \"AntiAirTurret\",\n    10: \"LaserTurret\",\n    11: \"Cruiser\",\n    13: \"Destroyer\",\n    14: \"UtilityTrawler\",\n    15: \"Frigate\",\n    16: \"AntiAirDestroyer\",\n    17: \"TransportShip\",\n    18: \"BuildingMisc\",\n    19: \"Doppelganger\",\n    20: \"DeadOrProjectile\",     # do not use this as GameEntityType\n    21: \"HOLDTHIS\",             # something from a scenario\n    22: \"Cliff\",\n    23: \"OceanFish\",\n    25: \"ShoreFish\",\n    26: \"AmbientFlag\",\n    27: \"BerryBush\",\n    28: \"Holocron\",\n    29: \"Nova\",\n    30: \"Ore\",\n    31: \"CarbonTree\",\n    32: \"Artillery\",\n    33: \"AntiAirMobile\",\n    34: \"MobileCannon\",\n    35: \"Pummel\",\n    36: \"Cannon\",\n    39: \"UnderwaterFrigate\",\n    40: \"AntiAirDestroyer\",\n    42: \"EyeCandy\",\n    43: \"Bomber\",\n    44: \"BountyHunter\",\n    45: \"CargoHovercraft\",\n    46: \"ScenarioUnit\",                  # should not be present in final modpack\n    47: \"Scout\",\n    48: \"Fighter\",\n    49: \"GrenadeTrooper\",\n    50: \"Jedi\",\n    51: \"JediWithHolocron\",\n    52: \"Trooper\",\n    53: \"Mech\",\n    54: \"Medic\",\n    55: \"AntiAirTrooper\",\n    56: \"MountedTrooper\",\n    57: \"FambaaShieldGenerator\",\n    58: \"Worker\",\n    59: \"AirTransport\",\n    60: \"Herdable\",\n    61: \"PowerDroid\",\n    62: \"AirCruiser\",\n    63: \"GeonosianWarrior\",\n    64: \"JediStarfighter\",\n}\n\n# key: genie unit id; value: Gather ability name\nGATHER_TASK_LOOKUPS = {\n    13: (\"Fish\", \"fish\"),  # fishing ship\n    56: (\"Fish\", \"fish\"),\n    57: (\"Fish\", \"fish\"),\n    120: (\"CollectBerries\", \"collect_berries\"),\n    354: (\"CollectBerries\", \"collect_berries\"),\n    122: (\"HarvestGame\", \"harvest_game\"),\n    216: (\"HarvestGame\", \"harvest_game\"),\n    123: (\"ExtractCarbon\", \"extract_carbon\"),\n    218: (\"ExtractCarbon\", \"extract_carbon\"),\n    124: (\"MineOre\", \"mine_ore\"),\n    220: (\"MineOre\", \"mine_ore\"),\n    214: (\"FarmCrops\", \"farm_crops\"),\n    259: (\"FarmCrops\", \"farm_crops\"),\n    579: (\"MineNova\", \"mine_nova\"),\n    581: (\"MineNova\", \"mine_nova\"),\n    590: (\"HarvestLivestock\", \"harvest_livestock\"),\n    592: (\"HarvestLivestock\", \"harvest_livestock\"),\n}\n\n# key: restock target unit id; value: Gather ability name\nRESTOCK_TARGET_LOOKUPS = {\n    50: (\"ReseedFarm\", \"reseed_farm\"),\n}\n\n# key: armor class; value: Gather ability name\nARMOR_CLASS_LOOKUPS = {\n    0: \"AirCraft\",\n    1: \"HeavyAssaultMech\",\n    2: \"HeavyMech\",\n    3: \"Energy\",\n    4: \"Melee\",\n    5: \"ForceUnit\",\n    6: \"AssaultMachine\",\n    7: \"Decimator\",\n    8: \"Shielded\",\n    9: \"Ship\",\n    10: \"Submarine\",\n    11: \"Building\",\n    13: \"DefenseBuilding\",\n    14: \"Trooper\",\n    15: \"MountedTrooper\",\n    16: \"Cruiser\",\n    17: \"HeavySiege\",\n    19: \"Worker\",\n    20: \"Destroyer\",\n    21: \"StandardBuilding\",\n    22: \"Wall\",\n    23: \"AirCruiser\",\n    24: \"WildAnimal\",\n    26: \"Fortress\",\n    30: \"TameAnimal\",\n}\n\n# key: command type; value: Apply*Effect ability name\nCOMMAND_TYPE_LOOKUPS = {\n    7: (\"Attack\", \"attack\"),\n    101: (\"Construct\", \"construct\"),\n    104: (\"Convert\", \"convert\"),\n    105: (\"Heal\", \"heal\"),\n    106: (\"Repair\", \"repair\"),\n    110: (\"Hunt\", \"hunt\"),\n}\n"
  },
  {
    "path": "openage/convert/value_object/init/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tgame_file_version.py\n\tgame_version.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/init/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nValue objects used for the converter initilization.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/init/game_file_version.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nAssociate files or filepaths with hash values to determine the exact version of a game.\n\"\"\"\n\n\nclass GameFileVersion:\n    \"\"\"\n    Associates a file hash with a specific version number.\n    This can be used to pinpoint the exact version of a game.\n    \"\"\"\n    hash_algo = \"SHA3-256\"\n\n    def __init__(self, filepaths: list[str], hashes: dict[str, str]):\n        \"\"\"\n        Create a new file hash to version association.\n\n        :param filepaths: Paths to the specified file. Only one of the paths\n                          needs to exist. The other paths are interpreted as\n                          alternatives, e.g. if the game is released on different\n                          platforms with different names for the same file.\n        :type filepaths: list\n        :param hashes: Maps hashes to a version number string.\n        :type hashes: dict\n        \"\"\"\n        self.paths = filepaths\n        if len(self.paths) < 1:\n            raise ValueError(f\"{self}: List of paths cannot be empty.\")\n\n        self.hashes = hashes\n\n    def get_paths(self) -> list[str]:\n        \"\"\"\n        Return all known paths to the file.\n        \"\"\"\n        return self.paths\n\n    def get_hashes(self) -> dict[str, str]:\n        \"\"\"\n        Return the hash-version association for the file paths.\n        \"\"\"\n        return self.hashes\n"
  },
  {
    "path": "openage/convert/value_object/init/game_version.py",
    "content": "# Copyright 2020-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-arguments\n\n\"\"\"\nStores information about base game editions and expansions.\n\"\"\"\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nimport enum\n\nfrom ..read.media_types import MediaType\nfrom .game_file_version import GameFileVersion\n\n\n@enum.unique\nclass Support(enum.Enum):\n    \"\"\"\n    Support state of a game version\n    \"\"\"\n    NOPE = \"not supported\"\n    YES = \"supported\"\n    BREAKS = \"presence breaks conversion\"\n\n\nclass GameBase:\n    \"\"\"\n    Common base class for GameEdition and GameExpansion\n    \"\"\"\n\n    def __init__(\n        self,\n        game_id: str,\n        support: Support,\n        game_version_info: list[tuple[list[str], dict[str, str]]],\n        media_paths: list[tuple[str, list[str]]],\n        modpacks: dict[str, dict[str, str]],\n        **flags\n    ):\n        \"\"\"\n        :param game_id: Unique id for the given game.\n        :type game_id: str\n        :param support: Whether the converter can read/convert\n                               the game to openage formats.\n        :type support: str\n        :param modpacks: List of modpacks.\n        :type modpacks: list\n        :param game_version_info: Versioning information about the game.\n        :type game_version_info: list[tuple]\n        :param media_paths: Media types and their paths\n        :type media_paths: list[tuple]\n        :param flags: Anything else specific to this version which is useful\n                      for the converter.\n        \"\"\"\n        self.game_id = game_id\n        self.flags = flags\n        self.target_modpacks = modpacks\n        self.support = Support[support.upper()]\n\n        self.game_file_versions = []\n        self.media_paths = {}\n\n        self.media_cache = None\n        if \"media_cache\" in flags:\n            self.media_cache = flags[\"media_cache\"]\n\n        for path, hash_map in game_version_info:\n            self.add_game_file_versions(path, hash_map)\n\n        for media_type, paths in media_paths:\n            self.add_media_paths(media_type, paths)\n\n    def add_game_file_versions(self, filepaths: list[str], hashes: dict[str, str]) -> None:\n        \"\"\"\n        Add a GameFileVersion object for files which are unique\n        to this version of the game.\n\n        :param filepaths: Paths to the specified file. Only one of the paths\n                          needs to exist. The other paths are interpreted as\n                          alternatives, e.g. if the game is released on different\n                          platforms with different names for the same file.\n        :type filepaths: list\n        :param hashes: Hash value mapped to a file version.\n        :type hashes: dict\n        \"\"\"\n        self.game_file_versions.append(GameFileVersion(filepaths, hashes))\n\n    def add_media_paths(self, media_type: str, paths: list[str]) -> None:\n        \"\"\"\n        Add a media type with the associated files.\n\n        :param media_type: The type of media file.\n        :type media_type: MediaType\n        :param paths: Paths to those media files.\n        :type paths: list\n        \"\"\"\n        self.media_paths[MediaType[media_type.upper()]] = paths\n\n    def __eq__(self, other: GameBase) -> bool:\n        \"\"\"\n        Compare equality by comparing IDs.\n        \"\"\"\n        return self.game_id == other.game_id\n\n    def __hash__(self) -> int:\n        \"\"\"\n        Reimplement hash to only consider the game ID.\n        \"\"\"\n        return hash(self.game_id)\n\n\nclass GameExpansion(GameBase):\n    \"\"\"\n    An optional expansion to a GameEdition.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        game_id: str,\n        support: Support,\n        game_version_info: list[tuple[list[str], dict[str, str]]],\n        media_paths: list[tuple[str, list[str]]],\n        modpacks: list[str],\n        **flags\n    ):\n        \"\"\"\n        Create a new GameExpansion instance.\n\n        :param name: Name of the game.\n        :type name: str\n        \"\"\"\n        super().__init__(\n            game_id,\n            support,\n            game_version_info,\n            media_paths,\n            modpacks,\n            **flags\n        )\n\n        self.expansion_name = name\n\n    def __str__(self):\n        return self.expansion_name\n\n\nclass GameEdition(GameBase):\n    \"\"\"\n    Standalone/base version of a game. Multiple standalone versions\n    may exist, e.g. AoC, HD, DE2 for AoE2.\n\n    Note that we treat AoE1+Rise of Rome and AoE2+The Conquerors as\n    standalone versions. AoE1 without Rise of Rome or AoK without\n    The Conquerors are considered \"downgrade\" expansions.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        game_id: str,\n        support: Support,\n        game_version_info: list[tuple[list[str], dict[str, str]]],\n        media_paths: list[tuple[str, list[str]]],\n        install_paths: dict[str, list[str]],\n        modpacks: list[str],\n        expansions: list[str],\n        **flags\n    ):\n        \"\"\"\n        Create a new GameEdition instance.\n\n        :param name: Name of the game.\n        :type name: str\n        :param expansions: A list of expansions.\n        :type expansion: list\n        \"\"\"\n        super().__init__(\n            game_id,\n            support,\n            game_version_info,\n            media_paths,\n            modpacks,\n            **flags\n        )\n\n        self.install_paths = install_paths\n        self.edition_name = name\n        self.expansions = tuple(expansions)\n\n    def __str__(self):\n        return self.edition_name\n\n\n@dataclass(frozen=True)\nclass GameVersion:\n    \"\"\"\n    Combination of edition and expansions that defines the exact version\n    of a detected game in a folder.\n    \"\"\"\n    edition: GameEdition\n    expansions: tuple[GameExpansion, ...] = tuple()\n"
  },
  {
    "path": "openage/convert/value_object/read/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tdynamic_loader.py\n\tgenie_structure.py\n\tmedia_types.py\n\tmember_access.py\n\tread_members.py\n\tvalue_members.py\n)\n\nadd_subdirectory(media)\n"
  },
  {
    "path": "openage/convert/value_object/read/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nValue objects used for reading data.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/read/dynamic_loader.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\n\n\"\"\"\nDynamically load and unload data from a file at runtime.\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.genie_structure import GenieStructure\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.value_members import ValueMember\n\n\nclass DynamicLoader:\n    \"\"\"\n    Member that can be loaded and unloaded at runtime, saving\n    memory in the process.\n    \"\"\"\n\n    __slots__ = ('name', 'datacls', 'game_version', 'srcdata', 'offset', 'members', '_loaded')\n\n    def __init__(\n        self,\n        name: str,\n        datacls: type[GenieStructure],\n        game_version: GameVersion,\n        srcdata: bytes,\n        offset: int\n    ):\n        self.datacls = datacls\n        self.game_version = game_version\n\n        self.srcdata = srcdata\n        self.offset = offset\n        self._loaded = False\n\n        self.name = name\n        self.members = None\n\n    def load(self) -> dict[str, ValueMember]:\n        \"\"\"\n        Read the members from the provided source data.\n        \"\"\"\n        datacls = self.datacls()\n        _, members = datacls.read(self.srcdata, self.offset, self.game_version, dynamic_load=False)\n\n        self.members = {}\n        for member in members:\n            self.members[member.name] = member\n\n        self._loaded = True\n\n        return self.members\n\n    def unload(self):\n        \"\"\"\n        Delete the loaded members.\n        \"\"\"\n        del self.members\n        self._loaded = False\n\n    def __getitem__(self, key) -> ValueMember:\n        \"\"\"\n        Retrieve submembers from the loaded members or load them temporarily\n        if they have not been loaded previously.\n        \"\"\"\n        if self._loaded:\n            return self.members[key]\n\n        # else (expensive)\n        self.load()\n        item = self.members[key]\n        self.unload()\n        return item\n\n    def __repr__(self) -> str:\n        return f\"DynamicLoader<{'loaded' if self._loaded else 'unloaded'}>\"\n"
  },
  {
    "path": "openage/convert/value_object/read/genie_structure.py",
    "content": "# Copyright 2014-2024 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\n\nfrom __future__ import annotations\nimport typing\n\nimport math\nimport re\nimport struct\n\nfrom openage.convert.value_object.read.dynamic_loader import DynamicLoader\n\nfrom ....util.strings import decode_until_null\nfrom .member_access import READ, READ_GEN, READ_UNKNOWN, SKIP, MemberAccess\nfrom .read_members import (IncludeMembers, ContinueReadMember,\n                           MultisubtypeMember, GroupMember, SubdataMember,\n                           ReadMember,\n                           EnumLookupMember)\nfrom .value_members import ContainerMember, ArrayMember, IntMember, FloatMember, \\\n    StringMember, BooleanMember, IDMember, BitfieldMember, ValueMember\nfrom .value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n\n\n# regex for matching type array definitions like int[1337]\n# group 1: type name, group 2: length\nVARARRAY_MATCH = re.compile(\"([{0}]+) *\\\\[([{0}]+)\\\\] *;?\".format(\"a-zA-Z0-9_\"))\n\n# match a simple number\nINTEGER_MATCH = re.compile(\"\\\\d+\")\n\n# type lookup for C -> python struct\nSTRUCT_TYPE_LOOKUP = {\n    \"char\":               \"b\",\n    \"unsigned char\":      \"B\",\n    \"int8_t\":             \"b\",\n    \"uint8_t\":            \"B\",\n    \"short\":              \"h\",\n    \"unsigned short\":     \"H\",\n    \"int16_t\":            \"h\",\n    \"uint16_t\":           \"H\",\n    \"int\":                \"i\",\n    \"unsigned int\":       \"I\",\n    \"int32_t\":            \"i\",\n    \"uint32_t\":           \"I\",\n    \"long\":               \"l\",\n    \"unsigned long\":      \"L\",\n    \"long long\":          \"q\",\n    \"unsigned long long\": \"Q\",\n    \"int64_t\":            \"q\",\n    \"uint64_t\":           \"Q\",\n    \"float\":              \"f\",\n    \"double\":             \"d\",\n    \"char[]\":             \"s\",\n}\n\n\nclass GenieStructure:\n    \"\"\"\n    superclass for all structures from Genie Engine games.\n    \"\"\"\n\n    dynamic_load = False\n\n    def __init__(self, **args):\n        # store passed arguments as members\n        self.__dict__.update(args)\n\n    def read(\n        self,\n        raw: bytes,\n        offset: int,\n        game_version: GameVersion,\n        cls: GenieStructure = None,\n        members: tuple = None,\n        dynamic_load = False\n    ) -> tuple[int, list[ValueMember]]:\n        \"\"\"\n        recursively read defined binary data from raw at given offset.\n\n        this is used to fill the python classes with data from the binary input.\n        \"\"\"\n        if cls:\n            target_class = cls\n        else:\n            target_class = self\n\n        # Members are returned at the end\n        generated_value_members = []\n\n        # break out of the current reading loop when members don't exist in\n        # source data file\n        stop_reading_members = False\n\n        if not members:\n            members = target_class.get_data_format(game_version,\n                                                   allowed_modes=(True,\n                                                                  READ,\n                                                                  READ_GEN,\n                                                                  READ_UNKNOWN,\n                                                                  SKIP),\n                                                   flatten_includes=False)\n\n        # Save the start offset in case dynamic loading is active\n        # we still need to read over the whole structure to know\n        # where it stops\n        start_offset = offset\n        for _, export, var_name, storage_type, var_type in members:\n            if export == READ_GEN and dynamic_load and self.dynamic_load:\n                # Do not create members if dynamic loading is active\n                export = READ\n\n            if stop_reading_members:\n                if isinstance(var_type, ReadMember):\n                    replacement_value = var_type.get_empty_value()\n                else:\n                    replacement_value = 0\n\n                setattr(self, var_name, replacement_value)\n                continue\n\n            if isinstance(var_type, GroupMember):\n                offset, gen_members = self._read_group(\n                    raw, offset, game_version, export,\n                    var_name, storage_type, var_type\n                )\n                generated_value_members.extend(gen_members)\n\n            elif isinstance(var_type, MultisubtypeMember):\n                offset, gen_members = self._read_multisubtye(\n                    raw, offset, game_version, export, var_name,\n                    storage_type, var_type, target_class\n                )\n                generated_value_members.extend(gen_members)\n\n            else:\n                offset, gen_members, stop_reading_members = self._read_primitive(\n                    raw, offset, export, var_name,\n                    storage_type, var_type\n                )\n                generated_value_members.extend(gen_members)\n\n        if dynamic_load and self.dynamic_load:\n            return offset, DynamicLoader(\"\", self.__class__, game_version, raw, start_offset)\n\n        return offset, generated_value_members\n\n    def _read_group(\n        self,\n        raw: bytes,\n        offset: int,\n        game_version: GameVersion,\n        export: MemberAccess,\n        var_name: str,\n        storage_type: StorageType,\n        var_type: GroupMember\n    ) -> tuple[int, list[ValueMember]]:\n        generated_value_members = []\n\n        if not issubclass(var_type.cls, GenieStructure):\n            raise TypeError(\"class where members should be \"\n                            \"included is not exportable: %s\" % (\n                                var_type.cls.__name__))\n\n        if isinstance(var_type, IncludeMembers):\n            # call the read function of the referenced class (cls),\n            # but store the data to the current object (self).\n            offset, gen_members = var_type.cls.read(self, raw, offset,\n                                                    game_version,\n                                                    cls=var_type.cls)\n\n            if export == READ_GEN:\n                # Push the passed members directly into the list of generated members\n                generated_value_members.extend(gen_members)\n\n        else:\n            # create new instance of ValueMember,\n            # depending on the storage type.\n            # then save the result as a reference named `var_name`\n            grouped_data = var_type.cls()\n            offset, gen_members = grouped_data.read(raw, offset, game_version)\n\n            setattr(self, var_name, grouped_data)\n\n            if export == READ_GEN:\n                # Store the data\n                if storage_type is StorageType.CONTAINER_MEMBER:\n                    # push the members into a ContainerMember\n                    container = ContainerMember(var_name, gen_members)\n\n                    generated_value_members.append(container)\n\n                elif storage_type is StorageType.ARRAY_CONTAINER:\n                    # create a container for the members first, then push the\n                    # container into an array\n                    container = ContainerMember(\"\", gen_members)\n                    allowed_member_type = StorageType.CONTAINER_MEMBER\n                    array = ArrayMember(var_name, allowed_member_type, [container])\n\n                    generated_value_members.append(array)\n\n                else:\n                    raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                      \"cannot be stored as %s;\"\n                                      \" expected %s or %s\"\n                                      % (var_name, offset, var_type, storage_type,\n                                          StorageType.CONTAINER_MEMBER,\n                                          StorageType.ARRAY_CONTAINER))\n\n        return offset, generated_value_members\n\n    def _read_multisubtye(\n        self,\n        raw: bytes,\n        offset: int,\n        game_version: GameVersion,\n        export: MemberAccess,\n        var_name: str,\n        storage_type: StorageType,\n        var_type: GroupMember,\n        target_class: type\n    ) -> tuple[int, list[ValueMember]]:\n        generated_value_members = []\n\n        # subdata reference implies recursive call for reading the\n        # binary data\n\n        # arguments passed to the next-level constructor.\n        varargs = dict()\n\n        if var_type.passed_args:\n            if isinstance(var_type.passed_args, str):\n                var_type.passed_args = set(var_type.passed_args)\n            for passed_member_name in var_type.passed_args:\n                varargs[passed_member_name] = getattr(\n                    self, passed_member_name)\n\n        # subdata list length has to be defined beforehand as a\n        # object member OR number.  it's name or count is specified\n        # at the subdata member definition by length.\n        list_len = var_type.get_length(self)\n\n        # prepare result storage lists\n        if isinstance(var_type, SubdataMember):\n            # single-subtype child data list\n            setattr(self, var_name, list())\n            single_type_subdata = True\n        else:\n            # multi-subtype child data list\n            setattr(self, var_name, {key: []\n                                     for key in var_type.class_lookup})\n            single_type_subdata = False\n\n        # List for storing the ValueMember instance of each subdata structure\n        subdata_value_members = []\n        allowed_member_type = StorageType.CONTAINER_MEMBER\n\n        # check if entries need offset checking\n        if var_type.offset_to:\n            offset_lookup = getattr(self, var_type.offset_to[0])\n        else:\n            offset_lookup = None\n\n        for i in range(list_len):\n\n            # List of subtype members filled if there's a subtype to be read\n            sub_members = []\n\n            # if datfile offset == 0, entry has to be skipped.\n            if offset_lookup:\n                if not var_type.offset_to[1](offset_lookup[i]):\n                    continue\n                # TODO: don't read sequentially, use the lookup as\n                #       new offset?\n\n            if single_type_subdata:\n                # append single data entry to the subdata object list\n                new_data_class = var_type.class_lookup[None]\n            else:\n                # to determine the subtype class, read the binary\n                # definition. this utilizes an on-the-fly definition\n                # of the data to be read.\n                offset, sub_members = self.read(\n                    raw, offset, game_version, cls=target_class,\n                    members=(((False,) + var_type.subtype_definition),)\n                )\n\n                # read the variable set by the above read call to\n                # use the read data to determine the denominaton of\n                # the member type\n                subtype_name = getattr(\n                    self, var_type.subtype_definition[1])\n\n                # look up the subtype class\n                new_data_class = var_type.class_lookup[subtype_name]\n\n            if not issubclass(new_data_class, GenieStructure):\n                raise TypeError(\"dumped data \"\n                                \"is not exportable: %s\" % (\n                                    new_data_class.__name__))\n\n            # create instance of submember class\n            new_data = new_data_class(**varargs)\n\n            # recursive call, read the subdata.\n            offset, gen_members = new_data.read(raw, offset, game_version, new_data_class)\n\n            # append the new data to the appropriate list\n            if single_type_subdata:\n                getattr(self, var_name).append(new_data)\n            else:\n                getattr(self, var_name)[subtype_name].append(new_data)\n\n            if export == READ_GEN:\n                # Append the data to the ValueMember list\n                if storage_type is StorageType.ARRAY_CONTAINER:\n                    # Put the subtype members in front\n                    sub_members.extend(gen_members)\n                    gen_members = sub_members\n                    # create a container for the retrieved members\n                    container = ContainerMember(\"\", gen_members)\n\n                    # Save the container to a list\n                    # The array is created after the for-loop\n                    subdata_value_members.append(container)\n\n                else:\n                    raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                      \"cannot be stored as %s;\"\n                                      \" expected %s\"\n                                      % (var_name, offset, var_type, storage_type,\n                                          StorageType.ARRAY_CONTAINER))\n\n        if export == READ_GEN:\n            # Create an array from the subdata structures\n            # and append it to the other generated members\n            array = ArrayMember(var_name, allowed_member_type, subdata_value_members)\n            generated_value_members.append(array)\n\n        return offset, generated_value_members\n\n    def _read_primitive(\n        self,\n        raw: bytes,\n        offset: int,\n        export: MemberAccess,\n        var_name: str,\n        storage_type: StorageType,\n        var_type: GroupMember\n    ) -> tuple[int, list[ValueMember], bool]:\n        generated_value_members = []\n        # reading binary data, as this member is no reference but\n        # actual content.\n\n        stop_reading_members = False\n\n        data_count = 1\n        is_array = False\n        is_custom_member = False\n\n        if isinstance(var_type, str):\n            is_array = VARARRAY_MATCH.match(var_type)\n\n            if is_array:\n                struct_type = is_array.group(1)\n                data_count = is_array.group(2)\n                if struct_type == \"char\":\n                    struct_type = \"char[]\"\n\n                if INTEGER_MATCH.match(data_count):\n                    # integer length\n                    data_count = int(data_count)\n                else:\n                    # dynamic length specified by member name\n                    data_count = getattr(self, data_count)\n\n                if storage_type not in (StorageType.STRING_MEMBER,\n                                        StorageType.ARRAY_INT,\n                                        StorageType.ARRAY_FLOAT,\n                                        StorageType.ARRAY_BOOL,\n                                        StorageType.ARRAY_ID,\n                                        StorageType.ARRAY_STRING):\n                    raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                      \"cannot be stored as %s;\"\n                                      \" expected ArrayMember format\"\n                                      % (var_name, offset, var_type, storage_type))\n\n            else:\n                struct_type = var_type\n                data_count = 1\n\n        elif isinstance(var_type, ReadMember):\n            # These could be EnumMember, EnumLookupMember, etc.\n\n            # special type requires having set the raw data type\n            struct_type = var_type.raw_type\n            data_count = var_type.get_length(self)\n            is_custom_member = True\n\n        else:\n            raise TypeError(\n                f\"unknown data member definition {var_type} for member '{var_name}'\")\n\n        if data_count < 0:\n            raise SyntaxError(\"invalid length %d < 0 in %s for member '%s'\" % (\n                data_count, var_type, var_name))\n\n        if struct_type not in STRUCT_TYPE_LOOKUP:\n            raise TypeError(\"%s: member %s requests unknown data type %s\" % (\n                repr(self), var_name, struct_type))\n\n        if export == READ_UNKNOWN:\n            # for unknown variables, generate uid for the unknown\n            # memory location\n            var_name = \"unknown-0x%08x\" % offset\n\n        # lookup c type to python struct scan type\n        symbol = STRUCT_TYPE_LOOKUP[struct_type]\n\n        # read that stuff!!11\n        struct_format = \"< %d%s\" % (data_count, symbol)\n\n        if export != SKIP:\n            result = struct.unpack_from(struct_format, raw, offset)\n\n            if is_custom_member:\n                if not var_type.verify_read_data(self, result):\n                    raise SyntaxError(\"invalid data when reading %s \"\n                                      \"at offset %# 08x\" % (var_name, offset))\n\n            # TODO: move these into a read entry hook/verification method\n            if symbol == \"s\":\n                # stringify char array\n                result = decode_until_null(result[0])\n\n                if export == READ_GEN:\n                    if storage_type is StorageType.STRING_MEMBER:\n                        gen_member = StringMember(var_name, result)\n\n                    else:\n                        raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                          \"cannot be stored as %s;\"\n                                          \" expected %s\"\n                                          % (var_name, offset, var_type, storage_type,\n                                              StorageType.STRING_MEMBER))\n\n                    generated_value_members.append(gen_member)\n\n            elif is_array:\n                if export == READ_GEN:\n                    # Turn every element of result into a member\n                    # and put them into an array\n                    array_members = []\n                    allowed_member_type = None\n\n                    if storage_type is StorageType.ARRAY_INT:\n                        allowed_member_type = StorageType.INT_MEMBER\n\n                    elif storage_type is StorageType.ARRAY_FLOAT:\n                        allowed_member_type = StorageType.FLOAT_MEMBER\n\n                    elif storage_type is StorageType.ARRAY_BOOL:\n                        allowed_member_type = StorageType.BOOLEAN_MEMBER\n\n                    elif storage_type is StorageType.ARRAY_ID:\n                        allowed_member_type = StorageType.ID_MEMBER\n\n                    elif storage_type is StorageType.ARRAY_STRING:\n                        allowed_member_type = StorageType.STRING_MEMBER\n\n                    for elem in result:\n                        if storage_type is StorageType.ARRAY_INT:\n                            gen_member = IntMember(var_name, elem)\n                            array_members.append(gen_member)\n\n                        elif storage_type is StorageType.ARRAY_FLOAT:\n                            gen_member = FloatMember(var_name, elem)\n                            array_members.append(gen_member)\n\n                        elif storage_type is StorageType.ARRAY_BOOL:\n                            gen_member = BooleanMember(var_name, elem)\n                            array_members.append(gen_member)\n\n                        elif storage_type is StorageType.ARRAY_ID:\n                            gen_member = IDMember(var_name, elem)\n                            array_members.append(gen_member)\n\n                        elif storage_type is StorageType.ARRAY_STRING:\n                            gen_member = StringMember(var_name, elem)\n                            array_members.append(gen_member)\n\n                        else:\n                            raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                              \"cannot be stored as %s;\"\n                                              \" expected %s, %s, %s, %s or %s\"\n                                              % (var_name, offset, var_type, storage_type,\n                                                  StorageType.ARRAY_INT,\n                                                  StorageType.ARRAY_FLOAT,\n                                                  StorageType.ARRAY_BOOL,\n                                                  StorageType.ARRAY_ID,\n                                                  StorageType.ARRAY_STRING))\n\n                    # Create the array\n                    array = ArrayMember(var_name, allowed_member_type, array_members)\n                    generated_value_members.append(array)\n\n            elif data_count == 1:\n                # store first tuple element\n                result = result[0]\n\n                if symbol == \"f\":\n                    if not math.isfinite(result):\n                        raise SyntaxError(\"invalid float when \"\n                                          \"reading %s at offset %# 08x\" % (\n                                              var_name, offset))\n\n                if export == READ_GEN:\n                    # Store the member as ValueMember\n                    if is_custom_member:\n                        lookup_result = var_type.entry_hook(result)\n\n                        if isinstance(var_type, EnumLookupMember):\n                            # store differently depending on storage type\n                            if storage_type is StorageType.INT_MEMBER:\n                                # store as plain integer value\n                                gen_member = IntMember(var_name, result)\n\n                            elif storage_type is StorageType.ID_MEMBER:\n                                # store as plain integer value\n                                gen_member = IDMember(var_name, result)\n\n                            elif storage_type is StorageType.BITFIELD_MEMBER:\n                                # store as plain integer value\n                                gen_member = BitfieldMember(var_name, result)\n\n                            elif storage_type is StorageType.STRING_MEMBER:\n                                # store by looking up value from dict\n                                gen_member = StringMember(var_name, lookup_result)\n\n                            else:\n                                raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                                  \"cannot be stored as %s;\"\n                                                  \" expected %s, %s, %s or %s\"\n                                                  % (var_name, offset, var_type, storage_type,\n                                                      StorageType.INT_MEMBER,\n                                                      StorageType.ID_MEMBER,\n                                                      StorageType.BITFIELD_MEMBER,\n                                                      StorageType.STRING_MEMBER))\n\n                        elif isinstance(var_type, ContinueReadMember):\n                            if storage_type is StorageType.BOOLEAN_MEMBER:\n                                gen_member = StringMember(var_name, lookup_result)\n\n                            else:\n                                raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                                  \"cannot be stored as %s;\"\n                                                  \" expected %s\"\n                                                  % (var_name, offset, var_type, storage_type,\n                                                      StorageType.BOOLEAN_MEMBER))\n\n                    else:\n                        if storage_type is StorageType.INT_MEMBER:\n                            gen_member = IntMember(var_name, result)\n\n                        elif storage_type is StorageType.FLOAT_MEMBER:\n                            gen_member = FloatMember(var_name, result)\n\n                        elif storage_type is StorageType.BOOLEAN_MEMBER:\n                            gen_member = BooleanMember(var_name, result)\n\n                        elif storage_type is StorageType.ID_MEMBER:\n                            gen_member = IDMember(var_name, result)\n\n                        else:\n                            raise SyntaxError(\"%s at offset %# 08x: Data read via %s \"\n                                              \"cannot be stored as %s;\"\n                                              \" expected %s, %s, %s or %s\"\n                                              % (var_name, offset, var_type, storage_type,\n                                                  StorageType.INT_MEMBER,\n                                                  StorageType.FLOAT_MEMBER,\n                                                  StorageType.BOOLEAN_MEMBER,\n                                                  StorageType.ID_MEMBER))\n\n                    generated_value_members.append(gen_member)\n\n            # run entry hook for non-primitive members\n            if is_custom_member:\n                result = var_type.entry_hook(result)\n\n                if result == ContinueReadMember.result.ABORT:\n                    # don't go through all other members of this class!\n                    stop_reading_members = True\n\n            # store member's data value\n            setattr(self, var_name, result)\n\n        # increase the current file position by the size we just read\n        offset += struct.calcsize(struct_format)\n\n        return offset, generated_value_members, stop_reading_members\n\n    @classmethod\n    def get_data_format(\n        cls,\n        game_version: GameVersion,\n        allowed_modes: tuple[MemberAccess] = None,\n        flatten_includes: bool = False,\n        is_parent: bool = False\n    ):\n        \"\"\"\n        return all members of this exportable (a struct.)\n\n        can filter by export modes and can also return included members:\n        inherited members can either be returned as to-be-included,\n        or can be fetched and displayed as if they weren't inherited.\n        \"\"\"\n        for member in cls.get_data_format_members(game_version):\n            if len(member) != 4:\n                print(member[1])\n\n            export, _, _, read_type = member\n\n            definitively_return_member = False\n\n            if isinstance(read_type, IncludeMembers):\n                if flatten_includes:\n                    # recursive call\n                    yield from read_type.cls.get_data_format(game_version,\n                                                             allowed_modes,\n                                                             flatten_includes,\n                                                             is_parent=True)\n                    continue\n\n            elif isinstance(read_type, ContinueReadMember):\n                definitively_return_member = True\n\n            if allowed_modes:\n                if export not in allowed_modes:\n                    if not definitively_return_member:\n                        continue\n\n            member_entry = (is_parent,) + member\n            yield member_entry\n\n    @classmethod\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n\n        struct format specification\n        ===========================================================\n        contains a list of 4-tuples that define\n        (read_mode, var_name, storage_type, read_type)\n\n        read_mode: Tells whether to read or skip values\n        var_name: The stored name of the extracted variable.\n                  Must be unique for each ConverterObject\n        storage_type: ValueMember type for storage\n                      (see value_members.StorageType)\n        read_type: ReadMember type for reading the values from bytes\n                   (see read_members.py)\n        ===========================================================\n\n        \"\"\"\n        raise NotImplementedError(\"Subclass has not implemented this function\")\n\n    def __repr__(self) -> str:\n        return self.__class__.__name__\n"
  },
  {
    "path": "openage/convert/value_object/read/media/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tblendomatic.py\n\tcolortable.py\n\tdrs.py\n\tlangcodes.py\n\tpefile.py\n\tperesource.py\n)\n\nadd_cython_modules(\n\tsld.pyx\n\tslp.pyx\n\tsmp.pyx\n\tsmx.pyx\n)\n\nadd_pxds(\n\t__init__.pxd\n)\n\nadd_subdirectory(datfile)\nadd_subdirectory(hardcoded)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/__init__.pxd",
    "content": ""
  },
  {
    "path": "openage/convert/value_object/read/media/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPython containers for the original media files.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/read/media/blendomatic.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=too-many-function-args\n\n\"\"\"\nConversion for the terrain blending masks.\nThose originate from blendomatic.dat.\n\nFor more information, see doc/media/blendomatic.md\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom math import sqrt\nfrom struct import Struct, unpack_from\nimport numpy\n\n\nfrom .....log import dbg\nfrom ..genie_structure import GenieStructure\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.export.texture import Texture\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n    from openage.convert.value_object.read.value_members import StorageType\n    from openage.util.fslike.wrapper import GuardedFile\n\n\nclass BlendingTile:\n    \"\"\"\n    One blending mask tile.\n    The blendomatic stores many of those.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    def __init__(self, row_data: list[list[int]], width: int, height: int):\n        self.row_data = row_data\n        self.width = width\n        self.height = height\n\n    def get_picture_data(self) -> numpy.array:\n        \"\"\"\n        Return a numpy array of image data for a blending tile.\n        \"\"\"\n        tile_rows = []\n\n        for picture_row in self.row_data:\n            tile_row_data = []\n\n            for alpha_data in picture_row:\n\n                if alpha_data == -1:\n                    # draw full transparency\n                    alpha = 0\n                    val = 0\n                else:\n                    if alpha_data == 128:\n                        alpha = 255\n                        val = 0\n                    else:\n                        # original data contains 7-bit values only\n                        alpha = 128\n                        val = (127 - (alpha_data & 0x7f)) * 2\n\n                tile_row_data.append((val, val, val, alpha))\n\n            tile_rows.append(tile_row_data)\n\n        return numpy.array(tile_rows)\n\n\nclass BlendingMode:\n    \"\"\"\n    One blending mode, which contains tiles that make up\n    for all the influence directions possible.\n    One mode is for example \"ice\" or \"grass\", i.e. the transition shape.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    def __init__(\n        self,\n        idx: int,\n        data_file: GuardedFile,\n        tile_count: int,\n        header: tuple\n    ):\n        \"\"\"\n        initialize one blending mode,\n        consisting of multiple frames for all blending directions\n\n        the bitmasks were used to decide whether this pixel has\n        to be used for calculations.\n\n        the alphamask is used to determine the alpha amount for blending.\n        \"\"\"\n\n        # should be 2353 -> number of pixels (single alpha byte values)\n        self.pxcount = header[0]\n        # tile_flags = header[1:]  # TODO what do they do?\n\n        dbg(\"blending mode %d tiles have %d pixels\", idx, self.pxcount)\n\n        # as we draw in isometric tile format, this is the row count\n        self.row_count = int(sqrt(self.pxcount)) + 1  # should be 49\n\n        # alpha_masks_raw is an array of bytes that will draw 32 images,\n        # which are bit masks.\n        #\n        # one of these masks also has 2353 pixels\n        # the storage of the bit masks is 4*tilesize, here's why:\n        #\n        # 4 * 8bit * 2353 pixels = 75296 bitpixels\n        # ==> 75296/(32 images) = 2353 bitpixel/image\n        #\n        # this means if we interprete the 75296 bitpixels as 32 images,\n        # each of these images gets 2353 bit as data.\n        # TODO: why 32 images? isn't that depending on tile_count?\n\n        alpha_masks_raw = unpack_from(f\"{self.pxcount * 4:d}B\",\n                                      data_file.read(self.pxcount * 4))\n\n        # list of alpha-mask tiles\n        self.alphamasks = []\n\n        # draw mask tiles for this blending mode\n        for _ in range(tile_count):\n            pixels = unpack_from(f\"{self.pxcount:d}B\",\n                                 data_file.read(self.pxcount))\n            self.alphamasks.append(self.get_tile_from_data(pixels))\n\n        bitvalues = []\n        for i in alpha_masks_raw:\n            for b_id in range(7, -1, -1):\n                # bitmask from 0b00000001 to 0b10000000\n                bit_mask = 2 ** b_id\n                bitvalues.append(i & bit_mask)\n\n        # list of bit-mask tiles\n        self.bitmasks = []\n\n        # TODO: is 32 really hardcoded?\n        for i in range(32):\n            pixels = bitvalues[i * self.pxcount:(i + 1) * self.pxcount]\n\n            self.bitmasks.append(self.get_tile_from_data(pixels))\n\n    def get_tile_from_data(self, data: list[int]) -> BlendingTile:\n        \"\"\"\n        get the data pixels, interprete them in isometric tile format\n\n          ....*....\n          ..*****..\n          *********\n          ..*****..\n          ....*....  like this, only bigger..\n\n        we end up drawing the rhombus with 49 rows.\n        the space indicated by . is added by the function.\n        \"\"\"\n\n        half_row_count = self.row_count // 2\n        tile_size = len(data)\n\n        read_so_far = 0\n        max_width = 0\n        tilerows = []\n\n        for y_pos in range(self.row_count):\n            if y_pos < half_row_count:\n                # upper half of the tile\n                # row i+1 has 4 more pixels than row i\n                # another +1 for the middle one\n                read_values = 1 + (4 * y_pos)\n            else:\n                # lower half of tile\n                read_values = (((self.row_count * 2) - 1) -\n                               (4 * (y_pos - half_row_count)))\n\n            if read_values > (tile_size - read_so_far):\n                raise SyntaxError(\"reading more bytes than tile has left\")\n            if read_values < 0:\n                raise SyntaxError(f\"reading negative count: {read_values:d}\")\n\n            # grab the pixels out of the big list\n            pixels = list(data[read_so_far:(read_so_far + read_values)])\n\n            # how many empty pixels on the left before the real data begins\n            space_count = self.row_count - 1 - (read_values // 2)\n\n            # insert padding to the left and right (-1 for fully transparent)\n            padding = [-1] * space_count\n            pixels = padding + pixels + padding\n\n            if len(pixels) > max_width:\n                max_width = len(pixels)\n\n            read_so_far += read_values\n            tilerows.append(pixels)\n\n        if read_so_far != tile_size:\n            raise SyntaxError(f\"got leftover bytes: {tile_size - read_so_far:d}\")\n\n        return BlendingTile(tilerows, max_width, self.row_count)\n\n\nclass Blendomatic(GenieStructure):\n    \"\"\"\n    Represents the blendomatic.dat file.\n    In it are multiple blending modes,\n    which then contain multiple tiles.\n\n    TODO: Blendomatic should work like SLP, meaning that the Blendomatic PNG\n          should not create the texture, but rather be created by Texture\n          during __init__ with a custom merger.\n    \"\"\"\n\n    name_struct = \"blending_mode\"\n    name_struct_file = \"blending_mode\"\n    struct_description = (\"describes one blending mode, \"\n                          \"a blending transition shape \"\n                          \"between two different terrain types.\")\n\n    # struct blendomatic_header {\n    #   unsigned int nr_blending_modes;\n    #   unsigned int nr_tiles;\n    # };\n    blendomatic_header = Struct(\"< I I\")\n\n    def __init__(self, fileobj: GuardedFile, custom_mode_count: int = None):\n        super().__init__()\n\n        buf = fileobj.read(Blendomatic.blendomatic_header.size)\n        self.header = Blendomatic.blendomatic_header.unpack_from(buf)\n\n        blending_mode_count, tile_count = self.header\n\n        dbg(\"%d blending modes, each %d tiles\",\n            blending_mode_count, tile_count)\n\n        if custom_mode_count:\n            blending_mode_count = custom_mode_count\n\n            dbg(\"reading only the first %d blending modes\",\n                custom_mode_count)\n\n        blending_mode = Struct(f\"< I {tile_count:d}B\")\n\n        self.blending_modes = []\n\n        for i in range(blending_mode_count):\n            header_data = fileobj.read(blending_mode.size)\n            bmode_header = blending_mode.unpack_from(header_data)\n\n            new_mode = BlendingMode(i, fileobj, tile_count, bmode_header)\n\n            self.blending_modes.append(new_mode)\n\n        fileobj.close()\n\n    def get_textures(self) -> list[Texture]:\n        \"\"\"\n        generate a list of textures.\n\n        one atlas per blending mode is generated,\n        each atlas contains all blending masks merged on one texture\n        \"\"\"\n        from ....entity_object.export.texture import Texture\n        return [Texture(b_mode) for b_mode in self.blending_modes]\n\n    @classmethod\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = (\n            (True, \"blend_mode\", None, \"int32_t\"),\n        )\n\n        return data_format\n\n    def __str__(self):\n        return str(self.blending_modes)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/colortable.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R,too-many-function-args\nfrom __future__ import annotations\nimport typing\n\nimport math\n\nimport numpy\n\n\nfrom .....log import dbg\nfrom ..genie_structure import GenieStructure\n\nif typing.TYPE_CHECKING:\n    from PIL import Image\n\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n    from openage.convert.value_object.read.value_members import StorageType\n    from openage.util.fslike.wrapper import GuardedFile\n\n\nclass ColorTable(GenieStructure):\n    __slots__ = ('header', 'version', 'palette')\n\n    def __init__(self, data: typing.Union[list, tuple, bytes]):\n        super().__init__()\n\n        if isinstance(data, list) or isinstance(data, tuple):\n            self.fill_from_array(data)\n        else:\n            self.fill(data)\n\n        # Fast access for media conversion\n        self.array = self.get_ndarray()\n\n    def fill_from_array(self, ar: typing.Union[list, tuple]) -> None:\n        self.palette = [tuple(e) for e in ar]\n\n    def fill(self, data: bytes) -> None:\n        # split all lines of the input data\n        # \\r\\n windows windows windows baby\n        lines = data.decode('ascii').split('\\r\\n')\n\n        self.header = lines[0]\n        self.version = lines[1]\n\n        # check for palette header\n        if not (self.header == \"JASC-PAL\" or self.header == \"JASC-PALX\"):\n            raise SyntaxError(\"No palette header 'JASC-PAL' or 'JASC-PALX' found, \"\n                              \"instead: %r\" % self.header)\n\n        if self.version != \"0100\":\n            raise SyntaxError(f\"palette version mispatch, got {self.version}\")\n\n        entry_count = int(lines[2])\n\n        entry_start = 3\n        if lines[3].startswith(\"$ALPHA\"):\n            # TODO: Definitive Editions have palettes with fixed alpha\n            entry_start = 4\n\n        self.palette = []\n\n        # data entries from 'entry_start' to n\n        for line in lines[entry_start:]:\n            # skip comments and empty lines\n            if not line or line.startswith(\"#\"):\n                continue\n\n            # one entry looks like \"13 37 42\",\n            # \"red green blue\"\n            # => red 13, 37 green and 42 blue.\n            # DE1 and DE2 have a fourth value, but it seems unused\n            self.palette.append(tuple(int(val) for val in line.split()))\n\n        if len(self.palette) != entry_count:\n            raise SyntaxError(\"read a %d palette entries \"\n                              \"but expected %d.\" % (\n                                  len(self.palette), entry_count))\n\n    def __getitem__(self, index):\n        return self.palette[index]\n\n    def __len__(self):\n        return len(self.palette)\n\n    def __repr__(self):\n        return \"ColorTable<%d entries>\" % len(self.palette)\n\n    def __str__(self):\n        return f\"{repr(self)}\\n{self.palette}\"\n\n    def gen_image(self, draw_text: bool = True, squaresize: int = 100) -> Image:\n        \"\"\"\n        writes this color table (palette) to a png image.\n        \"\"\"\n\n        from PIL import Image, ImageDraw\n\n        imgside_length = math.ceil(math.sqrt(len(self.palette)))\n        imgsize = imgside_length * squaresize\n\n        dbg(\"generating palette image with size %dx%d\", imgsize, imgsize)\n\n        palette_image = Image.new('RGBA', (imgsize, imgsize),\n                                  (255, 255, 255, 0))\n        draw = ImageDraw.ImageDraw(palette_image)\n\n        # dirty, i know...\n        text_padlength = len(str(len(self.palette)))\n        text_format = \"%%0%dd\" % (text_padlength)\n\n        drawn = 0\n\n        # squaresize 1 means draw single pixels\n        if squaresize == 1:\n            for y in range(imgside_length):\n                for x in range(imgside_length):\n                    if drawn < len(self.palette):\n                        r, g, b = self.palette[drawn]\n                        draw.point((x, y), fill=(r, g, b, 255))\n                        drawn = drawn + 1\n\n        # draw nice squares with given side length\n        elif squaresize > 1:\n            for y in range(imgside_length):\n                for x in range(imgside_length):\n                    if drawn < len(self.palette):\n                        sx = x * squaresize - 1\n                        sy = y * squaresize - 1\n                        ex = sx + squaresize - 1\n                        ey = sy + squaresize\n                        r, g, b = self.palette[drawn]\n                        # begin top-left, go clockwise:\n                        vertices = [(sx, sy), (ex, sy), (ex, ey), (sx, ey)]\n                        draw.polygon(vertices, fill=(r, g, b, 255))\n\n                        if draw_text and squaresize > 40:\n                            # draw the color id\n                            # insert current color id into string\n                            ctext = text_format % drawn\n                            tcolor = (255 - r, 255 - b, 255 - g, 255)\n\n                            # draw the text\n                            # TODO: use customsized font\n                            draw.text((sx + 3, sy + 1), ctext,\n                                      fill=tcolor, font=None)\n\n                        drawn = drawn + 1\n\n        else:\n            raise ValueError(\"fak u, no negative values for squaresize pls.\")\n\n        return palette_image\n\n    def get_ndarray(self) -> numpy.array:\n        return numpy.array(self.palette, dtype=numpy.uint8, order='C')\n\n    def save_visualization(self, fileobj: GuardedFile) -> None:\n        self.gen_image().save(fileobj, 'png')\n\n    @classmethod\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = (\n            (True, \"idx\", None, \"int32_t\"),\n            (True, \"r\", None,   \"uint8_t\"),\n            (True, \"g\", None,   \"uint8_t\"),\n            (True, \"b\", None,   \"uint8_t\"),\n            (True, \"a\", None,   \"uint8_t\"),\n        )\n\n        return data_format\n\n\nclass PlayerColorTable(GenieStructure):\n    \"\"\"\n    this class represents stock player color values.\n\n    each player has 8 subcolors, where 0 is the darkest and 7 is the lightest\n    \"\"\"\n\n    __slots__ = ('header', 'version', 'palette')\n\n    def __init__(self, base_table: ColorTable):\n        super().__init__()\n\n        if not isinstance(base_table, ColorTable):\n            raise TypeError(f\"no ColorTable supplied, instead: {type(base_table)}\")\n\n        self.header = base_table.header\n        self.version = base_table.version\n        self.palette = list()\n        # now, strip the base table entries to the player colors.\n\n        # entry id = ((playernum-1) * 8) + subcolor\n        players = range(1, 9)\n        psubcolors = range(8)\n\n        for i in players:\n            for subcol in psubcolors:\n                r, g, b = base_table[16 * i + subcol]\n                self.palette.append((r, g, b))\n\n    @classmethod\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = (\n            (True, \"idx\", None, \"int32_t\"),\n            (True, \"r\", None,   \"uint8_t\"),\n            (True, \"g\", None,   \"uint8_t\"),\n            (True, \"b\", None,   \"uint8_t\"),\n            (True, \"a\", None,   \"uint8_t\"),\n        )\n\n        return data_format\n\n    def __repr__(self):\n        return \"PlayerColorTable<%d entries>\" % len(self.palette)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tciv.py\n\tempiresdat.py\n\tgraphic.py\n\tlookup_dicts.py\n\tmaps.py\n\tplayercolor.py\n\tresearch.py\n\tsound.py\n\ttech.py\n\tterrain.py\n\tunit.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/__init__.py",
    "content": "# Copyright 2013-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThe various structs that make up empires.dat\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/civ.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom . import unit\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import MultisubtypeMember, EnumLookupMember\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass Civ(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            # always 1\n            (SKIP, \"player_type\", StorageType.INT_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (SKIP, \"name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_len]\"),\n            ])\n        else:\n            data_format.extend([\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[20]\"),\n            ])\n\n        data_format.extend([\n            (READ, \"resources_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            # links to effect bundle id (to apply its effects)\n            (READ_GEN, \"tech_tree_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            # links to tech id as well\n            data_format.append((READ_GEN, \"team_bonus_id\", StorageType.ID_MEMBER, \"int16_t\"))\n\n            if game_version.edition.game_id == \"SWGB\":\n                data_format.extend([\n                    (READ_GEN, \"name2\", StorageType.STRING_MEMBER, \"char[20]\"),\n                    (READ_GEN, \"unique_unit_techs\", StorageType.ARRAY_ID, \"int16_t[4]\"),\n                ])\n\n        data_format.extend([\n            (READ_GEN, \"resources\", StorageType.ARRAY_FLOAT, \"float[resources_count]\"),\n            # building icon set, trade cart graphics, changes no other graphics\n            (READ_GEN, \"icon_set\", StorageType.ID_MEMBER, \"int8_t\"),\n            (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ, \"unit_offsets\", StorageType.ARRAY_ID, \"int32_t[unit_count]\"),\n\n            (READ_GEN, \"units\", StorageType.ARRAY_CONTAINER, MultisubtypeMember(\n                type_name          = \"unit_types\",\n                subtype_definition = (READ_GEN, \"unit_type\", StorageType.ID_MEMBER, EnumLookupMember(\n                    type_name      = \"unit_type_id\",\n                    lookup_dict    = unit.unit_type_lookup,\n                    raw_type       = \"int8_t\",\n                )),\n                class_lookup       = unit.unit_type_class_lookup,\n                length             = \"unit_count\",\n                offset_to          = (\"unit_offsets\", lambda o: o > 0),\n            )),\n        ])\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/empiresdat.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom . import civ\nfrom . import graphic\nfrom . import maps\nfrom . import playercolor\nfrom . import research\nfrom . import sound\nfrom . import tech\nfrom . import terrain\nfrom . import unit\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, READ_UNKNOWN, SKIP\nfrom ....read.read_members import SubdataMember\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\n# this file can parse and represent the empires2_x1_p1.dat file.\n#\n# the dat file contain all the information needed for running the game.\n# all units, buildings, terrains, whatever are defined in this dat file.\n#\n# documentation for this can be found in `doc/gamedata`\n# the binary structure, which the dat file has, is in `doc/gamedata.struct`\nclass EmpiresDat(GenieStructure):\n    \"\"\"\n    class for fighting and beating the compressed empires2*.dat\n\n    represents the main game data file.\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [(READ_GEN, \"versionstr\", StorageType.STRING_MEMBER, \"char[8]\")]\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"civ_count_swgb\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_UNKNOWN, None, StorageType.INT_MEMBER, \"int32_t\"),\n                (READ_UNKNOWN, None, StorageType.INT_MEMBER, \"int32_t\"),\n                # Number of blend modes used in the game?\n                (READ_GEN, \"blend_mode_count_swgb\", StorageType.INT_MEMBER, \"int32_t\"),\n                # Number of blend modes that they wanted to use??\n                (READ_GEN, \"blend_mode_count_max_swgb\", StorageType.INT_MEMBER, \"int32_t\"),\n            ])\n\n        # terrain header data\n        data_format.extend([\n            (READ, \"terrain_restriction_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ, \"terrain_count\", StorageType.INT_MEMBER, \"uint16_t\"),   # number of \"used\" terrains\n            (READ, \"float_ptr_terrain_tables\", StorageType.ARRAY_ID,\n             \"int32_t[terrain_restriction_count]\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((READ, \"terrain_pass_graphics_ptrs\",\n                               StorageType.ARRAY_ID, \"int32_t[terrain_restriction_count]\"))\n\n        data_format.extend([\n            (READ_GEN, \"terrain_restrictions\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.TerrainRestriction,\n                length=\"terrain_restriction_count\",\n                passed_args={\"terrain_count\"},\n            )),\n\n            # player color data\n            (READ, \"player_color_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"player_colors\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=playercolor.PlayerColor,\n                length=\"player_color_count\",\n            )),\n\n            # sound data\n            (READ, \"sound_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"sounds\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=sound.Sound,\n                length=\"sound_count\",\n            )),\n\n            # graphic data\n            (READ, \"graphic_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ, \"graphic_ptrs\", StorageType.ARRAY_ID, \"uint32_t[graphic_count]\"),\n            (READ_GEN, \"graphics\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type  = graphic.Graphic,\n                length    = \"graphic_count\",\n                offset_to = (\"graphic_ptrs\", lambda o: o > 0),\n            )),\n\n            # terrain data\n            (SKIP, \"virt_function_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"map_pointer\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"map_width\", StorageType.INT_MEMBER, \"int32_t\"),\n            (SKIP, \"map_height\", StorageType.INT_MEMBER, \"int32_t\"),\n            (SKIP, \"world_width\", StorageType.INT_MEMBER, \"int32_t\"),\n            (SKIP, \"world_height\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ_GEN,  \"tile_sizes\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.TileSize,\n                length=19,      # number of tile types\n            )),\n            (SKIP, \"padding1\", StorageType.INT_MEMBER, \"int16_t\"),\n        ])\n\n        # Stored terrain number is hardcoded.\n        # Usually less terrains are used by the game\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.Terrain,\n                length=55,\n            )))\n        elif game_version.edition.game_id == \"AOE2DE\":\n            data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.Terrain,\n                length=200,\n            )))\n        elif game_version.edition.game_id == \"AOE1DE\":\n            data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.Terrain,\n                length=96,\n            )))\n        elif game_version.edition.game_id == \"HDEDITION\":\n            if len(game_version.expansions) > 0:\n                data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=terrain.Terrain,\n                    length=100,\n                )))\n\n            else:\n                data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=terrain.Terrain,\n                    length=42,\n                )))\n\n        elif game_version.edition.game_id == \"AOC\":\n            data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.Terrain,\n                length=42,\n            )))\n        else:  # game_version.edition.game_id == \"ROR\"\n            data_format.append((READ_GEN, \"terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=terrain.Terrain,\n                length=32,\n            )))\n\n        if game_version.edition.game_id != \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"terrain_border\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=terrain.TerrainBorder,\n                    length=16,\n                )),\n                (SKIP, \"map_row_offset\", StorageType.INT_MEMBER, \"int32_t\"),\n            ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"map_min_x\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"map_min_y\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"map_max_x\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"map_max_y\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"map_max_xplus1\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"map_min_yplus1\", StorageType.FLOAT_MEMBER, \"float\"),\n            ])\n\n        data_format.extend([\n            (READ, \"terrain_count_additional\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ, \"borders_used\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ, \"max_terrain\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"tile_width\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"tile_height\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"tile_half_height\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"tile_half_width\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"elev_height\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"current_row\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"current_column\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"block_beginn_row\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"block_end_row\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"block_begin_column\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"block_end_column\", StorageType.INT_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"search_map_ptr\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"search_map_rows_ptr\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"any_frame_change\", StorageType.INT_MEMBER, \"int8_t\"),\n            ])\n        else:\n            data_format.extend([\n                (SKIP, \"any_frame_change\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"search_map_ptr\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"search_map_rows_ptr\", StorageType.INT_MEMBER, \"int32_t\"),\n            ])\n\n        data_format.extend([\n            (SKIP, \"map_visible_flag\", StorageType.INT_MEMBER, \"int8_t\"),\n            (SKIP, \"fog_flag\", StorageType.INT_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id != \"AOE2DE\":\n            if game_version.edition.game_id == \"SWGB\":\n                data_format.extend([\n                    (READ_UNKNOWN, \"terrain_blob0\", StorageType.ARRAY_INT, \"uint8_t[25]\"),\n                    (READ_UNKNOWN, \"terrain_blob1\", StorageType.ARRAY_INT, \"uint32_t[157]\"),\n                ])\n            elif game_version.edition.game_id in (\"ROR\", \"AOE1DE\"):\n                data_format.extend([\n                    (READ_UNKNOWN, \"terrain_blob0\", StorageType.ARRAY_INT, \"uint8_t[2]\"),\n                    (READ_UNKNOWN, \"terrain_blob1\", StorageType.ARRAY_INT, \"uint32_t[5]\"),\n                ])\n            else:\n                data_format.extend([\n                    (READ_UNKNOWN, \"terrain_blob0\", StorageType.ARRAY_INT, \"uint8_t[21]\"),\n                    (READ_UNKNOWN, \"terrain_blob1\", StorageType.ARRAY_INT, \"uint32_t[157]\"),\n                ])\n\n        data_format.extend([\n            # random map config\n            (READ, \"random_map_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"random_map_ptr\", StorageType.ID_MEMBER, \"uint32_t\"),\n            (READ, \"map_infos\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=maps.MapInfo,\n                length=\"random_map_count\",\n            )),\n            (READ, \"maps\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=maps.Map,\n                length=\"random_map_count\",\n            )),\n\n            # technology effect data\n            (READ, \"effect_bundle_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ_GEN, \"effect_bundles\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=tech.EffectBundle,\n                length=\"effect_bundle_count\",\n            )),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ, \"unit_line_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"unit_lines\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=unit.UnitLine,\n                    length=\"unit_line_count\",\n                )),\n            ])\n\n        # unit header data\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"unit_headers\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=unit.UnitHeader,\n                    length=\"unit_count\",\n                )),\n            ])\n\n        # civilisation data\n        data_format.extend([\n            (READ, \"civ_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"civs\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=civ.Civ,\n                length=\"civ_count\"\n            )),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.append((READ_UNKNOWN, None, StorageType.INT_MEMBER, \"int8_t\"))\n\n        # research data\n        data_format.extend([\n            (READ, \"research_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"researches\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=research.Tech,\n                length=\"research_count\"\n            )),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.append((READ_UNKNOWN, None, StorageType.INT_MEMBER, \"int8_t\"))\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"time_slice\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"unit_kill_rate\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"unit_kill_total\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"unit_hitpoint_rate\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"unit_hitpoint_total\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"razing_kill_rate\", StorageType.INT_MEMBER, \"int32_t\"),\n                (SKIP, \"razing_kill_total\", StorageType.INT_MEMBER, \"int32_t\"),\n            ])\n\n            # technology tree data\n            data_format.extend([\n                (READ, \"age_connection_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ, \"building_connection_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n            ])\n\n            if game_version.edition.game_id == \"SWGB\":\n                data_format.append((READ, \"unit_connection_count\",\n                                   StorageType.INT_MEMBER, \"uint16_t\"))\n\n            else:\n                data_format.append((READ, \"unit_connection_count\",\n                                   StorageType.INT_MEMBER, \"uint8_t\"))\n\n            data_format.extend([\n                (READ, \"tech_connection_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"total_unit_tech_groups\", StorageType.INT_MEMBER, \"int32_t\"),\n                (READ_GEN, \"age_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=tech.AgeTechTree,\n                    length=\"age_connection_count\"\n                )),\n                (READ_GEN, \"building_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=tech.BuildingConnection,\n                    length=\"building_connection_count\"\n                )),\n                (READ_GEN, \"unit_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=tech.UnitConnection,\n                    length=\"unit_connection_count\"\n                )),\n                (READ_GEN, \"tech_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=tech.ResearchConnection,\n                    length=\"tech_connection_count\"\n                )),\n            ])\n\n        return data_format\n\n\nclass EmpiresDatWrapper(GenieStructure):\n    \"\"\"\n    This wrapper exists because the top-level element is discarded:\n    The gathered data fields are passed to the parent,\n    and are accumulated there to be processed further.\n\n    This class acts as the parent for the \"real\" data values,\n    and has no parent itself. Thereby this class is discarded\n    and the child classes use this as parent for their return values.\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"empiresdat\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=EmpiresDat,\n                length=1,\n            )),\n        ]\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/graphic.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import SubdataMember, EnumLookupMember\nfrom ....read.value_members import StorageType\nfrom .lookup_dicts import GRAPHICS_LAYER\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass GraphicDelta(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"padding_1\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"sprite_ptr\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ_GEN, \"offset_x\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"offset_y\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"display_angle\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"padding_2\", StorageType.INT_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass DE2SoundProp(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (SKIP, \"sound_delay0\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"sound_id0\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"wwise_sound0\", StorageType.ID_MEMBER, \"uint32_t\"),\n            (SKIP, \"sound_delay1\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"wwise_sound1\", StorageType.ID_MEMBER, \"uint32_t\"),\n            (SKIP, \"sound_id1\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"sound_delay2\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"wwise_sound2\", StorageType.ID_MEMBER, \"uint32_t\"),\n            (SKIP, \"sound_id2\", StorageType.ID_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass SoundProp(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (SKIP, \"sound_delay\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass GraphicAttackSound(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format = [\n                (SKIP, \"sound_props\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=DE2SoundProp,\n                    length=1,\n                )),\n            ]\n\n        else:\n            data_format = [\n                (SKIP, \"sound_props\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=SoundProp,\n                    length=3,\n                )),\n            ]\n\n        return data_format\n\n\nclass Graphic(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = []\n\n        # internal name: e.g. ARRG2NNE = archery range feudal Age north european\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (SKIP, \"name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_len]\"),\n                (SKIP, \"filename_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"filename_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[filename_len]\"),\n            ])\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (SKIP, \"particle_effect_name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (READ, \"particle_effect_name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (READ_GEN, \"particle_effect_name\", StorageType.STRING_MEMBER,\n                     \"char[particle_effect_name_len]\"),\n                ])\n            if game_version.edition.game_id == \"AOE1DE\":\n                data_format.extend([\n                    (READ_GEN, \"first_frame\", StorageType.ID_MEMBER, \"uint16_t\"),\n                ])\n\n        elif game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[25]\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[25]\"),\n            ])\n        else:\n            data_format.extend([\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[21]\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[13]\"),\n            ])\n\n        data_format.extend([\n            # id of the graphics file in the drs\n            (READ_GEN, \"slp_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"is_loaded\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),             # unused\n            (SKIP, \"old_color_flag\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),        # unused\n            (READ_GEN, \"layer\", StorageType.ID_MEMBER, EnumLookupMember(       # originally 40 layers, higher -> drawn on top\n                raw_type    = \"int8_t\",  # -> same layer -> order according to map position.\n                type_name   = \"graphics_layer\",\n                lookup_dict = GRAPHICS_LAYER\n            )),\n            (SKIP, \"player_color_force_id\", StorageType.ID_MEMBER,\n             \"int8_t\"),    # force given player color\n            # playercolor can be changed on sight (like sheep)\n            (SKIP, \"adapt_color\", StorageType.INT_MEMBER, \"int8_t\"),\n            (SKIP, \"transparent_selection\", StorageType.INT_MEMBER, \"uint8_t\"),  # loop animation\n            (READ, \"coordinates\", StorageType.ARRAY_INT, \"int16_t[4]\"),\n            (READ, \"delta_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (SKIP, \"sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"wwise_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        data_format.extend([\n            (READ, \"attack_sound_used\", StorageType.INT_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"frame_count\", StorageType.INT_MEMBER,\n             \"uint16_t\"),           # number of frames per angle\n            # number of heading angles stored, some of the frames must be mirrored\n            (READ_GEN, \"angle_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            # multiplies the speed of the unit this graphic is applied to\n            (SKIP, \"speed_adjust\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"frame_rate\", StorageType.FLOAT_MEMBER,\n             \"float\"),             # how long a frame is displayed\n            # seconds to wait before current_frame=0 again\n            (READ_GEN, \"replay_delay\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"sequence_type\", StorageType.ID_MEMBER, \"int8_t\"),\n            (READ_GEN, \"graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"mirroring_mode\", StorageType.ID_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            # sprite editor thing for AoK\n            data_format.append((SKIP, \"editor_flag\", StorageType.BOOLEAN_MEMBER, \"int8_t\"))\n\n        data_format.extend([\n            (READ_GEN, \"graphic_deltas\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=GraphicDelta,\n                length=\"delta_count\",\n            )),\n\n            # if attack_sound_used:\n            (SKIP, \"graphic_attack_sounds\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=GraphicAttackSound,\n                length=lambda o: \"angle_count\" if o.attack_sound_used != 0 else 0,\n            )),\n        ])\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/lookup_dicts.py",
    "content": "# Copyright 2021-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nLookup dicts for the EnumLookupMember instances.\n\"\"\"\n\nGRAPHICS_LAYER = {\n    0: \"TERRAIN\",      # cliff\n    1: \"GRASS_PATCH\",\n    2: \"DE2_CLIFF\",\n    3: \"AOE1_DIRT\",\n    4: \"DE1_DESTRUCTION\",\n    5: \"SHADOW\",       # farm fields as well\n    6: \"RUBBLE\",\n    7: \"PLANT\",\n    8: \"DE2_BRIDGE\",\n    9: \"SWGB_EFFECT\",\n    10: \"UNIT_LOW\",    # constructions, dead units, tree stumps, flowers, paths\n    11: \"FISH\",\n    18: \"SWGB_LAYER1\",\n    19: \"CRATER\",      # rugs\n    20: \"UNIT\",        # buildings, units, damage flames, animations (mill)\n    21: \"BLACKSMITH\",  # blacksmith smoke\n    22: \"BIRD\",        # hawk\n    30: \"PROJECTILE\",  # and explosions\n    31: \"SWGB_FLYING\",\n}\n\nRESOURCE_TYPES = {\n    -1: \"NONE\",\n    0: \"FOOD_STORAGE\",\n    1: \"WOOD_STORAGE\",\n    2: \"STONE_STORAGE\",\n    3: \"GOLD_STORAGE\",\n    4: \"POPULATION_HEADROOM\",\n    5: \"CONVERSION_RANGE\",\n    6: \"CURRENT_AGE\",\n    7: \"OWNED_RELIC_COUNT\",\n    8: \"TRADE_BONUS\",\n    9: \"TRADE_GOODS\",\n    10: \"TRADE_PRODUCTION\",\n    11: \"POPULATION\",           # both current population and population headroom\n    12: \"CORPSE_DECAY_TIME\",\n    13: \"DISCOVERY\",\n    14: \"RUIN_MONUMENTS_CAPTURED\",\n    15: \"MEAT_STORAGE\",\n    16: \"BERRY_STORAGE\",\n    17: \"FISH_STORAGE\",\n    18: \"UNKNOWN_18\",           # in starwars: power core range\n    19: \"TOTAL_UNITS_OWNED\",    # or just military ones? used for counting losses\n    20: \"UNITS_KILLED\",\n    21: \"RESEARCHED_TECHNOLOGIES_COUNT\",\n    22: \"MAP_EXPLORED_PERCENTAGE\",\n    23: \"CASTLE_AGE_TECH_INDEX\",      # default: 102\n    24: \"IMPERIAL_AGE_TECH_INDEX\",    # default: 103\n    25: \"FEUDAL_AGE_TECH_INDEX\",      # default: 101\n    26: \"ATTACK_WARNING_SOUND\",\n    27: \"ENABLE_MONK_CONVERSION\",\n    28: \"ENABLE_BUILDING_CONVERSION\",\n    30: \"BUILDING_COUNT\",              # default: 500\n    31: \"FOOD_COUNT\",\n    32: \"BONUS_POPULATION\",\n    33: \"MAINTENANCE\",\n    34: \"FAITH\",\n    35: \"FAITH_RECHARGE_RATE\",  # default: 1.6\n    36: \"FARM_FOOD_AMOUNT\",     # default: 175\n    37: \"CIVILIAN_POPULATION\",\n    38: \"UNKNOWN_38\",           # starwars: shields for bombers/fighters\n    39: \"ALL_TECHS_ACHIEVED\",   # default: 178\n    40: \"MILITARY_POPULATION\",  # -> largest army\n    41: \"UNITS_CONVERTED\",      # monk success count\n    42: \"WONDERS_STANDING\",\n    43: \"BUILDINGS_RAZED\",\n    44: \"KILL_RATIO\",\n    45: \"SURVIVAL_TO_FINISH\",   # bool\n    46: \"TRIBUTE_FEE\",          # default: 0.3\n    47: \"GOLD_MINING_PRODUCTIVITY\",  # default: 1\n    48: \"TOWN_CENTER_UNAVAILABLE\",  # -> you may build a new one\n    49: \"GOLD_COUNTER\",\n    50: \"REVEAL_ALLY\",          # bool, ==cartography discovered\n    51: \"HOUSES_COUNT\",\n    52: \"MONASTERY_COUNT\",\n    53: \"TRIBUTE_SENT\",\n    54: \"RUINES_CAPTURED_ALL\",  # bool\n    55: \"RELICS_CAPTURED_ALL\",  # bool\n    56: \"ORE_STORAGE\",\n    57: \"CAPTURED_UNITS\",\n    58: \"DARK_AGE_TECH_INDEX\",  # default: 104\n    59: \"TRADE_GOOD_QUALITY\",   # default: 1\n    60: \"TRADE_MARKET_LEVEL\",\n    61: \"FORMATIONS\",\n    62: \"BUILDING_HOUSING_RATE\",  # default: 20\n    63: \"GATHER_TAX_RATE\",        # default: 32000\n    64: \"GATHER_ACCUMULATOR\",\n    65: \"SALVAGE_DECAY_RATE\",     # default: 5\n    66: \"ALLOW_FORMATION\",        # bool, something with age?\n    67: \"ALLOW_CONVERSIONS\",      # bool\n    68: \"HIT_POINTS_KILLED\",      # unused\n    69: \"KILLED_PLAYER_1\",        # bool\n    70: \"KILLED_PLAYER_2\",        # bool\n    71: \"KILLED_PLAYER_3\",        # bool\n    72: \"KILLED_PLAYER_4\",        # bool\n    73: \"KILLED_PLAYER_5\",        # bool\n    74: \"KILLED_PLAYER_6\",        # bool\n    75: \"KILLED_PLAYER_7\",        # bool\n    76: \"KILLED_PLAYER_8\",        # bool\n    77: \"CONVERSION_RESISTANCE\",\n    78: \"TRADE_VIG_RATE\",             # default: 0.3\n    79: \"STONE_MINING_PRODUCTIVITY\",  # default: 1\n    80: \"QUEUED_UNITS\",\n    81: \"TRAINING_COUNT\",\n    82: \"START_PACKED_TOWNCENTER\",   # or raider, default: 2\n    83: \"BOARDING_RECHARGE_RATE\",\n    84: \"STARTING_VILLAGERS\",        # default: 3\n    85: \"RESEARCH_COST_MULTIPLIER\",\n    86: \"RESEARCH_TIME_MULTIPLIER\",\n    87: \"CONVERT_SHIPS_ALLOWED\",     # bool\n    88: \"FISH_TRAP_FOOD_AMOUNT\",     # default: 700\n    89: \"HEALING_RATE_MULTIPLIER\",\n    90: \"HEALING_RANGE\",\n    91: \"STARTING_FOOD\",\n    92: \"STARTING_WOOD\",\n    93: \"STARTING_STONE\",\n    94: \"STARTING_GOLD\",\n    95: \"TOWN_CENTER_PACKING\",        # or raider, default: 3\n    96: \"BERSERKER_HEAL_TIME\",        # in seconds\n    97: \"DOMINANT_ANIMAL_DISCOVERY\",  # bool, sheep/turkey\n    98: \"SCORE_OBJECT_COST\",          # object cost summary, economy?\n    99: \"SCORE_RESEARCH\",\n    100: \"RELIC_GOLD_COLLECTED\",\n    101: \"TRADE_PROFIT\",\n    102: \"TRIBUTE_P1\",\n    103: \"TRIBUTE_P2\",\n    104: \"TRIBUTE_P3\",\n    105: \"TRIBUTE_P4\",\n    106: \"TRIBUTE_P5\",\n    107: \"TRIBUTE_P6\",\n    108: \"TRIBUTE_P7\",\n    109: \"TRIBUTE_P8\",\n    110: \"KILL_SCORE_P1\",\n    111: \"KILL_SCORE_P2\",\n    112: \"KILL_SCORE_P3\",\n    113: \"KILL_SCORE_P4\",\n    114: \"KILL_SCORE_P5\",\n    115: \"KILL_SCORE_P6\",\n    116: \"KILL_SCORE_P7\",\n    117: \"KILL_SCORE_P8\",\n    118: \"RAZING_COUNT_P1\",\n    119: \"RAZING_COUNT_P2\",\n    120: \"RAZING_COUNT_P3\",\n    121: \"RAZING_COUNT_P4\",\n    122: \"RAZING_COUNT_P5\",\n    123: \"RAZING_COUNT_P6\",\n    124: \"RAZING_COUNT_P7\",\n    125: \"RAZING_COUNT_P8\",\n    126: \"RAZING_SCORE_P1\",\n    127: \"RAZING_SCORE_P2\",\n    128: \"RAZING_SCORE_P3\",\n    129: \"RAZING_SCORE_P4\",\n    130: \"RAZING_SCORE_P5\",\n    131: \"RAZING_SCORE_P6\",\n    132: \"RAZING_SCORE_P7\",\n    133: \"RAZING_SCORE_P8\",\n    134: \"STANDING_CASTLES\",\n    135: \"RAZINGS_HIT_POINTS\",\n    136: \"KILLS_BY_P1\",\n    137: \"KILLS_BY_P2\",\n    138: \"KILLS_BY_P3\",\n    139: \"KILLS_BY_P4\",\n    140: \"KILLS_BY_P5\",\n    141: \"KILLS_BY_P6\",\n    142: \"KILLS_BY_P7\",\n    143: \"KILLS_BY_P8\",\n    144: \"RAZINGS_BY_P1\",\n    145: \"RAZINGS_BY_P2\",\n    146: \"RAZINGS_BY_P3\",\n    147: \"RAZINGS_BY_P4\",\n    148: \"RAZINGS_BY_P5\",\n    149: \"RAZINGS_BY_P6\",\n    150: \"RAZINGS_BY_P7\",\n    151: \"RAZINGS_BY_P8\",\n    152: \"LOST_UNITS_SCORE\",\n    153: \"LOST_BUILDINGS_SCORE\",\n    154: \"LOST_UNITS\",\n    155: \"LOST_BUILDINGS\",\n    156: \"TRIBUTE_FROM_P1\",\n    157: \"TRIBUTE_FROM_P2\",\n    158: \"TRIBUTE_FROM_P3\",\n    159: \"TRIBUTE_FROM_P4\",\n    160: \"TRIBUTE_FROM_P5\",\n    161: \"TRIBUTE_FROM_P6\",\n    162: \"TRIBUTE_FROM_P7\",\n    163: \"TRIBUTE_FROM_P8\",\n    164: \"SCORE_UNITS_CURRENT\",\n    165: \"SCORE_BUILDINGS_CURRENT\",         # default: 275\n    166: \"COLLECTED_FOOD\",\n    167: \"COLLECTED_WOOD\",\n    168: \"COLLECTED_STONE\",\n    169: \"COLLECTED_GOLD\",\n    170: \"SCORE_MILITARY\",\n    171: \"TRIBUTE_RECEIVED\",\n    172: \"SCORE_RAZINGS\",\n    173: \"TOTAL_CASTLES\",\n    174: \"TOTAL_WONDERS\",\n    175: \"SCORE_ECONOMY_TRIBUTES\",\n    # used for resistance against monk conversions\n    176: \"CONVERT_ADJUSTMENT_MIN\",\n    177: \"CONVERT_ADJUSTMENT_MAX\",\n    178: \"CONVERT_RESIST_ADJUSTMENT_MIN\",\n    179: \"CONVERT_RESIST_ADJUSTMENT_MAX\",\n    180: \"CONVERT_BUILDIN_MIN\",             # default: 15\n    181: \"CONVERT_BUILDIN_MAX\",             # default: 25\n    182: \"CONVERT_BUILDIN_CHANCE\",          # default: 25\n    183: \"REVEAL_ENEMY\",\n    184: \"SCORE_SOCIETY\",                   # wonders, castles\n    185: \"SCORE_FOOD\",\n    186: \"SCORE_WOOD\",\n    187: \"SCORE_STONE\",\n    188: \"SCORE_GOLD\",\n    189: \"CHOPPING_PRODUCTIVITY\",           # default: 1\n    190: \"FOOD_GATHERING_PRODUCTIVITY\",     # default: 1\n    191: \"RELIC_GOLD_PRODUCTION_RATE\",      # default: 30\n    192: \"CONVERTED_UNITS_DIE\",             # bool\n    193: \"THEOCRACY_ACTIVE\",                # bool\n    194: \"CRENELLATIONS_ACTIVE\",            # bool\n    195: \"CONSTRUCTION_RATE_MULTIPLIER\",    # except for wonders\n    196: \"HUN_WONDER_BONUS\",\n    197: \"SPIES_DISCOUNT\",                  # or atheism_active?\n    198: \"AK_UNUSED_198\",\n    199: \"AK_UNUSED_199\",\n    200: \"AK_UNUSED_200\",\n    201: \"AK_UNUSED_201\",\n    202: \"AK_UNUSED_202\",\n    203: \"AK_UNUSED_203\",\n    204: \"AK_UNUSED_204\",\n    205: \"AK_FEITORIA_FOOD_PRODUCTIVITY\",\n    206: \"AK_FEITORIA_WOOD_PRODUCTIVITY\",\n    207: \"AK_FEITORIA_GOLD_PRODUCTIVITY\",\n    208: \"AK_FEITORIA_STONE_PRODUCTIVITY\",\n    209: \"RAJ_REVEAL_ENEMY_TOWN_CENTER\",\n    210: \"RAJ_REVEAL_RELIC\",\n    211: \"DE2_UNKNOWN_211\",\n    212: \"DE2_UNKNOWN_212\",\n    213: \"DE2_UNKNOWN_213\",\n    214: \"DE2_UNKNOWN_214\",\n    215: \"DE2_UNKNOWN_215\",\n    216: \"DE2_UNKNOWN_216\",\n    217: \"DE2_UNKNOWN_217\",\n    218: \"DE2_UNKNOWN_218\",\n    219: \"DE2_UNKNOWN_219\",\n    220: \"RELIC_FOOD_PRODUCTION_RATE\",\n    221: \"DE2_UNKNOWN_221\",\n    222: \"DE2_UNKNOWN_222\",\n    223: \"DE2_UNKNOWN_223\",\n    224: \"DE2_UNKNOWN_224\",\n    225: \"DE2_UNKNOWN_225\",\n    226: \"DE2_UNKNOWN_226\",\n    227: \"DE2_UNKNOWN_227\",\n    228: \"DE2_UNKNOWN_228\",\n    229: \"DE2_UNKNOWN_229\",\n    230: \"DE2_UNKNOWN_230\",\n    231: \"DE2_UNKNOWN_231\",\n    232: \"DE2_UNKNOWN_232\",\n    233: \"DE2_UNKNOWN_233\",\n    234: \"FIRST_CRUSADE\",\n    235: \"DE2_UNKNOWN_235\",\n    236: \"BURGUNDIAN_VINEYARDS\",\n    237: \"DE2_UNKNOWN_237\",\n    238: \"DE2_UNKNOWN_238\",\n    239: \"DE2_UNKNOWN_239\",\n    240: \"DE2_UNKNOWN_240\",\n    241: \"DE2_UNKNOWN_241\",\n    242: \"DE2_UNKNOWN_242\",\n    243: \"DE2_UNKNOWN_243\",\n    244: \"DE2_UNKNOWN_244\",\n    245: \"DE2_UNKNOWN_245\",\n    246: \"DE2_UNKNOWN_246\",\n    247: \"DE2_UNKNOWN_247\",\n    248: \"DE2_UNKNOWN_248\",\n    249: \"DE2_UNKNOWN_249\",\n    250: \"DE2_UNKNOWN_250\",\n    501: \"DE2_UNKNOWN_501\",\n    506: \"DE2_UNKNOWN_506\",\n}\n\nEFFECT_APPLY_TYPE = {\n    # unused assignage: a = -1, b = -1, c = -1, d = 0\n    255: \"DISABLED\",\n    # if a != -1: a == unit_id, else b == unit_class_id; c =\n    # attribute_id, d = new_value\n    0: \"ATTRIBUTE_ABSSET\",\n    # a == resource_id, if b == 0: then d = absval, else d = relval\n    # (for inc/dec)\n    1: \"RESOURCE_MODIFY\",\n    # a == unit_id, if b == 0: disable unit, else b == 1: enable\n    # unit\n    2: \"UNIT_ENABLED\",\n    # a == old_unit_id, b == new_unit_id\n    3: \"UNIT_UPGRADE\",\n    # if a != -1: unit_id, else b == unit_class_id; c=attribute_id,\n    # d=relval\n    4: \"ATTRIBUTE_RELSET\",\n    # if a != -1: unit_id, else b == unit_class_id; c=attribute_id,\n    # d=factor\n    5: \"ATTRIBUTE_MUL\",\n    # a == resource_id, d == factor\n    6: \"RESOURCE_MUL\",\n\n    # a == unit_id, b == building_id, c == amount\n    7: \"SPAWN_UNIT\",\n\n    # a == tech_id, b == action_id (5 = set?), c == amount\n    8: \"RESEARCH_TIME_MODIFY\",\n\n    # same as 0-6 but applied to team members\n    10: \"TEAM_ATTRIBUTE_ABSSET\",\n    11: \"TEAM_RESOURCE_MODIFY\",\n    12: \"TEAM_UNIT_ENABLED\",\n    13: \"TEAM_UNIT_UPGRADE\",\n    14: \"TEAM_ATTRIBUTE_RELSET\",\n    15: \"TEAM_ATTRIBUTE_MUL\",\n    16: \"TEAM_RESOURCE_MUL\",\n    17: \"TEAM_SPAWN_UNIT\",\n    18: \"TEAM_RESEARCH_TIME_MODIFY\",\n\n    # same as 0-6 but applied to enemies\n    20: \"ENEMY_ATTRIBUTE_ABSSET\",\n    21: \"ENEMY_RESOURCE_MODIFY\",\n    22: \"ENEMY_UNIT_ENABLED\",\n    23: \"ENEMY_UNIT_UPGRADE\",\n    24: \"ENEMY_ATTRIBUTE_RELSET\",\n    25: \"ENEMY_ATTRIBUTE_MUL\",\n    26: \"ENEMY_RESOURCE_MUL\",\n\n    # these are only used in technology trees, 103 even requires\n    # one\n    # a == research_id, b == resource_id, if c == 0: d==absval\n    # else: d == relval\n    101: \"TECHCOST_MODIFY\",\n    102: \"TECH_TOGGLE\",       # d == research_id\n    103: \"TECH_TIME_MODIFY\",  # a == research_id, if c == 0: d==absval else d==relval\n\n    # unknown; used in DE2 BfG\n    199: \"UNKNOWN\",\n    200: \"UNKNOWN\",\n    201: \"UNKNOWN\",\n\n    # attribute_id:\n    # 0: hit points\n    # 1: line of sight\n    # 2: garrison capacity\n    # 3: unit size x\n    # 4: unit size y\n    # 5: movement speed\n    # 6: rotation speed\n    # 7: unknown\n    # 8: armor                           # real_val = val + (256 * armor_id)\n    # 9: attack                          # real_val = val + (256 * attack_id)\n    # 10: attack reloading time\n    # 11: accuracy percent\n    # 12: max range\n    # 13: working rate\n    # 14: resource carriage\n    # 15: default armor\n    # 16: projectile unit\n    # 17: upgrade graphic (icon), graphics angle\n    # 18: terrain restriction to multiply damage received (always sets)\n    # 19: intelligent projectile aim 1=on, 0=off\n    # 20: minimum range\n    # 21: first resource storage\n    # 22: blast width (area damage)\n    # 23: search radius\n    # 80: boarding energy reload speed\n    # 100: resource cost\n    # 101: creation time\n    # 102: number of garrison arrows\n    # 103: food cost\n    # 104: wood cost\n    # 105: stone cost\n    # 106: gold cost\n    # 107: max total projectiles\n    # 108: garrison healing rate\n    # 109: regeneration rate\n}\n\nCONNECTION_MODE = {\n    0: \"AGE\",\n    1: \"BUILDING\",\n    2: \"UNIT\",\n    3: \"RESEARCH\",\n}\n\nCOMMAND_ABILITY = {\n    # the Action-Types found under RGE namespace:\n    -1: \"SWGB_UNUSED\",\n    0: \"UNUSED\",\n    1: \"MOVE_TO\",\n    2: \"FOLLOW\",\n    3: \"GARRISON\",  # also known as \"Enter\"\n    4: \"EXPLORE\",\n    5: \"GATHER\",\n    6: \"NATURAL_WONDERS_CHEAT\",  # also known as \"Graze\"\n    7: \"COMBAT\",       # this is converted to action-type 9 when once instanciated\n    8: \"MISSILE\",      # for projectiles\n    9: \"ATTACK\",\n    10: \"BIRD\",        # flying.\n    11: \"PREDATOR\",    # scares other animals when hunting\n    12: \"TRANSPORT\",\n    13: \"GUARD\",\n    14: \"TRANSPORT_OVER_WALL\",\n    20: \"RUN_AWAY\",\n    21: \"MAKE\",\n    # Action-Types found under TRIBE namespace:\n    101: \"BUILD\",\n    102: \"MAKE_OBJECT\",\n    103: \"MAKE_TECH\",\n    104: \"CONVERT\",\n    105: \"HEAL\",\n    106: \"REPAIR\",\n    107: \"CONVERT_AUTO\",  # \"Artifact\": can get auto-converted\n    108: \"DISCOVERY\",\n    109: \"SHOOTING_RANGE_RETREAT\",\n    110: \"HUNT\",\n    111: \"TRADE\",\n    120: \"WONDER_VICTORY_GENERATE\",\n    121: \"DESELECT_ON_TASK\",\n    122: \"LOOT\",\n    123: \"HOUSING\",\n    124: \"PACK\",\n    125: \"UNPACK_ATTACK\",\n    130: \"OFF_MAP_TRADE_0\",\n    131: \"OFF_MAP_TRADE_1\",\n    132: \"PICKUP_UNIT\",\n    133: \"PICKUP_133\",\n    134: \"PICKUP_134\",\n    135: \"KIDNAP_UNIT\",\n    136: \"DEPOSIT_UNIT\",\n    149: \"SHEAR\",\n    150: \"REGENERATION\",\n    151: \"FEITORIA\",\n    153: \"RESOURCE_FOLLOW\",\n    154: \"LOOT\",         # Chieftains tech; looting on killing villagers, monks, trade carts\n    155: \"BOOST_MOVE_AND_ATTACK\",\n    768: \"UNKNOWN_768\",\n    1024: \"UNKNOWN_1024\",\n}\n\nOWNER_TYPE = {\n    0: \"ANY_0\",               # select anything\n    1: \"OWNED_UNITS\",         # your own things\n    2: \"NEUTRAL_ENEMY\",       # enemy and neutral things (->attack)\n    3: \"NOTHING\",\n    4: \"GAIA_OWNED_ALLY\",     # any of gaia, owned or allied things\n    5: \"GAYA_NEUTRAL_ENEMY\",  # any of gaia, neutral or enemy things\n    6: \"NOT_OWNED\",           # all things that aren't yours\n    7: \"ANY_7\",\n}\n\nRESOURCE_HANDLING = {\n    0: \"DECAYABLE\",\n    1: \"KEEP_AFTER_DEATH\",\n    2: \"RESET_ON_DEATH_INSTANT\",\n    4: \"RESET_ON_DEATH_WHEN_COMPLETED\",\n    8: \"DE2_UNKNOWN_8\",\n    32: \"DE2_UNKNOWN_32\",\n}\n\nDAMAGE_DRAW_TYPE = {\n    0: \"TOP\",      # adds graphics on top (e.g. flames)\n    1: \"RANDOM\",   # adds graphics on top randomly\n    2: \"REPLACE\",  # replace original graphics (e.g. damaged walls)\n}\n\nARMOR_CLASS = {\n    -1: \"NONE\",\n    0: \"UNKNOWN_0\",\n    1: \"INFANTRY\",\n    2: \"SHIP_TURTLE\",\n    3: \"UNITS_PIERCE\",\n    4: \"UNITS_MELEE\",\n    5: \"WAR_ELEPHANT\",\n    6: \"SWGB_ASSAULT_MECHANIC\",\n    7: \"SWGB_DECIMATOR\",\n    8: \"CAVALRY\",\n    9: \"SWGB_SHIPS\",\n    10: \"SWGB_SUBMARINE\",\n    11: \"BUILDINGS_NO_PORT\",\n    12: \"AOE1_VILLAGER\",\n    13: \"STONE_DEFENSES\",\n    14: \"HD_PREDATOR\",\n    15: \"ARCHERS\",\n    16: \"SHIPS_CAMELS_SABOTEURS\",\n    17: \"RAMS\",\n    18: \"TREES\",\n    19: \"UNIQUE_UNITS\",\n    20: \"SIEGE_WEAPONS\",\n    21: \"BUILDINGS\",\n    22: \"WALLS_GATES\",\n    23: \"HD_GUNPOWDER\",\n    24: \"BOAR\",\n    25: \"MONKS\",\n    26: \"CASTLE\",\n    27: \"SPEARMEN\",\n    28: \"CAVALRY_ARCHER\",\n    29: \"EAGLE_WARRIOR\",\n    30: \"HD_CAMEL\",\n    31: \"DE2_UNKOWN_31\",\n    32: \"DE2_CONDOTTIERO\",\n    33: \"DE2_UNKOWN_33\",\n    34: \"DE2_FISHING_SHIP\",\n    35: \"DE2_MAMELUKE\",\n    36: \"DE2_HERO\",\n    37: \"DE2_SIEGE_BALLISTA\",\n    38: \"DE2_SKIRMISHER\",\n    39: \"DE2_CAMEL_RIDER\",\n    40: \"DE2_UNKNOWN_40\",\n    60: \"DE2_UNKNOWN_60\",\n}\n\nUNIT_CLASSES = {\n    -1: \"NONE\",\n    0: \"ARCHER\",\n    1: \"ARTIFACT\",\n    2: \"TRADE_BOAT\",\n    3: \"BUILDING\",\n    4: \"CIVILIAN\",\n    5: \"SEA_FISH\",\n    6: \"SOLDIER\",\n    7: \"BERRY_BUSH\",\n    8: \"STONE_MINE\",\n    9: \"PREY_ANIMAL\",\n    10: \"PREDATOR_ANIMAL\",\n    11: \"OTHER\",\n    12: \"CAVALRY\",\n    13: \"SIEGE_WEAPON\",\n    14: \"TERRAIN\",\n    15: \"TREES\",\n    16: \"TREE_STUMP\",\n    17: \"SWGB_TRANSPORT_SHIP\",\n    18: \"PRIEST\",\n    19: \"TRADE_CART\",\n    20: \"TRANSPORT_BOAT\",\n    21: \"FISHING_BOAT\",\n    22: \"WAR_BOAT\",\n    23: \"CONQUISTADOR\",\n    24: \"AOE1_WAR_ELEPPHANT\",\n    25: \"SWGB_SHORE_FISH\",\n    26: \"SWGB_MARKER\",\n    27: \"WALLS\",\n    28: \"PHALANX\",\n    29: \"ANIMAL_DOMESTICATED\",\n    30: \"FLAGS\",\n    31: \"DEEP_SEA_FISH\",\n    32: \"GOLD_MINE\",\n    33: \"SHORE_FISH\",\n    34: \"CLIFF\",\n    35: \"PETARD\",\n    36: \"CAVALRY_ARCHER\",\n    37: \"DOPPELGANGER\",\n    38: \"BIRDS\",\n    39: \"GATES\",\n    40: \"PILES\",\n    41: \"PILES_OF_RESOURCE\",\n    42: \"RELIC\",\n    43: \"MONK_WITH_RELIC\",\n    44: \"HAND_CANNONEER\",\n    45: \"TWO_HANDED_SWORD\",\n    46: \"PIKEMAN\",\n    47: \"SCOUT_CAVALRY\",\n    48: \"ORE_MINE\",\n    49: \"FARM\",\n    50: \"SPEARMAN\",\n    51: \"PACKED_SIEGE_UNITS\",\n    52: \"TOWER\",\n    53: \"BOARDING_BOAT\",\n    54: \"UNPACKED_SIEGE_UNITS\",\n    55: \"SCORPION\",\n    56: \"RAIDER\",\n    57: \"CAVALRY_RAIDER\",\n    58: \"HERDABLE\",\n    59: \"KING\",\n    60: \"SWGB_LIVESTOCK\",\n    61: \"HORSE\",\n    62: \"SWGB_AIR_CRUISER\",\n    63: \"SWGB_GEONOSIAN\",\n    64: \"SWGB_JEDI_STARFIGHTER\",\n}\n\nELEVATION_MODES = {\n    0: \"NONE\",                  # gates, farms, walls, towers\n    2: \"ZERO_ELEV_DIFFERENCE\",  # towncenter, port, trade workshop\n    3: \"ONE_ELEV_DIFFERENCE\",   # everything else\n}\n\nFOG_VISIBILITY = {\n    0: \"INVISIBLE\",     # people etc\n    1: \"VISIBLE\",       # buildings\n    2: \"VISIBLE_IF_ALIVE\",\n    3: \"ONLY_IN_FOG\",\n    4: \"DOPPELGANGER\",\n}\n\nTERRAIN_RESTRICTIONS = {\n    -0x01: \"NONE\",\n    0x00: \"ANY\",                  # projectiles\n    0x01: \"SHORELINE\",            # boar, deer, wolf\n    0x02: \"WATER\",                # unused in AoC\n    0x03: \"WATER_SHIP_0x03\",      # warships\n    0x04: \"FOUNDATION\",           # buildings\n    0x05: \"NOWHERE\",              # can't place anywhere\n    0x06: \"WATER_DOCK\",           # shallow water for dock placement\n    0x07: \"SOLID\",                # moving land units\n    0x08: \"NO_ICE_0x08\",          # resource piles (gold, stone, berries)\n    0x09: \"SWGB_ONLY_WATER0\",\n    0x0A: \"NO_ICE_0x0A\",          # gate, palisades, walls\n    0x0B: \"FOREST\",               # trees\n    0x0C: \"UNKNOWN_0x0C\",         # projectile explosions on bridge?\n    0x0D: \"WATER_0x0D\",           # great fish, fishtrap, fishing ship\n    0x0E: \"UNKNOWN_0x0E\",         # projectile decay on bridge?\n    0x0F: \"WATER_SHIP_0x0F\",      # transport ship, longboat\n    0x10: \"GRASS_SHORELINE\",      # for gates and walls\n    0x11: \"WATER_ANY_0x11\",\n    0x12: \"UNKNOWN_0x12\",         # projectile decay on bridge?\n    0x13: \"WATER_ICE\",            # small fish\n    0x14: \"NO_WATER\",             # siege units, trade cart\n    0x15: \"WATER_SHALLOW\",        # sea walls\n    0x16: \"SWGB_GRASS_SHORE\",\n    0x17: \"SWGB_ANY\",\n    0x18: \"SWGB_ONLY_WATER1\",\n    0x19: \"SWGB_LAND_IMPASSABLE_WATER0\",\n    0x1A: \"SWGB_LAND_IMPASSABLE_WATER1\",\n    0x1B: \"SWGB_DEEP_WATER\",\n    0x1C: \"SWGB_WASTELAND\",\n    0x1D: \"SWGB_ICE\",\n    0x1E: \"DE2_UNKNOWN\",\n    0x1F: \"SWGB_WATER2\",\n    0x20: \"SWGB_ROCK4\",\n}\n\nBLAST_DEFENSE_TYPES = {\n    0: \"UNIT_0\",    # projectile, dead, fish, relic, tree, gate, towncenter\n    1: \"OTHER\",     # 'other' things with multiple rotations\n    2: \"BUILDING\",  # buildings, gates, walls, towncenter, fishtrap\n    3: \"UNIT_3\"     # boar, farm, fishingship, villager, tradecart, sheep, turkey,\n                    # archers, junk, ships, monk, siege\n}\n\nCOMBAT_LEVELS = {\n    0: \"PROJECTILE_DEAD_RESOURCE\",\n    1: \"BOAR\",\n    2: \"BUILDING\",\n    3: \"CIVILIAN\",\n    4: \"MILITARY\",\n    5: \"OTHER\",\n}\n\nINTERACTION_MODES = {\n    0: \"NOTHING_0\",\n    1: \"BIRD\",\n    2: \"SELECTABLE\",\n    3: \"SELECT_ATTACK\",\n    4: \"SELECT_ATTACK_MOVE\",\n    5: \"SELECT_MOVE\",\n}\n\nMINIMAP_MODES = {\n    0: \"NO_DOT_0\",\n    1: \"SQUARE_DOT\",             # turns white when selected\n    2: \"DIAMOND_DOT\",            # dito\n    3: \"DIAMOND_DOT_KEEPCOLOR\",  # doesn't turn white when selected\n    4: \"LARGEDOT\",               # observable by all players, no attacked-blinking\n    5: \"NO_DOT_5\",\n    6: \"NO_DOT_6\",\n    7: \"NO_DOT_7\",\n    8: \"NO_DOT_8\",\n    9: \"NO_DOT_9\",\n    10: \"NO_DOT_10\",\n}\n\nUNIT_LEVELS = {\n    0: \"LIVING\",                # commands: delete, garrison, stop, attributes: hit points\n    1: \"ANIMAL\",                # animal\n    2: \"NONMILITARY_BULIDING\",  # civilian building (build page 1)\n    3: \"VILLAGER\",              # villager\n    4: \"MILITARY_UNIT\",         # military unit\n    5: \"TRADING_UNIT\",          # trading unit\n    6: \"MONK_EMPTY\",            # monk\n    7: \"TRANSPORT_SHIP\",        # transport ship\n    8: \"RELIC\",                 # relic / monk with relic\n    9: \"FISHING_SHIP\",          # fishing ship\n    10: \"MILITARY_BUILDING\",    # military building (build page 2)\n    11: \"SHIELDED_BUILDING\",    # shield building (build page 3)\n    12: \"UNKNOWN_12\",\n}\n\nOBSTRUCTION_TYPES = {\n    0: \"PASSABLE\",   # farm, gate, dead bodies, town center\n    2: \"BUILDING\",\n    3: \"BERSERK\",\n    5: \"UNIT\",\n    10: \"MOUNTAIN\",  # mountain (matches occlusion_mask)\n}\n\nSELECTION_EFFECTS = {\n    0: \"NONE\",\n    1: \"HPBAR_ON_OUTLINE_DARK\",  # permanent, editor only\n    2: \"HPBAR_ON_OUTLINE_NORMAL\",\n    3: \"HPBAR_OFF_SELECTION_SHADOW\",\n    4: \"HPBAR_OFF_OUTLINE_NORMAL\",\n    5: \"HPBAR_ON_5\",\n    6: \"HPBAR_OFF_6\",\n    7: \"HPBAR_OFF_7\",\n    8: \"HPBAR_ON_8\",\n    9: \"HPBAR_ON_9\",\n}\n\nATTACK_MODES = {\n    0: \"NO\",         # no attack\n    1: \"FOLLOWING\",  # by following\n    2: \"RUN\",        # run when attacked\n    3: \"UNKNOWN3\",\n    4: \"ATTACK\",\n}\n\nBOUNDARY_IDS = {\n    -1: \"NONE\",\n    4: \"BUILDING\",\n    6: \"DOCK\",\n    10: \"WALL\",\n}\n\nBLAST_OFFENSE_TYPES = {\n    0: \"RESOURCES\",\n    1: \"TREES\",\n    2: \"NEARBY_UNITS\",\n    3: \"TARGET_ONLY\",\n    6: \"UNKNOWN_6\",\n    10: \"UNKNOWN_10\",\n    18: \"UNKNOWN_18\",\n    34: \"UNKNOWN_34\",\n    66: \"UNKNOWN_66\",\n    130: \"DE2_PIERCE\",  # attack units behind target (Ghulam); used since Return of Rome\n    138: \"DE2_PIERCE\",  # attack units behind target (Ghulam); unused since Return of Rome\n}\n\nCREATABLE_TYPES = {\n    0: \"NONHUMAN\",  # animal, ship\n    1: \"VILLAGER\",  # villager, king\n    2: \"MELEE\",     # soldier, siege, predator, trader\n    3: \"MOUNTED\",   # camel rider\n    4: \"RELIC\",\n    5: \"RANGED_PROJECTILE\",  # archer\n    6: \"RANGED_MAGIC\",       # monk\n    21: \"TRANSPORT_SHIP\",\n    255: \"OTHER\",    # building, relic\n}\n\nGARRISON_TYPES = {\n    0x00: \"NONE\",\n    0x01: \"VILLAGER\",\n    0x02: \"INFANTRY\",\n    0x04: \"CAVALRY\",\n    0x07: \"SWGB_NO_JEDI\",\n    0x08: \"MONK\",\n    0x09: \"DE2_FORTIFIED_CHURCH\",\n    0x0b: \"NOCAVALRY\",\n    0x0f: \"ALL\",\n    0x10: \"SWGB_LIVESTOCK\",\n    0x40: \"DE2_UNKNOWN_40\",\n}\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/maps.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, SKIP\nfrom ....read.read_members import SubdataMember\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass MapInfo(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"map_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"border_south_west\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_north_west\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_north_east\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_south_east\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_usage\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"water_shape\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"base_terrain\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"land_coverage\", StorageType.INT_MEMBER, \"int32_t\"),\n            (SKIP, \"unused_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"base_zone_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"base_zone_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_terrain_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_terrain_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_unit_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_unit_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_elevation_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_elevation_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n        ]\n\n        return data_format\n\n\nclass MapLand(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"land_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"terrain\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"land_spacing\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"base_size\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"zone\", StorageType.INT_MEMBER, \"int8_t\"),\n            (READ, \"placement_type\", StorageType.ID_MEMBER, \"int8_t\"),\n            (SKIP, \"padding1\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"base_x\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"base_y\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"land_proportion\", StorageType.INT_MEMBER, \"int8_t\"),\n            (READ, \"by_player_flag\", StorageType.ID_MEMBER, \"int8_t\"),\n            (SKIP, \"padding2\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"start_area_radius\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"terrain_edge_fade\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"clumpiness\", StorageType.INT_MEMBER, \"int32_t\"),\n        ]\n\n        return data_format\n\n\nclass MapTerrain(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"proportion\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"terrain_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"number_of_clumps\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"edge_spacing\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"placement_zone\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"clumpiness\", StorageType.INT_MEMBER, \"int32_t\"),\n        ]\n\n        return data_format\n\n\nclass MapUnit(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"unit_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"host_terrain\", StorageType.ID_MEMBER, \"int32_t\"),   # -1 = land; 1 = water\n            (READ, \"group_placing\", StorageType.ID_MEMBER, \"int8_t\"),   # 0 =\n            (READ, \"scale_flag\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"padding1\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"objects_per_group\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"fluctuation\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"groups_per_player\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"group_radius\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"own_at_start\", StorageType.INT_MEMBER, \"int32_t\"),  # -1 = player unit; 0 = else\n            (READ, \"set_place_for_all_players\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"min_distance_to_players\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"max_distance_to_players\", StorageType.INT_MEMBER, \"int32_t\"),\n        ]\n\n        return data_format\n\n\nclass MapElevation(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"proportion\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"terrain\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"clump_count\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"base_terrain\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"base_elevation\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"tile_spacing\", StorageType.INT_MEMBER, \"int32_t\"),\n        ]\n\n        return data_format\n\n\nclass Map(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"border_south_west\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_north_west\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_north_east\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_south_east\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"border_usage\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"water_shape\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"base_terrain\", StorageType.INT_MEMBER, \"int32_t\"),\n            (READ, \"land_coverage\", StorageType.INT_MEMBER, \"int32_t\"),\n            (SKIP, \"unused_id\", StorageType.ID_MEMBER, \"int32_t\"),\n\n            (READ, \"base_zone_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"base_zone_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"base_zones\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=MapLand,\n                length=\"base_zone_count\",\n            )),\n\n            (READ, \"map_terrain_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_terrain_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_terrains\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=MapTerrain,\n                length=\"map_terrain_count\",\n            )),\n\n            (READ, \"map_unit_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_unit_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_units\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=MapUnit,\n                length=\"map_unit_count\",\n            )),\n\n            (READ, \"map_elevation_count\", StorageType.INT_MEMBER, \"uint32_t\"),\n            (READ, \"map_elevation_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ, \"map_elevations\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=MapElevation,\n                length=\"map_elevation_count\",\n            )),\n        ]\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/playercolor.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ_GEN, SKIP\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass PlayerColor(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format = [\n                (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int32_t\"),\n                # palette index offset, where the 8 player colors start\n                (READ_GEN, \"player_color_base\", StorageType.ID_MEMBER, \"int32_t\"),\n                # palette index\n                (READ_GEN, \"outline_color\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"unit_selection_color1\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"unit_selection_color2\", StorageType.ID_MEMBER, \"int32_t\"),\n                # palette index\n                (READ_GEN, \"minimap_color1\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"minimap_color2\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"minimap_color3\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"statistics_text_color\", StorageType.ID_MEMBER, \"int32_t\"),\n            ]\n        else:\n            data_format = [\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[30]\"),\n                (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int16_t\"),\n                (READ_GEN, \"resource_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                (READ_GEN, \"minimap_color\", StorageType.ID_MEMBER, \"uint8_t\"),\n                # 0 transform\n                # 1 transform player color\n                # 2 shadow\n                # 3 translucent\n                (READ_GEN, \"type\", StorageType.ID_MEMBER, \"uint8_t\"),\n            ]\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/research.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import SubdataMember, EnumLookupMember\nfrom ....read.value_members import StorageType\nfrom .lookup_dicts import RESOURCE_TYPES\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass TechResourceCost(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"type_id\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",\n                type_name=\"resource_types\",\n                lookup_dict=RESOURCE_TYPES\n            )),  # see unit/resource_cost\n            (READ_GEN, \"amount\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"enabled\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n        ]\n\n        return data_format\n\n\nclass Tech(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format = [\n                # research ids of techs that are required for activating the possible research\n                (READ_GEN, \"required_techs\", StorageType.ARRAY_ID, \"int16_t[6]\"),\n            ]\n        else:\n            data_format = [\n                # research ids of techs that are required for activating the possible research\n                (READ_GEN, \"required_techs\", StorageType.ARRAY_ID, \"int16_t[4]\"),\n            ]\n\n        data_format.extend([\n            (READ_GEN, \"research_resource_costs\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=TechResourceCost,\n                length=3,\n            )),\n            # a subset of the above required techs may be sufficient, this defines the minimum amount\n            (READ_GEN, \"required_tech_count\", StorageType.INT_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                # id of the civ that gets this technology\n                (READ_GEN, \"civilization_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                # 1: research is available when the full tech tree is activated on game start, 0: not\n                (READ_GEN, \"full_tech_mode\", StorageType.BOOLEAN_MEMBER, \"int16_t\"),\n            ])\n\n        data_format.extend([\n            # unit id, where the tech will appear to be researched\n            (READ_GEN, \"research_location_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"language_dll_name\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"language_dll_description\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"language_dll_name\", StorageType.ID_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"language_dll_description\", StorageType.ID_MEMBER, \"uint16_t\"),\n            ])\n\n        data_format.extend([\n            # time in seconds that are needed to finish this research\n            (READ_GEN, \"research_time\", StorageType.INT_MEMBER, \"int16_t\"),\n            # techage id that actually contains the research effect information\n            (READ_GEN, \"tech_effect_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # 0: normal tech, 2: show in Age progress bar\n            (READ_GEN, \"tech_type\", StorageType.ID_MEMBER, \"int16_t\"),\n            # frame id - 1 in icon slp (57029)\n            (SKIP, \"icon_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # button id as defined in the unit.py button matrix\n            (READ_GEN, \"button_id\", StorageType.ID_MEMBER, \"int8_t\"),\n            # 100000 + the language file id for the name/description\n            (SKIP, \"language_dll_help\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"language_dll_techtree\", StorageType.ID_MEMBER,\n             \"int32_t\"),     # 149000 + lang_dll_description\n            (READ_GEN, \"hotkey\", StorageType.ID_MEMBER, \"int32_t\"),                    # -1 for every tech\n        ])\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (SKIP, \"name_length_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_length]\"),\n            ])\n\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"repeatable\", StorageType.INT_MEMBER, \"int8_t\"),\n                ])\n\n        else:\n            data_format.extend([\n                (READ, \"name_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_length]\"),\n            ])\n\n            if game_version.edition.game_id == \"SWGB\":\n                data_format.extend([\n                    (READ, \"name2_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (SKIP, \"name2\", StorageType.STRING_MEMBER, \"char[name2_length]\"),\n                ])\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/sound.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ_GEN, READ, SKIP\nfrom ....read.read_members import SubdataMember\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass SoundItem(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = []\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (SKIP, \"name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_len]\"),\n            ])\n        elif game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[27]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[13]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"resource_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"probablilty\", StorageType.INT_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (READ_GEN, \"civilization_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                (READ_GEN, \"icon_set\", StorageType.ID_MEMBER, \"int16_t\"),\n            ])\n\n        return data_format\n\n\nclass Sound(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"play_delay\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ, \"file_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (SKIP, \"cache_time\", StorageType.INT_MEMBER, \"int32_t\"),                   # always 300000\n        ]\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (READ_GEN, \"total_probability\", StorageType.ID_MEMBER, \"int16_t\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"sound_items\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=SoundItem,\n                ref_to=\"id\",\n                length=\"file_count\",\n            )),\n        ])\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/tech.py",
    "content": "# Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import SubdataMember, EnumLookupMember\nfrom ....read.value_members import StorageType\nfrom .lookup_dicts import EFFECT_APPLY_TYPE, CONNECTION_MODE\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass Effect(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"type_id\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"uint8_t\",\n                type_name=\"effect_apply_type\",\n                lookup_dict=EFFECT_APPLY_TYPE\n            )),\n            (READ_GEN, \"attr_a\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"attr_b\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"attr_c\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"attr_d\", StorageType.FLOAT_MEMBER, \"float\"),\n        ]\n\n        return data_format\n\n\nclass EffectBundle(GenieStructure):  # also called techage in some other tools\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format = [\n                (SKIP, \"name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_len]\"),\n            ]\n        else:\n            data_format = [\n                # always CHUN4 (change unit 4-arg) in AoE1-AoC, later versions name them\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[31]\"),\n            ]\n\n        data_format.extend([\n            (READ, \"effect_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"effects\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=Effect,\n                length=\"effect_count\",\n            )),\n        ])\n\n        return data_format\n\n\nclass OtherConnection(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"other_connection\", StorageType.ID_MEMBER, EnumLookupMember(  # mode for unit_or_research0\n                raw_type=\"int32_t\",\n                type_name=\"connection_mode\",\n                lookup_dict=CONNECTION_MODE\n            )),\n        ]\n\n        return data_format\n\n\nclass AgeTechTree(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int32_t\"),\n            # 0=generic\n            # 1=TODO\n            # 2=default\n            # 3=marks as not available\n            # 4=upgrading, constructing, creating\n            # 5=research completed, building built\n            (READ_GEN, \"status\", StorageType.ID_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id != \"ROR\":\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[building_count]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[unit_count]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[research_count]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"connected_slots_used\", StorageType.INT_MEMBER, \"int32_t\"),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[20]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=20,\n                )),\n            ])\n        elif game_version.edition.game_id == \"ROR\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[5]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=5,\n                )),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[10]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=10,\n                )),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"building_level_count\", StorageType.INT_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"buildings_per_zone\", StorageType.ARRAY_INT, \"int8_t[20]\"),\n                (READ_GEN, \"group_length_per_zone\", StorageType.ARRAY_INT, \"int8_t[20]\"),\n            ])\n        elif game_version.edition.game_id == \"ROR\":\n            data_format.extend([\n                (READ_GEN, \"buildings_per_zone\", StorageType.ARRAY_INT, \"int8_t[3]\"),\n                (READ_GEN, \"group_length_per_zone\", StorageType.ARRAY_INT, \"int8_t[3]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"buildings_per_zone\", StorageType.ARRAY_INT, \"int8_t[10]\"),\n                (READ_GEN, \"group_length_per_zone\", StorageType.ARRAY_INT, \"int8_t[10]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"max_age_length\", StorageType.INT_MEMBER, \"int8_t\"),\n            # 1= Age\n            (READ_GEN, \"line_mode\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        return data_format\n\n\nclass BuildingConnection(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            # unit id of the current building\n            (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int32_t\"),\n            # 0=generic\n            # 1=TODO\n            # 2=default\n            # 3=marks as not available\n            # 4=upgrading, constructing, creating\n            # 5=research completed, building built\n            # maybe always 2 because we got 2 of them hardcoded below\n            # (unit_or_research, mode)\n            (READ, \"status\", StorageType.ID_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id != \"ROR\":\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                # new buildings available when this building was created\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[building_count]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                # new units\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[unit_count]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                # new researches\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[research_count]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"connected_slots_used\", StorageType.INT_MEMBER, \"int32_t\"),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[20]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=20,\n                )),\n            ])\n        elif game_version.edition.game_id == \"ROR\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[5]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=5,\n                )),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[10]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=10,\n                )),\n            ])\n\n        data_format.extend([\n            # minimum age, in which this building is available\n            (READ_GEN, \"location_in_age\", StorageType.ID_MEMBER, \"int8_t\"),\n            # total techs for each age (5 ages, post-imp probably counts as one)\n            (READ_GEN, \"unit_techs_total\", StorageType.ARRAY_INT, \"int8_t[5]\"),\n            (READ_GEN, \"unit_techs_first\", StorageType.ARRAY_INT, \"int8_t[5]\"),\n            # 5: >=1 connections, 6: no connections\n            (READ_GEN, \"line_mode\", StorageType.ID_MEMBER, \"int32_t\"),\n            # current building is unlocked by this research id, -1=no unlock needed\n            (READ_GEN, \"enabling_research\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        return data_format\n\n\nclass UnitConnection(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int32_t\"),\n            # 0=generic\n            # 1=TODO\n            # 2=default\n            # 3=marks as not available\n            # 4=upgrading, constructing, creating\n            # 5=research completed, building built\n            (READ_GEN, \"status\", StorageType.ID_MEMBER, \"int8_t\"),  # always 2: default\n            # building, where this unit is created\n            (READ_GEN, \"upper_building\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"connected_slots_used\", StorageType.INT_MEMBER, \"int32_t\"),\n        ]\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[20]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=20,\n                )),\n            ])\n        elif game_version.edition.game_id == \"ROR\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[5]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=5,\n                )),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[10]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=10,\n                )),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"vertical_line\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        if game_version.edition.game_id != \"ROR\":\n            data_format.extend([\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[unit_count]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"location_in_age\", StorageType.ID_MEMBER,\n             \"int32_t\"),    # 0=hidden, 1=first, 2=second\n            # min amount of researches to be discovered for this unit to be\n            # available\n            (READ_GEN, \"required_research\", StorageType.ID_MEMBER, \"int32_t\"),\n            # 2=first unit in line\n            # 3=unit that depends on a previous research in its line\n            (READ_GEN, \"line_mode\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"enabling_research\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        return data_format\n\n\nclass ResearchConnection(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int32_t\"),\n            # 0=generic\n            # 1=TODO\n            # 2=default\n            # 3=marks as not available\n            # 4=upgrading, constructing, creating\n            # 5=research completed, building built\n            (READ_GEN, \"status\", StorageType.ID_MEMBER, \"int8_t\"),\n            (READ_GEN, \"upper_building\", StorageType.ID_MEMBER, \"int32_t\"),\n        ]\n\n        if game_version.edition.game_id != \"ROR\":\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[building_count]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[unit_count]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[research_count]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ, \"building_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"buildings\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"unit_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"units\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n                (READ, \"research_count\", StorageType.INT_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"researches\", StorageType.ARRAY_ID, \"int32_t[40]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"connected_slots_used\", StorageType.INT_MEMBER, \"int32_t\"),\n        ])\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[20]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=20,\n                )),\n            ])\n        elif game_version.edition.game_id == \"ROR\":\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[5]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=5,\n                )),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"other_connected_ids\", StorageType.ARRAY_ID, \"int32_t[10]\"),\n                (READ_GEN, \"other_connections\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=OtherConnection,\n                    length=10,\n                )),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"vertical_line\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"location_in_age\", StorageType.ID_MEMBER,\n             \"int32_t\"),    # 0=hidden, 1=first, 2=second\n            # 0=first age unlocks\n            # 4=research\n            (READ_GEN, \"line_mode\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/terrain.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import ArrayMember, SubdataMember, IncludeMembers\nfrom ....read.value_members import StorageType\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass FrameData(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"frame_count\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"angle_count\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"shape_id\", StorageType.ID_MEMBER, \"int16_t\"),  # frame index\n        ]\n\n        return data_format\n\n\nclass TerrainPassGraphic(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            # when this restriction in unit a was selected, can the unit be placed on this terrain id? 0=no, -1=yes\n            (READ_GEN, \"slp_id_exit_tile\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"slp_id_enter_tile\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"slp_id_walk_tile\", StorageType.ID_MEMBER, \"int32_t\"),\n        ]\n\n        if game_version.edition.game_id == \"SWGB\":\n            data_format.append((READ_GEN, \"walk_sprite_rate\", StorageType.FLOAT_MEMBER, \"float\"))\n        else:\n            data_format.append((READ_GEN, \"replication_amount\", StorageType.INT_MEMBER, \"int32_t\"))\n\n        return data_format\n\n\nclass TerrainRestriction(GenieStructure):\n    \"\"\"\n    access policies for units on specific terrain.\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            # index of each array == terrain id\n            # when this restriction was selected, can the terrain be accessed?\n            # unit interaction_type activates this as damage multiplier\n            # See unit armor terrain restriction;\n            # pass-ability: [no: == 0, yes: > 0]\n            # build-ability: [<= 0.05 can't build here, > 0.05 can build]\n            # damage: [0: damage multiplier is 1, > 0: multiplier = value]\n            (READ_GEN, \"accessible_dmgmultiplier\", StorageType.ARRAY_FLOAT, \"float[terrain_count]\")\n        ]\n\n        if game_version.edition.game_id != \"ROR\":\n            data_format.append(\n                (READ_GEN, \"pass_graphics\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=TerrainPassGraphic,\n                    length=\"terrain_count\",\n                ))\n            )\n\n        return data_format\n\n\nclass TerrainAnimation(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"is_animated\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            # number of frames to animate\n            (READ_GEN, \"animation_frame_count\", StorageType.INT_MEMBER, \"int16_t\"),\n            # pause n * (frame rate) after last frame draw\n            (READ_GEN, \"pause_frame_count\", StorageType.INT_MEMBER, \"int16_t\"),\n            # time between frames\n            (READ_GEN, \"interval\", StorageType.FLOAT_MEMBER, \"float\"),\n            # pause time between frames\n            (READ_GEN, \"pause_between_loops\", StorageType.FLOAT_MEMBER, \"float\"),\n            # current frame (including animation and pause frames)\n            (READ_GEN, \"frame\", StorageType.INT_MEMBER, \"int16_t\"),\n            # current frame id to draw\n            (READ_GEN, \"draw_frame\", StorageType.INT_MEMBER, \"int16_t\"),\n            # last time animation frame was changed\n            (READ_GEN, \"animate_last\", StorageType.FLOAT_MEMBER, \"float\"),\n            # has the drawframe changed since terrain was drawn\n            (READ_GEN, \"frame_changed\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"drawn\", StorageType.BOOLEAN_MEMBER, \"int8_t\")\n        ]\n\n        return data_format\n\n\nclass Terrain(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"enabled\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"random\", StorageType.INT_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (READ_GEN, \"is_water\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n                (READ_GEN, \"hide_in_editor\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n                (READ_GEN, \"string_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            ])\n\n            if game_version.edition.game_id == \"AOE1DE\":\n                data_format.extend([\n                    (READ_GEN, \"blend_priority\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"blend_type\", StorageType.ID_MEMBER, \"int16_t\"),\n                ])\n\n            data_format.extend([\n                (SKIP, \"internal_name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"internal_name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"internal_name\", StorageType.STRING_MEMBER, \"char[internal_name_len]\"),\n                (SKIP, \"filename_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"filename_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[filename_len]\"),\n            ])\n        elif game_version.edition.game_id == \"SWGB\":\n            data_format.extend([\n                (READ_GEN, \"internal_name\", StorageType.STRING_MEMBER, \"char[17]\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[17]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"internal_name\", StorageType.STRING_MEMBER, \"char[13]\"),\n                (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[13]\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"slp_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"shape_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"sound_id\", StorageType.ID_MEMBER, \"int32_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"wwise_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"wwise_stop_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                # see doc/media/blendomatic.md for blending stuff\n                (READ_GEN, \"blend_priority\", StorageType.ID_MEMBER, \"int32_t\"),\n                (READ_GEN, \"blend_mode\", StorageType.ID_MEMBER, \"int32_t\"),\n            ])\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (SKIP, \"overlay_mask_name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (READ, \"overlay_mask_name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (READ_GEN, \"overlay_mask_name\", StorageType.STRING_MEMBER,\n                     \"char[overlay_mask_name_len]\"),\n                ])\n\n        data_format.extend([\n            # color of this terrain tile, mainly used in the minimap.\n            (READ_GEN, \"map_color_hi\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"map_color_med\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"map_color_low\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"map_color_cliff_lt\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"map_color_cliff_rt\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"passable_terrain\", StorageType.ID_MEMBER, \"int8_t\"),\n            (READ_GEN, \"impassable_terrain\", StorageType.ID_MEMBER, \"int8_t\"),\n\n            (READ_GEN, None, None, IncludeMembers(cls=TerrainAnimation)),\n\n            (READ_GEN, \"elevation_graphics\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=FrameData,   # tile Graphics: flat, 2 x 8 elevation, 2 x 1:1;\n                length=19,\n            )),\n\n            # draw this ground instead (e.g. forrest draws forrest ground)\n            (READ_GEN, \"terrain_replacement_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"terrain_to_draw0\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"terrain_to_draw1\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.append(\n                (READ_GEN, \"terrain_unit_masked_density\", StorageType.ARRAY_INT, \"int16_t[30]\")\n            )\n        elif game_version.edition.game_id == \"SWGB\":\n            data_format.append(\n                (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                    \"int16_t\",\n                    55\n                ))\n            )\n        elif game_version.edition.game_id == \"AOE1DE\":\n            data_format.append(\n                (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                    \"int16_t\",\n                    96\n                ))\n            )\n        elif game_version.edition.game_id == \"HDEDITION\":\n            if len(game_version.expansions) > 0:\n                data_format.append(\n                    (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                        \"int16_t\",\n                        100\n                    ))\n                )\n            else:\n                data_format.append(\n                    (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                        \"int16_t\",\n                        42\n                    ))\n                )\n\n        elif game_version.edition.game_id == \"AOC\":\n            data_format.append(\n                (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                    \"int16_t\",\n                    42\n                ))\n            )\n        else:  # game_version.edition.game_id == \"ROR\"\n            data_format.append(\n                (READ_GEN, \"borders\", StorageType.ARRAY_INT, ArrayMember(\n                    \"int16_t\",\n                    32\n                ))\n            )\n\n        data_format.extend([\n            # place these unit id on the terrain, with prefs from fields below\n            (READ_GEN, \"terrain_unit_id\", StorageType.ARRAY_ID, \"int16_t[30]\"),\n            # how many of the above units to place\n            (READ_GEN, \"terrain_unit_density\", StorageType.ARRAY_INT, \"int16_t[30]\"),\n            # when placing two terrain units on the same spot, selects which prevails(=1)\n            (READ_GEN, \"terrain_placement_flag\", StorageType.ARRAY_BOOL, \"int8_t[30]\"),\n            # how many entries of the above lists shall we use to place units implicitly when this terrain is placed\n            (READ_GEN, \"terrain_units_used_count\", StorageType.INT_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id != \"SWGB\":\n            data_format.append((READ, \"phantom\", StorageType.INT_MEMBER, \"int16_t\"))\n\n        return data_format\n\n\nclass TerrainBorder(GenieStructure):\n\n    dynamic_load = True\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"enabled\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"random\", StorageType.INT_MEMBER, \"int8_t\"),\n            (READ_GEN, \"internal_name\", StorageType.STRING_MEMBER, \"char[13]\"),\n            (READ_GEN, \"filename\", StorageType.STRING_MEMBER, \"char[13]\"),\n            (READ_GEN, \"slp_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"shape_ptr\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"sound_id\", StorageType.ID_MEMBER, \"int32_t\"),\n            (READ_GEN, \"color\", StorageType.ARRAY_ID, \"uint8_t[3]\"),\n\n            (READ_GEN, None, None, IncludeMembers(cls=TerrainAnimation)),\n\n            (READ_GEN, \"frames\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=FrameData,\n                length=19 * 12,  # number of tile types * 12\n            )),\n\n            (SKIP, \"draw_tile\", StorageType.INT_MEMBER, \"int16_t\"),         # always 0\n            (READ_GEN, \"underlay_terrain\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"border_style\", StorageType.INT_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass TileSize(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"width\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"height\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"delta_z\", StorageType.INT_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n"
  },
  {
    "path": "openage/convert/value_object/read/media/datfile/unit.py",
    "content": "# Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C,R,too-many-lines\nfrom __future__ import annotations\nimport typing\n\nfrom functools import cache\n\nfrom ...genie_structure import GenieStructure\nfrom ....read.member_access import READ, READ_GEN, SKIP\nfrom ....read.read_members import EnumLookupMember, ContinueReadMember, IncludeMembers, SubdataMember\nfrom ....read.value_members import StorageType\nfrom .lookup_dicts import COMMAND_ABILITY, OWNER_TYPE, RESOURCE_HANDLING, RESOURCE_TYPES, \\\n    DAMAGE_DRAW_TYPE, ARMOR_CLASS, UNIT_CLASSES, ELEVATION_MODES, FOG_VISIBILITY, \\\n    TERRAIN_RESTRICTIONS, BLAST_DEFENSE_TYPES, COMBAT_LEVELS, INTERACTION_MODES, \\\n    MINIMAP_MODES, UNIT_LEVELS, OBSTRUCTION_TYPES, SELECTION_EFFECTS, \\\n    ATTACK_MODES, BOUNDARY_IDS, BLAST_OFFENSE_TYPES, CREATABLE_TYPES, \\\n    GARRISON_TYPES\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.read_members import ReadMember\n\n\nclass UnitCommand(GenieStructure):\n    \"\"\"\n    also known as \"Task\" according to ES debug code,\n    this structure is the master for spawn-unit actions.\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            # Type (0 = Generic, 1 = Tribe)\n            (READ_GEN, \"command_used\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"command_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"is_default\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"type\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",\n                type_name=\"command_ability\",\n                lookup_dict=COMMAND_ABILITY\n            )),\n            (READ_GEN, \"class_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"terrain_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"resource_in\", StorageType.INT_MEMBER, \"int16_t\"),        # carry resource\n            # resource that multiplies the amount you can gather\n            (READ_GEN, \"resource_multiplier\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"resource_out\", StorageType.INT_MEMBER, \"int16_t\"),       # drop resource\n            (SKIP, \"unused_resource\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"work_value1\", StorageType.FLOAT_MEMBER, \"float\"),        # quantity\n            (READ_GEN, \"work_value2\", StorageType.FLOAT_MEMBER, \"float\"),        # execution radius?\n            (SKIP, \"work_range\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"search_mode\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"search_time\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"enable_targeting\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"combat_level_flag\", StorageType.ID_MEMBER, \"int8_t\"),\n            (SKIP, \"gather_type\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"work_mode2\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"owner_type\", StorageType.ID_MEMBER, EnumLookupMember(\n                # what can be selected as a target for the unit command?\n                raw_type=\"int8_t\",\n                type_name=\"selection_type\",\n                lookup_dict=OWNER_TYPE\n            )),\n            # checks if the targeted unit has > 0 resources\n            (SKIP, \"carry_check\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"state_build\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            # walking with tool but no resource\n            (READ_GEN, \"move_sprite_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # proceeding resource gathering or attack\n            (READ_GEN, \"proceed_sprite_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # actual execution or transformation graphic\n            (READ_GEN, \"work_sprite_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # display resources in hands\n            (READ_GEN, \"carry_sprite_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # sound to play when execution starts\n            (READ_GEN, \"resource_gather_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # sound to play on resource drop\n            (READ_GEN, \"resource_deposit_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ]\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"wwise_resource_gather_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                # sound to play on resource drop\n                (READ_GEN, \"wwise_resource_deposit_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        return data_format\n\n\nclass UnitHeader(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ, \"exists\", StorageType.BOOLEAN_MEMBER, ContinueReadMember(\"uint8_t\")),\n            (READ, \"unit_command_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"unit_commands\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=UnitCommand,\n                length=\"unit_command_count\",\n            )),\n        ]\n\n        return data_format\n\n\n# Only used in SWGB\nclass UnitLine(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ, \"name_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_length]\"),\n            (READ, \"unit_ids_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"unit_ids\", StorageType.ARRAY_ID, \"int16_t[unit_ids_count]\"),\n        ]\n\n        return data_format\n\n\nclass ResourceStorage(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"type\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"amount\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"used_mode\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int8_t\",\n                type_name=\"resource_handling\",\n                lookup_dict=RESOURCE_HANDLING\n            )),\n        ]\n\n        return data_format\n\n\nclass DamageGraphic(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"damage_percent\", StorageType.INT_MEMBER, \"int8_t\"),\n            # gets overwritten in aoe memory by the real apply_mode:\n            (SKIP, \"old_apply_mode\", StorageType.ID_MEMBER, \"int8_t\"),\n            (SKIP, \"apply_mode\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int8_t\",\n                type_name=\"damage_draw_type\",\n                lookup_dict=DAMAGE_DRAW_TYPE\n            )),\n        ]\n\n        return data_format\n\n\nclass HitType(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"type_id\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",\n                type_name=\"hit_class\",\n                lookup_dict=ARMOR_CLASS\n            )),\n            (READ_GEN, \"amount\", StorageType.INT_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass ResourceCost(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, \"type_id\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",\n                type_name=\"resource_types\",\n                lookup_dict=RESOURCE_TYPES\n            )),\n            (READ_GEN, \"amount\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"enabled\", StorageType.BOOLEAN_MEMBER, \"int16_t\"),\n        ]\n\n        return data_format\n\n\nclass BuildingAnnex(GenieStructure):\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (SKIP, \"unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"misplaced0\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"misplaced1\", StorageType.FLOAT_MEMBER, \"float\"),\n        ]\n\n        return data_format\n\n\nclass UnitObject(GenieStructure):\n    \"\"\"\n    base properties for every unit entry.\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        if game_version.edition.game_id not in (\"AOE1DE\", \"AOE2DE\"):\n            data_format = [\n                (READ, \"name_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n            ]\n        else:\n            data_format = []\n\n        data_format.extend([\n            (READ_GEN, \"id0\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"language_dll_name\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (SKIP, \"language_dll_creation\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"language_dll_name\", StorageType.ID_MEMBER, \"uint16_t\"),\n                (SKIP, \"language_dll_creation\", StorageType.ID_MEMBER, \"uint16_t\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"unit_class\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",\n                type_name=\"unit_classes\",\n                lookup_dict=UNIT_CLASSES\n            )),\n            (READ_GEN, \"idle_graphic0\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"idle_graphic1\", StorageType.ID_MEMBER, \"int16_t\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"dying_graphic\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"undead_graphic\", StorageType.ID_MEMBER, \"int16_t\"),\n            # 1 = become `dead_unit_id` (reviving does not make it usable again)\n            (SKIP, \"death_mode\", StorageType.ID_MEMBER, \"int8_t\"),\n            # unit health. -1=insta-die\n            (READ_GEN, \"hit_points\", StorageType.INT_MEMBER, \"int16_t\"),\n            (READ_GEN, \"line_of_sight\", StorageType.FLOAT_MEMBER, \"float\"),\n            # number of units that can garrison in there\n            (READ_GEN, \"garrison_capacity\", StorageType.INT_MEMBER, \"int8_t\"),\n            # size of the unit\n            (READ_GEN, \"radius_x\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"radius_y\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"radius_z\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"train_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((READ_GEN, \"damage_sound_id\", StorageType.ID_MEMBER, \"int16_t\"))\n\n        data_format.extend([\n            # unit id to become on death\n            (READ_GEN, \"dead_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (READ_GEN, \"blood_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            ])\n\n        data_format.extend([\n            # 0=placable on top of others in scenario editor, 5=can't\n            (SKIP, \"placement_mode\", StorageType.ID_MEMBER, \"int8_t\"),\n            (SKIP, \"can_be_built_on\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),  # 1=no footprints\n            # frame id of the icon slp (57029) to place on the creation button\n            (SKIP, \"icon_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"hidden_in_editor\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"old_portrait_icon_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # 0=unlocked by research, 1=insta-available\n            (READ_GEN, \"enabled\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((READ, \"disabled\", StorageType.BOOLEAN_MEMBER, \"int8_t\"))\n\n        data_format.extend([\n            # terrain id that's needed somewhere on the foundation (e.g. dock\n            # water)\n            (SKIP, \"placement_side_terrain0\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"placement_side_terrain1\", StorageType.ID_MEMBER, \"int16_t\"),    # second slot for ^\n            # terrain needed for placement (e.g. dock: water)\n            (SKIP, \"placement_terrain0\", StorageType.ID_MEMBER, \"int16_t\"),\n            # alternative terrain needed for placement (e.g. dock: shallows)\n            (SKIP, \"placement_terrain1\", StorageType.ID_MEMBER, \"int16_t\"),\n            # minimum space required to allow placement in editor\n            (READ_GEN, \"clearance_size_x\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"clearance_size_y\", StorageType.FLOAT_MEMBER, \"float\"),\n            # determines the maxmimum elevation difference for terrain under the unit\n            (READ_GEN, \"elevation_mode\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int8_t\",\n                type_name=\"elevation_modes\",\n                lookup_dict=ELEVATION_MODES\n            )),\n            (READ_GEN, \"visible_in_fog\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int8_t\",\n                type_name=\"fog_visibility\",\n                lookup_dict=FOG_VISIBILITY\n            )),\n            (READ_GEN, \"terrain_restriction\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int16_t\",      # determines on what type of ground the unit can be placed/walk\n                type_name=\"ground_type\",  # is actually the id of the terrain_restriction entry!\n                lookup_dict=TERRAIN_RESTRICTIONS\n            )),\n            # determines whether the unit can fly\n            (READ_GEN, \"fly_mode\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"resource_capacity\", StorageType.INT_MEMBER, \"int16_t\"),\n            # when animals rot, their resources decay\n            (READ_GEN, \"resource_decay\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"blast_defense_level\", StorageType.ID_MEMBER, EnumLookupMember(\n                # receive blast damage from units that have lower or same\n                # blast_attack_level.\n                raw_type=\"int8_t\",\n                type_name=\"blast_types\",\n                lookup_dict=BLAST_DEFENSE_TYPES\n            )),\n            (SKIP, \"combat_level\", StorageType.ID_MEMBER, EnumLookupMember(\n                raw_type=\"int8_t\",\n                type_name=\"combat_levels\",\n                lookup_dict=COMBAT_LEVELS\n            )),\n            (READ_GEN, \"interaction_mode\", StorageType.ID_MEMBER, EnumLookupMember(\n                # what can be done with this unit?\n                raw_type=\"int8_t\",\n                type_name=\"interaction_modes\",\n                lookup_dict=INTERACTION_MODES\n            )),\n            (SKIP, \"map_draw_level\", StorageType.ID_MEMBER, EnumLookupMember(\n                # how does the unit show up on the minimap?\n                raw_type=\"int8_t\",\n                type_name=\"minimap_modes\",\n                lookup_dict=MINIMAP_MODES\n            )),\n            (SKIP, \"unit_level\", StorageType.ID_MEMBER, EnumLookupMember(\n                # selects the available ui command buttons for the unit\n                raw_type=\"int8_t\",\n                type_name=\"command_attributes\",\n                lookup_dict=UNIT_LEVELS\n            )),\n            (SKIP, \"attack_reaction\", StorageType.FLOAT_MEMBER, \"float\"),\n            # palette color id for the minimap\n            (SKIP, \"minimap_color\", StorageType.ID_MEMBER, \"int8_t\"),\n            # help text for this unit, stored in the translation dll.\n            (SKIP, \"language_dll_help\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"language_dll_hotkey_text\", StorageType.ID_MEMBER, \"int32_t\"),\n            # language dll dependent (kezb lazouts!)\n            (SKIP, \"hot_keys\", StorageType.ID_MEMBER, \"int32_t\"),\n            (SKIP, \"recyclable\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"enable_auto_gather\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"doppelgaenger_on_death\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"resource_gather_drop\", StorageType.INT_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            # bit 0 == 1 && val != 7: mask shown behind buildings,\n            # bit 0 == 0 && val != {6, 10}: no mask displayed,\n            # val == {-1, 7}: in open area mask is partially displayed\n            # val == {6, 10}: building, causes mask to appear on units behind it\n            data_format.extend([\n                (SKIP, \"occlusion_mode\", StorageType.ID_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"obstruction_type\", StorageType.ID_MEMBER, EnumLookupMember(\n                    raw_type=\"int8_t\",\n                    type_name=\"obstruction_types\",\n                    lookup_dict=OBSTRUCTION_TYPES\n                )),\n                (READ_GEN, \"obstruction_class\", StorageType.ID_MEMBER, \"int8_t\"),\n\n                # bitfield of unit attributes:\n                # bit 0: allow garrison,\n                # bit 1: don't join formation,\n                # bit 2: stealth unit,\n                # bit 3: detector unit,\n                # bit 4: mechanical unit,\n                # bit 5: biological unit,\n                # bit 6: self-shielding unit,\n                # bit 7: invisible unit\n                (READ_GEN, \"trait\", StorageType.ID_MEMBER, \"uint8_t\"),\n                (READ_GEN, \"civilization_id\", StorageType.ID_MEMBER, \"int8_t\"),\n                # leftover from trait+civ variable\n                (SKIP, \"attribute_piece\", StorageType.INT_MEMBER, \"int16_t\"),\n            ])\n        elif game_version.edition.game_id == \"AOE1DE\":\n            data_format.extend([\n                (READ_GEN, \"obstruction_type\", StorageType.ID_MEMBER, EnumLookupMember(\n                    raw_type=\"int8_t\",\n                    type_name=\"obstruction_types\",\n                    lookup_dict=OBSTRUCTION_TYPES\n                )),\n                (READ_GEN, \"obstruction_class\", StorageType.ID_MEMBER, \"int8_t\"),\n            ])\n\n        data_format.extend([\n            (SKIP, \"selection_effect\", StorageType.ID_MEMBER, EnumLookupMember(\n                # things that happen when the unit was selected\n                raw_type=\"int8_t\",\n                type_name=\"selection_effects\",\n                lookup_dict=SELECTION_EFFECTS\n            )),\n            # 0: default, -16: fish trap, farm, 52: deadfarm, OLD-*, 116: flare,\n            # whale, dolphin -123: fish\n            (READ, \"editor_selection_color\", StorageType.ID_MEMBER, \"uint8_t\"),\n            (READ_GEN, \"selection_shape_x\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"selection_shape_y\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"selection_shape_z\", StorageType.FLOAT_MEMBER, \"float\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ, \"scenario_trigger_data0\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ, \"scenario_trigger_data1\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        data_format.extend([\n            (READ_GEN, \"resource_storage\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=ResourceStorage,\n                length=3,\n            )),\n            (READ, \"damage_graphic_count\", StorageType.INT_MEMBER, \"int8_t\"),\n            (READ_GEN, \"damage_graphics\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=DamageGraphic,\n                length=\"damage_graphic_count\",\n            )),\n            (READ_GEN, \"selection_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"dying_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"wwise_creation_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"wwise_damage_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"wwise_selection_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"wwise_dying_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        data_format.extend([\n            (SKIP, \"old_attack_mode\", StorageType.ID_MEMBER, EnumLookupMember(  # obsolete, as it's copied when converting the unit\n                raw_type=\"int8_t\",     # things that happen when the unit was selected\n                type_name=\"attack_modes\",\n                lookup_dict=ATTACK_MODES\n            )),\n            # leftover from alpha. would et units change terrain under them\n            (SKIP, \"convert_terrain\", StorageType.INT_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id in (\"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (SKIP, \"name_len_debug\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ, \"name_len\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_len]\"),\n            ])\n\n        else:\n            data_format.extend([\n                (SKIP, \"name\", StorageType.STRING_MEMBER, \"char[name_length]\"),\n            ])\n\n            if game_version.edition.game_id == \"SWGB\":\n                data_format.extend([\n                    (READ, \"name2_length\", StorageType.INT_MEMBER, \"uint16_t\"),\n                    (READ_GEN, \"name2\", StorageType.STRING_MEMBER, \"char[name2_length]\"),\n                    (READ_GEN, \"unit_line_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"min_tech_level\", StorageType.ID_MEMBER, \"int8_t\"),\n                ])\n\n        data_format.append((SKIP, \"id1\", StorageType.ID_MEMBER, \"int16_t\"))\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((SKIP, \"id2\", StorageType.ID_MEMBER, \"int16_t\"))\n        elif game_version.edition.game_id == \"AOE1DE\":\n            data_format.append((READ_GEN, \"telemetry_id\", StorageType.ID_MEMBER, \"int16_t\"))\n\n        return data_format\n\n\nclass TreeUnit(UnitObject):\n    \"\"\"\n    type_id == 90\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=UnitObject)),\n        ]\n\n        return data_format\n\n\nclass AnimatedUnit(UnitObject):\n    \"\"\"\n    type_id >= 20\n    Animated master object\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=UnitObject)),\n            (READ_GEN, \"speed\", StorageType.FLOAT_MEMBER, \"float\"),\n        ]\n\n        return data_format\n\n\nclass DoppelgangerUnit(AnimatedUnit):\n    \"\"\"\n    type_id >= 25\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=AnimatedUnit)),\n        ]\n\n        return data_format\n\n\nclass MovingUnit(DoppelgangerUnit):\n    \"\"\"\n    type_id >= 30\n    Moving master object\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=DoppelgangerUnit)),\n            (READ_GEN, \"move_graphics\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"run_graphics\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"turn_speed\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"old_size_class\", StorageType.ID_MEMBER, \"int8_t\"),\n            # unit id for the ground traces\n            (SKIP, \"trail_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # ground traces: -1: no tracking present, 2: projectiles with tracking unit\n            (SKIP, \"trail_opsions\", StorageType.ID_MEMBER, \"uint8_t\"),\n            # ground trace spacing: 0: no tracking, 0.5: trade cart, 0.12: some\n            # projectiles, 0.4: other projectiles\n            (SKIP, \"trail_spacing\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"old_move_algorithm\", StorageType.ID_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"turn_radius\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"turn_radius_speed\", StorageType.FLOAT_MEMBER, \"float\"),\n                (READ_GEN, \"max_yaw_per_sec_moving\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"stationary_yaw_revolution_time\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"max_yaw_per_sec_stationary\", StorageType.FLOAT_MEMBER, \"float\"),\n            ])\n\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"min_collision_size_multiplier\", StorageType.FLOAT_MEMBER, \"float\"),\n                ])\n\n        return data_format\n\n\nclass ActionUnit(MovingUnit):\n    \"\"\"\n    type_id >= 40\n    Action master object\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=MovingUnit)),\n            # callback unit action id when found.\n            # monument and sheep: 107 = enemy convert.\n            # all auto-convertible units: 0, most other units: -1\n            # e.g. when sheep are discovered\n            (SKIP, \"default_task_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (READ_GEN, \"search_radius\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"work_rate\", StorageType.FLOAT_MEMBER, \"float\"),\n        ]\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ, \"drop_sites_count\", StorageType.INT_MEMBER, \"int16_t\"),\n                (READ_GEN, \"drop_sites\", StorageType.ARRAY_ID, \"int16_t[drop_sites_count]\"),\n            ])\n        else:\n            data_format.extend([\n                (READ_GEN, \"drop_sites\", StorageType.ARRAY_ID, \"int16_t[2]\"),\n            ])\n\n        data_format.extend([\n            # 1: male villager; 2: female villager; 3+: free slots\n            (READ_GEN, \"task_group\", StorageType.ID_MEMBER, \"int8_t\"),\n            # basically this\n            # creates a \"swap\n            # group id\" where you\n            # can place\n            # different-graphic\n            # units together.\n            # sound played when a command is instanciated\n            (READ_GEN, \"command_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # sound when the command is done (e.g. unit stops at target position)\n            (SKIP, \"stop_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.extend([\n                (READ_GEN, \"wwise_command_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                (READ_GEN, \"wwise_stop_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n            ])\n\n        data_format.extend([\n            # how animals run around randomly\n            (SKIP, \"run_pattern\", StorageType.ID_MEMBER, \"int8_t\"),\n        ])\n\n        if game_version.edition.game_id in (\"ROR\", \"AOE1DE\", \"AOE2DE\"):\n            data_format.extend([\n                (READ, \"unit_command_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n                (READ_GEN, \"unit_commands\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=UnitCommand,\n                    length=\"unit_command_count\",\n                )),\n            ])\n\n        return data_format\n\n\nclass ProjectileUnit(ActionUnit):\n    \"\"\"\n    type_id >= 60\n    Projectile master object\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=ActionUnit)),\n        ]\n\n        if game_version.edition.game_id == \"ROR\":\n            data_format.append((SKIP, \"default_armor\", StorageType.INT_MEMBER, \"uint8_t\"))\n        else:\n            data_format.append((SKIP, \"default_armor\", StorageType.INT_MEMBER, \"int16_t\"))\n\n        data_format.extend([\n            (READ, \"attack_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"attacks\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=HitType,\n                length=\"attack_count\",\n            )),\n            (READ, \"armor_count\", StorageType.INT_MEMBER, \"uint16_t\"),\n            (READ_GEN, \"armors\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=HitType,\n                length=\"armor_count\",\n            )),\n            (SKIP, \"boundary_id\", StorageType.ID_MEMBER, EnumLookupMember(\n                # the damage received by this unit is multiplied by\n                # the accessible values on the specified terrain restriction\n                raw_type=\"int16_t\",\n                type_name=\"boundary_ids\",\n                lookup_dict=BOUNDARY_IDS\n            )),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.append((READ_GEN, \"bonus_damage_resistance\",\n                               StorageType.FLOAT_MEMBER, \"float\"))\n\n        data_format.extend([\n            (READ_GEN, \"weapon_range_max\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"blast_range\", StorageType.FLOAT_MEMBER, \"float\"),\n            (READ_GEN, \"attack_speed\", StorageType.FLOAT_MEMBER, \"float\"),  # = \"reload time\"\n            # which projectile to use?\n            (READ_GEN, \"projectile_id0\", StorageType.ID_MEMBER, \"int16_t\"),\n            # probablity of attack hit in percent\n            (READ_GEN, \"accuracy\", StorageType.INT_MEMBER, \"int16_t\"),\n            # = tower mode?; not used anywhere\n            (SKIP, \"break_off_combat\", StorageType.INT_MEMBER, \"int8_t\"),\n            # the frame number at which the missile is fired, = delay\n            (READ_GEN, \"frame_delay\", StorageType.INT_MEMBER, \"int16_t\"),\n            # graphics displacement in x, y and z\n            (READ_GEN, \"weapon_offset\", StorageType.ARRAY_FLOAT, \"float[3]\"),\n            (READ_GEN, \"blast_level_offence\", StorageType.ID_MEMBER, EnumLookupMember(\n                # blasts damage units that have higher or same blast_defense_level\n                raw_type=\"uint8_t\",\n                type_name=\"range_damage_type\",\n                lookup_dict=BLAST_OFFENSE_TYPES\n            )),\n            # minimum range that this projectile requests for display\n            (READ_GEN, \"weapon_range_min\", StorageType.FLOAT_MEMBER, \"float\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((READ_GEN, \"accuracy_dispersion\", StorageType.FLOAT_MEMBER, \"float\"))\n\n        data_format.extend([\n            (READ_GEN, \"attack_sprite_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            (SKIP, \"melee_armor_displayed\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"attack_displayed\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"range_displayed\", StorageType.FLOAT_MEMBER, \"float\"),\n            (SKIP, \"reload_time_displayed\", StorageType.FLOAT_MEMBER, \"float\"),\n        ])\n\n        if game_version.edition.game_id == \"AOE2DE\":\n            data_format.append((READ_GEN, \"blast_damage\", StorageType.FLOAT_MEMBER, \"float\"))\n\n        return data_format\n\n\nclass MissileUnit(ProjectileUnit):\n    \"\"\"\n    type_id == 60\n    Missile master object\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=ProjectileUnit)),\n            # 0 = default; 1 = projectile falls vertically to the bottom of the\n            # map; 3 = teleporting projectiles\n            (READ_GEN, \"projectile_type\", StorageType.ID_MEMBER, \"int8_t\"),\n            # \"better aiming\". tech attribute 19 changes this: 0 = shoot at current pos; 1 = shoot at predicted pos\n            (READ_GEN, \"smart_mode\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (READ_GEN, \"drop_animation_mode\", StorageType.ID_MEMBER, \"int8_t\"),  # 1 = disappear on hit\n            # 1 = pass through hit object; 0 = stop projectile on hit; (only for\n            # graphics, not pass-through damage)\n            (READ_GEN, \"penetration_mode\", StorageType.ID_MEMBER, \"int8_t\"),\n            (READ_GEN, \"area_of_effect_special\", StorageType.INT_MEMBER, \"int8_t\"),\n            (READ_GEN, \"projectile_arc\", StorageType.FLOAT_MEMBER, \"float\"),\n        ]\n\n        return data_format\n\n\nclass LivingUnit(ProjectileUnit):\n    \"\"\"\n    type_id >= 70\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=ProjectileUnit)),\n            (READ_GEN, \"resource_cost\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                ref_type=ResourceCost,\n                length=3,\n            )),\n            (READ_GEN, \"creation_time\", StorageType.INT_MEMBER, \"int16_t\"),     # in seconds\n            (READ_GEN, \"train_location_id\", StorageType.ID_MEMBER,\n             \"int16_t\"),  # e.g. 118 = villager builder\n\n            # where to place the button with the given icon\n            # creation page:\n            # +------------------------+\n            # | 01 | 02 | 03 | 04 | 05 |\n            # |----|----|----|----|----|\n            # | 06 | 07 | 08 | 09 | 10 |\n            # |----|----|----|----|----|\n            # | 11 | 12 | 13 | 14 | 15 |\n            # +------------------------+\n            #\n            # additional page (dock):\n            # +------------------------+\n            # | 21 | 22 | 23 | 24 | 25 |\n            # |----|----|----|----|----|\n            # | 26 | 27 | 28 | 29 | 30 |\n            # |----|----|----|----|----|\n            # | 31 | 32 | 33 | 34 | 35 |\n            # +------------------------+\n            (READ, \"creation_button_id\", StorageType.ID_MEMBER, \"int8_t\"),\n        ]\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"heal_timer\", StorageType.FLOAT_MEMBER, \"float\"),\n                ])\n            else:\n                data_format.extend([\n                    (SKIP, \"rear_attack_modifier\", StorageType.FLOAT_MEMBER, \"float\"),\n                ])\n\n            data_format.extend([\n                (SKIP, \"flank_attack_modifier\", StorageType.FLOAT_MEMBER, \"float\"),\n                (READ_GEN, \"creatable_type\", StorageType.ID_MEMBER, EnumLookupMember(\n                    raw_type=\"uint8_t\",\n                    type_name=\"creatable_types\",\n                    lookup_dict=CREATABLE_TYPES\n                )),\n                # if building: \"others\" tab in editor, if living unit: \"heroes\" tab,\n                # regenerate health + monk immunity\n                (SKIP, \"hero_mode\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n                # graphic to display when units are garrisoned\n                (READ_GEN, \"garrison_graphic\", StorageType.ID_MEMBER, \"int32_t\"),\n                # projectile count when nothing garrisoned, including both normal and\n                # duplicated projectiles\n            ])\n\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"spawn_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"upgrade_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"hero_glow_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"max_charge\", StorageType.FLOAT_MEMBER, \"float\"),\n                    (READ_GEN, \"charge_regen_rate\", StorageType.FLOAT_MEMBER, \"float\"),\n                    (READ_GEN, \"charge_cost\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"charge_type\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"min_convert_mod\", StorageType.FLOAT_MEMBER, \"float\"),\n                    (READ_GEN, \"max_convert_mod\", StorageType.FLOAT_MEMBER, \"float\"),\n                    (READ_GEN, \"convert_chance_mod\", StorageType.FLOAT_MEMBER, \"float\"),\n                ])\n\n            data_format.extend([\n                (READ_GEN, \"projectile_min_count\", StorageType.INT_MEMBER, \"float\"),\n                # total projectiles when fully garrisoned\n                (READ_GEN, \"projectile_max_count\", StorageType.INT_MEMBER, \"int8_t\"),\n                (READ_GEN, \"projectile_spawning_area_width\", StorageType.FLOAT_MEMBER, \"float\"),\n                (READ_GEN, \"projectile_spawning_area_length\", StorageType.FLOAT_MEMBER, \"float\"),\n                # placement randomness, 0=from single spot, 1=random, 1<less random\n                (READ_GEN, \"projectile_spawning_area_randomness\", StorageType.FLOAT_MEMBER, \"float\"),\n                # uses its own attack values\n                (READ_GEN, \"projectile_id1\", StorageType.ID_MEMBER, \"int32_t\"),\n                # used just before unit reaches its target enemy, configuration:\n                (SKIP, \"special_graphic_id\", StorageType.ID_MEMBER, \"int32_t\"),\n                # determines adjacent unit graphics, if 1: building can adapt graphics\n                # by adjacent buildings\n                (SKIP, \"special_activation\", StorageType.ID_MEMBER, \"int8_t\"),\n                # 0: default: only works when facing the hit angle.\n                # 1: block: activates special graphic when receiving damage and not pursuing the attacker.\n                # while idle, blocking decreases damage taken by 1/3.\n                # also: a wall changes the graphics (when not-an-end piece) because of this.\n                # 2: counter charge: activates special graphic when idle and enemy is near.\n                # while idle, attacks back once on first received hit.\n                # enemy must be unit type 70 and have less than 0.2 max range.\n                # 3: charge: activates special graphic when closer than two tiles to the target.\n                # deals 2X damage on 1st\n                # hit\n            ])\n\n        # unit stats display of pierce armor\n        data_format.append((SKIP, \"pierce_armor_displayed\", StorageType.INT_MEMBER, \"int16_t\"))\n\n        return data_format\n\n\nclass BuildingUnit(LivingUnit):\n    \"\"\"\n    type_id >= 80\n    \"\"\"\n\n    @classmethod\n    @cache\n    def get_data_format_members(\n        cls,\n        game_version: GameVersion\n    ) -> list[tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]]]:\n        \"\"\"\n        Return the members in this struct.\n        \"\"\"\n        data_format = [\n            (READ_GEN, None, None, IncludeMembers(cls=LivingUnit)),\n            (READ_GEN, \"construction_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ]\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.append((SKIP, \"snow_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"))\n\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"destruction_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"destruction_rubble_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"research_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                    (READ_GEN, \"research_complete_graphic_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                ])\n\n        data_format.extend([\n            # 1=adjacent units may change the graphics\n            (SKIP, \"adjacent_mode\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            (SKIP, \"graphics_angle\", StorageType.INT_MEMBER, \"int16_t\"),\n            (SKIP, \"disappears_when_built\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n            # second building to place directly on top\n            (READ_GEN, \"stack_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # change underlying terrain to this id when building completed\n            (READ_GEN, \"foundation_terrain_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # deprecated terrain-like structures knowns as \"Overlays\" from alpha\n            # AOE used for roads\n            (SKIP, \"old_overlay_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            # research_id to be enabled when building creation\n            (READ_GEN, \"research_id\", StorageType.ID_MEMBER, \"int16_t\"),\n        ])\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            data_format.extend([\n                (SKIP, \"can_burn\", StorageType.BOOLEAN_MEMBER, \"int8_t\"),\n                (SKIP, \"building_annex\", StorageType.ARRAY_CONTAINER, SubdataMember(\n                    ref_type=BuildingAnnex,\n                    length=4\n                )),\n                # building at which an annex building is attached to\n                (READ_GEN, \"head_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                # destination unit id when unit shall transform (e.g. unpack)\n                (READ_GEN, \"transform_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                (READ_GEN, \"transform_sound_id\", StorageType.ID_MEMBER, \"int16_t\"),\n            ])\n\n        data_format.append((READ_GEN, \"construction_sound_id\", StorageType.ID_MEMBER, \"int16_t\"))\n\n        if game_version.edition.game_id not in (\"ROR\", \"AOE1DE\"):\n            if game_version.edition.game_id == \"AOE2DE\":\n                data_format.extend([\n                    (READ_GEN, \"wwise_construction_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                    (READ_GEN, \"wwise_transform_sound_id\", StorageType.ID_MEMBER, \"uint32_t\"),\n                ])\n\n            data_format.extend([\n                (READ_GEN, \"garrison_type\", StorageType.BITFIELD_MEMBER, EnumLookupMember(\n                    raw_type=\"int8_t\",\n                    type_name=\"garrison_types\",\n                    lookup_dict=GARRISON_TYPES\n                )),\n                (READ_GEN, \"garrison_heal_rate\", StorageType.FLOAT_MEMBER, \"float\"),\n                (SKIP, \"garrison_repair_rate\", StorageType.FLOAT_MEMBER, \"float\"),\n                # id of the unit used for salvages\n                (SKIP, \"salvage_unit_id\", StorageType.ID_MEMBER, \"int16_t\"),\n                # list of attributes for salvages (looting table)\n                (SKIP, \"salvage_attributes\", StorageType.ARRAY_INT, \"int8_t[6]\"),\n            ])\n\n        return data_format\n\n\n# unit type id => human readable name\n# used as member name in the resulting struct\nunit_type_lookup = {\n    10: \"object\",\n    20: \"animated\",\n    25: \"doppelganger\",\n    30: \"moving\",\n    40: \"action\",\n    60: \"missile\",\n    70: \"living\",\n    80: \"building\",\n    90: \"tree\",\n}\n\n\n# name => attribute class\nunit_type_class_lookup = {\n    \"object\":         UnitObject,\n    \"animated\":       AnimatedUnit,\n    \"doppelganger\":   DoppelgangerUnit,\n    \"moving\":         MovingUnit,\n    \"action\":         ActionUnit,\n    \"missile\":        MissileUnit,\n    \"living\":         LivingUnit,\n    \"building\":       BuildingUnit,\n    \"tree\":           TreeUnit,\n}\n"
  },
  {
    "path": "openage/convert/value_object/read/media/drs.py",
    "content": "# Copyright 2013-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCode for reading Genie .DRS archives.\n\nNote that .DRS archives can't store file names; they just store the file\nextension, and a file number.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....log import spam, dbg\nfrom .....util.filelike.stream import StreamFragment\nfrom .....util.fslike.filecollection import FileCollection, FileEntry\nfrom .....util.strings import decode_until_null\nfrom .....util.struct import NamedStruct\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.init.game_version import GameVersion\n    from openage.util.fslike.wrapper import GuardedFile\n\n\n# version of the drs files, hardcoded for now\nCOPYRIGHT_ENSEMBLE = b\"Copyright (c) 1997 Ensemble Studios\"\nCOPYRIGHT_SIZE_ENSEMBLE = 40\nCOPYRIGHT_SIZE_LUCAS = 60\n\n\nclass DRSHeaderEnsemble(NamedStruct):\n    \"\"\"\n    DRS file header for AoE1 and AoE2; see doc/media/drs-files\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness       = \"<\"\n\n    copyright        = str(COPYRIGHT_SIZE_ENSEMBLE) + \"s\"\n    version          = \"4s\"\n    ftype            = \"12s\"\n    table_count      = \"i\"\n    file_offset      = \"i\"     # offset of the first file\n\n\nclass DRSHeaderLucasArts(NamedStruct):\n    \"\"\"\n    DRS file header for SWGB; see doc/media/drs-files\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness       = \"<\"\n\n    copyright        = str(COPYRIGHT_SIZE_LUCAS) + \"s\"\n    version          = \"4s\"\n    ftype            = \"12s\"\n    table_count      = \"i\"\n    file_offset      = \"i\"     # offset of the first file\n\n\nclass DRSTableInfo(NamedStruct):\n    \"\"\"\n    DRS table header\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness       = \"<\"\n\n    file_extension   = \"4s\"    # reversed (for reasons) extension\n    file_info_offset = \"i\"     # table offset\n    file_count       = \"i\"     # number of files in table\n\n\nclass DRSFileInfo(NamedStruct):\n    \"\"\"\n    DRS file header\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness       = \"<\"\n\n    file_id          = \"i\"\n    file_data_offset = \"i\"\n    file_size        = \"i\"\n\n\nclass DRSEntry(FileEntry):\n    \"\"\"\n    Entry in a DRS archive.\n    \"\"\"\n\n    def __init__(self, fileobj: GuardedFile, offset: int, size: int):\n        self.fileobj = fileobj\n        self.offset = offset\n        self.entry_size = size\n\n    def open_r(self):\n        return StreamFragment(self.fileobj, self.offset, self.entry_size)\n\n    def size(self) -> int:\n        return self.entry_size\n\n\nclass DRS(FileCollection):\n    \"\"\"\n    represents a file archive in DRS format.\n    \"\"\"\n\n    def __init__(self, fileobj: GuardedFile, game_version: GameVersion):\n        super().__init__()\n\n        # queried from the outside\n        self.fileobj = fileobj\n\n        # read header\n        if game_version.edition.game_id == \"SWGB\":\n            header = DRSHeaderLucasArts.read(fileobj)\n\n        else:\n            # Try Ensemble header by default\n            header = DRSHeaderEnsemble.read(fileobj)\n\n            if not header.copyright.startswith(COPYRIGHT_ENSEMBLE):\n                # different copyright string probably means it's SWGB\n                fileobj.seek(0)\n                header = DRSHeaderLucasArts.read(fileobj)\n\n        header.copyright = decode_until_null(header.copyright).strip()\n        header.version = decode_until_null(header.version)\n        header.ftype = decode_until_null(header.ftype)\n        self.header = header\n\n        dbg(header)\n\n        # read table info\n        self.tables: list[DRSTableInfo] = []\n        for _ in range(header.table_count):\n            table_header = DRSTableInfo.read(fileobj)\n\n            # decode and un-flip the file extension\n            # see doc/media/drs-files.md\n            fileext = table_header.file_extension\n            fileext = fileext.decode('latin-1').lower()[::-1].rstrip()\n            table_header.file_extension = fileext\n\n            dbg(table_header)\n            self.tables.append(table_header)\n\n        for filename, offset, size in self.read_tables():\n            file_entry = DRSEntry(self.fileobj, offset, size)\n\n            self.add_fileentry([filename.encode()], file_entry)\n\n    def read_tables(self) -> typing.Generator[tuple[str, str, str], None, None]:\n        \"\"\"\n        Reads the tables from self.tables, and yields tuples of\n        filename, offset, size.\n        \"\"\"\n        # read file tables\n        for header in self.tables:\n            self.fileobj.seek(header.file_info_offset)\n\n            for _ in range(header.file_count):\n                fileinfo = DRSFileInfo.read(self.fileobj)\n\n                file_name = str(fileinfo.file_id) + '.' + header.file_extension\n                spam(\"%s: %s\", file_name, fileinfo)\n\n                yield file_name, fileinfo.file_data_offset, fileinfo.file_size\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tinterface.py\n\ttermcolors.py\n\tterrain_tile_size.py\n\ttexture.py\n)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/__init__.py",
    "content": "# Copyright 2013-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nVarious constants.\n\"\"\"\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/interface.py",
    "content": "# Copyright 2016-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nAdditional hardcoded information about AoC user interface assets\n\"\"\"\n\n\nINGAME_HUD_BACKGROUNDS = [\n    51141,\n    51142,\n    51143,\n    51144,\n    51145,\n    51146,\n    51147,\n    51148,\n    51149,\n    51150,\n    51151,\n    51152,\n    51153,\n    51154,\n    51157,\n    51158,\n    51159,\n    51160,\n]\n\nINGAME_HUD_BACKGROUNDS_SET = set(INGAME_HUD_BACKGROUNDS)\n\nASSETS = {\n    '50721': 'hudactions'\n}\n\nTOP_STRIP_PATTERN_CORNERS = (400, 0, 464, 32)\nTOP_STRIP_PATTERN_SEARCH_AREA_CORNERS = (400, 0, 1024, 32)\n\nMID_STRIP_PATTERN_CORNERS = (339, 806, 403, 818)\nMID_STRIP_PATTERN_SEARCH_AREA_CORNERS = (339, 806, 850, 818)\n\nKNOWN_SUBTEX_CORNER_COORDS = [\n    (0, 806, 339, 1024),\n    (850, 806, 1280, 1024),\n    (8, 10, 34, 27),\n    (85, 10, 111, 27),\n    (162, 10, 188, 27),\n    (239, 10, 265, 27),\n    (316, 10, 342, 27),\n]\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/termcolors.py",
    "content": "# Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThe 256 colors for use by the in-game terminal.\n\nProudly determined from an URXVT screenshot.\n\"\"\"\n\nURXVTCOLS = [\n    (0, 0, 0),\n    (205, 0, 0),\n    (0, 205, 0),\n    (205, 205, 0),\n    (0, 0, 205),\n    (205, 0, 205),\n    (0, 205, 205),\n    (229, 229, 229),\n    (77, 77, 77),\n    (255, 0, 0),\n    (0, 255, 0),\n    (255, 255, 0),\n    (0, 0, 255),\n    (255, 0, 255),\n    (0, 255, 255),\n    (255, 255, 255),\n    (0, 0, 0),\n    (0, 0, 95),\n    (0, 0, 135),\n    (0, 0, 175),\n    (0, 0, 215),\n    (0, 0, 255),\n    (0, 95, 0),\n    (0, 95, 95),\n    (0, 95, 135),\n    (0, 95, 175),\n    (0, 95, 215),\n    (0, 95, 255),\n    (0, 135, 0),\n    (0, 135, 95),\n    (0, 135, 135),\n    (0, 135, 175),\n    (0, 135, 215),\n    (0, 135, 255),\n    (0, 175, 0),\n    (0, 175, 95),\n    (0, 175, 135),\n    (0, 175, 175),\n    (0, 175, 215),\n    (0, 175, 255),\n    (0, 215, 0),\n    (0, 215, 95),\n    (0, 215, 135),\n    (0, 215, 175),\n    (0, 215, 215),\n    (0, 215, 255),\n    (0, 255, 0),\n    (0, 255, 95),\n    (0, 255, 135),\n    (0, 255, 175),\n    (0, 255, 215),\n    (0, 255, 255),\n    (95, 0, 0),\n    (95, 0, 95),\n    (95, 0, 135),\n    (95, 0, 175),\n    (95, 0, 215),\n    (95, 0, 255),\n    (95, 95, 0),\n    (95, 95, 95),\n    (95, 95, 135),\n    (95, 95, 175),\n    (95, 95, 215),\n    (95, 95, 255),\n    (95, 135, 0),\n    (95, 135, 95),\n    (95, 135, 135),\n    (95, 135, 175),\n    (95, 135, 215),\n    (95, 135, 255),\n    (95, 175, 0),\n    (95, 175, 95),\n    (95, 175, 135),\n    (95, 175, 175),\n    (95, 175, 215),\n    (95, 175, 255),\n    (95, 215, 0),\n    (95, 215, 95),\n    (95, 215, 135),\n    (95, 215, 175),\n    (95, 215, 215),\n    (95, 215, 255),\n    (95, 255, 0),\n    (95, 255, 95),\n    (95, 255, 135),\n    (95, 255, 175),\n    (95, 255, 215),\n    (95, 255, 255),\n    (135, 0, 0),\n    (135, 0, 95),\n    (135, 0, 135),\n    (135, 0, 175),\n    (135, 0, 215),\n    (135, 0, 255),\n    (135, 95, 0),\n    (135, 95, 95),\n    (135, 95, 135),\n    (135, 95, 175),\n    (135, 95, 215),\n    (135, 95, 255),\n    (135, 135, 0),\n    (135, 135, 95),\n    (135, 135, 135),\n    (135, 135, 175),\n    (135, 135, 215),\n    (135, 135, 255),\n    (135, 175, 0),\n    (135, 175, 95),\n    (135, 175, 135),\n    (135, 175, 175),\n    (135, 175, 215),\n    (135, 175, 255),\n    (135, 215, 0),\n    (135, 215, 95),\n    (135, 215, 135),\n    (135, 215, 175),\n    (135, 215, 215),\n    (135, 215, 255),\n    (135, 255, 0),\n    (135, 255, 95),\n    (135, 255, 135),\n    (135, 255, 175),\n    (135, 255, 215),\n    (135, 255, 255),\n    (175, 0, 0),\n    (175, 0, 95),\n    (175, 0, 135),\n    (175, 0, 175),\n    (175, 0, 215),\n    (175, 0, 255),\n    (175, 95, 0),\n    (175, 95, 95),\n    (175, 95, 135),\n    (175, 95, 175),\n    (175, 95, 215),\n    (175, 95, 255),\n    (175, 135, 0),\n    (175, 135, 95),\n    (175, 135, 135),\n    (175, 135, 175),\n    (175, 135, 215),\n    (175, 135, 255),\n    (175, 175, 0),\n    (175, 175, 95),\n    (175, 175, 135),\n    (175, 175, 175),\n    (175, 175, 215),\n    (175, 175, 255),\n    (175, 215, 0),\n    (175, 215, 95),\n    (175, 215, 135),\n    (175, 215, 175),\n    (175, 215, 215),\n    (175, 215, 255),\n    (175, 255, 0),\n    (175, 255, 95),\n    (175, 255, 135),\n    (175, 255, 175),\n    (175, 255, 215),\n    (175, 255, 255),\n    (215, 0, 0),\n    (215, 0, 95),\n    (215, 0, 135),\n    (215, 0, 175),\n    (215, 0, 215),\n    (215, 0, 255),\n    (215, 95, 0),\n    (215, 95, 95),\n    (215, 95, 135),\n    (215, 95, 175),\n    (215, 95, 215),\n    (215, 95, 255),\n    (215, 135, 0),\n    (215, 135, 95),\n    (215, 135, 135),\n    (215, 135, 175),\n    (215, 135, 215),\n    (215, 135, 255),\n    (215, 175, 0),\n    (215, 175, 95),\n    (215, 175, 135),\n    (215, 175, 175),\n    (215, 175, 215),\n    (215, 175, 255),\n    (215, 215, 0),\n    (215, 215, 95),\n    (215, 215, 135),\n    (215, 215, 175),\n    (215, 215, 215),\n    (215, 215, 255),\n    (215, 255, 0),\n    (215, 255, 95),\n    (215, 255, 135),\n    (215, 255, 175),\n    (215, 255, 215),\n    (215, 255, 255),\n    (255, 0, 0),\n    (255, 0, 95),\n    (255, 0, 135),\n    (255, 0, 175),\n    (255, 0, 215),\n    (255, 0, 255),\n    (255, 95, 0),\n    (255, 95, 95),\n    (255, 95, 135),\n    (255, 95, 175),\n    (255, 95, 215),\n    (255, 95, 255),\n    (255, 135, 0),\n    (255, 135, 95),\n    (255, 135, 135),\n    (255, 135, 175),\n    (255, 135, 215),\n    (255, 135, 255),\n    (255, 175, 0),\n    (255, 175, 95),\n    (255, 175, 135),\n    (255, 175, 175),\n    (255, 175, 215),\n    (255, 175, 255),\n    (255, 215, 0),\n    (255, 215, 95),\n    (255, 215, 135),\n    (255, 215, 175),\n    (255, 215, 215),\n    (255, 215, 255),\n    (255, 255, 0),\n    (255, 255, 95),\n    (255, 255, 135),\n    (255, 255, 175),\n    (255, 255, 215),\n    (255, 255, 255),\n    (8, 8, 8),\n    (18, 18, 18),\n    (28, 28, 28),\n    (38, 38, 38),\n    (48, 48, 48),\n    (58, 58, 58),\n    (68, 68, 68),\n    (78, 78, 78),\n    (88, 88, 88),\n    (98, 98, 98),\n    (108, 108, 108),\n    (118, 118, 118),\n    (128, 128, 128),\n    (138, 138, 138),\n    (148, 148, 148),\n    (158, 158, 158),\n    (168, 168, 168),\n    (178, 178, 178),\n    (188, 188, 188),\n    (198, 198, 198),\n    (208, 208, 208),\n    (218, 218, 218),\n    (228, 228, 228),\n    (238, 238, 238),\n]\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/terrain_tile_size.py",
    "content": "# Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTile size for terrain pieces.\n\"\"\"\n\nTILE_HALFSIZE = {\n    \"x\": 48,\n    \"y\": 24,\n}\n"
  },
  {
    "path": "openage/convert/value_object/read/media/hardcoded/texture.py",
    "content": "# Copyright 2016-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nConstants for texture generation.\n\"\"\"\n\n# The maximum allowed texture dimension.\n# TODO: Maximum allowed dimension needs to\n#       be determined by converter.\nMAX_TEXTURE_DIMENSION = 100000\n\n# Margin between subtextures in atlas to avoid texture bleeding.\nMARGIN = 1\n\n# The aspect ratio of terrain tiles.\nTERRAIN_ASPECT_RATIO = 97 / 94\n"
  },
  {
    "path": "openage/convert/value_object/read/media/langcodes.py",
    "content": "# Copyright 2013-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTranslates the language codes in PE files or text resources to their\nstring equivalent.\n\"\"\"\n\nLANGCODES_AOC = {\n    1: 'ar',\n    2: 'bg',\n    3: 'ca',\n    4: 'zh_Hans',\n    5: 'cs',\n    6: 'da',\n    7: 'de',\n    8: 'el',\n    9: 'en',\n    10: 'es',\n    11: 'fi',\n    12: 'fr',\n    13: 'he',\n    14: 'hu',\n    15: 'is',\n    16: 'it',\n    17: 'ja',\n    18: 'ko',\n    19: 'nl',\n    20: 'no',\n    21: 'pl',\n    22: 'pt',\n    23: 'rm',\n    24: 'ro',\n    25: 'ru',\n    26: 'bs',\n    27: 'sk',\n    28: 'sq',\n    29: 'sv',\n    30: 'th',\n    31: 'tr',\n    32: 'ur',\n    33: 'id',\n    34: 'uk',\n    35: 'be',\n    36: 'sl',\n    37: 'et',\n    38: 'lv',\n    39: 'lt',\n    40: 'tg',\n    41: 'fa',\n    42: 'vi',\n    43: 'hy',\n    44: 'az',\n    45: 'eu',\n    46: 'dsb',\n    47: 'mk',\n    48: 'st',\n    49: 'ts',\n    50: 'tn',\n    51: 've',\n    52: 'xh',\n    53: 'zu',\n    54: 'af',\n    55: 'ka',\n    56: 'fo',\n    57: 'hi',\n    58: 'mt',\n    59: 'se',\n    60: 'ga',\n    61: 'yi',\n    62: 'ms',\n    63: 'kk',\n    64: 'ky',\n    65: 'sw',\n    66: 'tk',\n    67: 'uz',\n    68: 'tt',\n    69: 'bn',\n    70: 'pa',\n    71: 'gu',\n    72: 'or',\n    73: 'ta',\n    74: 'te',\n    75: 'kn',\n    76: 'ml',\n    77: 'as',\n    78: 'mr',\n    79: 'sa',\n    80: 'mn',\n    81: 'bo',\n    82: 'cy',\n    83: 'km',\n    84: 'lo',\n    85: 'my',\n    86: 'gl',\n    87: 'kok',\n    88: 'mni',\n    89: 'sd',\n    90: 'syr',\n    91: 'si',\n    92: 'chr',\n    93: 'iu',\n    94: 'am',\n    95: 'tzm',\n    96: 'ks',\n    97: 'ne',\n    98: 'fy',\n    99: 'ps',\n    100: 'fil',\n    101: 'dv',\n    102: 'bin',\n    103: 'ff',\n    104: 'ha',\n    105: 'ibb',\n    106: 'yo',\n    107: 'quz',\n    108: 'nso',\n    109: 'ba',\n    110: 'lb',\n    111: 'kl',\n    112: 'ig',\n    113: 'kr',\n    114: 'om',\n    115: 'ti',\n    116: 'gn',\n    117: 'haw',\n    118: 'la',\n    119: 'so',\n    120: 'ii',\n    121: 'pap',\n    122: 'arn',\n    124: 'moh',\n    126: 'br',\n    128: 'ug',\n    129: 'mi',\n    130: 'oc',\n    131: 'co',\n    132: 'gsw',\n    133: 'sah',\n    134: 'qut',\n    135: 'rw',\n    136: 'wo',\n    140: 'prs',\n    145: 'gd',\n    146: 'ku',\n    1025: 'ar_SA',\n    1026: 'bg_BG',\n    1027: 'ca_ES',\n    1028: 'zh_TW',\n    1029: 'cs_CZ',\n    1030: 'da_DK',\n    1031: 'de_DE',\n    1032: 'el_GR',\n    1033: 'en_US',\n    1034: 'es_ES_tradnl',\n    1035: 'fi_FI',\n    1036: 'fr_FR',\n    1037: 'he_IL',\n    1038: 'hu_HU',\n    1039: 'is_IS',\n    1040: 'it_IT',\n    1041: 'ja_JP',\n    1042: 'ko_KR',\n    1043: 'nl_NL',\n    1044: 'nb_NO',\n    1045: 'pl_PL',\n    1046: 'pt_BR',\n    1047: 'rm_CH',\n    1048: 'ro_RO',\n    1049: 'ru_RU',\n    1050: 'hr_HR',\n    1051: 'sk_SK',\n    1052: 'sq_AL',\n    1053: 'sv_SE',\n    1054: 'th_TH',\n    1055: 'tr_TR',\n    1056: 'ur_PK',\n    1057: 'id_ID',\n    1058: 'uk_UA',\n    1059: 'be_BY',\n    1060: 'sl_SI',\n    1061: 'et_EE',\n    1062: 'lv_LV',\n    1063: 'lt_LT',\n    1064: 'tg_Cyrl_TJ',\n    1065: 'fa_IR',\n    1066: 'vi_VN',\n    1067: 'hy_AM',\n    1068: 'az_Latn_AZ',\n    1069: 'eu_ES',\n    1070: 'hsb_DE',\n    1071: 'mk_MK',\n    1072: 'st_ZA',\n    1073: 'ts_ZA',\n    1074: 'tn_ZA',\n    1075: 've_ZA',\n    1076: 'xh_ZA',\n    1077: 'zu_ZA',\n    1078: 'af_ZA',\n    1079: 'ka_GE',\n    1080: 'fo_FO',\n    1081: 'hi_IN',\n    1082: 'mt_MT',\n    1083: 'se_NO',\n    1085: 'yi_Hebr',\n    1086: 'ms_MY',\n    1087: 'kk_KZ',\n    1088: 'ky_KG',\n    1089: 'sw_KE',\n    1090: 'tk_TM',\n    1091: 'uz_Latn_UZ',\n    1092: 'tt_RU',\n    1093: 'bn_IN',\n    1094: 'pa_IN',\n    1095: 'gu_IN',\n    1096: 'or_IN',\n    1097: 'ta_IN',\n    1098: 'te_IN',\n    1099: 'kn_IN',\n    1100: 'ml_IN',\n    1101: 'as_IN',\n    1102: 'mr_IN',\n    1103: 'sa_IN',\n    1104: 'mn_MN',\n    1105: 'bo_CN',\n    1106: 'cy_GB',\n    1107: 'km_KH',\n    1108: 'lo_LA',\n    1109: 'my_MM',\n    1110: 'gl_ES',\n    1111: 'kok_IN',\n    1112: 'mni_IN',\n    1113: 'sd_Deva_IN',\n    1114: 'syr_SY',\n    1115: 'si_LK',\n    1116: 'chr_Cher_US',\n    1117: 'iu_Cans_CA',\n    1118: 'am_ET',\n    1119: 'tzm_Arab_MA',\n    1120: 'ks_Arab',\n    1121: 'ne_NP',\n    1122: 'fy_NL',\n    1123: 'ps_AF',\n    1124: 'fil_PH',\n    1125: 'dv_MV',\n    1126: 'bin_NG',\n    1127: 'fuv_NG',\n    1128: 'ha_Latn_NG',\n    1129: 'ibb_NG',\n    1130: 'yo_NG',\n    1131: 'quz_BO',\n    1132: 'nso_ZA',\n    1133: 'ba_RU',\n    1134: 'lb_LU',\n    1135: 'kl_GL',\n    1136: 'ig_NG',\n    1137: 'kr_NG',\n    1138: 'om_ET',\n    1139: 'ti_ET',\n    1140: 'gn_PY',\n    1141: 'haw_US',\n    1142: 'la_Latn',\n    1143: 'so_SO',\n    1144: 'ii_CN',\n    1145: 'pap_029',\n    1146: 'arn_CL',\n    1148: 'moh_CA',\n    1150: 'br_FR',\n    1152: 'ug_CN',\n    1153: 'mi_NZ',\n    1154: 'oc_FR',\n    1155: 'co_FR',\n    1156: 'gsw_FR',\n    1157: 'sah_RU',\n    1158: 'qut_GT',\n    1159: 'rw_RW',\n    1160: 'wo_SN',\n    1164: 'prs_AF',\n    1165: 'plt_MG',\n    1166: 'zh_yue_HK',\n    1167: 'tdd_Tale_CN',\n    1168: 'khb_Talu_CN',\n    1169: 'gd_GB',\n    1170: 'ku_Arab_IQ',\n    1171: 'quc_CO',\n    1281: 'qps_ploc',\n    1534: 'qps_ploca',\n    2049: 'ar_IQ',\n    2051: 'ca_ES_valencia',\n    2052: 'zh_CN',\n    2055: 'de_CH',\n    2057: 'en_GB',\n    2058: 'es_MX',\n    2060: 'fr_BE',\n    2064: 'it_CH',\n    2065: 'ja_Ploc_JP',\n    2067: 'nl_BE',\n    2068: 'nn_NO',\n    2070: 'pt_PT',\n    2072: 'ro_MD',\n    2073: 'ru_MD',\n    2074: 'sr_Latn_CS',\n    2077: 'sv_FI',\n    2080: 'ur_IN',\n    2092: 'az_Cyrl_AZ',\n    2094: 'dsb_DE',\n    2098: 'tn_BW',\n    2107: 'se_SE',\n    2108: 'ga_IE',\n    2110: 'ms_BN',\n    2115: 'uz_Cyrl_UZ',\n    2117: 'bn_BD',\n    2118: 'pa_Arab_PK',\n    2121: 'ta_LK',\n    2128: 'mn_Mong_CN',\n    2129: 'bo_BT',\n    2137: 'sd_Arab_PK',\n    2141: 'iu_Latn_CA',\n    2143: 'tzm_Latn_DZ',\n    2144: 'ks_Deva',\n    2145: 'ne_IN',\n    2151: 'ff_Latn_SN',\n    2155: 'quz_EC',\n    2163: 'ti_ER',\n    2559: 'qps_plocm',\n    3073: 'ar_EG',\n    3076: 'zh_HK',\n    3079: 'de_AT',\n    3081: 'en_AU',\n    3082: 'es_ES',\n    3084: 'fr_CA',\n    3098: 'sr_Cyrl_CS',\n    3131: 'se_FI',\n    3152: 'mn_Mong_MN',\n    3167: 'tmz_MA',\n    3179: 'quz_PE',\n    4097: 'ar_LY',\n    4100: 'zh_SG',\n    4103: 'de_LU',\n    4105: 'en_CA',\n    4106: 'es_GT',\n    4108: 'fr_CH',\n    4122: 'hr_BA',\n    4155: 'smj_NO',\n    4191: 'tzm_Tfng_MA',\n    5121: 'ar_DZ',\n    5124: 'zh_MO',\n    5127: 'de_LI',\n    5129: 'en_NZ',\n    5130: 'es_CR',\n    5132: 'fr_LU',\n    5146: 'bs_Latn_BA',\n    5179: 'smj_SE',\n    6145: 'ar_MA',\n    6153: 'en_IE',\n    6154: 'es_PA',\n    6156: 'fr_MC',\n    6170: 'sr_Latn_BA',\n    6203: 'sma_NO',\n    7169: 'ar_TN',\n    7177: 'en_ZA',\n    7178: 'es_DO',\n    7194: 'sr_Cyrl_BA',\n    7227: 'sma_SE',\n    8193: 'ar_OM',\n    8201: 'en_JM',\n    8202: 'es_VE',\n    8204: 'fr_RE',\n    8218: 'bs_Cyrl_BA',\n    8251: 'sms_FI',\n    9217: 'ar_YE',\n    9225: 'en_029',\n    9226: 'es_CO',\n    9228: 'fr_CD',\n    9242: 'sr_Latn_RS',\n    9275: 'smn_FI',\n    10241: 'ar_SY',\n    10249: 'en_BZ',\n    10250: 'es_PE',\n    10252: 'fr_SN',\n    10266: 'sr_Cyrl_RS',\n    11265: 'ar_JO',\n    11273: 'en_TT',\n    11274: 'es_AR',\n    11276: 'fr_CM',\n    11290: 'sr_Latn_ME',\n    12289: 'ar_LB',\n    12297: 'en_ZW',\n    12298: 'es_EC',\n    12300: 'fr_CI',\n    12314: 'sr_Cyrl_ME',\n    13313: 'ar_KW',\n    13321: 'en_PH',\n    13322: 'es_CL',\n    13324: 'fr_ML',\n    14337: 'ar_AE',\n    14345: 'en_ID',\n    14346: 'es_UY',\n    14348: 'fr_MA',\n    15361: 'ar_BH',\n    15369: 'en_HK',\n    15370: 'es_PY',\n    15372: 'fr_HT',\n    16385: 'ar_QA',\n    16393: 'en_IN',\n    16394: 'es_BO',\n    17409: 'ar_Ploc_SA',\n    17417: 'en_MY',\n    17418: 'es_SV',\n    18433: 'ar_145',\n    18441: 'en_SG',\n    18442: 'es_HN',\n    19465: 'en_AE',\n    19466: 'es_NI',\n    20489: 'en_BH',\n    20490: 'es_PR',\n    21513: 'en_EG',\n    21514: 'es_US',\n    22537: 'en_JO',\n    22538: 'es_419',\n    23561: 'en_KW',\n    24585: 'en_TR',\n    25609: 'en_YE',\n    25626: 'bs_Cyrl',\n    26650: 'bs_Latn',\n    27674: 'sr_Cyrl',\n    28698: 'sr_Latn',\n    28731: 'smn',\n    29740: 'az_Cyrl',\n    29755: 'sms',\n    30724: 'zh',\n    30740: 'nn',\n    30746: 'bs',\n    30764: 'az_Latn',\n    30779: 'sma',\n    30787: 'uz_Cyrl',\n    30800: 'mn_Cyrl',\n    30813: 'iu_Cans',\n    30815: 'tzm_Tfng',\n    31748: 'zh_Hant',\n    31764: 'nb',\n    31770: 'sr',\n    31784: 'tg_Cyrl',\n    31790: 'dsb',\n    31803: 'smj',\n    31811: 'uz_Latn',\n    31814: 'pa_Arab',\n    31824: 'mn_Mong',\n    31833: 'sd_Arab',\n    31836: 'chr_Cher',\n    31837: 'iu_Latn',\n    31839: 'tzm_Latn',\n    31847: 'ff_Latn',\n    31848: 'ha_Latn',\n    31890: 'ku_Arab',\n    65663: 'x_IV_mathan',\n    66567: 'de_DE_phoneb',\n    66574: 'hu_HU_tchncl',\n    66615: 'ka_GE_modern',\n    133124: 'zh_CN_stroke',\n    135172: 'zh_SG_stroke',\n    136196: 'zh_MO_stroke',\n    197636: 'zh_TW_pronun',\n    263172: 'zh_TW_radstr',\n    263185: 'ja_JP_radstr',\n    265220: 'zh_HK_radstr',\n    267268: 'zh_MO_radstr',\n}\n\nLANGCODES_SWGB = {\n    1041: 'de_DE',\n}\n\nLANGCODES_HD = {\n    'br': 'pt_BR',\n    'cn': 'zh_CN',\n    'de': 'de_DE',\n    'en': 'en_US',\n    'es': 'es_ES',\n    'fr': 'fr_FR',\n    'it': 'it_IT',\n    'ja': 'ja_JP',\n    'ko': 'ko_KR',\n    'nl': 'nl_NL',\n    'ru': 'ru_RU',\n}\n\nLANGCODES_DE1 = {\n    'br': 'pt_BR',\n    'de': 'de_DE',\n    'en': 'en_US',\n    'es': 'es_ES',\n    'fr': 'fr_FR',\n    'hi': 'hi_IN',\n    'it': 'it_IT',\n    'jp': 'ja_JP',\n    'ko': 'ko_KR',\n    'mx': 'es_MX',\n    'ru': 'ru_RU',\n    'vi': 'vi',\n    'zhs': 'zh_CN',\n    'zht': 'zh_TW',\n}\n\nLANGCODES_DE2 = {\n    'br': 'pt_BR',\n    'de': 'de_DE',\n    'en': 'en_US',\n    'es': 'es_ES',\n    'fr': 'fr_FR',\n    'hi': 'hi_IN',\n    'it': 'it_IT',\n    'jp': 'ja_JP',\n    'ko': 'ko_KR',\n    'ms': 'ms',\n    'mx': 'es_MX',\n    'ru': 'ru_RU',\n    'tr': 'tr',\n    'tw': 'tw',\n    'vi': 'vi',\n    'zh': 'zh_CN',\n}\n"
  },
  {
    "path": "openage/convert/value_object/read/media/pefile.py",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides PEFile, a class for reading MS portable executable files.\n\nPrimary doc sources:\nhttp://www.csn.ul.ie/~caolan/pub/winresdump/winresdump/doc/pefile2.html\nhttp://en.wikibooks.org/wiki/X86_Disassembly/Windows_Executable_Files\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom .....util.filelike.stream import StreamFragment\nfrom .....util.struct import NamedStruct\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media.peresource import PEResources\n    from openage.util.fslike.wrapper import GuardedFile\n\n\nclass PEDOSHeader(NamedStruct):\n    \"\"\"\n    The (legacy) DOS-compatible PE header.\n\n    In all modern PE files, only the 'lfanew' pointer is relevant.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    signature            = \"2s\"   # always 'MZ'\n    bytes_lastpage       = \"H\"    # bytes on the last page of file\n    count_pages          = \"H\"    # pages in file\n    crlc                 = \"H\"    # relocations\n    cparhdr              = \"H\"    # size of header in paragraphs\n    minalloc             = \"H\"    # minimum extra paragraphs needed\n    maxalloc             = \"H\"    # maximum extra paragraphs needed\n    initial_ss           = \"H\"    # initial (relative) SS value\n    initial_sp           = \"H\"    # initial sp value\n    checksum             = \"H\"    # checksum\n    initial_ip           = \"H\"    # initial IP value\n    initial_cs           = \"H\"    # initial (relative) CS value\n    lfarlc               = \"H\"    # file address of relocation table\n    ovno                 = \"H\"    # overlay number\n    reserved0            = \"8s\"   # reserved block #0\n    oemid                = \"H\"    # OEM identifier (for oeminfo)\n    oeminfo              = \"H\"    # OEM information; oemid-specific\n    reserved1            = \"20s\"  # reserved block #1\n    coffheaderpos        = \"I\"    # address of new EXE header\n\n\nclass PECOFFHeader(NamedStruct):\n    \"\"\"\n    The new (win32) PE and object file header.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    signature            = \"4s\"   # always 'PE\\0\\0'\n    machine              = \"H\"    # architecture; 332 means x86\n    number_of_sections   = \"H\"\n    time_stamp           = \"I\"\n    symbol_table_ptr     = \"I\"\n    symbol_count         = \"I\"\n    opt_header_size      = \"H\"\n    characteristics      = \"H\"    # 2: exe; 512: non-relocatable; 8192: dll\n\n\nclass PEOptionalHeader(NamedStruct):\n    \"\"\"\n    This \"optional\" header is required for linked files (but not object files).\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    signature            = \"H\"    # 267: x86; 523: x86_64\n    major_linker_ver     = \"B\"\n    minor_linker_ver     = \"B\"\n    size_of_code         = \"I\"\n    size_of_data         = \"I\"\n    size_of_bss          = \"I\"\n    entry_point_addr     = \"I\"    # RVA of code entry point\n    base_of_code         = \"I\"\n    base_of_data         = \"I\"\n    image_base           = \"I\"    # preferred memory location\n    section_alignment    = \"I\"\n    file_alignment       = \"I\"\n    major_os_ver         = \"H\"\n    minor_os_ver         = \"H\"\n    major_img_ver        = \"H\"\n    minor_img_ver        = \"H\"\n    major_subsys_ver     = \"H\"\n    minor_subsys_ver     = \"H\"\n    reserved             = \"I\"\n    size_of_image        = \"I\"\n    size_of_headers      = \"I\"\n    checksum             = \"I\"\n\n    # the windows subsystem to run this executable.\n    # 1: native, 2: GUI, 3: non-GUI, 5: OS/2, 7: POSIX\n    subsystem            = \"H\"\n\n    dll_characteristics  = \"H\"    # some flags we're not interested in.\n    stack_reserve_size   = \"I\"\n    stack_commit_size    = \"I\"\n    heap_reserve_size    = \"I\"\n    heap_commit_size     = \"I\"\n    loader_flags         = \"I\"    # we're not interested in those either.\n\n    # describes the number of data directory headers that follow this header.\n    # always 16.\n    data_directory_count = \"I\"\n\n    # written manually at some later point\n    data_directories     = None\n\n\nclass PEDataDirectory(NamedStruct):\n    \"\"\"\n    Provides the locations of various metadata structures,\n    which are used to set up the execution environment.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    rva                = \"I\"\n    size               = \"I\"\n\n\nclass PESection(NamedStruct):\n    \"\"\"\n    Describes a section in a PE file (like an ELF section).\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    name               = \"8s\"    # first char must be '.'.\n    virtual_size       = \"I\"     # size in memory\n    virtual_address    = \"I\"     # RVA where the section will be loaded.\n    size_on_disk       = \"I\"\n    file_offset        = \"I\"\n    reserved           = \"12s\"\n    flags              = \"I\"     # some flags we don't care about\n\n\nclass PEFile:\n    \"\"\"\n    Reads Microsoft PE files.\n\n    The constructor takes a file-like object.\n    \"\"\"\n\n    def __init__(self, fileobj: GuardedFile):\n        # read DOS header\n        doshdr = PEDOSHeader.read(fileobj)\n        if doshdr.signature != b'MZ':\n            raise SyntaxError(\"not a PE file\")\n\n        # read COFF header\n        fileobj.seek(doshdr.coffheaderpos)\n        coffhdr = PECOFFHeader.read(fileobj)\n\n        if coffhdr.signature != b'PE\\0\\0':\n            raise SyntaxError(\"not a Win32 PE file\")\n\n        if coffhdr.opt_header_size != 224:\n            raise SyntaxError(\"unknown optional header size\")\n\n        # read optional header\n        opthdr = PEOptionalHeader.read(fileobj)\n\n        if opthdr.signature not in {267, 523}:\n            raise SyntaxError(\"Not an x86{_64} file\")\n\n        # read data directories\n        opthdr.data_directories = []\n        for _ in range(opthdr.data_directory_count):\n            opthdr.data_directories.append(PEDataDirectory.read(fileobj))\n\n        # read section headers\n        sections: dict[str, tuple] = {}\n\n        for _ in range(coffhdr.number_of_sections):\n            section = PESection.read(fileobj)\n\n            section.name = section.name.decode('ascii').rstrip('\\0')\n            if not section.name.startswith('.'):\n                raise SyntaxError(\"Invalid section name: \" + section.name)\n\n            sections[section.name] = section\n\n        # store all read header info\n        self.fileobj = fileobj\n\n        self.doshdr = doshdr\n        self.coffhdr = coffhdr\n        self.opthdr = opthdr\n\n        self.sections = sections\n\n    def open_section(self, section_name: str) -> StreamFragment:\n        \"\"\"\n        Returns a tuple of data, va for the given section.\n\n        data is a file-like object (StreamFragment),\n        and va is the RVA of the section start.\n        \"\"\"\n        if section_name not in self.sections:\n            raise SyntaxError(\"no such section in PE file: \" + section_name)\n\n        section = self.sections[section_name]\n\n        return StreamFragment(\n            self.fileobj,\n            section.file_offset,\n            section.virtual_size), section.virtual_address\n\n    def resources(self) -> PEResources:\n        \"\"\"\n        Returns a PEResources object for self.\n        \"\"\"\n        from .peresource import PEResources\n        return PEResources(self)\n"
  },
  {
    "path": "openage/convert/value_object/read/media/peresource.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides PEResources, which reads the resource section from a PEFile.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom collections import defaultdict\n\n\nfrom .....util.filelike.stream import StreamFragment\nfrom .....util.struct import NamedStruct\nfrom .langcodes import LANGCODES_AOC\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.media.pefile import PEFile\n    from openage.util.fslike.wrapper import GuardedFile\n\n\n# types for id in resource directory root node\nRESOURCE_TYPES = {\n    0: 'unknown',\n    1: 'cursor',\n    2: 'bitmap',\n    3: 'icon',\n    4: 'menu',\n    5: 'dialog',\n    6: 'string',\n    7: 'fontdir',\n    8: 'font',\n    9: 'accelerator',\n    10: 'rcdata',\n    11: 'messagetable',\n    12: 'group_cursor',\n    14: 'group_icon',\n    16: 'version',\n    17: 'dlginclude',\n    19: 'plugplay',\n    20: 'vxd',\n    21: 'anicursor',\n    22: 'aniicon',\n    23: 'html',\n    24: 'manifest'\n}\n\n# reverse look-up table\nRESOURCE_IDS = {value: key for key, value in RESOURCE_TYPES.items()}\n\n# string resource-specific constants\nSTRINGTABLE_SIZE = 16\n\n\nclass ResourceDirectory(NamedStruct):\n    \"\"\"\n    Resource directory header.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    characteristics       = \"I\"     # some irrelevant flags.\n    timestamp             = \"I\"     # an irrelevant timestamp.\n    version_major         = \"H\"\n    version_minor         = \"H\"\n\n    # the number of named directory entries that follow immediately after.\n    named_entry_count     = \"H\"\n\n    # the number of id directory entries that follow immediately after.\n    id_entry_count        = \"H\"\n\n    subdirs               = None\n    leaves                = None\n\n\nclass ResourceDirectoryEntry(NamedStruct):\n    \"\"\"\n    these follow immediately after the directory.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    # if the high bit is set, the lower 31 bits point to the (utf-16-le) name.\n    name                  = \"I\"\n\n    # if the high bit is set, the lower 31 bits point to an other directory.\n    # if the high bit is cleared, they point to a leaf node.\n    data                  = \"I\"\n\n    # set manually later\n    name_is_str           = None\n    is_subdir             = None\n\n\nclass ResourceLeaf(NamedStruct):\n    \"\"\"\n    header for a leaf node in the resource tree.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    data_ptr              = \"I\"\n    data_size             = \"I\"\n\n    codepage              = \"I\"     # code page id of the data.\n    reserved              = \"I\"     # 0\n\n    # filled in later\n    fileobj               = None    # used for open\n\n    def open(self) -> StreamFragment:\n        \"\"\"\n        Returns a file-like object for this resource.\n        \"\"\"\n        self.fileobj.seek(self.data_ptr)\n        return StreamFragment(self.fileobj, self.data_ptr, self.data_size)\n\n\nclass StringLiteral(NamedStruct):\n    \"\"\"\n    A simple length-prefixed little-endian utf-16 string.\n    \"\"\"\n\n    # pylint: disable=too-few-public-methods\n\n    endianness = \"<\"\n\n    length             = \"H\"\n\n    # filled later by read_content()\n    value: str         = None\n\n    @classmethod\n    def readall(cls, fileobj: GuardedFile) -> StringLiteral:\n        \"\"\"\n        In addition to the static data, reads the string.\n        \"\"\"\n        result = cls.read(fileobj)\n        result.value = fileobj.read(result.length * 2).decode('utf-16-le')\n        return result\n\n\nclass PEResources:\n    \"\"\"\n    .rsrc section of a PE file\n\n    The constructor takes a PEFile object.\n    \"\"\"\n\n    def __init__(self, pefile: PEFile):\n        self.data, self.datava = pefile.open_section('.rsrc')\n\n        # self.data is at position 0 (i.e. it points at the root directory).\n        self.rootdir = self.read_directory()\n        self.strings = self.read_strings()\n\n    def __getitem__(self, key):\n        return self.rootdir[key]\n\n    def read_directory(self) -> dict[str, typing.Any]:\n        \"\"\"\n        reads the directory that's currently pointed at by self.data.\n\n        descends recursively for subdirectories.\n\n        returns a ResourceDirectory object with both subdirs and leaves filled\n        in.\n        \"\"\"\n        directory = ResourceDirectory.read(self.data)\n\n        result = {}\n\n        # read directory entries\n        entry_count = directory.named_entry_count + directory.id_entry_count\n        for idx in range(entry_count):\n            entry = ResourceDirectoryEntry.read(self.data)\n\n            # check the entry name\n            entry.name_is_str = bool(entry.name & (1 << 31))\n\n            if entry.name_is_str and idx >= directory.named_entry_count:\n                raise SyntaxError(\"expected an id entry, but got a str entry\")\n\n            if not entry.name_is_str and idx < directory.named_entry_count:\n                raise SyntaxError(\"expected a str entry, but got an id entry\")\n\n            # read the entry name string, if needed\n            if entry.name_is_str:\n                data_pos = self.data.tell()\n                self.data.seek(entry.name - (1 << 31))\n                entry.name = StringLiteral.readall(self.data).value\n                self.data.seek(data_pos)\n\n            # read the struct pointed at by entry.data\n            entry.is_subdir = bool(entry.data & (1 << 31))\n            data_pos = self.data.tell()\n            if entry.is_subdir:\n                # recursively descend\n                self.data.seek(entry.data - (1 << 31))\n                result[entry.name] = self.read_directory()\n            else:\n                # read leaf metadata\n                self.data.seek(entry.data)\n                leaf = ResourceLeaf.read(self.data)\n                leaf.data_ptr -= self.datava\n                leaf.fileobj = self.data\n                result[entry.name] = leaf\n            self.data.seek(data_pos)\n\n        return result\n\n    def read_strings(self) -> dict[str, dict[int, str]]:\n        \"\"\"\n        reads all ressource strings from self.root;\n        returns a dict of dicts: {languageid: {stringid: str}}\n        \"\"\"\n        result = defaultdict(lambda: {})\n\n        try:\n            string_dir = self.rootdir[RESOURCE_IDS['string']]\n        except KeyError:\n            return result\n\n        # strings are grouped in tables of 16 each.\n        for table_id, table_dir in string_dir.items():\n            # counting from 1. great idea, Microsoft.\n            base_string_id = (table_id - 1) * STRINGTABLE_SIZE\n\n            # each table has leafs for one or more languages\n            for lang_id, string_table in table_dir.items():\n                langcode = LANGCODES_AOC[lang_id]\n                string_table_resource = string_table.open()\n                for idx in range(STRINGTABLE_SIZE):\n                    string = StringLiteral.readall(string_table_resource).value\n                    if string:\n                        result[langcode][base_string_id + idx] = string\n\n                if sum(string_table_resource.read()) > 0:\n                    raise SyntaxError(\"string table invalid: the padding \"\n                                      \"contains data.\")\n\n        return result\n"
  },
  {
    "path": "openage/convert/value_object/read/media/sld.pyx",
    "content": "# Copyright 2022-2023 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n\nfrom enum import Enum\nimport numpy\nfrom struct import Struct, unpack_from\n\nfrom .....log import spam, dbg\n\ncimport cython\ncimport numpy\n\nfrom libc.stdint cimport uint8_t\nfrom libcpp cimport bool\nfrom libcpp.pair cimport pair\nfrom libcpp.vector cimport vector\n\n# SLD files have little endian byte order\nendianness = \"< \"\n\ncdef struct pixel:\n    uint8_t r\n    uint8_t g\n    uint8_t b\n    uint8_t a\n\n\nclass SLDLayerType(Enum):\n    \"\"\"\n    SLD layer types.\n    \"\"\"\n    MAIN        = \"main\"\n    SHADOW      = \"shadow\"\n    OUTLINE     = \"outline\"\n    DAMAGE      = \"damage\"\n    PLAYERCOLOR = \"playercolor\"\n\n\ncdef public dict LAYER_TYPES = {\n    0: SLDLayerType.MAIN,\n    1: SLDLayerType.SHADOW,\n    2: SLDLayerType.OUTLINE,\n    3: SLDLayerType.DAMAGE,\n    4: SLDLayerType.PLAYERCOLOR,\n}\n\n\ncdef class SLD:\n    \"\"\"\n    Class for reading/converting compressed SLD files (delivered\n    with AoE2:DE since version 66692 by default).\n    \"\"\"\n\n    # struct sld_header {\n    #   char[4]        signature;\n    #   unsigned short version;\n    #   unsigned short frame_count;\n    #   unsigned short unknown1;\n    #   unsigned short unknown2;\n    #   unsigned int   unknown3;\n    # };\n    sld_header = Struct(endianness + \"4s 4H I\")\n\n    # struct sld_frame_header {\n    #   unsigned short canvas_width;\n    #   unsigned short canvas_height;\n    #   unsigned short canvas_hotspot_x;\n    #   unsigned short canvas_hotspot_y;\n    #   char           frame_type;\n    #   char           unknown5;\n    #   unsigned short frame_index;\n    # };\n    sld_frame_header = Struct(endianness + \"4H 2B H\")\n\n    # struct sld_layer_length {\n    #   unsigned int length;\n    # };\n    sld_layer_length = Struct(endianness + \"I\")\n\n    # struct sld_layer_header_graphics {\n    #   unsigned short offset_x1;\n    #   unsigned short offset_y1;\n    #   unsigned short offset_x2;\n    #   unsigned short offset_y2;\n    #   char           unknown1;\n    #   char           unknown2;\n    # };\n    sld_layer_header_graphics = Struct(endianness + \"4H 2B\")\n\n    # struct sld_layer_header_mask {\n    #   char           unknown1;\n    #   char           unknown2;\n    # };\n    sld_layer_header_mask = Struct(endianness + \"2B\")\n\n    cdef bytes sld_type\n    cdef public list main_frames\n    cdef public list shadow_frames\n    cdef public list outline_frames\n    cdef public list dmg_mask_frames\n    cdef public list playercolor_mask_frames\n\n    cdef const uint8_t[::1] data\n\n    def __init__(self, data):\n        \"\"\"\n        Read an SLD image file.\n\n        :param data: File content as bytes.\n        :type data: bytes, bytearray\n        \"\"\"\n\n        sld_header = SLD.sld_header.unpack_from(data)\n        self.sld_type = sld_header[0]\n        version, frame_count, _, _, _ = sld_header[1:]\n\n        dbg(\"SLD\")\n        dbg(\" version:     %s\",   version)\n        dbg(\" frame count: %s\",   frame_count)\n\n        # SLD graphic frames are created from several layers\n        self.main_frames = list()\n        self.shadow_frames = list()\n        self.outline_frames = list()\n        self.dmg_mask_frames = list()\n        self.playercolor_mask_frames = list()\n\n        # File bytes\n        self.data = data\n\n        # Reference to previous layer\n        # SLD reuses their pixel data on some occasions\n        cdef (unsigned short, unsigned short) previous_size = (0, 0)\n        cdef (unsigned short, unsigned short) previous_offset = (0, 0)\n        cdef vector[vector[pixel]] *previous_layer = NULL\n        cdef SLDLayer previous_main\n        cdef SLDLayer previous_shadow\n        cdef SLDLayer previous_outline\n        cdef SLDLayer previous_dmg_mask\n        cdef SLDLayer previous_playercolor\n\n        # Header info\n        cdef SLDLayerHeader layer_header\n\n        # Dimensions of main layer (reused for dmg mask and playercolor)\n        cdef unsigned short main_width\n        cdef unsigned short main_height\n        cdef short main_hotspot_x\n        cdef short main_hotspot_y\n\n        # Our position in the file bytes\n        cdef unsigned int current_offset\n\n        spam(SLDLayerHeader.repr_header())\n\n        # SLD files have no offsets, we have to calculate them\n        # from length fields\n        current_offset = SLD.sld_header.size\n        for _ in range(frame_count):\n\n            canvas_width, canvas_height, canvas_hotspot_x, canvas_hotspot_y,\\\n                frame_type , _, frame_index = SLD.sld_frame_header.unpack_from(\n                data, current_offset)\n\n            frame_header = SLDFrameHeader(\n                canvas_width,\n                canvas_height,\n                canvas_hotspot_x,\n                canvas_hotspot_y,\n                frame_type,\n                frame_index\n            )\n\n            current_offset += SLD.sld_frame_header.size\n\n            layer_types = []\n\n            if frame_type & 0x01:\n                layer_types.append(SLDLayerType.MAIN)\n\n            if frame_type & 0x02:\n                layer_types.append(SLDLayerType.SHADOW)\n\n            if frame_type & 0x04:\n                layer_types.append(SLDLayerType.OUTLINE)\n\n            if frame_type & 0x08:\n                layer_types.append(SLDLayerType.DAMAGE)\n\n            if frame_type & 0x10:\n                layer_types.append(SLDLayerType.PLAYERCOLOR)\n\n            main_width = 0\n            main_height = 0\n            main_hotspot_x = 0\n            main_hotspot_y = 0\n            for layer_type in layer_types:\n                layer_length = SLD.sld_layer_length.unpack_from(data, current_offset)[0]\n                start_offset = current_offset\n                current_offset += SLD.sld_layer_length.size\n\n                # Header unpacking\n                if layer_type in (SLDLayerType.MAIN, SLDLayerType.SHADOW):\n                    offset_x1, offset_y1, offset_x2, offset_y2, flag0, flag1 = \\\n                        SLD.sld_layer_header_graphics.unpack_from(data, current_offset)\n\n                    layer_width = offset_x2 - offset_x1\n                    layer_height = offset_y2 - offset_y1\n                    layer_hotspot_x = canvas_hotspot_x - offset_x1\n                    layer_hotspot_y = canvas_hotspot_y - offset_y1\n                    if layer_type is SLDLayerType.MAIN:\n                        main_width = layer_width\n                        main_height = layer_height\n                        main_hotspot_x = layer_hotspot_x\n                        main_hotspot_y = layer_hotspot_y\n\n                    current_offset += SLD.sld_layer_header_graphics.size\n\n                elif layer_type in (SLDLayerType.OUTLINE, ):\n                    # TODO\n                    pass\n\n                elif layer_type in (SLDLayerType.DAMAGE, SLDLayerType.PLAYERCOLOR):\n                    flag0, flag1 = SLD.sld_layer_header_mask.unpack_from(data, current_offset)\n\n                    layer_width = main_width\n                    layer_height = main_height\n                    layer_hotspot_x = main_hotspot_x\n                    layer_hotspot_y = main_hotspot_y\n\n                    current_offset += SLD.sld_layer_header_mask.size\n\n                # Command array\n                command_array_size = Struct(\"< H\").unpack_from(data, current_offset)[0]\n                current_offset += 2\n                command_array_offset = current_offset\n\n                # Compressed data block\n                current_offset += 2 * command_array_size\n                compressed_data_offset = current_offset\n\n                layer_header = SLDLayerHeader(\n                    layer_type,\n                    frame_type,\n                    offset_x1, offset_y1,\n                    layer_width, layer_height,\n                    layer_hotspot_x, layer_hotspot_y,\n                    command_array_size,\n                    command_array_offset,\n                    compressed_data_offset\n                )\n\n                if layer_type is SLDLayerType.MAIN:\n                    layer_def = SLDLayerBC1(frame_header, layer_header)\n                    self.main_frames.append(layer_def)\n\n                    if flag0 & 0x80 and frame_index > 0:\n                        previous = previous_main\n                        previous_layer = previous.get_pcolor()\n                        previous_size = previous.layer_info.size\n                        previous_offset = previous.layer_info.offset\n\n                        layer_def.set_previous_layer(\n                            previous_size[0],\n                            previous_size[1],\n                            previous_offset[0],\n                            previous_offset[1],\n                            previous_layer\n                        )\n\n                    previous_main = layer_def\n\n                elif layer_type is SLDLayerType.SHADOW:\n                    layer_def = SLDLayerBC4(frame_header, layer_header)\n                    self.shadow_frames.append(layer_def)\n\n                    if flag0 & 0x80 and frame_index > 0:\n                        previous = previous_shadow\n                        previous_layer = previous.get_pcolor()\n                        previous_size = previous.layer_info.size\n                        previous_offset = previous.layer_info.offset\n\n                        layer_def.set_previous_layer(\n                            previous_size[0],\n                            previous_size[1],\n                            previous_offset[0],\n                            previous_offset[1],\n                            previous_layer\n                        )\n\n                    previous_shadow = layer_def\n\n                elif layer_type is SLDLayerType.OUTLINE:\n                    # TODO\n                    pass\n\n                elif layer_type is SLDLayerType.DAMAGE:\n                    layer_def = SLDLayerBC1(frame_header, layer_header)\n                    self.dmg_mask_frames.append(layer_def)\n\n                    if flag0 & 0x80 and frame_index > 0:\n                        previous = previous_dmg_mask\n                        previous_layer = previous.get_pcolor()\n                        previous_size = previous.layer_info.size\n                        previous_offset = previous.layer_info.offset\n\n                        layer_def.set_previous_layer(\n                            previous_size[0],\n                            previous_size[1],\n                            previous_offset[0],\n                            previous_offset[1],\n                            previous_layer\n                        )\n\n                    previous_dmg_mask = layer_def\n\n                elif layer_type is SLDLayerType.PLAYERCOLOR:\n                    layer_def = SLDLayerBC4(frame_header, layer_header)\n                    self.playercolor_mask_frames.append(layer_def)\n\n                    if flag0 & 0x80 and frame_index > 0:\n                        previous = previous_playercolor\n                        previous_layer = previous.get_pcolor()\n                        previous_size = previous.layer_info.size\n                        previous_offset = previous.layer_info.offset\n\n                        layer_def.set_previous_layer(\n                            previous_size[0],\n                            previous_size[1],\n                            previous_offset[0],\n                            previous_offset[1],\n                            previous_layer\n                        )\n\n                    previous_playercolor = layer_def\n\n                # Jump to next layer offset\n                current_offset = start_offset + layer_length\n                # padding to size % 4\n                current_offset += (4 - current_offset) % 4\n\n    cpdef get_frames(self, layer: int = 0):\n        \"\"\"\n        Get the frames in the SLD.\n\n        :param layer: Position of the layer (see LAYER_TYPES)\n                        - 0 = main graphics\n                        - 1 = shadow graphics\n                        - 2 = ???\n                        - 3 = damage mask\n                        - 4 = playercolor mask\n        :type layer: int\n        \"\"\"\n        cdef list frames\n        cdef SLDLayer layer_def\n\n        layer_type = LAYER_TYPES.get(\n            layer,\n            SLDLayerType.MAIN\n        )\n\n        if layer_type is SLDLayerType.MAIN:\n            frames = self.main_frames\n\n        elif layer_type is SLDLayerType.SHADOW:\n            frames = self.shadow_frames\n\n        elif layer_type is SLDLayerType.OUTLINE:\n            frames = self.outline_frames\n\n        elif layer_type is SLDLayerType.DAMAGE:\n            frames = self.dmg_mask_frames\n\n        elif layer_type is SLDLayerType.PLAYERCOLOR:\n            frames = self.playercolor_mask_frames\n\n        else:\n            frames = []\n\n        for layer_def in frames:\n            layer_def.process_drawing_cmds(\n                self.data,\n                layer_def.layer_info.command_array_size,\n                layer_def.layer_info.command_array_offset,\n                layer_def.layer_info.compressed_data_offset\n            )\n\n        return frames\n\n    def __str__(self):\n        ret = list()\n\n        ret.extend([repr(self), \"\\n\", SLDLayerHeader.repr_header(), \"\\n\"])\n        for frame in self.main_frames:\n            ret.extend([repr(frame), \"\\n\"])\n        return \"\".join(ret)\n\n    def __repr__(self):\n        return f\"{self.sld_type} image<{len(self.main_frames):d} frames>\"\n\n\ncdef class SLDFrameHeader:\n    \"\"\"\n    Header info for an SLD frame.\n    \"\"\"\n    cdef (unsigned short, unsigned short) canvas_size\n    cdef (unsigned short, unsigned short) hotspot\n    cdef unsigned char frame_type\n    cdef unsigned short frame_index\n\n    def __init__(\n        self,\n        width, height,\n        hotspot_x, hotspot_y,\n        frame_type,\n        frame_index\n    ):\n        self.canvas_size = (width, height)\n        self.hotspot = (hotspot_x, hotspot_y)\n\n        self.frame_type = frame_type\n        self.frame_index = frame_index\n\n    @staticmethod\n    def repr_header():\n        return \" index | frame type | width x height |  hotspot  | \"\n\n    def __repr__(self):\n        ret = (\n            \"% 5d | \" % self.frame_index,\n            \"% 10x | \" % self.frame_type,\n            \"% 5d x% 7d | \" % self.size,\n            \"% 3d x% 3d | \" % self.hotspot,\n        )\n        return \"\".join(ret)\n\n\ncdef class SLDLayerHeader:\n    \"\"\"\n    Header info for an SLD layer.\n    \"\"\"\n    cdef (unsigned short, unsigned short) size\n    cdef (unsigned short, unsigned short) offset\n    cdef (short, short) hotspot\n    cdef object layer_type\n    cdef unsigned char frame_type\n    cdef size_t command_array_size\n    cdef unsigned int command_array_offset\n    cdef unsigned int compressed_data_offset\n\n    def __init__(\n        self,\n        layer_type,\n        frame_type,\n        offset_x, offset_y,\n        width, height,\n        hotspot_x, hotspot_y,\n        command_array_size,\n        command_array_offset,\n        compressed_data_offset\n    ):\n        self.size = (width, height)\n        self.offset = (offset_x, offset_y)\n        self.hotspot = (hotspot_x, hotspot_y)\n\n        self.layer_type = layer_type\n        self.frame_type = frame_type\n\n        self.command_array_size = command_array_size\n        self.command_array_offset = command_array_offset\n        self.compressed_data_offset = compressed_data_offset\n\n    @staticmethod\n    def repr_header():\n        return (\n            \"layer type | width x height |  hotspot  | \"\n            \"cmd array size | cmd array offset | data block | \"\n        )\n\n    def __repr__(self):\n        ret = (\n            \"% 11s | \" % self.layer_type.value,\n            \"% 5d x% 7d | \" % self.size,\n            \"% 3d x% 3d | \" % self.hotspot,\n            \"% 14d | \" % self.command_array_size,\n            \"% 16x | \" % self.command_array_offset,\n            \"% 10x | \" % self.compressed_data_offset,\n        )\n        return \"\".join(ret)\n\n\ncdef class SLDLayer:\n    \"\"\"\n    Layer inside the SLD frame.\n    \"\"\"\n    # frame information\n    cdef SLDFrameHeader frame_info\n\n    # layer information\n    cdef SLDLayerHeader layer_info\n\n    # matrix representing the 4x4 blocks and the pixels in the image\n    cdef vector[vector[pixel]] pcolor\n\n    # Previous layer\n    cdef (unsigned short, unsigned short) previous_size\n    cdef (unsigned short, unsigned short) previous_offset\n    cdef vector[vector[pixel]] *previous_layer\n\n    def __init__(self, frame_header, layer_header):\n        \"\"\"\n        SMX layer definition superclass. There can be various types of\n        layers inside an SMX frame.\n\n        :param frame_header: Header definition of the frame.\n        :type frame_header: SLDFrameHeader\n        :param layer_header: Header definition of the layer.\n        :type layer_header: SLDLayerHeader\n        \"\"\"\n        self.frame_info = frame_header\n        self.layer_info = layer_header\n\n        self.pcolor.reserve((self.layer_info.size[0] // 4) * (self.layer_info.size[1] // 4))\n\n        self.previous_size = (0, 0)\n        self.previous_offset = (0, 0)\n        self.previous_layer = NULL\n\n    @cython.boundscheck(False)\n    @cython.wraparound(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   unsigned int cmd_size,\n                                   unsigned int first_cmd_offset,\n                                   unsigned int first_data_offset):\n        \"\"\"\n        Process skip and draw commands from the command array.\n        \"\"\"\n        cdef vector[pixel] transparent_block = vector[pixel](16)\n\n        cdef unsigned char skip_count\n        cdef unsigned char draw_count\n\n        cdef unsigned int cmd_offset = first_cmd_offset\n        cdef unsigned int data_offset = first_data_offset\n        cdef unsigned int block_idx = 0\n\n        for _ in range(cmd_size):\n            skip_count = data_raw[cmd_offset]\n            for _ in range(skip_count):\n                if self.previous_layer == NULL:\n                    self.pcolor.push_back(transparent_block)\n\n                else:\n                    previous_block_idx = get_block_index(\n                        self.layer_info.size[0],\n                        self.previous_size[0],\n                        self.previous_size[1],\n                        self.layer_info.offset[0],\n                        self.layer_info.offset[1],\n                        self.previous_offset[0],\n                        self.previous_offset[1],\n                        block_idx\n                    )\n                    if previous_block_idx >= 0:\n                        prev_block = self.previous_layer.at(previous_block_idx)\n                        self.pcolor.push_back(prev_block)\n\n                    else:\n                        self.pcolor.push_back(transparent_block)\n\n                block_idx += 1\n\n            cmd_offset += 1\n\n            draw_count = data_raw[cmd_offset]\n            for _ in range(draw_count):\n                self.pcolor.push_back(self.decompress_block(data_raw, data_offset))\n                data_offset += 8\n                block_idx += 1\n\n            cmd_offset += 1\n\n        return\n\n    @cython.boundscheck(False)\n    cdef vector[pixel] decompress_block(self,\n                                        const uint8_t[::1] &data_raw,\n                                        Py_ssize_t block_offset):\n        \"\"\"\n        Decompress a 4x4 pixel block.\n        \"\"\"\n        pass\n\n    cdef inline void set_previous_layer(self,\n        unsigned short width,\n        unsigned short height,\n        unsigned short offset_x,\n        unsigned short offset_y,\n        vector[vector[pixel]] *previous\n    ):\n        \"\"\"\n        Set a reference to the previous layer.\n        \"\"\"\n        self.previous_size = (width, height)\n        self.previous_offset = (offset_x, offset_y)\n        self.previous_layer = previous\n\n    cdef inline vector[vector[pixel]] *get_pcolor(self):\n        \"\"\"\n        Get the pixel data for the layer.\n        \"\"\"\n        return &self.pcolor\n\n    def get_picture_data(self):\n        \"\"\"\n        Convert the palette index matrix to a RGBA image.\n\n        :return: Array of RGBA values.\n        :rtype: numpy.ndarray\n        \"\"\"\n        return determine_rgba_matrix(\n            self.pcolor,\n            self.layer_info.size[0],\n            self.layer_info.size[1]\n        )\n\n    def get_hotspot(self):\n        \"\"\"\n        Return the layer's hotspot (the \"center\" of the image)\n        \"\"\"\n        return self.layer_info.hotspot\n\n    def __repr__(self):\n        return repr(self.layer_info)\n\n\ncdef class SLDLayerBC1(SLDLayer):\n    \"\"\"\n    Compressed SLD layer using BC1 block compression.\n    \"\"\"\n    def __init__(self, frame_header, layer_header):\n        super().__init__(frame_header, layer_header)\n\n    @cython.boundscheck(False)\n    cdef inline vector[pixel] decompress_block(self,\n                                               const uint8_t[::1] &data_raw,\n                                               Py_ssize_t block_offset):\n        \"\"\"\n        Decompress a 4x4 pixel block.\n        \"\"\"\n        cdef size_t offset\n        cdef unsigned char byte_val\n        cdef unsigned char index\n        cdef unsigned char mask = 0b0000_0011\n        cdef vector[pixel] block\n        block.reserve(16)\n\n        # Lookup table\n        cdef pixel c0\n        cdef pixel c1\n        cdef pixel c2\n        cdef pixel c3\n\n        cdef unsigned short c0_val = data_raw[block_offset] + (data_raw[block_offset + 1] << 8)\n        cdef unsigned short c1_val = data_raw[block_offset + 2] + (data_raw[block_offset + 3] << 8)\n\n        # Color 0\n        c0.r = (data_raw[block_offset + 1] & 0b1111_1000) >> 3\n        c0.g = ((data_raw[block_offset + 1] & 0b0000_0111) << 3) +\\\n            ((data_raw[block_offset] & 0b1110_0000) >> 5)\n        c0.b = data_raw[block_offset] & 0b0001_1111\n\n        # Expand to RGBA32 color space\n        c0.r *= 8\n        c0.g *= 4\n        c0.b *= 8\n        c0.a = 255\n\n        # Color 1\n        c1.r = (data_raw[block_offset + 3] & 0b1111_1000) >> 3\n        c1.g = ((data_raw[block_offset + 3] & 0b0000_0111) << 3) +\\\n            ((data_raw[block_offset + 2] & 0b1110_0000) >> 5)\n        c1.b = data_raw[block_offset + 2] & 0b0001_1111\n\n        # Expand to RGBA32 color space\n        c1.r *= 8\n        c1.g *= 4\n        c1.b *= 8\n        c1.a = 255\n\n        # Color 2 + 3\n        if c0_val > c1_val:\n            c2.r = (2 * c0.r + c1.r + 1) // 3\n            c2.g = (2 * c0.g + c1.g + 1) // 3\n            c2.b = (2 * c0.b + c1.b + 1) // 3\n            c2.a = 255\n\n            c3.r = (c0.r + 2 * c1.r + 1) // 3\n            c3.g = (c0.g + 2 * c1.g + 1) // 3\n            c3.b = (c0.b + 2 * c1.b + 1) // 3\n            c3.a = 255\n\n        else:\n            c2.r = (c0.r + c1.r) // 2\n            c2.g = (c0.g + c1.g) // 2\n            c2.b = (c0.b + c1.b) // 2\n            c2.a = 255\n\n            c3.r = 0\n            c3.g = 0\n            c3.b = 0\n            c3.a = 0\n\n        # Lookup pixels\n        offset = block_offset + 4\n        for _ in range(4):\n            byte_val = data_raw[offset]\n            for _ in range(4):\n                index = byte_val & mask\n\n                if index == 0b00:\n                    block.push_back(c0)\n\n                elif index == 0b01:\n                    block.push_back(c1)\n\n                elif index == 0b10:\n                    block.push_back(c2)\n\n                elif index == 0b11:\n                    block.push_back(c3)\n\n                byte_val = byte_val >> 2\n\n            offset += 1\n\n        return block\n\n    def get_picture_data(self):\n        \"\"\"\n        Convert the palette index matrix to a RGBA image.\n\n        :return: Array of RGBA values.\n        :rtype: numpy.ndarray\n        \"\"\"\n        return determine_rgba_matrix(\n            self.pcolor,\n            self.layer_info.size[0],\n            self.layer_info.size[1]\n        )\n\n\ncdef class SLDLayerBC4(SLDLayer):\n    \"\"\"\n    Compressed SLD layer using BC4 block compression.\n    \"\"\"\n    def __init__(self, frame_header, layer_header):\n        super().__init__(frame_header, layer_header)\n\n    @cython.boundscheck(False)\n    cdef inline vector[pixel] decompress_block(self,\n                                               const uint8_t[::1] &data_raw,\n                                               Py_ssize_t block_offset):\n        \"\"\"\n        Decompress a 4x4 pixel block.\n        \"\"\"\n        cdef size_t offset\n        cdef unsigned int pixel_indices\n        cdef unsigned char index\n        cdef unsigned char mask = 0b0000_0111\n        cdef vector[pixel] block\n        block.reserve(16)\n\n        # Lookup table\n        cdef pixel c0\n        cdef pixel c1\n        cdef pixel c2\n        cdef pixel c3\n        cdef pixel c4\n        cdef pixel c5\n        cdef pixel c6\n        cdef pixel c7\n\n        # Color 0\n        c0.r = data_raw[block_offset]\n        c0.g = 0\n        c0.b = 0\n        c0.a = 255\n\n        # Color 1\n        c1.r = data_raw[block_offset + 1]\n        c1.g = 0\n        c1.b = 0\n        c1.a = 255\n\n        # Color 2 + 3\n        if c0.r > c1.r:\n            c2.r = (6 * c0.r + 1 * c1.r) // 7\n            c2.g = 0\n            c2.b = 0\n            c2.a = 255\n            c3.r = (5 * c0.r + 2 * c1.r) // 7\n            c3.g = 0\n            c3.b = 0\n            c3.a = 255\n            c4.r = (4 * c0.r + 3 * c1.r) // 7\n            c4.g = 0\n            c4.b = 0\n            c4.a = 255\n            c5.r = (3 * c0.r + 4 * c1.r) // 7\n            c5.g = 0\n            c5.b = 0\n            c5.a = 255\n            c6.r = (2 * c0.r + 5 * c1.r) // 7\n            c6.g = 0\n            c6.b = 0\n            c6.a = 255\n            c7.r = (1 * c0.r + 6 * c1.r) // 7\n            c7.g = 0\n            c7.b = 0\n            c7.a = 255\n\n        else:\n            c2.r = (4 * c0.r + 1 * c1.r) // 5\n            c2.g = 0\n            c2.b = 0\n            c2.a = 255\n            c3.r = (3 * c0.r + 2 * c1.r) // 5\n            c3.g = 0\n            c3.b = 0\n            c3.a = 255\n            c4.r = (2 * c0.r + 3 * c1.r) // 5\n            c4.g = 0\n            c4.b = 0\n            c4.a = 255\n            c5.r = (1 * c0.r + 4 * c1.r) // 5\n            c5.g = 0\n            c5.b = 0\n            c5.a = 255\n            c6.r = 0\n            c6.g = 0\n            c6.b = 0\n            c6.a = 0\n            c7.r = 255\n            c7.g = 0\n            c7.b = 0\n            c7.a = 255\n\n        # Lookup pixels\n        offset = block_offset + 2\n        for _ in range(2):\n            pixel_indices = data_raw[offset] | (data_raw[offset + 1] << 8) | (data_raw[offset + 2] << 16)\n            for _ in range(8):\n                # apply mask 0b111 to get 3 bits for index\n                index = pixel_indices & mask\n\n                if index == 0b000:\n                    block.push_back(c0)\n\n                elif index == 0b001:\n                    block.push_back(c1)\n\n                elif index == 0b010:\n                    block.push_back(c2)\n\n                elif index == 0b011:\n                    block.push_back(c3)\n\n                elif index == 0b100:\n                    block.push_back(c4)\n\n                elif index == 0b101:\n                    block.push_back(c5)\n\n                elif index == 0b110:\n                    block.push_back(c6)\n\n                elif index == 0b111:\n                    block.push_back(c7)\n\n                # Shift indices to read next 3 bits\n                pixel_indices = pixel_indices >> 3\n\n            offset += 3\n\n        return block\n\n    def get_picture_data(self):\n        \"\"\"\n        Convert the palette index matrix to a RGBA image.\n\n        :return: Array of RGBA values.\n        :rtype: numpy.ndarray\n        \"\"\"\n        # TODO: Greyscale export support\n        # return determine_greyscale_matrix(self.pcolor, self.layer_info.size[0], self.layer_info.size[1])\n        return determine_rgba_matrix(\n            self.pcolor,\n            self.layer_info.size[0],\n            self.layer_info.size[1]\n        )\n\n\n@cython.cdivision(True)\ncdef inline short get_block_index(\n    unsigned short width1,\n    unsigned short width2,\n    unsigned short height2,\n    unsigned short offset1_x,\n    unsigned short offset1_y,\n    unsigned short offset2_x,\n    unsigned short offset2_y,\n    unsigned short block_idx1\n):\n    \"\"\"\n    Get the vector index of a pixel block for a previous layer.\n\n    SLDs allow layers to reuse/copy pixel blocks from previous frames.\n    The pixel block is at the same in the current frame and the\n    previous frame. Because we only store the SLD layers and never the\n    full frame, we have to find the position of the pixel block\n    in the previous layer by diffing the layer offsets and\n    calculating the index in the block array of the previous layer.\n    \"\"\"\n    cdef unsigned short w1_blocks = width1 // 4\n    cdef unsigned short w2_blocks = width2 // 4\n    cdef unsigned short h2_blocks = height2 // 4\n\n    # Relative (x,y) coordinates of the block in the current layer\n    cdef unsigned short block1_pos_x = block_idx1 % w1_blocks\n    cdef unsigned short block1_pos_y = block_idx1 // w1_blocks\n\n    # Difference between the layer offsets (i.e. their positions in the frame)\n    # in blocks\n    cdef short offset_diff_x = (offset1_x - offset2_x) // 4\n    cdef short offset_diff_y = (offset1_y - offset2_y) // 4\n\n    # Relative (x,y) coordinates of the block in the previous layer\n    cdef short block2_pos_x = block1_pos_x + offset_diff_x\n    cdef short block2_pos_y = block1_pos_y + offset_diff_y\n\n    # Check if the coordinates in the previous layer are outside\n    # of the layer\n    if not (0 <= block2_pos_x < w2_blocks and 0 <= block2_pos_y < h2_blocks):\n        return -1\n\n    cdef unsigned short block_idx2 = block2_pos_x + block2_pos_y * w2_blocks\n\n    return block_idx2\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &block_matrix,\n                                         Py_ssize_t width,\n                                         Py_ssize_t height):\n    \"\"\"\n    converts blocks to an rgba matrix.\n\n    :param image_matrix: A 2-dimensional array of SLD pixels.\n    \"\"\"\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef vector[pixel] current_block\n\n    cdef size_t block_x\n    cdef size_t block_y\n\n    cdef size_t img_x = 0\n    cdef size_t img_y = 0\n\n    for block_idx in range(block_matrix.size()):\n        current_block = block_matrix[block_idx]\n\n        block_x = 0\n        block_y = 0\n        for x in range(16):\n            px = current_block[x]\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[img_y + block_y, img_x + block_x, 0] = px.r\n            array_data[img_y + block_y, img_x + block_x, 1] = px.g\n            array_data[img_y + block_y, img_x + block_x, 2] = px.b\n            array_data[img_y + block_y, img_x + block_x, 3] = px.a\n\n            block_x += 1\n            if block_x > 3:\n                block_x = 0\n                block_y += 1\n\n        img_x += 4\n        if img_x >= width:\n            img_x = 0\n            img_y += 4\n\n    return array_data\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_greyscale_matrix(vector[vector[pixel]] &block_matrix,\n                                              Py_ssize_t width,\n                                              Py_ssize_t height):\n    \"\"\"\n    converts blocks to an rgba matrix.\n\n    :param image_matrix: A 2-dimensional array of SLD pixels.\n    \"\"\"\n    cdef numpy.ndarray[numpy.uint8_t, ndim=2, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width), dtype=numpy.uint8)\n\n    cdef vector[pixel] current_block\n\n    cdef size_t block_x\n    cdef size_t block_y\n\n    cdef size_t img_x = 0\n    cdef size_t img_y = 0\n\n    for block_idx in range(block_matrix.size()):\n        current_block = block_matrix[block_idx]\n\n        block_x = 0\n        block_y = 0\n        for x in range(16):\n            px = current_block[x]\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[img_y + block_y, img_x + block_x] = px.r\n\n            block_x += 1\n            if block_x > 3:\n                block_x = 0\n                block_y += 1\n\n        img_x += 4\n        if img_x >= width:\n            img_x = 0\n            img_y += 4\n\n    return array_data\n"
  },
  {
    "path": "openage/convert/value_object/read/media/slp.pyx",
    "content": "# Copyright 2013-2023 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n\nfrom enum import Enum\nimport lz4.block\nimport numpy\nfrom struct import Struct, unpack_from\n\nfrom .....log import spam, dbg\n\n\ncimport cython\ncimport numpy\n\nfrom libc.stdint cimport uint8_t\nfrom libcpp cimport bool\nfrom libcpp.vector cimport vector\n\n\n\n# SLP files have little endian byte order\nendianness = \"< \"\n\n\n# command ids may have encoded the pixel length.\n# this is used when unpacked.\ncdef struct cmd_pack:\n    uint8_t count\n    Py_ssize_t dpos\n\n\ncdef struct boundary_def:\n    Py_ssize_t left\n    Py_ssize_t right\n    bool full_row\n\n\n# SLP pixels can be very special.\ncdef enum pixel_type:\n    color_standard      # standard pixel\n    color_shadow        # shadow pixel\n    color_shadow_v4     # shadow pixel in the 4.0X version\n    color_transparent   # transparent pixel\n    color_player        # non-outline player color pixel\n    color_player_v4     # non-outline player color pixel\n    color_black         # black outline pixel\n    color_special_1     # player color outline pixel\n    color_special_2     # black outline pixel\n\n\n# SLPs with version 4.0+ have special\n# rules for shadows\ncdef enum slp_type:\n    slp_standard        # standard type\n    slp_shadow          # shadow SLP (v4.0 and higher)\n\n# One SLP pixel.\ncdef struct pixel:\n    pixel_type type\n    uint8_t value\n\n\ncdef struct pixel32:\n    pixel_type type\n    uint8_t r\n    uint8_t g\n    uint8_t b\n    uint8_t a\n\n\nclass SLPLayerType(Enum):\n    \"\"\"\n    SLP layer types.\n    \"\"\"\n    MAIN    = \"main\"\n    SHADOW  = \"shadow\"\n\n\ncdef public dict LAYER_TYPES = {\n    0: SLPLayerType.MAIN,\n    1: SLPLayerType.SHADOW,\n}\n\n\nclass SLP:\n    \"\"\"\n    Class for reading/converting the greatest image format ever: SLP.\n    This format is used to store all graphics within AOE.\n    \"\"\"\n\n    # struct slp_version {\n    #   char version[4];\n    # };\n    slp_version = Struct(endianness + \"4s\")\n\n    # struct slp_header {\n    #   int frame_count;\n    #   char comment[24];\n    # };\n    slp_header = Struct(endianness + \"i 24s\")\n\n    # struct slp_header_v4 {\n    #   unsigned short frame_count;\n    #   unsigned short angles;\n    #   unsigned short unknown;\n    #   unsigned short frame_count_alt;\n    #   unsigned int checksum;\n    #   int offset_main;\n    #   int offset_shadow;\n    #   padding 8 bytes;\n    # };\n    slp_header_v4 = Struct(endianness + \"H H H H i i i 8x\")\n\n    # struct slp_frame_info {\n    #   unsigned int qdl_table_offset;\n    #   unsigned int outline_table_offset;\n    #   unsigned int palette_offset;\n    #   unsigned int properties;\n    #   int          width;\n    #   int          height;\n    #   int          hotspot_x;\n    #   int          hotspot_y;\n    # };\n    slp_frame_info = Struct(endianness + \"I I I I i i i i\")\n\n    # struct slp42_uncompressed_size {\n    #   unsigned int uncompressed_size;\n    # };\n    slp42_uncompressed_size = Struct(endianness + \"I\")\n\n    def __init__(self, data):\n        self.version = SLP.slp_version.unpack_from(data)[0]\n        self.compressed = False\n\n        if self.version == b'4.2P':\n            self.compressed = True\n\n            # Extract LZ4 container\n            uncompressed_size = SLP.slp42_uncompressed_size.unpack_from(data, SLP.slp_version.size)\n\n            dbg(\"Decompressing SLP\")\n            dbg(\" version:               %s\",  self.version.decode('ascii'))\n            dbg(\" uncompressed size:     %sB\", uncompressed_size)\n\n            data = lz4.block.decompress(data[4:])\n\n            # Read decompressed version\n            self.version = SLP.slp_version.unpack_from(data)[0]\n\n        if self.version in (b'4.0X', b'4.1X'):\n            header = SLP.slp_header_v4.unpack_from(data, SLP.slp_version.size)\n            frame_count, angles, _, _, checksum, offset_main, offset_shadow = header\n\n            dbg(\"SLP\")\n            dbg(\" version:               %s\", self.version.decode('ascii'))\n            dbg(\" frame count:           %s\", frame_count)\n            dbg(\" offset main graphic:   %s\", offset_main)\n            dbg(\" offset shadow graphic: %s\", offset_shadow)\n\n        else:\n            header = SLP.slp_header.unpack_from(data, SLP.slp_version.size)\n            frame_count, comment = header\n\n            dbg(\"SLP\")\n            dbg(\" version:     %s\", self.version.decode('ascii'))\n            dbg(\" frame count: %s\", frame_count)\n            dbg(\" comment:     %s\", comment.decode('ascii'))\n\n        self.main_frames = list()\n        self.shadow_frames = list()\n\n        spam(FrameInfo.repr_header())\n\n        # read all slp_frame_info structs\n        for i in range(frame_count):\n            frame_header_offset = (SLP.slp_version.size +\n                                   SLP.slp_header.size +\n                                   i * SLP.slp_frame_info.size)\n\n            frame_info = FrameInfo(*SLP.slp_frame_info.unpack_from(\n                data, frame_header_offset), self.version, slp_standard)\n            spam(frame_info)\n\n            if frame_info.properties & 0x07 == 0x07:\n                self.main_frames.append(SLPFrame32(frame_info, data))\n\n            elif self.version in (b'3.0\\x00', b'4.0X'):\n                self.main_frames.append(SLPMainFrameDE(frame_info, data))\n\n            elif self.version in (b'4.1X',):\n                self.main_frames.append(SLPMainFrameDE41(frame_info, data))\n\n            else:\n                self.main_frames.append(SLPMainFrameAoC(frame_info, data))\n\n        if self.version in (b'4.0X', b'4.1X') and offset_shadow != 0x00000000:\n            # 4.0X SLPs contain a shadow SLP inside them\n            # read all slp_frame_info of shadow\n            for i in range(frame_count):\n                frame_header_offset = (offset_shadow +\n                                       i * SLP.slp_frame_info.size)\n\n                frame_info = FrameInfo(*SLP.slp_frame_info.unpack_from(\n                    data, frame_header_offset), self.version, slp_shadow)\n                spam(frame_info)\n                self.shadow_frames.append(SLPShadowFrame(frame_info, data))\n\n    def get_frames(self, layer: int = 0):\n        \"\"\"\n        Get the frames in the SLP.\n\n        :param layer: Position of the layer (see LAYER_TYPES)\n                        - 0 = main graphics\n                        - 1 = shadow graphics\n        :type layer: int\n        \"\"\"\n        cdef list frames\n\n        layer_type = LAYER_TYPES.get(\n            layer,\n            SLPLayerType.MAIN\n        )\n\n        if layer_type is SLPLayerType.MAIN:\n            frames = self.main_frames\n\n        elif layer_type is SLPLayerType.SHADOW:\n            frames = self.shadow_frames\n\n        else:\n            frames = []\n\n        return frames\n\n    def __str__(self):\n        ret = list()\n\n        ret.extend([repr(self), \"\\n\", FrameInfo.repr_header(), \"\\n\"])\n        for frame in self.main_frames:\n            ret.extend([repr(frame), \"\\n\"])\n        return \"\".join(ret)\n\n    def __repr__(self):\n        return f\"SLP image<{len(self.main_frames):d} frames>\"\n\n\nclass FrameInfo:\n    def __init__(self, qdl_table_offset, outline_table_offset,\n                 palette_offset, properties, width, height,\n                 hotspot_x, hotspot_y, version, frame_type):\n\n        # offset of command table\n        self.qdl_table_offset = qdl_table_offset\n\n        # offset of transparent outline table\n        self.outline_table_offset = outline_table_offset\n\n        self.palette_offset = palette_offset\n\n        # used for palette index in DE1\n        self.properties = properties\n\n        self.size = (width, height)\n        self.hotspot = (hotspot_x, hotspot_y)\n\n        # meta info\n        self.version = version\n        self.frame_type = frame_type\n\n    @staticmethod\n    def repr_header():\n        return (\"offset (qdl table|outline table|palette) |\"\n                \" properties | width x height | hotspot x/y |\"\n                \" version\")\n\n    def __repr__(self):\n        ret = ( \"%s\\n\" % self.repr_header(),\n            \"             \"\n            \"(% #9x|\" % self.qdl_table_offset,\n            \"% #13x|\" % self.outline_table_offset,\n            \"% #7x) | \" % self.palette_offset,\n            \"% 10d | \" % self.properties,\n            \"% 5d x% 7d | \" % self.size,\n            \"% 4d /% 5d | \" % self.hotspot,\n            \"% 4s\" % self.version.decode('ascii'),\n        )\n        return \"\".join(ret)\n\n\ncdef class SLPFrame:\n    \"\"\"\n    one image inside the SLP. you can imagine it as a frame of a video.\n    \"\"\"\n\n    # struct slp_frame_row_edge {\n    #   unsigned short left_space;\n    #   unsigned short right_space;\n    # };\n    slp_frame_row_edge = Struct(endianness + \"H H\")\n\n    # struct slp_command_offset {\n    #   unsigned int offset;\n    # }\n    slp_command_offset = Struct(endianness + \"I\")\n\n    # frame information\n    cdef object info\n\n    # for each row:\n    # contains (left, right, full_row) number of boundary pixels\n    cdef vector[boundary_def] boundaries\n\n    # stores the file offset for the first drawing command\n    cdef vector[int] cmd_offsets\n\n    # palette index matrix representing the final image\n    cdef vector[vector[pixel]] pcolor\n\n    def __init__(self, frame_info, data):\n        self.info = frame_info\n\n        if not (isinstance(data, bytes) or isinstance(data, bytearray)):\n            raise ValueError(\"Frame data must be some bytes object\")\n\n        # memory pointer\n        # convert the bytes obj to char*\n        cdef const uint8_t[::1] data_raw = data\n\n        cdef unsigned short left\n        cdef unsigned short right\n\n        cdef size_t i\n        cdef int cmd_offset\n\n        cdef size_t row_count = self.info.size[1]\n        self.pcolor.reserve(row_count)\n\n        # process bondary table\n        for i in range(row_count):\n            outline_entry_position = (self.info.outline_table_offset + i *\n                                      SLPFrame.slp_frame_row_edge.size)\n\n            left, right = SLPFrame.slp_frame_row_edge.unpack_from(\n                data, outline_entry_position\n            )\n\n            # is this row completely transparent?\n            if left == 0x8000 or right == 0x8000:\n                self.boundaries.push_back(boundary_def(0, 0, True))\n            else:\n                self.boundaries.push_back(boundary_def(left, right, False))\n\n        # process cmd table\n        for i in range(row_count):\n            cmd_table_position = (self.info.qdl_table_offset + i *\n                                  SLPFrame.slp_command_offset.size)\n            cmd_offset = SLPFrame.slp_command_offset.unpack_from(\n                data, cmd_table_position\n            )[0]\n            self.cmd_offsets.push_back(cmd_offset)\n\n        for i in range(row_count):\n            self.pcolor.push_back(self.create_palette_color_row(data_raw, i))\n\n    cdef vector[pixel] create_palette_color_row(self,\n                                                const uint8_t[::1] &data_raw,\n                                                Py_ssize_t rowid):\n        \"\"\"\n        create palette indices (colors) for the given rowid.\n        \"\"\"\n\n        cdef vector[pixel] row_data\n        cdef Py_ssize_t i\n\n        first_cmd_offset = self.cmd_offsets[rowid]\n        cdef boundary_def bounds = self.boundaries[rowid]\n        cdef size_t pixel_count = self.info.size[0]\n\n        # preallocate memory\n        row_data.reserve(pixel_count)\n\n        # row is completely transparent\n        if bounds.full_row:\n            for _ in range(pixel_count):\n                row_data.push_back(pixel(color_transparent, 0))\n\n            return row_data\n\n        # start drawing the left transparent space\n        for i in range(bounds.left):\n            row_data.push_back(pixel(color_transparent, 0))\n\n        # process the drawing commands for this row.\n        self.process_drawing_cmds(data_raw,\n                                  row_data,\n                                  rowid,\n                                  first_cmd_offset,\n                                  pixel_count - bounds.right)\n\n        # finish by filling up the right transparent space\n        for i in range(bounds.right):\n            row_data.push_back(pixel(color_transparent, 0))\n\n        # verify size of generated row\n        if row_data.size() != pixel_count:\n            got = row_data.size()\n            summary = (\n                f\"{got:d}/{pixel_count:d} -> row {rowid:d}, \"\n                f\"offset {first_cmd_offset:d} / {first_cmd_offset:#x}\"\n            )\n            message = (\n                f\"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, \"\n                f\"missing: {abs(pixel_count - got):d}\"\n            )\n\n            raise Exception(message)\n\n        return row_data\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        pass\n\n    def get_picture_data(self, palette):\n        \"\"\"\n        Convert the palette index matrix to a colored image.\n        \"\"\"\n        return determine_rgba_matrix(self.pcolor, palette)\n\n    def get_hotspot(self):\n        \"\"\"\n        Return the frame's hotspot (the \"center\" of the image)\n        \"\"\"\n        return self.info.hotspot\n\n    def get_palette_number(self):\n        \"\"\"\n        Return the frame's palette number.\n\n        :return: Palette number of the frame.\n        :rtype: int\n        \"\"\"\n        if self.info.version in (b'3.0\\x00', b'4.0X', b'4.1X'):\n            if self.info.properties > 0xFFFFFF:\n                # DE1 effects palette\n                return 54\n\n            return self.info.properties >> 16\n\n        else:\n            return self.info.palette_offset + 50500\n\n    def __repr__(self):\n        return repr(self.info)\n\n\ncdef class SLPMainFrameAoC(SLPFrame):\n    \"\"\"\n    SLPFrame for the main graphics sprite up to SLP version 2.0.\n    \"\"\"\n\n    def __init__(self, frame_info, data):\n        super().__init__(frame_info, data)\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        create palette indices (colors) for the drawing commands\n        found for this row in the SLP frame.\n        \"\"\"\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t color\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_nibble\n        cdef uint8_t higher_nibble\n        cdef uint8_t lowest_crumb\n        cdef cmd_pack cpack\n        cdef int pixel_count\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d}, \" +\n                    f\"but we have {row_data.size():d} already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            lower_nibble = 0x0f & cmd\n            higher_nibble = 0xf0 & cmd\n            lowest_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_nibble == 0x0F:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n                continue\n\n            elif lowest_crumb == 0b00000000:\n                # color_list command\n                # draw the following bytes as palette colors\n\n                pixel_count = cmd >> 2\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lowest_crumb == 0b00000001:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = cmd >> 2; if count == 0: count = nextbyte\n\n                cpack = cmd_or_next(data_raw, cmd, 2, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x02:\n                # big_color_list command\n                # draw (higher_nibble << 4 + nextbyte) following palette colors\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x03:\n                # big_skip command\n                # draw (higher_nibble << 4 + nextbyte)\n                # transparent pixels\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x06:\n                # player_color_list command\n                # we have to draw the player color for cmd>>4 times,\n                # or if that is 0, as often as the next byte says.\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    row_data.push_back(pixel(color_player, color))\n\n            elif lower_nibble == 0x07:\n                # fill command\n                # draw 'count' pixels with color of next byte\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x0A:\n                # fill player color command\n                # draw the player color for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_player, color))\n\n            elif lower_nibble == 0x0B:\n                # shadow command\n                # draw a transparent shadow pixel for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_shadow, 0))\n\n            elif lower_nibble == 0x0E:\n                # \"extended\" commands. higher nibble specifies the instruction.\n\n                if higher_nibble == 0x00:\n                    # render hint xflip command\n                    # render hint: only draw the following command,\n                    # if this sprite is not flipped left to right\n                    spam(\"render hint: xfliptest\")\n\n                elif higher_nibble == 0x10:\n                    # render h notxflip command\n                    # render hint: only draw the following command,\n                    # if this sprite IS flipped left to right.\n                    spam(\"render hint: !xfliptest\")\n\n                elif higher_nibble == 0x20:\n                    # table use normal command\n                    # set the transform color table to normal,\n                    # for the standard drawing commands\n                    spam(\"image wants normal color table now\")\n\n                elif higher_nibble == 0x30:\n                    # table use alternat command\n                    # set the transform color table to alternate,\n                    # this affects all following standard commands\n                    spam(\"image wants alternate color table now\")\n\n                elif higher_nibble == 0x40:\n                    # outline_1 command\n                    # the next pixel shall be drawn as special color 1,\n                    # if it is obstructed later in rendering\n                    row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x60:\n                    # outline_2 command\n                    # same as above, but special color 2\n                    row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x50:\n                    # outline_span_1 command\n                    # same as above, but span special color 1 nextbyte times.\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x70:\n                    # outline_span_2 command\n                    # same as above, using special color 2\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x80:\n                    # dither command\n                    raise NotImplementedError(\"dither not implemented\")\n\n                elif higher_nibble in (0x90, 0xA0):\n                    # 0x90: premultiplied alpha\n                    # 0xA0: original alpha\n                    raise NotImplementedError(\"extended alpha not implemented\")\n\n            else:\n                raise Exception(\n                    f\"unknown slp drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\")\n\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\ncdef class SLPMainFrameDE(SLPFrame):\n    \"\"\"\n    SLPFrame for the main graphics sprite since SLP version 3.0.\n    \"\"\"\n\n    def __init__(self, frame_info, data):\n        super().__init__(frame_info, data)\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        create palette indices (colors) for the drawing commands\n        found for this row in the SLP frame.\n        \"\"\"\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t color\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_nibble\n        cdef uint8_t higher_nibble\n        cdef uint8_t lowest_crumb\n        cdef cmd_pack cpack\n        cdef int pixel_count\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d}, \"\n                    f\"but we have {row_data.size():d} already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            lower_nibble = 0x0f & cmd\n            higher_nibble = 0xf0 & cmd\n            lowest_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_nibble == 0x0F:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n                continue\n\n            elif lowest_crumb == 0b00000000:\n                # color_list command\n                # draw the following bytes as palette colors\n\n                pixel_count = cmd >> 2\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lowest_crumb == 0b00000001:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = cmd >> 2; if count == 0: count = nextbyte\n\n                cpack = cmd_or_next(data_raw, cmd, 2, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x02:\n                # big_color_list command\n                # draw (higher_nibble << 4 + nextbyte) following palette colors\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x03:\n                # big_skip command\n                # draw (higher_nibble << 4 + nextbyte)\n                # transparent pixels\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x06:\n                # player_color_list command\n                # we have to draw the player color for cmd>>4 times,\n                # or if that is 0, as often as the next byte says.\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    # version 3.0 uses extra palettes for player colors\n                    row_data.push_back(pixel(color_player_v4, color))\n\n            elif lower_nibble == 0x07:\n                # fill command\n                # draw 'count' pixels with color of next byte\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x0A:\n                # fill player color command\n                # draw the player color for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    # version 3.0 uses extra palettes for player colors\n                    row_data.push_back(pixel(color_player_v4, color))\n\n            elif lower_nibble == 0x0B:\n                # shadow command\n                # draw a transparent shadow pixel for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_shadow, 0))\n\n            elif lower_nibble == 0x0E:\n                # \"extended\" commands. higher nibble specifies the instruction.\n\n                if higher_nibble == 0x00:\n                    # render hint xflip command\n                    # render hint: only draw the following command,\n                    # if this sprite is not flipped left to right\n                    spam(\"render hint: xfliptest\")\n\n                elif higher_nibble == 0x10:\n                    # render h notxflip command\n                    # render hint: only draw the following command,\n                    # if this sprite IS flipped left to right.\n                    spam(\"render hint: !xfliptest\")\n\n                elif higher_nibble == 0x20:\n                    # table use normal command\n                    # set the transform color table to normal,\n                    # for the standard drawing commands\n                    spam(\"image wants normal color table now\")\n\n                elif higher_nibble == 0x30:\n                    # table use alternat command\n                    # set the transform color table to alternate,\n                    # this affects all following standard commands\n                    spam(\"image wants alternate color table now\")\n\n                elif higher_nibble == 0x40:\n                    # outline_1 command\n                    # the next pixel shall be drawn as special color 1,\n                    # if it is obstructed later in rendering\n                    row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x60:\n                    # outline_2 command\n                    # same as above, but special color 2\n                    row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x50:\n                    # outline_span_1 command\n                    # same as above, but span special color 1 nextbyte times.\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x70:\n                    # outline_span_2 command\n                    # same as above, using special color 2\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x80:\n                    # dither command\n                    raise NotImplementedError(\"dither not implemented\")\n\n                elif higher_nibble in (0x90, 0xA0):\n                    # 0x90: premultiplied alpha\n                    # 0xA0: original alpha\n                    raise NotImplementedError(\"extended alpha not implemented\")\n\n            else:\n                raise Exception(\n                    f\"unknown slp drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\")\n\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\ncdef class SLPMainFrameDE41(SLPFrame):\n    \"\"\"\n    SLPFrame for the main graphics sprite since SLP version 4.1.\n    These have additional decay data inside the color pixels.\n    \"\"\"\n\n    def __init__(self, frame_info, data):\n        super().__init__(frame_info, data)\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        create palette indices (colors) for the drawing commands\n        found for this row in the SLP frame.\n        \"\"\"\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t color\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_nibble\n        cdef uint8_t higher_nibble\n        cdef uint8_t lowest_crumb\n        cdef cmd_pack cpack\n        cdef int pixel_count\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d}, \"\n                    f\"but we have {row_data.size():d} already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            lower_nibble = 0x0f & cmd\n            higher_nibble = 0xf0 & cmd\n            lowest_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_nibble == 0x0F:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n                continue\n\n            elif lowest_crumb == 0b00000000:\n                # color_list command\n                # draw the following bytes as palette colors\n\n                pixel_count = cmd >> 2\n                for _ in range(pixel_count):\n                    dpos += 2\n                    color = data_raw[dpos]\n\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lowest_crumb == 0b00000001:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = cmd >> 2; if count == 0: count = nextbyte\n\n                cpack = cmd_or_next(data_raw, cmd, 2, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x02:\n                # big_color_list command\n                # draw (higher_nibble << 4 + nextbyte) following palette colors\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    dpos += 2\n                    color = data_raw[dpos]\n\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x03:\n                # big_skip command\n                # draw (higher_nibble << 4 + nextbyte)\n                # transparent pixels\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x06:\n                # player_color_list command\n                # we have to draw the player color for cmd>>4 times,\n                # or if that is 0, as often as the next byte says.\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    dpos += 2\n                    color = data_raw[dpos]\n\n                    # version 3.0 uses extra palettes for player colors\n                    row_data.push_back(pixel(color_player_v4, color))\n\n            elif lower_nibble == 0x07:\n                # fill command\n                # draw 'count' pixels with color of next byte\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_standard, color))\n\n            elif lower_nibble == 0x0A:\n                # fill player color command\n                # draw the player color for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    # since version 3.0 uses extra palettes for player colors\n                    row_data.push_back(pixel(color_player_v4, color))\n\n            elif lower_nibble == 0x0B:\n                # shadow command\n                # draw a transparent shadow pixel for 'count' times\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_shadow, 0))\n\n            elif lower_nibble == 0x0E:\n                # \"extended\" commands. higher nibble specifies the instruction.\n\n                if higher_nibble == 0x00:\n                    # render hint xflip command\n                    # render hint: only draw the following command,\n                    # if this sprite is not flipped left to right\n                    spam(\"render hint: xfliptest\")\n\n                elif higher_nibble == 0x10:\n                    # render h notxflip command\n                    # render hint: only draw the following command,\n                    # if this sprite IS flipped left to right.\n                    spam(\"render hint: !xfliptest\")\n\n                elif higher_nibble == 0x20:\n                    # table use normal command\n                    # set the transform color table to normal,\n                    # for the standard drawing commands\n                    spam(\"image wants normal color table now\")\n\n                elif higher_nibble == 0x30:\n                    # table use alternat command\n                    # set the transform color table to alternate,\n                    # this affects all following standard commands\n                    spam(\"image wants alternate color table now\")\n\n                elif higher_nibble == 0x40:\n                    # outline_1 command\n                    # the next pixel shall be drawn as special color 1,\n                    # if it is obstructed later in rendering\n                    row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x60:\n                    # outline_2 command\n                    # same as above, but special color 2\n                    row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x50:\n                    # outline_span_1 command\n                    # same as above, but span special color 1 nextbyte times.\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_1, 0))\n\n                elif higher_nibble == 0x70:\n                    # outline_span_2 command\n                    # same as above, using special color 2\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_special_2, 0))\n\n                elif higher_nibble == 0x80:\n                    # dither command\n                    raise NotImplementedError(\"dither not implemented\")\n\n                elif higher_nibble in (0x90, 0xA0):\n                    # 0x90: premultiplied alpha\n                    # 0xA0: original alpha\n                    raise NotImplementedError(\"extended alpha not implemented\")\n\n            else:\n                raise Exception(\n                    f\"unknown slp drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\")\n\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\ncdef class SLPShadowFrame(SLPFrame):\n    \"\"\"\n    SLPFrame for the shadow graphics in SLP version 4.0 and 4.1.\n    \"\"\"\n\n    def __init__(self, frame_info, data):\n        super().__init__(frame_info, data)\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        create palette indices (colors) for the drawing commands\n        found for this row in the SLP frame.\n        \"\"\"\n\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t color\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_nibble\n        cdef uint8_t higher_nibble\n        cdef uint8_t lowest_crumb\n        cdef cmd_pack cpack\n        cdef int pixel_count\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size} pixels should be drawn in row {rowid:d}, \"\n                    f\"but we have {row_data.size():d} already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            lower_nibble = 0x0f & cmd\n            higher_nibble = 0xf0 & cmd\n            lowest_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_nibble == 0x0F:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n                continue\n\n            elif lowest_crumb == 0b00000000:\n                # color_list command\n                # draw the following bytes as palette colors\n\n                pixel_count = cmd >> 2\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    # shadows in v4.0 draw a different color\n                    row_data.push_back(pixel(color_shadow_v4, color))\n\n            elif lowest_crumb == 0b00000001:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = cmd >> 2; if count == 0: count = nextbyte\n\n                cpack = cmd_or_next(data_raw, cmd, 2, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x02:\n                # big_color_list command\n                # draw (higher_nibble << 4 + nextbyte) following palette colors\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    dpos += 1\n                    color = data_raw[dpos]\n                    row_data.push_back(pixel(color_shadow_v4, color))\n\n            elif lower_nibble == 0x03:\n                # big_skip command\n                # draw (higher_nibble << 4 + nextbyte)\n                # transparent pixels\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0))\n\n            elif lower_nibble == 0x06:\n                # player_color_list command\n                # we have to draw the player color for cmd>>4 times,\n                # or if that is 0, as often as the next byte says.\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    dpos += 1\n                    color = data_raw[dpos]\n\n                    # version 3.0 uses extra palettes for player colors\n                    row_data.push_back(pixel(color_player_v4, color))\n\n            elif lower_nibble == 0x07:\n                # fill command\n                # draw 'count' pixels with color of next byte\n\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    # shadows in v4.0 draw a different color\n                    row_data.push_back(pixel(color_shadow_v4, color))\n\n            else:\n                raise Exception(\n                    f\"unknown slp shadow drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\"\n                )\n\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\ncdef class SLPFrame32:\n    \"\"\"\n    An RGBA color frames inside the SLP.\n    \"\"\"\n\n    # struct slp_frame_row_edge {\n    #   unsigned short left_space;\n    #   unsigned short right_space;\n    # };\n    slp_frame_row_edge = Struct(endianness + \"H H\")\n\n    # struct slp_command_offset {\n    #   unsigned int offset;\n    # }\n    slp_command_offset = Struct(endianness + \"I\")\n\n    # frame information\n    cdef object info\n\n    # for each row:\n    # contains (left, right, full_row) number of boundary pixels\n    cdef vector[boundary_def] boundaries\n\n    # stores the file offset for the first drawing command\n    cdef vector[int] cmd_offsets\n\n    # palette index matrix representing the final image\n    cdef vector[vector[pixel32]] pcolor\n\n    def __init__(self, frame_info, data):\n        self.info = frame_info\n\n        if not (isinstance(data, bytes) or isinstance(data, bytearray)):\n            raise ValueError(\"Frame data must be some bytes object\")\n\n        # memory pointer\n        # convert the bytes obj to char*\n        cdef const uint8_t[::1] data_raw = data\n\n        cdef unsigned short left\n        cdef unsigned short right\n\n        cdef size_t i\n        cdef int cmd_offset\n\n        cdef size_t row_count = self.info.size[1]\n        self.pcolor.reserve(row_count)\n\n        # process bondary table\n        for i in range(row_count):\n            outline_entry_position = (self.info.outline_table_offset + i *\n                                      SLPFrame.slp_frame_row_edge.size)\n\n            left, right = SLPFrame.slp_frame_row_edge.unpack_from(\n                data, outline_entry_position\n            )\n\n            # is this row completely transparent?\n            if left == 0x8000 or right == 0x8000:\n                self.boundaries.push_back(boundary_def(0, 0, True))\n            else:\n                self.boundaries.push_back(boundary_def(left, right, False))\n\n        # process cmd table\n        for i in range(row_count):\n            cmd_table_position = (self.info.qdl_table_offset + i *\n                                  SLPFrame.slp_command_offset.size)\n            cmd_offset = SLPFrame.slp_command_offset.unpack_from(\n                data, cmd_table_position\n            )[0]\n            self.cmd_offsets.push_back(cmd_offset)\n\n        for i in range(row_count):\n            self.pcolor.push_back(self.create_palette_color_row(data_raw, i))\n\n    cdef vector[pixel32] create_palette_color_row(self,\n                                                  const uint8_t[::1] &data_raw,\n                                                  Py_ssize_t rowid):\n        \"\"\"\n        create palette indices (colors) for the given rowid.\n        \"\"\"\n        cdef vector[pixel32] row_data\n        cdef Py_ssize_t i\n\n        first_cmd_offset = self.cmd_offsets[rowid]\n        cdef boundary_def bounds = self.boundaries[rowid]\n        cdef size_t pixel_count = self.info.size[0]\n\n        # preallocate memory\n        row_data.reserve(pixel_count)\n\n        # row is completely transparent\n        if bounds.full_row:\n            for _ in range(pixel_count):\n                row_data.push_back(pixel32(color_transparent, 0, 0, 0, 0))\n\n            return row_data\n\n        # start drawing the left transparent space\n        for i in range(bounds.left):\n            row_data.push_back(pixel32(color_transparent, 0, 0, 0, 0))\n\n        # process the drawing commands for this row.\n        self.process_drawing_cmds(data_raw,\n                                  row_data,\n                                  rowid,\n                                  first_cmd_offset,\n                                  pixel_count - bounds.right)\n\n        # finish by filling up the right transparent space\n        for i in range(bounds.right):\n            row_data.push_back(pixel32(color_transparent, 0, 0, 0, 0))\n\n        # verify size of generated row\n        if row_data.size() != pixel_count:\n            got = row_data.size()\n            summary = (\n                f\"{got:d}/{pixel_count:d} -> row {rowid:d}, \"\n                f\"offset {first_cmd_offset:d} / {first_cmd_offset:#x}\"\n            )\n            message = (\n                f\"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, \"\n                f\"missing: {abs(pixel_count - got):d}\"\n            )\n\n            raise Exception(message)\n\n        return row_data\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel32] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        create palette indices (colors) for the drawing commands\n        found for this row in the SLP frame.\n        \"\"\"\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t player_color\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_nibble\n        cdef uint8_t higher_nibble\n        cdef uint8_t lowest_crumb\n        cdef cmd_pack cpack\n        cdef int pixel_count\n\n        # Color channels are saved in BGRA order\n        cdef vector[uint8_t] pixel_data\n        pixel_data.reserve(4)\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d}, \" +\n                    f\"but we have {row_data.size():d} already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            lower_nibble = 0x0f & cmd\n            higher_nibble = 0xf0 & cmd\n            lowest_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_nibble == 0x0F:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n                continue\n\n            elif lowest_crumb == 0b00000000:\n                # color_list command\n                # draw the following bytes as palette colors\n                pixel_count = cmd >> 2\n                for _ in range(pixel_count):\n                    for _ in range(4):\n                        dpos += 1\n                        pixel_data.push_back(data_raw[dpos])\n\n                    row_data.push_back(pixel32(color_standard,\n                                               pixel_data[2],\n                                               pixel_data[1],\n                                               pixel_data[0],\n                                               255))\n\n                    pixel_data.clear()\n\n            elif lowest_crumb == 0b00000001:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = cmd >> 2; if count == 0: count = nextbyte\n                cpack = cmd_or_next(data_raw, cmd, 2, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel32(color_transparent, 0, 0, 0, 0))\n\n            elif lower_nibble == 0x02:\n                # big_color_list command\n                # draw (higher_nibble << 4 + nextbyte) following palette colors\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    for _ in range(4):\n                        dpos += 1\n                        pixel_data.push_back(data_raw[dpos])\n\n                    row_data.push_back(pixel32(color_standard,\n                                               pixel_data[2],\n                                               pixel_data[1],\n                                               pixel_data[0],\n                                               255))\n\n                    pixel_data.clear()\n\n            elif lower_nibble == 0x03:\n                # big_skip command\n                # draw (higher_nibble << 4 + nextbyte)\n                # transparent pixels\n\n                dpos += 1\n                nextbyte = data_raw[dpos]\n                pixel_count = (higher_nibble << 4) + nextbyte\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel32(color_transparent, 0, 0, 0, 0))\n\n            elif lower_nibble == 0x06:\n                # player_color_list command\n                # we have to draw the player color for cmd>>4 times,\n                # or if that is 0, as often as the next byte says.\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n                for _ in range(cpack.count):\n                    dpos += 1\n                    player_color = data_raw[dpos]\n\n                    row_data.push_back(pixel32(color_player, player_color, 0, 0, 0))\n\n            elif lower_nibble == 0x07:\n                # fill command\n                # draw 'count' pixels with color of next byte\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                for _ in range(cpack.count):\n                    for _ in range(4):\n                        dpos += 1\n                        pixel_data.push_back(data_raw[dpos])\n\n                    row_data.push_back(pixel32(color_standard,\n                                               pixel_data[2],\n                                               pixel_data[1],\n                                               pixel_data[0],\n                                               255))\n\n                    pixel_data.clear()\n\n            elif lower_nibble == 0x0A:\n                # fill player color command\n                # draw the player color for 'count' times\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                dpos += 1\n                player_color = data_raw[dpos]\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel32(color_player, player_color, 0, 0, 0))\n\n            elif lower_nibble == 0x0B:\n                # shadow command\n                # draw a transparent shadow pixel for 'count' times\n                cpack = cmd_or_next(data_raw, cmd, 4, dpos)\n                dpos = cpack.dpos\n\n                for _ in range(cpack.count):\n                    row_data.push_back(pixel32(color_shadow, 0, 0, 0, 0))\n\n            elif lower_nibble == 0x0E:\n                # \"extended\" commands. higher nibble specifies the instruction.\n\n                if higher_nibble == 0x00:\n                    # render hint xflip command\n                    # render hint: only draw the following command,\n                    # if this sprite is not flipped left to right\n                    spam(\"render hint: xfliptest\")\n\n                elif higher_nibble == 0x10:\n                    # render h notxflip command\n                    # render hint: only draw the following command,\n                    # if this sprite IS flipped left to right.\n                    spam(\"render hint: !xfliptest\")\n\n                elif higher_nibble == 0x20:\n                    # table use normal command\n                    # set the transform color table to normal,\n                    # for the standard drawing commands\n                    spam(\"image wants normal color table now\")\n\n                elif higher_nibble == 0x30:\n                    # table use alternat command\n                    # set the transform color table to alternate,\n                    # this affects all following standard commands\n                    spam(\"image wants alternate color table now\")\n\n                elif higher_nibble == 0x40:\n                    # outline_1 command\n                    # the next pixel shall be drawn as special color 1,\n                    # if it is obstructed later in rendering\n                    row_data.push_back(pixel32(color_special_1, 0, 0, 0, 0))\n\n                elif higher_nibble == 0x60:\n                    # outline_2 command\n                    # same as above, but special color 2\n                    row_data.push_back(pixel32(color_special_2, 0, 0, 0, 0))\n\n                elif higher_nibble == 0x50:\n                    # outline_span_1 command\n                    # same as above, but span special color 1 nextbyte times.\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel32(color_special_1, 0, 0, 0, 0))\n\n                elif higher_nibble == 0x70:\n                    # outline_span_2 command\n                    # same as above, using special color 2\n\n                    dpos += 1\n                    pixel_count = data_raw[dpos]\n\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel32(color_special_2, 0, 0, 0, 0))\n\n                elif higher_nibble == 0x80:\n                    # dither command\n                    raise NotImplementedError(\"dither not implemented\")\n\n                elif higher_nibble == 0x90:\n                    # 0x90: premultiplied alpha\n                    dpos += 1\n                    nextbyte = data_raw[dpos]\n                    pixel_count = nextbyte\n\n                    for _ in range(pixel_count):\n                        for _ in range(4):\n                            dpos += 1\n                            pixel_data.push_back(data_raw[dpos])\n\n                        row_data.push_back(pixel32(color_standard,\n                                                   pixel_data[2],\n                                                   pixel_data[1],\n                                                   pixel_data[0],\n                                                   255 - pixel_data[3]))\n\n                        pixel_data.clear()\n\n                elif higher_nibble == 0xA0:\n                    # 0xA0: original alpha\n                    raise NotImplementedError(\"original alpha not implemented\")\n\n            else:\n                raise Exception(\n                    f\"unknown slp drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\")\n\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\n    def get_picture_data(self, palette):\n        \"\"\"\n        Convert the palette index matrix to a colored image.\n        \"\"\"\n        return determine_rgba_matrix32(self.pcolor)\n\n    def get_hotspot(self):\n        \"\"\"\n        Return the frame's hotspot (the \"center\" of the image)\n        \"\"\"\n        return self.info.hotspot\n\n    def get_palette_number(self):\n        \"\"\"\n        Return the frame's palette number.\n\n        :return: Palette number of the frame.\n        :rtype: int\n        \"\"\"\n        return None\n\n    def __repr__(self):\n        return repr(self.info)\n\n\n@cython.boundscheck(False)\ncdef inline cmd_pack cmd_or_next(const uint8_t[::1] &data_raw,\n                                 uint8_t cmd,\n                                 uint8_t n,\n                                 Py_ssize_t pos):\n    \"\"\"\n    to save memory, the draw amount may be encoded into\n    the drawing command itself in the upper n bits.\n    \"\"\"\n    cdef uint8_t packed_in_cmd = cmd >> n\n\n    if packed_in_cmd != 0:\n        return cmd_pack(packed_in_cmd, pos)\n\n    else:\n        pos += 1\n        return cmd_pack(data_raw[pos], pos)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix,\n                                         numpy.ndarray[numpy.uint8_t, ndim=2, mode=\"c\"] palette):\n    \"\"\"\n    converts a palette index image matrix to an rgba matrix.\n    \"\"\"\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t[:, ::1] m_lookup = palette\n\n    cdef uint8_t r\n    cdef uint8_t g\n    cdef uint8_t b\n    cdef uint8_t alpha\n\n    cdef vector[pixel] current_row\n    cdef pixel px\n    cdef pixel_type px_type\n    cdef int px_val\n\n    cdef size_t x\n    cdef size_t y\n\n    for y in range(height):\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n            px_type = px.type\n            px_val = px.value\n\n            if px_type == color_standard:\n                # simply look up the color index in the table\n                r = m_lookup[px_val][0]\n                g = m_lookup[px_val][1]\n                b = m_lookup[px_val][2]\n                alpha = 255\n\n            elif px_type == color_transparent:\n                r, g, b, alpha = 0, 0, 0, 0\n\n            elif px_type == color_shadow:\n                r, g, b, alpha = 0, 0, 0, 100\n\n            elif px_type == color_shadow_v4:\n                r, g, b = 0, 0, 0\n                alpha = 255 - (px_val << 2)\n\n                # change alpha values to match openage texture formats\n                # even alphas are used for commands marking *special* pixels (player color, etc.)\n                # odd alphas are used for normal pixels (= displayed as-is with transparency)\n                alpha = alpha | 0x01\n\n            else:\n                if px_type == color_player_v4 or px_type == color_player:\n                    # mark this pixel as player color\n                    alpha = 254\n\n                elif px_type == color_special_2 or\\\n                     px_type == color_black:\n                    alpha = 250  # mark this pixel as special outline\n\n                elif px_type == color_special_1:\n                    alpha = 252  # mark this pixel as outline\n\n                else:\n                    raise ValueError(\"unknown pixel type: %d\" % px_type)\n\n                # Store player color index in g channel\n                r, b = 0, 0\n                g = px_val\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_rgba_matrix32(vector[vector[pixel32]] &image_matrix):\n    \"\"\"\n    converts a 32-Bit SLP image matrix to an rgba matrix.\n    \"\"\"\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t r\n    cdef uint8_t g\n    cdef uint8_t b\n    cdef uint8_t alpha\n\n    cdef vector[pixel32] current_row\n    cdef pixel32 px\n    cdef pixel_type px_type\n    cdef int px_val\n\n    cdef size_t x\n    cdef size_t y\n\n    for y in range(height):\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n            px_type = px.type\n\n            if px_type == color_standard:\n                # simply look up the color index in the table\n                r = px.r\n                g = px.g\n                b = px.b\n                alpha = px.a\n\n                # change alpha values to match openage texture formats\n                # even alphas are used for commands marking *special* pixels (player color, etc.)\n                # odd alphas are used for normal pixels (= displayed as-is with transparency)\n                alpha = alpha | 0x01\n\n            elif px_type == color_transparent:\n                r, g, b, alpha = 0, 0, 0, 0\n\n            elif px_type == color_shadow:\n                r, g, b, alpha = 0, 0, 0, 100\n\n            elif px_type == color_shadow_v4:\n                r, g, b = 0, 0, 0\n                alpha = 255 - (px.r << 2)\n\n                # change alpha values to match openage texture formats\n                # even alphas are used for commands marking *special* pixels (player color, etc.)\n                # odd alphas are used for normal pixels (= displayed as-is with transparency)\n                alpha = alpha | 0x01\n\n            else:\n                if px_type == color_player_v4 or px_type == color_player:\n                    # mark this pixel as player color\n                    alpha = 254\n\n                elif px_type == color_special_2 or\\\n                     px_type == color_black:\n                    alpha = 250  # mark this pixel as special outline\n\n                elif px_type == color_special_1:\n                    alpha = 252  # mark this pixel as outline\n\n                else:\n                    raise ValueError(\"unknown pixel type: %d\" % px_type)\n\n                # Store player color index in g channel\n                r, b = 0, 0\n                g = px.r\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n"
  },
  {
    "path": "openage/convert/value_object/read/media/smp.pyx",
    "content": "# Copyright 2013-2025 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n\nfrom enum import Enum\nimport numpy\nfrom struct import Struct, unpack_from\n\nfrom .....log import spam, dbg\n\n\ncimport cython\ncimport numpy\n\nfrom libc.stdint cimport uint8_t, uint16_t\nfrom libcpp cimport bool\nfrom libcpp.vector cimport vector\n\n\n\n# SMP files have little endian byte order\nendianness = \"< \"\n\n\n# Boundary of a row in a SMP layer.\ncdef struct boundary_def:\n    Py_ssize_t left\n    Py_ssize_t right\n    bool full_row\n\n\n# SMP pixels are super special.\ncdef enum pixel_type:\n    color_standard      # standard pixel\n    color_shadow        # shadow pixel\n    color_transparent   # transparent pixel\n    color_player        # non-outline player color pixel\n    color_outline       # player color outline pixel\n\n\n# One SMP pixel.\ncdef struct pixel:\n    pixel_type type\n    uint8_t index               # index in a palette section\n    uint8_t palette             # palette number and palette section\n    uint8_t damage_modifier_1   # modifier for damage (part 1)\n    uint8_t damage_modifier_2   # modifier for damage (part 2)\n\n\nclass SMPLayerType(Enum):\n    \"\"\"\n    SMP layer types.\n    \"\"\"\n    MAIN    = \"main\"\n    SHADOW  = \"shadow\"\n    OUTLINE = \"outline\"\n\n\ncdef public dict LAYER_TYPES = {\n    0: SMPLayerType.MAIN,\n    1: SMPLayerType.SHADOW,\n    2: SMPLayerType.OUTLINE,\n}\n\n\ncdef class SMPMainLayer:\n    \"\"\"\n    Main graphic layer of an SMP. Stores the color information.\n    \"\"\"\n    pass\n\n\ncdef class SMPShadowLayer:\n    \"\"\"\n    Shadow layer of an SMP.\n    \"\"\"\n    pass\n\n\ncdef class SMPOutlineLayer:\n    \"\"\"\n    Outline layer of an SMP.\n    \"\"\"\n    pass\n\n\n# fused type for the layer variants\nctypedef fused SMPLayerVariant:\n    SMPMainLayer\n    SMPShadowLayer\n    SMPOutlineLayer\n\n\nclass SMP:\n    \"\"\"\n    Class for reading/converting the SMP image format (successor of SLP).\n    This format is used to store all graphics within AoE2: Definitive Edition (Beta).\n    \"\"\"\n\n    # struct smp_header {\n    #   char         signature[4];\n    #   unsigned int version;\n    #   unsigned int frame_count;\n    #   unsigned int facet_count;\n    #   unsigned int frames_per_facet;\n    #   unsigned int checksum;\n    #   unsigned int file_size;\n    #   unsigned int source_format;\n    #   char         comment[32];\n    # };\n    smp_header = Struct(endianness + \"4s 7I 32s\")\n\n    # struct smp_frame_header_offset {\n    #   unsigned int frame_info_offset;\n    # };\n    smp_frame_header_offset = Struct(endianness + \"I\")\n\n    # struct smp_frame_header_size {\n    #   padding 28 bytes;\n    #   int frame_header_size;\n    # };\n    smp_frame_header_size = Struct(endianness + \"28x i\")\n\n    # struct smp_layer_header {\n    #   int          width;\n    #   int          height;\n    #   int          hotspot_x;\n    #   int          hotspot_y;\n    #   int          layer_type;\n    #   unsigned int outline_table_offset;\n    #   unsigned int qdl_table_offset;\n    #   ???          flags;\n    # };\n    smp_layer_header = Struct(endianness + \"i i i i I I I I\")\n\n    def __init__(self, data: bytes) -> None:\n        \"\"\"\n        Read the SMP file and store the frames in the object.\n\n        :param data: SMP file data.\n        \"\"\"\n        smp_header = SMP.smp_header.unpack_from(data)\n        signature, version, frame_count, facet_count, frames_per_facet,\\\n            checksum, file_size, source_format, comment = smp_header\n\n        dbg(\"SMP\")\n        spam(\" signature:            %s\", signature.decode('ascii'))\n        spam(\" version:              %s\", version)\n        dbg(\" frame count:          %s\", frame_count)\n        dbg(\" facet count:          %s\", facet_count)\n        dbg(\" facets per animation: %s\", frames_per_facet)\n        spam(\" checksum:             %s\", checksum)\n        dbg(\" file size:            %s B\", file_size)\n        dbg(\" source format:        %s\", source_format)\n        dbg(\" comment:              %s\", comment.decode('ascii'))\n\n        # Frames store main graphic, shadow and outline layer headers\n        frame_header_offsets = list()\n\n        # read offsets of the smp frames\n        for i in range(frame_count):\n            frame_header_pointer = (SMP.smp_header.size +\n                                    i * SMP.smp_frame_header_offset.size)\n\n            frame_header_offset = SMP.smp_frame_header_offset.unpack_from(\n                data, frame_header_pointer)[0]\n\n            frame_header_offsets.append(frame_header_offset)\n\n        # SMP graphic frames are created from overlaying\n        # the main graphic layer with a shadow layer and\n        # and (for units) an outline layer\n        self.main_frames = list()\n        self.shadow_frames = list()\n        self.outline_frames = list()\n\n        spam(SMPLayerHeader.repr_header())\n\n        # read all smp_frame_header structs in a frame\n        for frame_offset in frame_header_offsets:\n\n            # how many layer headers are in the frame\n            frame_header_size = SMP.smp_frame_header_size.unpack_from(\n                data, frame_offset)[0]\n\n            for i in range(1, frame_header_size + 1):\n                layer_header_offset = (frame_offset +\n                                       i * SMP.smp_layer_header.size)\n\n                layer_header = SMPLayerHeader(*SMP.smp_layer_header.unpack_from(\n                    data, layer_header_offset), frame_offset)\n\n                if layer_header.layer_type == 0x02:\n                    # layer that store the main graphic\n                    self.main_frames.append(SMPLayer(SMPMainLayer(), layer_header, data))\n\n                elif layer_header.layer_type == 0x04:\n                    # layer that stores a shadow\n                    self.shadow_frames.append(SMPLayer(SMPShadowLayer(), layer_header, data))\n\n                elif layer_header.layer_type == 0x08 or \\\n                     layer_header.layer_type == 0x10:\n                    # layer that stores an outline\n                    self.outline_frames.append(SMPLayer(SMPOutlineLayer(), layer_header, data))\n\n                else:\n                    raise Exception(\n                        f\"unknown layer type: {layer_header.layer_type:#x} at offset {layer_header_offset:#x}\"\n                    )\n\n                spam(layer_header)\n\n    def get_frames(self, layer: int = 0) -> list[SMPLayer]:\n        \"\"\"\n        Get the frames in the SMP.\n\n        :param layer: Position of the layer (see LAYER_TYPES)\n                        - 0 = main graphics\n                        - 1 = shadow graphics\n                        - 2 = outline\n        \"\"\"\n        cdef list frames\n\n        layer_type = LAYER_TYPES.get(\n            layer,\n            SMPLayerType.MAIN\n        )\n\n        if layer_type is SMPLayerType.MAIN:\n            frames = self.main_frames\n\n        elif layer_type is SMPLayerType.SHADOW:\n            frames = self.shadow_frames\n\n        elif layer_type is SMPLayerType.OUTLINE:\n            frames = self.outline_frames\n\n        else:\n            frames = []\n\n        return frames\n\n    def __str__(self) -> str:\n        ret = list()\n\n        ret.extend([repr(self), \"\\n\", SMPLayerHeader.repr_header(), \"\\n\"])\n        for frame in self.main_frames:\n            ret.extend([repr(frame), \"\\n\"])\n        return \"\".join(ret)\n\n    def __repr__(self) -> str:\n        return f\"SMP image<{len(self.main_frames):d} frames>\"\n\n\nclass SMPLayerHeader:\n    \"\"\"\n    Header of a layer in the SMP file.\n    \"\"\"\n    def __init__(\n        self,\n        width: int,\n        height: int,\n        hotspot_x: int,\n        hotspot_y: int,\n        layer_type: int,\n        outline_table_offset: int,\n        qdl_table_offset: int,\n        flags: int,\n        frame_offset: int\n    ) -> None:\n        \"\"\"\n        Create a SMP layer header.\n\n        :param width: Width of the layer sprites.\n        :param height: Height of the layer sprites.\n        :param hotspot_x: X coordinate of the anchor point.\n        :param hotspot_y: Y coordinate of the anchor point.\n        :param layer_type: Type of the layer.\n        :param outline_table_offset: Offset of the outline table.\n        :param qdl_table_offset: Offset of the pixel command table.\n        :param flags: Flags of the layer.\n        :param frame_offset: Offset of the frame.\n        \"\"\"\n\n        self.size = (width, height)\n        self.hotspot = (hotspot_x, hotspot_y)\n\n        # 2 = normal, 4 = shadow, 8 = outline\n        self.layer_type = layer_type\n\n        # table offsets are relative to the frame offset\n        self.outline_table_offset = outline_table_offset + frame_offset\n        self.qdl_table_offset = qdl_table_offset + frame_offset\n\n        # the absolute offset of the frame\n        self.frame_offset = frame_offset\n\n        # flags\n        self.flags = flags\n\n        self.palette_number = -1\n\n    @staticmethod\n    def repr_header() -> str:\n        return (\"width x height | hotspot x/y | \"\n                \"layer type | \"\n                \"offset (outline table|qdl table)\"\n                )\n\n    def __repr__(self) -> str:\n        ret = (\n            \"% 5d x% 7d | \" % self.size,\n            \"% 4d /% 5d | \" % self.hotspot,\n            \"% 4d | \" % self.layer_type,\n            \"% 13d| \" % self.outline_table_offset,\n            \"        % 9d|\" % self.qdl_table_offset,\n        )\n        return \"\".join(ret)\n\n\ncdef class SMPLayer:\n    \"\"\"\n    Layer inside the SMP. you can imagine it as a frame of an animation.\n    \"\"\"\n\n    # struct smp_frame_row_edge {\n    #   unsigned short left_space;\n    #   unsigned short right_space;\n    # };\n    smp_frame_row_edge = Struct(endianness + \"H H\")\n\n    # struct smp_command_offset {\n    #   unsigned int offset;\n    # }\n    smp_command_offset = Struct(endianness + \"I\")\n\n    # layer and frame information\n    cdef object info\n\n    # for each row:\n    # contains (left, right, full_row) number of boundary pixels\n    cdef vector[boundary_def] boundaries\n\n    # stores the file offset for the first drawing command\n    cdef vector[int] cmd_offsets\n\n    # pixel matrix representing the final image\n    cdef vector[vector[pixel]] pcolor\n\n    def __init__(\n        self,\n        variant, # this argument must not be typed because cython can't handle it\n        layer_header: SMPLayerHeader,\n        data: bytes\n    ) -> None:\n        \"\"\"\n        Create a SMP layer.\n\n        :param variant: Type of the layer.\n        :param layer_header: Header information of the layer.\n        :param data: Layer data.\n        \"\"\"\n        self.init(variant, layer_header, data)\n\n    def init(self, SMPLayerVariant variant, layer_header: SMPLayerHeader, data: bytes) -> None:\n        \"\"\"\n        Read the pixel information of the SMP layer.\n\n        :param variant: Type of the layer.\n        :param layer_header: Header information of the layer.\n        :param data: Layer data.\n        \"\"\"\n        self.info = layer_header\n\n        if not (isinstance(data, bytes) or isinstance(data, bytearray)):\n            raise ValueError(\"Layer data must be some bytes object\")\n\n        # memory pointer\n        # convert the bytes obj to char*\n        cdef const uint8_t[::1] data_raw = data\n\n        cdef unsigned short left\n        cdef unsigned short right\n\n        cdef size_t i\n        cdef int cmd_offset\n\n        cdef size_t row_count = self.info.size[1]\n        self.pcolor.reserve(row_count)\n\n        # process bondary table\n        for i in range(row_count):\n            outline_entry_position = (self.info.outline_table_offset +\n                                      i * SMPLayer.smp_frame_row_edge.size)\n\n            left, right = SMPLayer.smp_frame_row_edge.unpack_from(\n                data, outline_entry_position\n            )\n\n            # is this row completely transparent?\n            if left == 0xFFFF or right == 0xFFFF:\n                self.boundaries.push_back(boundary_def(0, 0, True))\n            else:\n                self.boundaries.push_back(boundary_def(left, right, False))\n\n        # process cmd table\n        for i in range(row_count):\n            cmd_table_position = (self.info.qdl_table_offset +\n                                  i * SMPLayer.smp_command_offset.size)\n\n            cmd_offset = SMPLayer.smp_command_offset.unpack_from(\n                data, cmd_table_position)[0] + self.info.frame_offset\n            self.cmd_offsets.push_back(cmd_offset)\n\n        for i in range(row_count):\n            self.pcolor.push_back(self.create_color_row(variant, data_raw, i))\n\n    cdef vector[pixel] create_color_row(self,\n                                        SMPLayerVariant variant,\n                                        const uint8_t[::1] &data_raw,\n                                        Py_ssize_t rowid):\n        \"\"\"\n        Extract colors (pixels) for a pixel row in the layer.\n\n        :param variant: Type of the layer.\n        :param data_raw: Raw data of the layer.\n        :param rowid: Index of the current row in the layer.\n        \"\"\"\n\n        cdef vector[pixel] row_data\n        cdef Py_ssize_t i\n\n        first_cmd_offset = self.cmd_offsets[rowid]\n        cdef boundary_def bounds = self.boundaries[rowid]\n        cdef size_t pixel_count = self.info.size[0]\n\n        # preallocate memory\n        row_data.reserve(pixel_count)\n\n        # row is completely transparent\n        if bounds.full_row:\n            for _ in range(pixel_count):\n                row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n            return row_data\n\n        # start drawing the left transparent space\n        for i in range(bounds.left):\n            row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n        # process the drawing commands for this row.\n        self.process_drawing_cmds(variant,\n                                  data_raw,\n                                  row_data, rowid,\n                                  first_cmd_offset,\n                                  pixel_count - bounds.right)\n\n        # finish by filling up the right transparent space\n        for i in range(bounds.right):\n            row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n        # verify size of generated row\n        if row_data.size() != pixel_count:\n            got = row_data.size()\n            summary = (\n                f\"{got:d}/{pixel_count:d} -> row {rowid:d}, \"\n                f\"layer type {self.info.layer_type:x}, \"\n                f\"offset {first_cmd_offset:d} / {first_cmd_offset:#x}\"\n            )\n            message = (\n                f\"got {'LESS' if got < pixel_count else 'MORE'} pixels than expected: {summary}, \"\n                f\"missing: {abs(pixel_count - got):d}\"\n            )\n\n            raise Exception(message)\n\n        return row_data\n\n\n    @cython.boundscheck(False)\n    cdef void process_drawing_cmds(self,\n                                   SMPLayerVariant variant,\n                                   const uint8_t[::1] &data_raw,\n                                   vector[pixel] &row_data,\n                                   Py_ssize_t rowid,\n                                   Py_ssize_t first_cmd_offset,\n                                   size_t expected_size):\n        \"\"\"\n        Extract colors (pixels) from the drawing commands for a row in the layer.\n\n        :param variant: Type of the layer.\n        :param data_raw: Raw data of the layer.\n        :param row_data: Stores the extracted pixels. May be prefilled with transparent pixels.\n        :param rowid: Index of the current row in the layer.\n        :param first_cmd_offset: Offset of the first drawing command in the data.\n        :param expected_size: Expected number of pixels in the row.\n        \"\"\"\n\n        # position in the data blob, we start at the first command of this row\n        cdef Py_ssize_t dpos = first_cmd_offset\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd\n        cdef uint8_t nextbyte\n        cdef uint8_t lower_crumb\n        cdef int pixel_count\n\n        cdef vector[uint8_t] pixel_data\n        pixel_data.reserve(4)\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d} \" +\n                    f\"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d}\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos]\n\n            # Last 2 bits store command type\n            lower_crumb = 0b00000011 & cmd\n\n            # opcode: cmd, rowid: rowid\n\n            if lower_crumb == 0b00000011:\n                # eol (end of line) command, this row is finished now.\n                eor = True\n\n                if SMPLayerVariant is SMPShadowLayer:\n                    if row_data.size() < expected_size:\n                        # copy the last drawn pixel\n                        # (still stored in nextbyte)\n                        #\n                        # TODO: confirm that this is the\n                        #       right way to do it\n                        row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0))\n\n                continue\n\n            elif lower_crumb == 0b00000000:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = (cmd >> 2) + 1\n\n                pixel_count = (cmd >> 2) + 1\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n            elif lower_crumb == 0b00000001:\n                # color_list command\n                # draw the following 'count' pixels\n                # pixels are stored as 4 byte palette and meta infos\n                # count = (cmd >> 2) + 1\n\n                pixel_count = (cmd >> 2) + 1\n\n                for _ in range(pixel_count):\n                    if SMPLayerVariant is SMPShadowLayer:\n                        dpos += 1\n                        nextbyte = data_raw[dpos]\n                        row_data.push_back(pixel(color_shadow, nextbyte, 0, 0, 0))\n                    elif SMPLayerVariant is SMPOutlineLayer:\n                        # we don't know the color the game wants\n                        # so we just draw index 0\n                        row_data.push_back(pixel(color_outline, 0, 0, 0, 0))\n                    elif SMPLayerVariant is SMPMainLayer:\n                        for _ in range(4):\n                            dpos += 1\n                            pixel_data.push_back(data_raw[dpos])\n\n                        row_data.push_back(pixel(color_standard,\n                                                pixel_data[0],\n                                                pixel_data[1],\n                                                pixel_data[2],\n                                                pixel_data[3] & 0x1F)) # remove \"usage\" bit here\n                        pixel_data.clear()\n\n            elif lower_crumb == 0b00000010:\n                if (SMPLayerVariant is SMPMainLayer or SMPLayerVariant is SMPShadowLayer):\n                    # player_color command\n                    # draw the following 'count' pixels\n                    # pixels are stored as 4 byte palette and meta infos\n                    # count = (cmd >> 2) + 1\n\n                    pixel_count = (cmd >> 2) + 1\n\n                    for _ in range(pixel_count):\n                        if SMPLayerVariant is SMPShadowLayer:\n                            dpos += 1\n                            nextbyte = data_raw[dpos]\n                            row_data.push_back(pixel(color_shadow,\n                                                    nextbyte, 0, 0, 0))\n                        else:\n                            for _ in range(4):\n                                dpos += 1\n                                pixel_data.push_back(data_raw[dpos])\n\n                            row_data.push_back(pixel(color_player,\n                                                    pixel_data[0],\n                                                    pixel_data[1],\n                                                    pixel_data[2],\n                                                    pixel_data[3] & 0x1F))  # remove \"usage\" bit here\n                            pixel_data.clear()\n                else:\n                    raise Exception(\n                        f\"unknown smp {self.info.layer_type} layer drawing command: \" +\n                        f\"{cmd:#x} in row {rowid:d}\"\n                )\n\n            else:\n                raise Exception(\n                    f\"unknown smp {self.info.layer_type} layer drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\"\n                )\n\n            # process next command\n            dpos += 1\n\n        # end of row reached, return the created pixel array.\n        return\n\n\n    def get_picture_data(self, palette) -> numpy.ndarray:\n        \"\"\"\n        Convert the palette index matrix to a RGBA image.\n\n        :param palette: Color palette used for pixels in the sprite.\n        :type palette: .colortable.ColorTable\n        :return: Array of RGBA values.\n        \"\"\"\n        return determine_rgba_matrix(self.pcolor, palette)\n\n    def get_damage_mask(self) -> numpy.ndarray:\n        \"\"\"\n        Convert the 4th pixel byte to a mask used for damaged units.\n\n        :return: Damage mask of the layer.\n        \"\"\"\n        return determine_damage_matrix(self.pcolor)\n\n    def get_hotspot(self) -> tuple[int, int]:\n        \"\"\"\n        Return the layer's hotspot (the \"center\" of the image).\n\n        :return: Hotspot of the layer.\n        \"\"\"\n        return self.info.hotspot\n\n    def get_palette_number(self) -> int:\n        \"\"\"\n        Return the layer's palette number.\n\n        :return: Palette number of the layer.\n        \"\"\"\n        return self.pcolor[0][0].palette & 0b00111111\n\n    def __repr__(self) -> str:\n        return repr(self.info)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix,\n                                         numpy.ndarray[numpy.uint8_t, ndim=2, mode=\"c\"] palette):\n    \"\"\"\n    converts a palette index image matrix to an rgba matrix.\n\n    :param image_matrix: A 2-dimensional array of SMP pixels.\n    :param palette: Color palette used for normal pixels in the sprite.\n    \"\"\"\n\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t[:, ::1] m_lookup = palette\n\n    cdef uint8_t m_color_size = len(m_lookup[0])\n\n    cdef uint8_t r\n    cdef uint8_t g\n    cdef uint8_t b\n    cdef uint8_t alpha\n\n    cdef vector[pixel] current_row\n    cdef pixel px\n    cdef pixel_type px_type\n    cdef uint8_t px_index\n    cdef uint8_t px_palette\n\n    cdef uint16_t palette_section\n\n    cdef size_t x\n    cdef size_t y\n\n    for y in range(height):\n\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n            px_type = px.type\n            px_index = px.index\n            px_palette = px.palette\n\n            if px_type == color_standard:\n                # look up the palette secition\n                # palettes have 1024 entries\n                # divided into 4 sections\n                palette_section = px_palette & 0x03\n\n                # the index has to be adjusted\n                # to the palette section\n                index = px_index + (palette_section * 256)\n\n                # look up the color index in the\n                # main graphics table\n                if m_color_size == 3:\n                    # RGB tables (leftover from HD edition)\n                    r = m_lookup[index][0]\n                    g = m_lookup[index][1]\n                    b = m_lookup[index][2]\n\n                elif m_color_size == 4:\n                    # RGBA tables (but alpha is often unused)\n                    r = m_lookup[index][0]\n                    g = m_lookup[index][1]\n                    b = m_lookup[index][2]\n                    alpha = m_lookup[index][3]\n\n                # alpha values are unused\n                # in 0x0C and 0x0B version of SMPs\n                alpha = 255\n\n            elif px_type == color_transparent:\n                r, g, b, alpha = 0, 0, 0, 0\n\n            elif px_type == color_shadow:\n                r, g, b, alpha = 0, 0, 0, px_index\n\n                # change alpha values to match openage texture formats\n                # even alphas are used for commands marking *special* pixels (player color, etc.)\n                # odd alphas are used for normal pixels (= displayed as-is with transparency)\n                alpha = alpha | 0x01\n\n            else:\n                if px_type == color_player:\n                    alpha = 254\n\n                elif px_type == color_outline:\n                    alpha = 252\n\n                else:\n                    raise ValueError(\"unknown pixel type: %d\" % px_type)\n\n                # Store player color index in g channel\n                r, b = 0, 0\n                g = px_index\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix):\n    \"\"\"\n    converts the damage modifier values to an image using the RG values.\n\n    :param image_matrix: A 2-dimensional array of SMP pixels.\n    \"\"\"\n\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t r\n    cdef uint8_t g\n    cdef uint8_t b\n    cdef uint8_t alpha\n\n    cdef vector[pixel] current_row\n    cdef pixel px\n\n    cdef size_t x\n    cdef size_t y\n\n    for y in range(height):\n\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n\n            r, g, b, alpha = px.damage_modifier_1, px.damage_modifier_2, 0, 255\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n"
  },
  {
    "path": "openage/convert/value_object/read/media/smx.pyx",
    "content": "# Copyright 2019-2025 the openage authors. See copying.md for legal info.\n#\n# cython: infer_types=True\n\nfrom enum import Enum\nimport numpy\nfrom struct import Struct, unpack_from\n\nfrom .....log import spam, dbg\n\n\ncimport cython\ncimport numpy\n\nfrom libc.stdint cimport uint8_t, uint16_t\nfrom libcpp cimport bool\nfrom libcpp.vector cimport vector\n\n\n\n# SMX files have little endian byte order\nendianness = \"< \"\n\n\n# Boundary of a row in a SMX layer.\ncdef struct boundary_def:\n    Py_ssize_t left\n    Py_ssize_t right\n    bool full_row\n\n\n# SMX uses SMP pixel tytes\ncdef enum pixel_type:\n    color_standard      # standard pixel\n    color_shadow        # shadow pixel\n    color_transparent   # transparent pixel\n    color_player        # non-outline player color pixel\n    color_outline       # player color outline pixel\n\n\n# One uncompressed SMP pixel.\ncdef struct pixel:\n    pixel_type type\n    uint8_t index               # index in a palette section\n    uint8_t palette             # palette number and palette section\n    uint8_t damage_modifier_1   # modifier for damage (part 1)\n    uint8_t damage_modifier_2   # modifier for damage (part 2)\n\n\nclass SMXLayerType(Enum):\n    \"\"\"\n    SMX layer types.\n    \"\"\"\n    MAIN    = \"main\"\n    SHADOW  = \"shadow\"\n    OUTLINE = \"outline\"\n\n\ncdef public dict LAYER_TYPES = {\n    0: SMXLayerType.MAIN,\n    1: SMXLayerType.SHADOW,\n    2: SMXLayerType.OUTLINE,\n}\n\n\ncdef class SMXMainLayer8to5:\n    \"\"\"\n    Main graphics layer of an SMX (compressed with 8to5).\n    \"\"\"\n    pass\n\ncdef class SMXMainLayer4plus1:\n    \"\"\"\n    Main graphics layer of an SMX (compressed with 4plus1).\n    \"\"\"\n    pass\n\ncdef class SMXShadowLayer:\n    \"\"\"\n    Shadow layer of an SMX.\n    \"\"\"\n    pass\n\ncdef class SMXOutlineLayer:\n    \"\"\"\n    Outline layer of an SMX.\n    \"\"\"\n    pass\n\n\n# fused type for SMX layer variants\nctypedef fused SMXLayerVariant:\n    SMXMainLayer8to5\n    SMXMainLayer4plus1\n    SMXShadowLayer\n    SMXOutlineLayer\n\n\nclass SMX:\n    \"\"\"\n    Class for reading/converting compressed SMX files (delivered\n    with AoE2:DE by default).\n    \"\"\"\n\n    # struct smx_header {\n    #   char[4]        signature;\n    #   unsigned short version;\n    #   unsigned short frame_count;\n    #   unsigned int   file_size_comp;\n    #   unsigned int   file_size_uncomp;\n    #   char[16]       comment;\n    # };\n    smx_header = Struct(endianness + \"4s H H I I 16s\")\n\n    # struct smx_frame_header {\n    #   unsigned char frame_type;\n    #   unsigned char palette_index;\n    #   unsigned int  uncomp_size;\n    # };\n    smx_frame_header = Struct(endianness + \"B B I\")\n\n    # struct smx_layer_header {\n    #   unsigned short width;\n    #   unsigned short height;\n    #   short          hotspot_x;\n    #   short          hotspot_y;\n    #   unsigned int   distance_next_frame;\n    #   int            4 bytes;\n    # };\n    smx_layer_header = Struct(endianness + \"H H h h I i\")\n\n    def __init__(self, data: bytes):\n        \"\"\"\n        Read an SMX image file and store the frames in the object.\n\n        :param data: SMX file data.\n        \"\"\"\n        smx_header = SMX.smx_header.unpack_from(data)\n        self.smp_type, version, frame_count, file_size_comp,\\\n            file_size_uncomp, comment = smx_header\n\n        dbg(\"SMX\")\n        dbg(\" version:                %s\",   version)\n        dbg(\" frame count:            %s\",   frame_count)\n        dbg(\" file size compressed:   %s B\", file_size_comp + 0x20)   # 0x20 = SMX header size\n        dbg(\" file size uncompressed: %s B\", file_size_uncomp + 0x40) # 0x80 = SMP header size\n        dbg(\" comment:                %s\",   comment.decode('ascii'))\n\n        # SMX graphic frames are created from overlaying\n        # the main graphic frame with a shadow layer and\n        # and (for units) an outline layer\n        self.main_frames = list()\n        self.shadow_frames = list()\n        self.outline_frames = list()\n\n        spam(SMXLayerHeader.repr_header())\n\n        # SMX files have no offsets, we have to calculate them\n        current_offset = SMX.smx_header.size\n        for i in range(frame_count):\n            frame_header = SMX.smx_frame_header.unpack_from(\n                data, current_offset)\n\n            frame_type , palette_number, _ = frame_header\n\n            current_offset += SMX.smx_frame_header.size\n\n            layer_types = []\n\n            if frame_type & 0x01:\n                layer_types.append(SMXLayerType.MAIN)\n\n            if frame_type & 0x02:\n                layer_types.append(SMXLayerType.SHADOW)\n\n            if frame_type & 0x04:\n                layer_types.append(SMXLayerType.OUTLINE)\n\n            for layer_type in layer_types:\n                layer_header_data = SMX.smx_layer_header.unpack_from(\n                    data, current_offset)\n\n                width, height, hotspot_x, hotspot_y,\\\n                    distance_next_frame, _ = layer_header_data\n\n                current_offset += SMX.smx_layer_header.size\n\n                outline_table_offset = current_offset\n\n                # Skip outline table\n                current_offset += 4 * height\n\n                qdl_command_array_size = Struct(\"< I\").unpack_from(data, current_offset)[0]\n                current_offset += 4\n\n                # Read length of color table\n                if layer_type is SMXLayerType.MAIN:\n                    qdl_color_table_size = Struct(\"< I\").unpack_from(data, current_offset)[0]\n                    current_offset += 4\n                    qdl_color_table_offset = current_offset + qdl_command_array_size\n\n                else:\n                    qdl_color_table_size = 0\n                    qdl_color_table_offset = current_offset\n\n                qdl_command_table_offset = current_offset\n                current_offset += qdl_color_table_size + qdl_command_array_size\n\n                layer_header = SMXLayerHeader(layer_type, frame_type,\n                                              palette_number,\n                                              width, height, hotspot_x,\n                                              hotspot_y, outline_table_offset,\n                                              qdl_command_table_offset,\n                                              qdl_color_table_offset)\n\n                if layer_type is SMXLayerType.MAIN:\n                    if layer_header.compression_type == 0x08:\n                        self.main_frames.append(SMXLayer(SMXMainLayer8to5(), layer_header, data))\n\n                    elif layer_header.compression_type == 0x00:\n                        self.main_frames.append(SMXLayer(SMXMainLayer4plus1(), layer_header, data))\n\n                elif layer_type is SMXLayerType.SHADOW:\n                    self.shadow_frames.append(SMXLayer(SMXShadowLayer(), layer_header, data))\n\n                elif layer_type is SMXLayerType.OUTLINE:\n                    self.outline_frames.append(SMXLayer(SMXOutlineLayer(), layer_header, data))\n\n    def get_frames(self, layer: int = 0) -> list[SMXLayer]:\n        \"\"\"\n        Get the frames in the SMX.\n\n        :param layer: Position of the layer (see LAYER_TYPES)\n                        - 0 = main graphics\n                        - 1 = shadow graphics\n                        - 2 = outline\n        \"\"\"\n        cdef list frames\n\n        layer_type = LAYER_TYPES.get(\n            layer,\n            SMXLayerType.MAIN\n        )\n\n        if layer_type is SMXLayerType.MAIN:\n            frames = self.main_frames\n\n        elif layer_type is SMXLayerType.SHADOW:\n            frames = self.shadow_frames\n\n        elif layer_type is SMXLayerType.OUTLINE:\n            frames = self.outline_frames\n\n        else:\n            frames = []\n\n        return frames\n\n    def __str__(self):\n        ret = list()\n\n        ret.extend([repr(self), \"\\n\", SMXLayerHeader.repr_header(), \"\\n\"])\n        for frame in self.main_frames:\n            ret.extend([repr(frame), \"\\n\"])\n        return \"\".join(ret)\n\n    def __repr__(self):\n        return f\"{self.smp_type} image<{len(self.main_frames):d} frames>\"\n\n\nclass SMXLayerHeader:\n    def __init__(\n        self,\n        layer_type: SMXLayerType,\n        frame_type: int,\n        palette_number: int,\n        width: int,\n        height: int,\n        hotspot_x: int,\n        hotspot_y: int,\n        outline_table_offset: int,\n        qdl_command_table_offset: int,\n        qdl_color_table_offset: int\n    ) -> None:\n        \"\"\"\n        Stores the header of a layer including additional info about its frame.\n\n        :param layer_type: Type of layer.\n        :param frame_type: Type of the frame the layer belongs to.\n        :param palette_number: Palette number used for pixels in the frame.\n        :param width: Width of layer in pixels.\n        :param height: Height of layer in pixels.\n        :param hotspot_x: x coordinate of the hotspot used for anchoring.\n        :param hotspot_y: y coordinate of the hotspot used for anchoring.\n        :param outline_table_offset: Absolute position of the layer's outline table in the file.\n        :param qdl_command_table_offset: Absolute position of the layer's command table in the file.\n        :param qdl_color_table_offset: Absolute position of the layer's pixel data table in the file.\n        \"\"\"\n\n        self.size = (width, height)\n        self.hotspot = (hotspot_x, hotspot_y)\n\n        self.layer_type = layer_type\n        self.frame_type = frame_type\n        self.compression_type = frame_type & 8\n        self.palette_number = palette_number\n\n        self.outline_table_offset = outline_table_offset\n        self.qdl_command_table_offset = qdl_command_table_offset\n        self.qdl_color_table_offset = qdl_color_table_offset\n\n    @staticmethod\n    def repr_header() -> str:\n        return (\"layer type | width x height | \"\n                \"hotspot x/y | \"\n                )\n\n    def __repr__(self) -> str:\n        ret = (\n            \"% s | \" % self.layer_type,\n            \"% 5d x% 7d | \" % self.size,\n            \"% 4d /% 5d | \" % self.hotspot,\n            \"% 13d | \" % self.outline_table_offset,\n            \"        % 9d |\" % self.qdl_command_table_offset,\n            \"        % 9d |\" % self.qdl_color_table_offset,\n        )\n        return \"\".join(ret)\n\n\ncdef class SMXLayer:\n    \"\"\"\n    Layer inside the compressed SMX.\n    \"\"\"\n\n    # struct smp_layer_row_edge {\n    #   unsigned short left_space;\n    #   unsigned short right_space;\n    # };\n    smp_layer_row_edge = Struct(endianness + \"H H\")\n\n    # layer and frame information\n    cdef object info\n\n    # for each row:\n    # contains (left, right, full_row) number of boundary pixels\n    cdef vector[boundary_def] boundaries\n\n    # pixel matrix representing the final image\n    cdef vector[vector[pixel]] pcolor\n\n    def __init__(\n        self,\n        variant, # this argument must not be typed because cython can't handle it\n        layer_header: SMXLayerHeader,\n        data: bytes\n    ) -> None:\n        \"\"\"\n        Create a SMX layer.\n\n        :param variant: Type of the layer.\n        :param layer_header: Header information of the layer.\n        :param data: Layer data.\n        \"\"\"\n        self.init(variant, layer_header, data)\n\n    def init(self, SMXLayerVariant variant, layer_header: SMXLayerHeader, data: bytes) -> None:\n        \"\"\"\n        SMX layer definition. There can be various types of layers inside an SMX frame.\n\n        :param variant: Type of the layer.\n        :param layer_header: Header definition of the layer.\n        :param data: File content as bytes.\n        \"\"\"\n        self.info = layer_header\n\n        if not (isinstance(data, bytes) or isinstance(data, bytearray)):\n            raise ValueError(\"Layer data must be some bytes object\")\n\n        # memory pointer\n        # convert the bytes obj to char*\n        cdef const uint8_t[::1] data_raw = data\n\n        cdef unsigned short left\n        cdef unsigned short right\n\n        cdef size_t i\n        cdef size_t row_count = self.info.size[1]\n        self.pcolor.reserve(row_count)\n\n        # process bondary table\n        for i in range(row_count):\n            outline_entry_position = (self.info.outline_table_offset +\n                                      i * SMXLayer.smp_layer_row_edge.size)\n\n            left, right = SMXLayer.smp_layer_row_edge.unpack_from(\n                data, outline_entry_position\n            )\n\n            # is this row completely transparent?\n            if left == 0xFFFF or right == 0xFFFF:\n                self.boundaries.push_back(boundary_def(0, 0, True))\n            else:\n                self.boundaries.push_back(boundary_def(left, right, False))\n\n        cdef int cmd_offset = self.info.qdl_command_table_offset\n        cdef int color_offset = self.info.qdl_color_table_offset\n        cdef int chunk_pos = 0\n\n        # process cmd table\n        for i in range(row_count):\n            cmd_offset, color_offset, chunk_pos, row_data = \\\n                self.create_color_row(variant, data_raw, i, cmd_offset, color_offset, chunk_pos)\n            self.pcolor.push_back(row_data)\n\n\n    cdef inline (int, int, int, vector[pixel]) create_color_row(self,\n                                                                SMXLayerVariant variant,\n                                                                const uint8_t[::1] &data_raw,\n                                                                Py_ssize_t rowid,\n                                                                int cmd_offset,\n                                                                int color_offset,\n                                                                int chunk_pos):\n        \"\"\"\n        Extract colors (pixels) for a pixel row in the layer.\n\n        :param variant: Type of the layer.\n        :param data_raw: Raw data of the layer.\n        :param rowid: Index of the current row in the layer.\n        :param cmd_offset: Offset of the command table of the layer.\n        :param color_offset: Offset of the color table of the layer.\n        :param chunk_pos: Current position in the compressed chunk.\n        \"\"\"\n\n        cdef vector[pixel] row_data\n        cdef Py_ssize_t i\n\n        cdef int first_cmd_offset = cmd_offset\n        cdef int first_color_offset = color_offset\n        cdef boundary_def bounds = self.boundaries[rowid]\n        cdef size_t pixel_count = self.info.size[0]\n\n        # preallocate memory\n        row_data.reserve(pixel_count)\n\n        # row is completely transparent\n        if bounds.full_row:\n            for _ in range(pixel_count):\n                row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n            return cmd_offset, color_offset, chunk_pos, row_data\n\n        # start drawing the left transparent space\n        for i in range(bounds.left):\n            row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n        # process the drawing commands for this row.\n        next_cmd_offset, next_color_offset, chunk_pos, row_data = \\\n            self.process_drawing_cmds(\n                variant,\n                data_raw,\n                row_data,\n                rowid,\n                first_cmd_offset,\n                first_color_offset,\n                chunk_pos,\n                pixel_count - bounds.right\n            )\n\n        # finish by filling up the right transparent space\n        for i in range(bounds.right):\n            row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n        # verify size of generated row\n        if row_data.size() != pixel_count:\n            got = row_data.size()\n            summary = \"%d/%d -> row %d, layer type %s, offset %d / %#x\" % (\n                got, pixel_count, rowid, repr(self.info.layer_type),\n                first_cmd_offset, first_cmd_offset\n                )\n            txt = \"got %%s pixels than expected: %s, missing: %d\" % (\n                summary, abs(pixel_count - got))\n\n            raise Exception(txt % (\"LESS\" if got < pixel_count else \"MORE\"))\n\n        return next_cmd_offset, next_color_offset, chunk_pos, row_data\n\n\n    @cython.boundscheck(False)\n    cdef inline (int, int, int, vector[pixel]) process_drawing_cmds(self,\n                                                                    SMXLayerVariant variant,\n                                                                    const uint8_t[::1] &data_raw,\n                                                                    vector[pixel] &row_data,\n                                                                    Py_ssize_t rowid,\n                                                                    Py_ssize_t first_cmd_offset,\n                                                                    Py_ssize_t first_color_offset,\n                                                                    int chunk_pos,\n                                                                    size_t expected_size):\n        \"\"\"\n        extract colors (pixels) for the drawing commands that were\n        compressed with 8to5 compression.\n\n        :param variant: Type of the layer.\n        :param data_raw: Raw data of the layer.\n        :param row_data: Stores the extracted pixels. May be prefilled with transparent pixels.\n        :param rowid: Row index.\n        :param first_cmd_offset: Offset of the first drawing command in the data.\n        :param first_color_offset: Offset of the first color command in the data.\n        :param chunk_pos: Current position in the compressed chunk.\n        :param expected_size: Expected number of pixels in the row.\n        \"\"\"\n        # position in the command array, we start at the first command of this row\n        cdef Py_ssize_t dpos_cmd = first_cmd_offset\n\n        # Position in the pixel data array\n        cdef Py_ssize_t dpos_color = first_color_offset\n\n        # Position in the compression chunk.\n        cdef bool odd = chunk_pos\n        cdef int px_dpos = 0 # For loop iterator\n\n        # is the end of the current row reached?\n        cdef bool eor = False\n\n        cdef uint8_t cmd = 0\n        cdef uint8_t lower_crumb = 0\n        cdef int pixel_count = 0\n        cdef vector[uint8_t] pixel_data\n        pixel_data.reserve(4)\n\n        # Pixel data temporary values that need further decompression\n        cdef uint8_t pixel_data_odd_0 = 0\n        cdef uint8_t pixel_data_odd_1 = 0\n        cdef uint8_t pixel_data_odd_2 = 0\n        cdef uint8_t pixel_data_odd_3 = 0\n\n        # Mask for even indices\n        # cdef uint8_t pixel_mask_even_0 = 0xFF\n        cdef uint8_t pixel_mask_even_1 = 0b00000011\n        cdef uint8_t pixel_mask_even_2 = 0b11110000\n        cdef uint8_t pixel_mask_even_3 = 0b00111111\n\n        if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1:\n            # Position in the pixel data array\n            dpos_color = first_color_offset\n\n        cdef uint8_t palette_section_block = 0\n        cdef uint8_t palette_section = 0\n        cdef uint8_t nextbyte = 0\n\n        # work through commands till end of row.\n        while not eor:\n            if row_data.size() > expected_size:\n                raise Exception(\n                    f\"Only {expected_size:d} pixels should be drawn in row {rowid:d} \"\n                    f\"with layer type {self.info.layer_type:#x}, but we have {row_data.size():d} \"\n                    f\"already!\"\n                )\n\n            # fetch drawing instruction\n            cmd = data_raw[dpos_cmd]\n\n            # Last 2 bits store command type\n            lower_crumb = 0b00000011 & cmd\n\n            if lower_crumb == 0b00000011:\n                # eor (end of row) command, this row is finished now.\n                eor = True\n                dpos_cmd += 1\n\n                # shadows sometimes need an extra pixel at\n                # the end\n                if SMXLayerVariant is SMXShadowLayer:\n                    if row_data.size() < expected_size:\n                        # copy the last drawn pixel\n                        # (still stored in nextbyte)\n                        #\n                        # TODO: confirm that this is the\n                        #       right way to do it\n                        row_data.push_back(pixel(color_shadow,\n                                                nextbyte, 0, 0, 0))\n                continue\n\n            elif lower_crumb == 0b00000000:\n                # skip command\n                # draw 'count' transparent pixels\n                # count = (cmd >> 2) + 1\n\n                pixel_count = (cmd >> 2) + 1\n\n                for _ in range(pixel_count):\n                    row_data.push_back(pixel(color_transparent, 0, 0, 0, 0))\n\n            elif lower_crumb == 0b00000001:\n                # color_list command\n                # draw the following 'count' pixels\n                # pixels are stored in 5 byte chunks\n                # even pixel indices have their info stored\n                # in byte[0] - byte[3]. odd pixel indices have\n                # their info stored in byte[1] - byte[4].\n                # count = (cmd >> 2) + 1\n\n                pixel_count = (cmd >> 2) + 1\n\n                if SMXLayerVariant is SMXMainLayer8to5:\n                    pixel_data.reserve(4)\n                    for _ in range(pixel_count):\n                        # Start fetching pixel data\n                        if odd:\n                            # Odd indices require manual extraction of each of the 4 values\n\n                            # Palette index. Essentially a rotation of (byte[1]byte[2])\n                            # by 6 to the left, then masking with 0x00FF.\n                            pixel_data_odd_0 = data_raw[dpos_color + 1]\n                            pixel_data_odd_1 = data_raw[dpos_color + 2]\n                            pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6))\n\n                            # Palette section. Described in byte[2] in bits 4-5.\n                            pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03)\n\n                            # Damage mask 1. Essentially a rotation of (byte[3]byte[4])\n                            # by 6 to the left, then masking with 0x00F0.\n                            pixel_data_odd_2 = data_raw[dpos_color + 3]\n                            pixel_data_odd_3 = data_raw[dpos_color + 4]\n                            pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0)\n\n                            # Damage mask 2. Described in byte[4] in bits 0-5.\n                            pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F)\n\n                            row_data.push_back(pixel(color_standard,\n                                                    pixel_data[0],\n                                                    pixel_data[1],\n                                                    pixel_data[2],\n                                                    pixel_data[3]))\n\n                            # Go to next pixel\n                            dpos_color += 5\n\n                        else:\n                            # Even indices can be read \"as is\". They just have to be masked.\n                            for i in range(4):\n                                pixel_data.push_back(data_raw[dpos_color + i])\n\n                            row_data.push_back(pixel(color_standard,\n                                                    pixel_data[0],\n                                                    pixel_data[1] & pixel_mask_even_1,\n                                                    pixel_data[2] & pixel_mask_even_2,\n                                                    pixel_data[3] & pixel_mask_even_3))\n\n                        odd = not odd\n                        pixel_data.clear()\n\n                if SMXLayerVariant is SMXMainLayer4plus1:\n                    palette_section_block = data_raw[dpos_color + (4 - chunk_pos)]\n\n                    for _ in range(pixel_count):\n                        # Start fetching pixel data\n                        palette_section = (\n                            palette_section_block >> (2 * chunk_pos)) & 0x03\n                        row_data.push_back(pixel(color_standard,\n                                                data_raw[dpos_color],\n                                                palette_section,\n                                                0,\n                                                0))\n\n                        dpos_color += 1\n                        chunk_pos += 1\n\n                        # Skip to next chunk\n                        if chunk_pos > 3:\n                            chunk_pos = 0\n                            dpos_color += 1  # Skip palette section block\n                            palette_section_block = data_raw[dpos_color + 4]\n\n                if SMXLayerVariant is SMXShadowLayer:\n                    for _ in range(pixel_count):\n                        dpos_cmd += 1\n                        nextbyte = data_raw[dpos_cmd]\n\n                        row_data.push_back(pixel(color_shadow,\n                                                nextbyte, 0, 0, 0))\n\n                if SMXLayerVariant is SMXOutlineLayer:\n                    # we don't know the color the game wants\n                    # so we just draw index 0\n                    for _ in range(pixel_count):\n                        row_data.push_back(pixel(color_outline,\n                                                0, 0, 0, 0))\n\n            elif lower_crumb == 0b00000010:\n                if SMXLayerVariant is SMXMainLayer8to5:\n                    # player_color command\n                    # draw the following 'count' pixels\n                    # pixels are stored in 5 byte chunks\n                    # even pixel indices have their info stored\n                    # in byte[0] - byte[3]. odd pixel indices have\n                    # their info stored in byte[1] - byte[4].\n                    # count = (cmd >> 2) + 1\n\n                    pixel_count = (cmd >> 2) + 1\n\n                    for _ in range(pixel_count):\n                        # Start fetching pixel data\n                        if odd:\n                            # Odd indices require manual extraction of each of the 4 values\n\n                            # Palette index. Essentially a rotation of (byte[1]byte[2])\n                            # by 6 to the left, then masking with 0x00FF.\n                            pixel_data_odd_0 = data_raw[dpos_color + 1]\n                            pixel_data_odd_1 = data_raw[dpos_color + 2]\n                            pixel_data.push_back((pixel_data_odd_0 >> 2) | (pixel_data_odd_1 << 6))\n\n                            # Palette section. Described in byte[2] in bits 4-5.\n                            pixel_data.push_back((pixel_data_odd_1 >> 2) & 0x03)\n\n                            # Damage modifier 1. Essentially a rotation of (byte[3]byte[4])\n                            # by 6 to the left, then masking with 0x00F0.\n                            pixel_data_odd_2 = data_raw[dpos_color + 3]\n                            pixel_data_odd_3 = data_raw[dpos_color + 4]\n                            pixel_data.push_back(((pixel_data_odd_2 >> 2) | (pixel_data_odd_3 << 6)) & 0xF0)\n\n                            # Damage modifier 2. Described in byte[4] in bits 0-5.\n                            pixel_data.push_back((pixel_data_odd_3 >> 2) & 0x3F)\n\n                            row_data.push_back(pixel(color_player,\n                                                    pixel_data[0],\n                                                    pixel_data[1],\n                                                    pixel_data[2],\n                                                    pixel_data[3]))\n\n                            # Go to next pixel\n                            dpos_color += 5\n\n                        else:\n                            # Even indices can be read \"as is\". They just have to be masked.\n                            for px_dpos in range(4):\n                                pixel_data.push_back(data_raw[dpos_color + px_dpos])\n\n                            row_data.push_back(pixel(color_player,\n                                                    pixel_data[0],\n                                                    pixel_data[1] & pixel_mask_even_1,\n                                                    pixel_data[2] & pixel_mask_even_2,\n                                                    pixel_data[3] & pixel_mask_even_3))\n\n                        odd = not odd\n                        pixel_data.clear()\n\n                elif SMXLayerVariant is SMXMainLayer4plus1:\n                    # player_color command\n                    # draw the following 'count' pixels\n                    # 4 pixels are stored in every 5 byte chunk.\n                    # palette indices are contained in byte[0] - byte[3]\n                    # palette sections are stored in byte[4]\n                    # count = (cmd >> 2) + 1\n\n                    pixel_count = (cmd >> 2) + 1\n\n                    for _ in range(pixel_count):\n                        # Start fetching pixel data\n                        palette_section = (palette_section_block >> (2 * chunk_pos)) & 0x03\n                        row_data.push_back(pixel(color_player,\n                                                data_raw[dpos_color],\n                                                palette_section,\n                                                0,\n                                                0))\n\n                        dpos_color += 1\n                        chunk_pos += 1\n\n                        # Skip to next chunk\n                        if chunk_pos > 3:\n                            chunk_pos = 0\n                            dpos_color += 1 # Skip palette section block\n                            palette_section_block = data_raw[dpos_color + 4]\n\n            else:\n                raise Exception(\n                    f\"unknown smx main graphics layer drawing command: \" +\n                    f\"{cmd:#x} in row {rowid:d}\"\n                )\n\n            # Process next command\n            dpos_cmd += 1\n\n        if SMXLayerVariant is SMXMainLayer8to5 or SMXLayerVariant is SMXMainLayer4plus1:\n            return dpos_cmd, dpos_color, chunk_pos, row_data\n        elif SMXLayerVariant is SMXOutlineLayer or SMXLayerVariant is SMXShadowLayer:\n            return dpos_cmd, dpos_cmd, chunk_pos, row_data\n\n\n    def get_picture_data(self, palette) -> numpy.ndarray:\n        \"\"\"\n        Convert the palette index matrix to a RGBA image.\n\n        :param palette: Color palette used for pixels in the sprite.\n        :type palette: .colortable.ColorTable\n        :return: Array of RGBA values.\n        \"\"\"\n        return determine_rgba_matrix(self.pcolor, palette)\n\n    def get_hotspot(self) -> tuple[int, int]:\n        \"\"\"\n        Return the layer's hotspot (the \"center\" of the image).\n\n        :return: Hotspot of the layer.\n        \"\"\"\n        return self.info.hotspot\n\n    def get_palette_number(self) -> int:\n        \"\"\"\n        Return the layer's palette number.\n\n        :return: Palette number of the layer.\n        \"\"\"\n        return self.info.palette_number\n\n    def __repr__(self):\n        return repr(self.info)\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_rgba_matrix(vector[vector[pixel]] &image_matrix,\n                                         numpy.ndarray[numpy.uint8_t, ndim=2, mode=\"c\"] palette):\n    \"\"\"\n    converts a palette index image matrix to an rgba matrix.\n\n    :param image_matrix: A 2-dimensional array of SMP pixels.\n    :param palette: Color palette used for normal pixels in the sprite.\n    \"\"\"\n\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t[:, ::1] m_lookup = palette\n\n    cdef uint8_t r = 0\n    cdef uint8_t g = 0\n    cdef uint8_t b = 0\n    cdef uint8_t alpha = 0\n\n    cdef vector[pixel] current_row\n    cdef pixel px\n    cdef pixel_type px_type\n    cdef uint8_t px_index = 0\n    cdef uint8_t px_palette = 0\n\n    cdef uint16_t palette_section = 0\n\n    cdef size_t x = 0\n    cdef size_t y = 0\n\n    for y in range(height):\n\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n\n            px_type = px.type\n            px_index = px.index\n            px_palette = px.palette\n\n            if px_type == color_standard:\n                # look up the palette secition\n                # palettes have 1024 entries\n                # divided into 4 sections\n                palette_section = px_palette\n\n                # the index has to be adjusted\n                # to the palette section\n                index = px_index + (palette_section * 256)\n\n                # look up the color index in the\n                # main graphics table\n                r = m_lookup[index][0]\n                g = m_lookup[index][1]\n                b = m_lookup[index][2]\n                alpha = m_lookup[index][3]\n\n                # alpha values are unused\n                # in 0x0C and 0x0B version of SMP/SMX\n                alpha = 255\n\n            elif px_type == color_transparent:\n                r, g, b, alpha = 0, 0, 0, 0\n\n            elif px_type == color_shadow:\n                r, g, b, alpha = 0, 0, 0, px_index\n\n                # change alpha values to match openage texture formats\n                # even alphas are used for commands marking *special* pixels (player color, etc.)\n                # odd alphas are used for normal pixels (= displayed as-is with transparency)\n                alpha = alpha | 0x01\n\n            else:\n                if px_type == color_player:\n                    alpha = 254\n\n                elif px_type == color_outline:\n                    alpha = 252\n\n                else:\n                    raise ValueError(f\"unknown pixel type: {px_type:#x}\")\n\n                # Store player color index in g channel\n                r, b = 0, 0\n                g = px_index\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n\n\n@cython.boundscheck(False)\n@cython.wraparound(False)\ncdef numpy.ndarray determine_damage_matrix(vector[vector[pixel]] &image_matrix):\n    \"\"\"\n    converts the damage modifier values to an image using the RG values.\n\n    :param image_matrix: A 2-dimensional array of SMP pixels.\n    \"\"\"\n\n    cdef size_t height = image_matrix.size()\n    cdef size_t width = image_matrix[0].size()\n\n    cdef numpy.ndarray[numpy.uint8_t, ndim=3, mode=\"c\"] array_data = \\\n        numpy.zeros((height, width, 4), dtype=numpy.uint8)\n\n    cdef uint8_t r = 0\n    cdef uint8_t g = 0\n    cdef uint8_t b = 0\n    cdef uint8_t alpha = 0\n\n    cdef vector[pixel] current_row\n    cdef pixel px\n\n    cdef size_t x = 0\n    cdef size_t y = 0\n\n    for y in range(height):\n\n        current_row = image_matrix[y]\n\n        for x in range(width):\n            px = current_row[x]\n\n            r, g, b, alpha = px.damage_modifier_1, px.damage_modifier_2, 0, 255\n\n            # array_data[y, x] = (r, g, b, alpha)\n            array_data[y, x, 0] = r\n            array_data[y, x, 1] = g\n            array_data[y, x, 2] = b\n            array_data[y, x, 3] = alpha\n\n    return array_data\n"
  },
  {
    "path": "openage/convert/value_object/read/media_types.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nMedia types used in games. Media types refer to a group\nof file types used in the game.\n\"\"\"\n\nfrom enum import Enum\n\n\nclass MediaType(Enum):\n    \"\"\"\n    A type of media. Stores the mount point as the value.\n    \"\"\"\n    DATFILE   = \"data\"\n    GAMEDATA  = \"gamedata\"\n    GRAPHICS  = \"graphics\"\n    INTERFACE = \"interface\"\n    LANGUAGE  = \"language\"\n    PALETTES  = \"palettes\"\n    TERRAIN   = \"terrain\"\n    SOUNDS    = \"sounds\"\n    BLEND     = \"blend\"\n    BORDER    = \"border\"\n"
  },
  {
    "path": "openage/convert/value_object/read/member_access.py",
    "content": "# Copyright 2014-2020 the openage authors. See copying.md for legal info.\n\n# TODO pylint: disable=C\n\nfrom enum import Enum\n\n\n# global member type modifiers\nclass MemberAccess(Enum):\n    # pylint doesn't understand that this Enum doesn't need any members.\n    # pylint: disable=too-few-public-methods\n\n    READ          = \"READ\"\n    READ_GEN      = \"READ_GEN\"\n    NOREAD_EXPORT = \"NOREAD_EXPORT\"\n    READ_UNKNOWN  = \"READ_UNKNOWN\"\n    SKIP          = \"SKIP\"\n\n\n# TODO those values are made available in the module's global namespace\n#      for legacy reasons, to avoid breaking other modules that import\n#      them this way.\n\nREAD = MemberAccess.READ                        # Reads the data\nREAD_GEN = MemberAccess.READ_GEN\nNOREAD_EXPORT = MemberAccess.NOREAD_EXPORT\nREAD_UNKNOWN = MemberAccess.READ_UNKNOWN        # For unknown chunks of data\nSKIP = MemberAccess.SKIP\n"
  },
  {
    "path": "openage/convert/value_object/read/read_members.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n#\n# TODO pylint: disable=C,R,abstract-method\n\nfrom __future__ import annotations\nimport typing\n\nfrom enum import Enum\n\n\nif typing.TYPE_CHECKING:\n    from openage.convert.value_object.read.genie_structure import GenieStructure\n    from openage.convert.value_object.read.member_access import MemberAccess\n    from openage.convert.value_object.read.value_members import StorageType\n\n\nclass ReadMember:\n    \"\"\"\n    member variable of data files and generated structs.\n\n    equals:\n    * data field in the .dat file\n    \"\"\"\n\n    def __init__(self):\n        self.length = 1\n        self.raw_type = None\n        self.do_raw_read = True\n\n    def entry_hook(self, data: typing.Any) -> typing.Any:\n        \"\"\"\n        allows the data member class to modify the input data\n\n        is used e.g. for the number => enum lookup\n        \"\"\"\n        return data\n\n    def get_empty_value(self) -> int:\n        \"\"\"\n        when this data field is not filled, use the returned value instead.\n        \"\"\"\n        return 0\n\n    def get_length(self, obj: typing.Any = None) -> int:\n        del obj  # unused\n        return self.length\n\n    def verify_read_data(self, obj: typing.Any, data: typing.Any) -> bool:\n        \"\"\"\n        gets called for each entry. used to check for storage validity (e.g. 0 expected)\n        \"\"\"\n        del obj, data  # unused\n        return True\n\n    def __repr__(self):\n        raise NotImplementedError(\n            f\"return short description of the member type {type(self)}\")\n\n\nclass GroupMember(ReadMember):\n    \"\"\"\n    member that references to another class, pretty much like the SubdataMember,\n    but with a fixed length of 1.\n\n    this is just a reference to a single struct instance.\n    \"\"\"\n\n    def __init__(self, cls: ReadMember):\n        super().__init__()\n        self.cls = cls\n\n    def __repr__(self):\n        return f\"GroupMember<{repr(self.cls)}>\"\n\n\nclass IncludeMembers(GroupMember):\n    \"\"\"\n    a member that requires evaluating the given class\n    as a include first.\n\n    example:\n    the unit class \"building\" and \"movable\" will both have\n    common members that have to be read first.\n    \"\"\"\n\n    def __repr__(self):\n        return f\"IncludeMember<{repr(self.cls)}>\"\n\n\nclass DynLengthMember(ReadMember):\n    \"\"\"\n    a member that can have a dynamic length.\n    \"\"\"\n\n    any_length = \"any_length\"\n\n    def __init__(self, length: typing.Union[typing.Callable, int, str, typing.Literal[\"any_length\"]]):\n        super().__init__()\n\n        type_ok = False\n\n        if isinstance(length, int) or isinstance(length, str) or (length is self.any_length):\n            type_ok = True\n\n        if callable(length):\n            type_ok = True\n\n        if not type_ok:\n            raise TypeError(\"invalid length type passed to %s: %s<%s>\" % (\n                type(self), length, type(length)))\n\n        self.length = length\n\n    def get_length(self, obj: typing.Any = None) -> int:\n        if self.is_dynamic_length():\n            if self.length is self.any_length:\n                return self.any_length\n\n            if not obj:\n                raise ValueError(\"dynamic length query requires source object\")\n\n            if callable(self.length):\n                # length is a lambda that determines the length by some fancy manner\n                # pass the target object as lambda parameter.\n                length_def = self.length(obj)\n\n                # if the lambda returns a non-dynamic length (aka a number)\n                # return it directly. otherwise, the returned variable name\n                # has to be looked up again.\n                if not self.is_dynamic_length(target=length_def):\n                    return length_def\n\n            else:\n                # self.length specifies the attribute name where the length is\n                # stored\n                length_def = self.length\n\n            # look up the given member name and return the value.\n            if not isinstance(length_def, str):\n                raise TypeError(\"length lookup definition is not str: %s<%s>\" % (\n                    length_def, type(length_def)))\n\n            return getattr(obj, length_def)\n\n        else:\n            # non-dynamic length (aka plain number) gets returned directly\n            return self.length\n\n    def is_dynamic_length(\n        self,\n        target: typing.Union[typing.Callable, int, str, typing.Literal[\"any_length\"]] = None\n    ):\n        if target is None:\n            target = self.length\n\n        if target is self.any_length:\n            return True\n        elif isinstance(target, str):\n            return True\n        elif isinstance(target, int):\n            return False\n        elif callable(target):\n            return True\n        else:\n            raise TypeError(f\"unknown length definition supplied: {target}\")\n\n\nclass RefMember(ReadMember):\n    \"\"\"\n    a struct member that can be referenced/references another struct.\n    \"\"\"\n\n    def __init__(self, type_name: str, file_name: str):\n        ReadMember.__init__(self)\n        self.type_name = type_name\n        self.file_name = file_name\n\n        # xrefs not supported yet.\n        # would allow reusing a struct definition that lies in another file\n        self.resolved = False\n\n\nclass NumberMember(ReadMember):\n    \"\"\"\n    this struct member/data column contains simple numbers\n    \"\"\"\n\n    # primitive types, directly parsable by sscanf\n    type_scan_lookup = {\n        \"char\":          \"hhd\",\n        \"int8_t\":        \"hhd\",\n        \"uint8_t\":       \"hhu\",\n        \"int16_t\":       \"hd\",\n        \"uint16_t\":      \"hu\",\n        \"int\":           \"d\",\n        \"int32_t\":       \"d\",\n        \"uint\":          \"u\",\n        \"uint32_t\":      \"u\",\n        \"float\":         \"f\",\n    }\n\n    def __init__(self, number_def: str):\n        super().__init__()\n        if number_def not in self.type_scan_lookup:\n            raise TypeError(\n                f\"created number column from unknown type {number_def}\")\n\n        # type used for the output struct\n        self.number_type = number_def\n        self.raw_type = number_def\n\n    def __repr__(self):\n        return self.number_type\n\n\nclass ZeroMember(NumberMember):\n    \"\"\"\n    data field that is known to always needs to be zero.\n    neat for finding offset errors.\n    \"\"\"\n\n    def __init__(self, raw_type: ReadMember, length: int = 1):\n        super().__init__(raw_type)\n        self.length = length\n\n    def verify_read_data(self, obj: typing.Any, data: typing.Collection) -> bool:\n        # fail if a single value of data != 0\n        if any(False if v == 0 else True for v in data):\n            return False\n        else:\n            return True\n\n\nclass ContinueReadMemberResult(Enum):\n    ABORT = \"data_absent\"\n    CONTINUE = \"data_exists\"\n\n    def __str__(self):\n        return str(self.value)\n\n\nclass ContinueReadMember(NumberMember):\n    \"\"\"\n    data field that aborts reading further members of the class\n    when its value == 0.\n    \"\"\"\n\n    result = ContinueReadMemberResult\n\n    def entry_hook(self, data: int) -> str:\n        if data == 0:\n            return self.result.ABORT\n        else:\n            return self.result.CONTINUE\n\n    def get_empty_value(self) -> int:\n        return 0\n\n\nclass EnumMember(RefMember):\n    \"\"\"\n    this struct member/data column is a enum.\n    \"\"\"\n\n    def __init__(\n        self,\n        type_name: str,\n        values: dict[typing.Any, typing.Any],\n        file_name: str = None\n    ):\n        super().__init__(type_name, file_name)\n        self.values = values\n        self.resolved = True    # TODO, xrefs not supported yet.\n\n    def validate_value(self, value: typing.Any) -> bool:\n        return value in self.values\n\n    def __repr__(self):\n        return f\"enum {self.type_name}\"\n\n\nclass EnumLookupMember(EnumMember):\n    \"\"\"\n    enum definition, does lookup of raw datfile data => enum value\n    \"\"\"\n\n    def __init__(\n        self,\n        type_name: str,\n        lookup_dict: dict[typing.Any, typing.Any],\n        raw_type: str,\n        file_name: str = None\n    ):\n        super().__init__(\n            type_name,\n            [v for k, v in sorted(lookup_dict.items())],\n            file_name\n        )\n        self.lookup_dict = lookup_dict\n        self.raw_type = raw_type\n\n    def entry_hook(self, data: typing.Any) -> typing.Any:\n        \"\"\"\n        perform lookup of raw data -> enum member name\n        \"\"\"\n\n        try:\n            return self.lookup_dict[data]\n        except KeyError:\n            try:\n                h = f\" = {hex(data)}\"\n            except TypeError:\n                h = \"\"\n            raise KeyError(\"failed to find %s%s in lookup dict %s!\" %\n                           (str(data), h, self.type_name)) from None\n\n\nclass CharArrayMember(DynLengthMember):\n    \"\"\"\n    struct member/column type that allows to store equal-length char[n].\n    \"\"\"\n\n    def __init__(self, length: int):\n        super().__init__(length)\n        self.raw_type = \"char[]\"\n\n    def get_empty_value(self) -> typing.Literal[\"\"]:\n        return \"\"\n\n    def __repr__(self):\n        return f\"{self.raw_type}[{self.length}]\"\n\n\nclass StringMember(CharArrayMember):\n    \"\"\"\n    member with unspecified string length, aka std::string\n    \"\"\"\n\n    def __init__(self):\n        super().__init__(DynLengthMember.any_length)\n\n\nclass MultisubtypeMember(RefMember, DynLengthMember):\n    \"\"\"\n    struct member/data column that groups multiple references to\n    multiple other data sets.\n    \"\"\"\n\n    def __init__(\n        self,\n        type_name: str,\n        subtype_definition: tuple[MemberAccess, str, StorageType, typing.Union[str, ReadMember]],\n        class_lookup: dict[typing.Any, GenieStructure],\n        length: typing.Union[typing.Callable, int, str, typing.Literal[\"any_length\"]],\n        passed_args: list[str] = None,\n        ref_to: str = None,\n        offset_to: tuple[str, typing.Callable] = None,\n        file_name: str = None,\n        ref_type_params=None\n    ):\n\n        RefMember.__init__(self, type_name, file_name)\n        DynLengthMember.__init__(self, length)\n\n        # to determine the subtype for each entry,\n        # read this value to do the class_lookup\n        self.subtype_definition = subtype_definition\n\n        # dict to look up type_name => exportable class\n        self.class_lookup = class_lookup\n\n        # list of member names whose values will be passed to the new class\n        self.passed_args = passed_args\n\n        # add this member name's value to the filename\n        self.ref_to = ref_to\n\n        # link to member name which is a list of binary file offsets\n        self.offset_to = offset_to\n\n        # dict to specify type_name => constructor arguments\n        self.ref_type_params = ref_type_params\n\n        # no xrefs supported yet.. just set to true as if they were resolved.\n        self.resolved = True\n\n    def get_empty_value(self) -> list:\n        return list()\n\n    def get_contained_types(self):\n        return {\n            contained_type.get_effective_type()\n            for contained_type in self.class_lookup.values()\n        }\n\n    def __repr__(self):\n        return f\"MultisubtypeMember<{self.type_name}:len={self.length}>\"\n\n\nclass SubdataMember(MultisubtypeMember):\n    \"\"\"\n    Struct member/data column that references to just one another data set.\n    It's a special case of the multisubtypemember with one subtype.\n    \"\"\"\n\n    def __init__(\n        self,\n        ref_type: GenieStructure,\n        length: typing.Union[typing.Callable, int, str, typing.Literal[\"any_length\"]],\n        offset_to: tuple[str, typing.Callable] = None,\n        ref_to: str = None,\n        ref_type_params=None,\n        passed_args=None\n    ):\n        super().__init__(\n            type_name=None,\n            subtype_definition=None,\n            class_lookup={None: ref_type},\n            length=length,\n            offset_to=offset_to,\n            ref_to=ref_to,\n            ref_type_params={None: ref_type_params},\n            passed_args=passed_args,\n        )\n\n    def get_subdata_type_name(self):\n        return self.class_lookup[None].__name__\n\n    def __repr__(self):\n        return f\"SubdataMember<{self.get_subdata_type_name()}:len={self.length}>\"\n\n\nclass ArrayMember(DynLengthMember):\n    \"\"\"\n    subdata member for C-type arrays like float[8].\n    \"\"\"\n\n    def __init__(self, raw_type: ReadMember, length: int):\n        super().__init__(length)\n        self.raw_type = raw_type\n\n    def __repr__(self):\n        return f\"ArrayMember<{self.raw_type}:len={self.length}>\"\n"
  },
  {
    "path": "openage/convert/value_object/read/value_members.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n# TODO pylint: disable=C,R,abstract-method\n\n\"\"\"\nStorage format for values from data file entries.\nData from ReadMembers is supposed to be transferred\nto these objects for easier handling during the conversion\nprocess and advanced features like creating diffs.\n\nQuick usage guide on when to use which ValueMember:\n    - IntMember, FloatMember, BooleanMember and StringMember: should\n      be self explanatory.\n    - IDMember: References to other structures in form of identifiers.\n                Also useful for flags with more than two options.\n    - BitfieldMember: Value is used as a bitfield.\n    - ContainerMember: For modelling specific substructures. ContainerMembers\n                       can store members with different types. However, the\n                       member names must be unique.\n                       (e.g. a unit object)\n    - ArrayMember: Stores a list of members with uniform type. Can be used\n                   when repeating substructures appear in a data file.\n                   (e.g. multiple unit objects, list of coordinates)\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom enum import Enum\nfrom math import isclose\nfrom abc import ABC, abstractmethod\n\nfrom .dynamic_loader import DynamicLoader\n\n\nclass ValueMember(ABC):\n    \"\"\"\n    Stores a value member from a data file.\n    \"\"\"\n\n    __slots__ = ('_name', '_value')\n\n    def __init__(self, name: str):\n        self._name = name\n        self._value = None\n\n    @property\n    def name(self) -> str:\n        \"\"\"\n        Returns the name of the member.\n        \"\"\"\n        return self._name\n\n    @property\n    def value(self) -> typing.Any:\n        \"\"\"\n        Returns the value of a member.\n        \"\"\"\n        return self._value\n\n    @abstractmethod\n    def get_type(self) -> StorageType:\n        \"\"\"\n        Returns the type of a member.\n        \"\"\"\n\n    @abstractmethod\n    def diff(self, other: ValueMember) -> ValueMember:\n        \"\"\"\n        Returns a new member object that contains the diff between\n        self's and other's values.\n\n        If they are equal, return a NoDiffMember.\n        \"\"\"\n\n    def __repr__(self):\n        return f\"{self.__class__.__name__}<{self.name}>\"\n\n\nclass IntMember(ValueMember):\n    \"\"\"\n    Stores numeric integer values.\n    \"\"\"\n\n    def __init__(self, name: str, value: typing.Union[int, float]):\n        super().__init__(name)\n\n        self._value = int(value)\n\n    def get_type(self) -> StorageType:\n        return StorageType.INT_MEMBER\n\n    def diff(\n        self,\n        other: IntMember\n    ) -> typing.Union[NoDiffMember, IntMember]:\n        if self.get_type() is other.get_type():\n\n            if self.value == other.value:\n                return NoDiffMember(self.name, self)\n\n            else:\n                diff_value = other.value - self.value\n\n                return IntMember(self.name, diff_value)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n\nclass FloatMember(ValueMember):\n    \"\"\"\n    Stores numeric floating point values.\n    \"\"\"\n\n    def __init__(self, name: str, value: typing.Union[int, float]):\n        super().__init__(name)\n\n        self._value = float(value)\n\n    def get_type(self) -> StorageType:\n        return StorageType.FLOAT_MEMBER\n\n    def diff(\n        self,\n        other: FloatMember\n    ) -> typing.Union[NoDiffMember, FloatMember]:\n        if self.get_type() is other.get_type():\n            # Float must have the last 6 digits in common\n            if isclose(self.value, other.value, rel_tol=1e-7):\n                return NoDiffMember(self.name, self)\n\n            else:\n                diff_value = other.value - self.value\n\n                return FloatMember(self.name, diff_value)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n\nclass BooleanMember(ValueMember):\n    \"\"\"\n    Stores boolean values.\n    \"\"\"\n\n    def __init__(self, name: str, value: bool):\n        super().__init__(name)\n\n        self._value = bool(value)\n\n    def get_type(self) -> StorageType:\n        return StorageType.BOOLEAN_MEMBER\n\n    def diff(\n        self,\n        other: BooleanMember\n    ) -> typing.Union[NoDiffMember, BooleanMember]:\n        if self.get_type() is other.get_type():\n            if self.value == other.value:\n                return NoDiffMember(self.name, self)\n\n            else:\n                return BooleanMember(self.name, other.value)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n\nclass IDMember(ValueMember):\n    \"\"\"\n    Stores references to media/resource IDs.\n    \"\"\"\n\n    def __init__(self, name: str, value: int):\n        super().__init__(name)\n\n        self._value = int(value)\n\n    def get_type(self) -> StorageType:\n        return StorageType.ID_MEMBER\n\n    def diff(\n        self,\n        other: IDMember\n    ) -> typing.Union[NoDiffMember, IDMember]:\n        if self.get_type() is other.get_type():\n            if self.value == other.value:\n                return NoDiffMember(self.name, self)\n\n            else:\n                return IDMember(self.name, other.value)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n\nclass BitfieldMember(ValueMember):\n    \"\"\"\n    Stores bit field members.\n    \"\"\"\n\n    def __init__(self, name: str, value: int):\n        super().__init__(name)\n\n        self._value = value\n\n    def get_value_at_pos(self, pos: int) -> bool:\n        \"\"\"\n        Return the boolean value stored at a specific position\n        in the bitfield.\n\n        :param pos: Position in the bitfield, starting with the least significant bit.\n        :type pos: int\n        \"\"\"\n        return bool(self.value & (2 ** pos))\n\n    def get_type(self) -> StorageType:\n        return StorageType.BITFIELD_MEMBER\n\n    def diff(\n        self,\n        other: BitfieldMember\n    ) -> typing.Union[NoDiffMember, BitfieldMember]:\n        \"\"\"\n        Uses XOR to determine which bits are different in 'other'.\n        \"\"\"\n        if self.get_type() is other.get_type():\n            if self.value == other.value:\n                return NoDiffMember(self.name, self)\n\n            else:\n                difference = self.value ^ other.value\n                return BitfieldMember(self.name, difference)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n    def __len__(self):\n        return len(self.value)\n\n\nclass StringMember(ValueMember):\n    \"\"\"\n    Stores string values.\n    \"\"\"\n\n    def __init__(self, name: str, value: StringMember):\n        super().__init__(name)\n\n        self._value = str(value)\n\n    def get_type(self) -> StorageType:\n        return StorageType.STRING_MEMBER\n\n    def diff(\n        self,\n        other: StringMember\n    ) -> typing.Union[NoDiffMember, StringMember]:\n        if self.get_type() is other.get_type():\n            if self.value == other.value:\n                return NoDiffMember(self.name, self)\n\n            else:\n                return StringMember(self.name, other.value)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n    def __len__(self):\n        return len(self.value)\n\n\nclass ContainerMember(ValueMember):\n    \"\"\"\n    Stores multiple members as key-value pairs.\n\n    The name of the members are the keys, the member objects\n    are the value of the dict.\n    \"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        submembers: list[typing.Union[IntMember,\n                                      FloatMember,\n                                      BooleanMember,\n                                      IDMember,\n                                      BitfieldMember,\n                                      StringMember,\n                                      ArrayMember,\n                                      ContainerMember]]\n    ):\n        \"\"\"\n        :param submembers: Stored members as a list or dict\n        :type submembers: list, dict\n        \"\"\"\n        super().__init__(name)\n\n        self._value = {}\n\n        if isinstance(submembers, (dict, DynamicLoader)):\n            # submembers is a list or loads dynamically\n            self._value = submembers\n\n        else:\n            # submembers is a list of members\n            self._create_dict(submembers)\n\n    def get_type(self) -> StorageType:\n        return StorageType.CONTAINER_MEMBER\n\n    def diff(\n        self,\n        other: ContainerMember\n    ) -> typing.Union[NoDiffMember, ContainerMember]:\n        if self.get_type() is other.get_type():\n            diff_dict = {}\n\n            other_dict = other.value\n\n            for key in self.value.keys():\n                if key in other.value.keys():\n                    diff_value = self.value[key].diff(other_dict[key])\n\n                else:\n                    # Key is missing in other dict\n                    diff_value = RightMissingMember(key, self.value[key])\n\n                diff_dict.update({key: diff_value})\n\n            for key in other.value.keys():\n                if key not in self.value.keys():\n                    # Key is missing in this dict\n                    diff_value = LeftMissingMember(key, other_dict[key])\n                    diff_dict.update({key: diff_value})\n\n            if all(isinstance(member, NoDiffMember) for member in diff_dict.values()):\n                return NoDiffMember(self.name, self)\n\n            return ContainerMember(self.name, diff_dict)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n    def _create_dict(self, member_list: list[typing.Union[IntMember,\n                                                          FloatMember,\n                                                          BooleanMember,\n                                                          IDMember,\n                                                          BitfieldMember,\n                                                          StringMember,\n                                                          ArrayMember,\n                                                          ContainerMember]]) -> None:\n        \"\"\"\n        Creates the dict from the member list passed to __init__.\n        \"\"\"\n        for member in member_list:\n            self._value.update({member.name: member})\n\n    def __getitem__(self, key):\n        \"\"\"\n        Short command for getting a member in the container.\n        \"\"\"\n        return self.value[key]\n\n    def __len__(self):\n        return len(self.value)\n\n\nclass ArrayMember(ValueMember):\n    \"\"\"\n    Stores an ordered list of members with the same type.\n    \"\"\"\n\n    __slots__ = ('_allowed_member_type')\n\n    def __init__(\n        self,\n        name: str,\n        allowed_member_type: StorageType,\n        members: list[typing.Union[IntMember,\n                                   FloatMember,\n                                   BooleanMember,\n                                   IDMember,\n                                   BitfieldMember,\n                                   StringMember,\n                                   ArrayMember,\n                                   ContainerMember]]\n    ):\n        super().__init__(name)\n\n        self._value = members\n\n        self._allowed_member_type = allowed_member_type\n\n        # Check if members have correct type\n        for member in members:\n            if not isinstance(member, (NoDiffMember, LeftMissingMember, RightMissingMember)):\n                if member.get_type() is not self._allowed_member_type:\n                    raise TypeError(\"%s has type %s, but this ArrayMember only allows %s\"\n                                    % (member, member.get_type(), allowed_member_type))\n\n    def get_type(self) -> StorageType:\n        if self._allowed_member_type is StorageType.INT_MEMBER:\n            return StorageType.ARRAY_INT\n\n        elif self._allowed_member_type is StorageType.FLOAT_MEMBER:\n            return StorageType.ARRAY_FLOAT\n\n        elif self._allowed_member_type is StorageType.BOOLEAN_MEMBER:\n            return StorageType.ARRAY_BOOL\n\n        elif self._allowed_member_type is StorageType.ID_MEMBER:\n            return StorageType.ARRAY_ID\n\n        elif self._allowed_member_type is StorageType.BITFIELD_MEMBER:\n            return StorageType.ARRAY_BITFIELD\n\n        elif self._allowed_member_type is StorageType.STRING_MEMBER:\n            return StorageType.ARRAY_STRING\n\n        elif self._allowed_member_type is StorageType.CONTAINER_MEMBER:\n            return StorageType.ARRAY_CONTAINER\n\n        raise TypeError(f\"{self} has no valid member type\")\n\n    def get_container(\n        self,\n        key_member_name: str,\n        force_not_found: bool = False,\n        force_duplicate: bool = False\n    ) -> ContainerMember:\n        \"\"\"\n        Returns a ContainerMember generated from an array with type ARRAY_CONTAINER.\n        It uses the values of the members with the specified name as keys.\n        By default, this method raises an exception if a member with this\n        name does not exist or the same key is used twice.\n\n        :param key_member_name: A member in the containers whos value is used as the key.\n        :type key_member_name: str\n        :param force_not_found: Do not raise an exception if the member is not found.\n        :type force_not_found: bool\n        :param force_duplicate: Do not raise an exception if the same key value is used twice.\n        :type force_duplicate: bool\n        \"\"\"\n        if self.get_type() is not StorageType.ARRAY_CONTAINER:\n            raise TypeError(\"%s: Container can only be generated from arrays with\"\n                            \" type 'contarray', not %s\"\n                            % (self, self.get_type()))\n\n        member_dict = {}\n        for container in self.value:\n            if key_member_name not in container.value.keys():\n                if force_not_found:\n                    continue\n\n                raise KeyError(\"%s: Container %s has no member called %s\"\n                               % (self, container, key_member_name))\n\n            key_member_value = container[key_member_name].value\n\n            if key_member_value in member_dict.keys():\n                if force_duplicate:\n                    continue\n\n                raise KeyError(\"%s: Duplicate key %s for container member %s\"\n                               % (self, key_member_value, key_member_name))\n\n            member_dict.update({key_member_value: container})\n\n        return ContainerMember(self.name, member_dict)\n\n    def diff(\n        self,\n        other: ArrayMember\n    ) -> typing.Union[NoDiffMember, ArrayMember]:\n        if self.get_type() == other.get_type():\n            diff_list = []\n            other_list = other.value\n\n            index = 0\n            if len(self) <= len(other):\n                while index < len(self):\n                    diff_value = self.value[index].diff(other_list[index])\n                    diff_list.append(diff_value)\n                    index += 1\n\n                while index < len(other):\n                    diff_value = other_list[index]\n                    diff_list.append(LeftMissingMember(diff_value.name, diff_value))\n                    index += 1\n\n            else:\n                while index < len(other):\n                    diff_value = self.value[index].diff(other_list[index])\n                    diff_list.append(diff_value)\n                    index += 1\n\n                while index < len(self):\n                    diff_value = self.value[index]\n                    diff_list.append(RightMissingMember(diff_value.name, diff_value))\n                    index += 1\n\n            if all(isinstance(member, NoDiffMember) for member in diff_list):\n                return NoDiffMember(self.name, self)\n\n            return ArrayMember(self.name, self._allowed_member_type, diff_list)\n\n        else:\n            raise TypeError(\n                f\"type {type(self)} member cannot be diffed with type {type(other)}\")\n\n    def __getitem__(self, key):\n        \"\"\"\n        Short command for getting a member in the array.\n        \"\"\"\n        return self.value[key]\n\n    def __len__(self):\n        return len(self.value)\n\n\nclass NoDiffMember(ValueMember):\n    \"\"\"\n    Is returned when no difference between two members is found.\n    \"\"\"\n\n    def __init__(self, name: str, value: ValueMember):\n        \"\"\"\n        :param value: Reference to the one of the diffed members.\n        :type value: ValueMember\n        \"\"\"\n        super().__init__(name)\n\n        self._value = value\n\n    @property\n    def ref(self) -> ValueMember:\n        \"\"\"\n        Returns the reference to the diffed object.\n        \"\"\"\n        return self._value\n\n    @property\n    def value(self) -> typing.Any:\n        \"\"\"\n        Returns the value of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have values; use 'ref' instead\")\n\n    def get_type(self) -> typing.NoReturn:\n        \"\"\"\n        Returns the type of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have a type\")\n\n    def diff(self, other: typing.Any) -> typing.NoReturn:\n        \"\"\"\n        Returns a new member object that contains the diff between\n        self's and other's values.\n\n        If they are equal, return a NoDiffMember.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot be diffed\")\n\n\nclass LeftMissingMember(ValueMember):\n    \"\"\"\n    Is returned when an array or container on the left side of\n    the comparison has no member to compare. It stores the right\n    side member as value.\n    \"\"\"\n\n    def __init__(self, name: str, value: ValueMember):\n        \"\"\"\n        :param value: Reference to the right member's object.\n        :type value: ValueMember\n        \"\"\"\n        super().__init__(name)\n\n        self._value = value\n\n    @property\n    def ref(self) -> ValueMember:\n        \"\"\"\n        Returns the reference to the diffed object.\n        \"\"\"\n        return self._value\n\n    @property\n    def value(self) -> typing.Any:\n        \"\"\"\n        Returns the value of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have values; use 'ref' instead\")\n\n    def get_type(self) -> typing.NoReturn:\n        \"\"\"\n        Returns the type of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have a type\")\n\n    def diff(self, other: typing.Any) -> typing.NoReturn:\n        \"\"\"\n        Returns a new member object that contains the diff between\n        self's and other's values.\n\n        If they are equal, return a NoDiffMember.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot be diffed\")\n\n\nclass RightMissingMember(ValueMember):\n    \"\"\"\n    Is returned when an array or container on the right side of\n    the comparison has no member to compare. It stores the left\n    side member as value.\n    \"\"\"\n\n    def __init__(self, name: str, value: ValueMember):\n        \"\"\"\n        :param value: Reference to the left member's object.\n        :type value: ValueMember\n        \"\"\"\n        super().__init__(name)\n\n        self._value = value\n\n    @property\n    def ref(self) -> ValueMember:\n        \"\"\"\n        Returns the reference to the diffed object.\n        \"\"\"\n        return self._value\n\n    @property\n    def value(self) -> typing.Any:\n        \"\"\"\n        Returns the value of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have values; use 'ref' instead\")\n\n    def get_type(self) -> typing.NoReturn:\n        \"\"\"\n        Returns the type of a member.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot have a type\")\n\n    def diff(self, other: typing.Any) -> typing.NoReturn:\n        \"\"\"\n        Returns a new member object that contains the diff between\n        self's and other's values.\n\n        If they are equal, return a NoDiffMember.\n        \"\"\"\n        raise NotImplementedError(\n            f\"{type(self)} cannot be diffed\")\n\n\nclass StorageType(Enum):\n    \"\"\"\n    Types for values members.\n    \"\"\"\n\n    INT_MEMBER       = \"int\"\n    FLOAT_MEMBER     = \"float\"\n    BOOLEAN_MEMBER   = \"boolean\"\n    ID_MEMBER        = \"id\"\n    BITFIELD_MEMBER  = \"bitfield\"\n    STRING_MEMBER    = \"string\"\n    CONTAINER_MEMBER = \"container\"\n\n    # Array types                       # array of:\n    ARRAY_INT        = \"intarray\"       # IntegerMembers\n    ARRAY_FLOAT      = \"floatarray\"     # FloatMembers\n    ARRAY_BOOL       = \"boolarray\"      # BooleanMembers\n    ARRAY_ID         = \"idarray\"        # IDMembers\n    ARRAY_BITFIELD   = \"bitfieldarray\"  # BitfieldMembers\n    ARRAY_STRING     = \"stringarray\"    # StringMembers\n    ARRAY_CONTAINER  = \"contarray\"      # ContainerMembers\n"
  },
  {
    "path": "openage/cppinterface/CMakeLists.txt",
    "content": "add_cython_modules(\n\texctranslate.pyx\n\texctranslate_tests.pyx\n\tpyobject.pyx\n\tsetup_checker.pyx\n)\n\nadd_pxds(\n\ttypedefs.pxd\n)\n\nadd_py_modules(\n\t__init__.py\n\tsetup.py\n)\n"
  },
  {
    "path": "openage/cppinterface/__init__.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nCode for interfacing C++.\n\nThis includes using the C++ logger and translating C++ exceptions to\npython exceptions.\n\"\"\"\n"
  },
  {
    "path": "openage/cppinterface/exctranslate.pyx",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides the raise_py_exception and describe_py_exception callbacks for\nexctranslate.cpp.\n\"\"\"\n\nfrom cpython.ref cimport PyObject, Py_XDECREF\nfrom cpython.exc cimport (\n    PyErr_Occurred,\n    PyErr_Fetch,\n    PyErr_NormalizeException,\n    PyErr_SetObject\n)\n\nfrom libcpp cimport bool as cppbool\n\nfrom libopenage.log.message cimport message\nfrom libopenage.error.error cimport Error\nfrom libopenage.error.backtrace cimport backtrace_symbol\nfrom libopenage.pyinterface.functional cimport Func1\nfrom libopenage.pyinterface.pyexception cimport (\n    PyException,\n    pyexception_bt_get_symbols\n)\n\nfrom libopenage.pyinterface.exctranslate cimport (\n    init_exc_message,\n    set_exc_translation_funcs\n)\n\nfrom ..testing.testing import TestError\nfrom ..log import info\n\ncdef extern from \"Python.h\":\n    int PyException_SetTraceback(PyObject *ex, PyObject *tb)\n\n# _PyTraceback_Add has been made private in Python 3.13\n# see https://github.com/python/cpython/pull/108453\n# TODO: Find another solution to add tracebacks\n# cdef extern from \"traceback.h\":\n#     void _PyTraceback_Add(const char *funcname, const char *filename, int lineno)\n\n\ncdef void PyTraceback_Add(const char *functionname, const char *filename, int lineno) noexcept with gil:\n    \"\"\"\n    Add a new traceback stack frame.\n\n    Note: Currently does nothing, because _PyTraceback_Add is no longer\n          accessible since Python 3.13.\n\n    TODO: Find another solution to add tracebacks.\n    \"\"\"\n    # _PyTraceback_Add(functionname, filename, lineno)\n\n\ncdef class CPPMessageObject:\n    \"\"\"\n    Holds a C++ message object.\n    \"\"\"\n    cdef message val\n\n\nclass CPPException(Exception):\n    def __init__(self, bytes typename, bytes text):\n        super().__init__(text.decode(errors='replace'))\n        self.typename = typename\n\n\ncdef void py_add_backtrace_frame_from_symbol(const backtrace_symbol *symbol) noexcept with gil:\n    \"\"\"\n    For use as a callback with Error->backtrace->get_symbols(), from within\n    raise_exception.\n    \"\"\"\n    PyTraceback_Add(\n        <char *> symbol.functionname.c_str(),\n        <char *> symbol.filename.c_str(),\n        <int> symbol.lineno)\n\n\n# the callback object that contains the above function, for passing to get_symbols().\ncdef Func1[void, const backtrace_symbol *] py_backtrace_adder\npy_backtrace_adder.bind_noexcept0(py_add_backtrace_frame_from_symbol)\n\n\ncdef void raise_cpp_error_common(Error *cpp_error_obj, object obj_to_raise) noexcept:\n    \"\"\"\n    Common code that is used by both raise_cpp_error and raise_cpp_pyexception.\n\n    Takes a pointer to a C++ error::Error, and a \"prepared\" Python object.\n\n     - sets the cpp_msg_obj member of the object,\n     - recursively translates a potential cause exception,\n     - sets the object as the active Python exception.\n    \"\"\"\n\n    cdef CPPMessageObject msg\n\n    if not hasattr(obj_to_raise, \"cpp_msg_obj\"):\n        msg = CPPMessageObject()\n        msg.val = cpp_error_obj.msg\n        obj_to_raise.cpp_msg_obj = msg\n\n    # get the cause from cpp_error_obj\n    try:\n        # if cpp_error_obj has no cause, this does nothing.\n        # else, rethrow_cause throws a C++ exception. being marked 'except +',\n        # the cpp -> py exception translator (this one) is invoked to translate\n        # the exception to a Py exception.\n        # This method will throw an Exception object, and the except clause will\n        # be entered.\n        # If the cause exception has a cause itself, further recursion occurs.\n        #\n        # If the cause was a std::exception, it is \"converted\" to a openage error\n        # in order to prevent crashing everything.\n        cpp_error_obj.rethrow_cause()\n\n    except Exception as cause_exc:\n        obj_to_raise.__cause__ = cause_exc\n\n    PyErr_SetObject(type(obj_to_raise), obj_to_raise)\n\n\ncdef void raise_cpp_error(Error *cpp_error_obj) except * with gil:\n    # see the doc of the function pointer in exctranslate.h\n    # raises a py exception that corresponds to the given Error obj.\n\n    cdef bytes typename = cpp_error_obj.type_name()\n\n    if typename == b\"openage::testing::TestError\":\n        exc = TestError(cpp_error_obj.what().decode(errors='replace'))\n    else:\n        exc = CPPException(typename, <bytes> cpp_error_obj.what())\n\n    raise_cpp_error_common(cpp_error_obj, exc)\n    # A Python exception is active from this point on.\n    # Don't use any Python functionality.\n\n\n    # add traceback lines for cpp message obj metadata\n    PyTraceback_Add(cpp_error_obj.msg.functionname,\n                    cpp_error_obj.msg.filename,\n                    <int> cpp_error_obj.msg.lineno)\n\n    # add traceback lines for cpp_error_obj.backtrace\n    if cpp_error_obj.backtrace != NULL:\n        # Fix the C++ exception object's backtrace (remove unneeded entries)\n        # Let's just hope that none of these throws...\n        cpp_error_obj.trim_backtrace()\n        cpp_error_obj.backtrace.get_symbols(py_backtrace_adder, False)\n\n\ncdef void raise_cpp_pyexception(PyException *cpp_pyexception_obj) except * with gil:\n    # see the doc of the function pointer in exctranslate.h\n    # raises a py exception that corresponds to the given PyException obj.\n\n    exc = <object> <PyObject *> cpp_pyexception_obj.py_obj.get_ref()\n\n    raise_cpp_error_common(cpp_pyexception_obj, exc)\n    # A Python exception is active from this point on!\n\n    # exc still has its proper backtrace from last time;\n    # No new data was added during its C++ life.\n\n\ncdef cppbool check_exception() noexcept with gil:\n    # see the doc of the function pointer in exctranslate.h\n    return (PyErr_Occurred() != NULL)\n\n\ncdef void describe_exception(PyException *pyex) except * with gil:\n    # see the doc of the function pointer in exctranslate.h\n    # describes the current py exc and stores the desc in pyex.\n\n    # implementation notice:\n    # take care not to access any python functionality while a Python exception\n    # is currently active!\n\n    cdef PyObject *exc_type_ptr = NULL\n    cdef PyObject *exc_value_ptr = NULL\n    cdef PyObject *exc_traceback_ptr = NULL\n\n    PyErr_Fetch(&exc_type_ptr, &exc_value_ptr, &exc_traceback_ptr)\n    # no Python exception is active anymore from this point on.\n    PyErr_NormalizeException(&exc_type_ptr, &exc_value_ptr, &exc_traceback_ptr)\n    if exc_traceback_ptr != NULL:\n        traceback = <object> exc_traceback_ptr\n        PyException_SetTraceback(exc_value_ptr, exc_traceback_ptr)\n    else:\n        traceback = None\n\n    # create a \"real\" object from the value ptr.\n    cdef object exc = <object> exc_value_ptr\n\n    # we no longer need those raw pointers.\n    Py_XDECREF(exc_type_ptr)\n    Py_XDECREF(exc_traceback_ptr)\n    # hand the value raw pointer to pyex.py_obj.\n    pyex.py_obj.set_ref_without_incrementing(exc_value_ptr)\n\n    # set exc.msg\n    if hasattr(exc, \"cpp_msg_obj\"):\n        pyex.msg = (<CPPMessageObject> exc.cpp_msg_obj).val\n    else:\n        try:\n            pyex.msg.text = str(exc).encode()\n        except Exception as fetcherror:\n            pyex.msg.text = (\n                \"Exception while fetching Py exception message: \" +\n                str(fetcherror)).encode(errors='replace')\n\n        # for the metadata, we require the last traceback frame.\n        if traceback is None:\n            init_exc_message(&pyex.msg, b\"\", 0, b\"\")\n        else:\n            while traceback.tb_next is not None:\n                traceback = traceback.tb_next\n\n            # init meta with that frame's filename, lineno, functionname.\n            init_exc_message(\n                &pyex.msg,\n                traceback.tb_frame.f_code.co_filename.encode(),\n                traceback.tb_lineno,\n                traceback.tb_frame.f_code.co_name.encode())\n\n    # init exc.backtrace (possible after set_ref)\n    pyex.init_backtrace()\n\n    # set the cause exception as the active exception, to allow recursive\n    # processing by translate_exc_py_to_cpp.\n    cdef object cause = None\n    if getattr(exc, \"__cause__\", None) is not None:\n        cause = exc.__cause__\n    elif getattr(exc, \"__context__\", None) is not None:\n        cause = Exception(\"During handling, another exception occurred\")\n        cause.__cause__ = exc.__context__\n\n    if cause is not None:\n        PyErr_SetObject(type(cause), cause)\n        # now a Python exception is active again.\n\n\ncdef void pyexception_bt_get_symbols_impl(\n        PyObject *py_obj,\n        Func1[void, const backtrace_symbol *] callback) except * with gil:\n\n    cdef object py_exc = <object> py_obj\n    cdef object frame = getattr(py_exc, \"__traceback__\", None)\n\n    cdef backtrace_symbol symbol\n    symbol.pc = NULL\n\n    while frame is not None:\n        symbol.filename = frame.tb_frame.f_code.co_filename.encode()\n        symbol.functionname = frame.tb_frame.f_code.co_name.encode()\n        symbol.lineno = frame.tb_lineno\n\n        callback.call(&symbol)\n\n        frame = frame.tb_next\n\n\ndef setup(args):\n    \"\"\"\n    Installs the functions defined here in their PyFunc pointers.\n    \"\"\"\n    set_exc_translation_funcs(\n        raise_cpp_error,\n        raise_cpp_pyexception,\n        check_exception,\n        describe_exception)\n\n\n    if args.trap_exceptions:\n        info(\"Throwing errors will break into an attached debugger\")\n        Error.debug_break_on_create(True)\n    else:\n        Error.debug_break_on_create(False)\n\n    pyexception_bt_get_symbols.bind0(pyexception_bt_get_symbols_impl)\n"
  },
  {
    "path": "openage/cppinterface/exctranslate_tests.pyx",
    "content": "# Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTesting code for exctranslate.pyx.\nAlso see the sister file, cpp/pyinterface/exctranslate_tests.h.\n\"\"\"\n\nfrom libopenage.pyinterface.exctranslate_tests cimport (\n    err_py_to_cpp_helper,\n    err_cpp_to_py_helper,\n    bounce_call_py,\n    bounce_call as bounce_call_cpp,\n)\n\nfrom libopenage.pyinterface.functional cimport Func0\n\nimport argparse\nimport traceback\n\nfrom ..testing.testing import TestError, assert_value\n\nfrom .exctranslate import CPPException\n\n\ncdef void call_cpp_thrower() except * with gil:\n    err_cpp_to_py_helper()\n\n\ndef cpp_to_py_demo(args):\n    \"\"\"\n    Calls a C++ method that throws a complicated exception, and prints\n    that exception.\n    \"\"\"\n    cdef Func0[void] cpp_thrower\n\n    cli = argparse.ArgumentParser(\"cpp_to_py_demo\")\n    cli.add_argument(\"bounce_count\", type=int, default=0, nargs='?')\n    cdef int bounce_count = cli.parse_args(args).bounce_count\n\n    try:\n        if bounce_count == 0:\n            call_cpp_thrower()\n        else:\n            # the first bounce already happens in the bound function.\n            cpp_thrower.bind0(call_cpp_thrower)\n            bounce_call(cpp_thrower, bounce_count - 1)\n\n    except CPPException as exc:\n        print(\"translated exception:\\n\")\n        traceback.print_exc()\n\n\ndef cpp_to_py(int bounce_count = 0):\n    \"\"\"\n    Calls a C++ method that throws a exception, and tests whether\n    that exception is properly translated.\n    \"\"\"\n    cdef Func0[void] cpp_thrower\n\n    try:\n        if bounce_count == 0:\n            call_cpp_thrower()\n        else:\n            # the first bounce already happens in the bound function.\n            cpp_thrower.bind0(call_cpp_thrower)\n            bounce_call(cpp_thrower, bounce_count - 1)\n\n    except CPPException as exc:\n        # everything looks good. store the exc object for later analysis.\n        excobj = exc\n    except Exception as exc:\n        raise TestError(\n            \"Expected a CPPException, but got something else.\") from exc\n    else:\n        raise TestError(\"Expected a CPPException, but method returned.\")\n\n    is_openage_error = lambda value: b\"openage::error::Error\" in value\n\n    # now let's see about the detailed contents of excobj.\n    assert_value(excobj.args[0], \"foo\")\n    assert_value(excobj.typename, None, is_openage_error)\n\n    cause = getattr(excobj, \"__cause__\", None)\n\n    assert_value(type(cause), None, lambda value: value in {CPPException, TestError})\n\n    causeofcause = getattr(cause, \"__cause__\", None)\n\n    assert_value(type(causeofcause), CPPException)\n    assert_value(causeofcause.args[0], \"rofl\")\n    assert_value(causeofcause.typename, None, is_openage_error)\n\n    assert_value(getattr(causeofcause, \"__cause__\", None), None)\n\n\ndef cpp_to_py_bounce():\n    \"\"\"\n    Like cpp_to_py, but constructs a huge stack before throwing, so the\n    exception gets translated back and forth quite often.\n    \"\"\"\n    cpp_to_py(42)\n\n\nclass Bar(Exception):\n    \"\"\" For testing (via throw_bar()) \"\"\"\n    pass\n\n\ndef throw_bar(cause, ctr=3):\n    \"\"\" Invoked by the method below, to add some flavor to the stack trace. \"\"\"\n    if ctr > 0:\n        throw_bar(cause, ctr - 1)\n    else:\n        raise Bar(\"bar\") from cause\n\n\ncdef void py_exc_raiser() except * with gil:\n    \"\"\" Raises some Py exceptions for C++ to analyze and convert to C++. \"\"\"\n    try:\n        raise TestError(\"foo\")\n    except Exception as exc:\n        throw_bar(exc)\n\n\ncdef void bounce_call(Func0[void] func, int times) except * with gil:\n    \"\"\"\n    Creates a callstack consisting of 'times' alterations of this function\n    and its C++ counterpart.\n    On the top of that stack, calls func.\n    \"\"\"\n    if times <= 0:\n        func.call()\n    else:\n        bounce_call_cpp(func, times - 1)\n\n\ndef setup():\n    \"\"\" Installs the functions defined here in their PyFunc pointers. \"\"\"\n    err_py_to_cpp_helper.bind0(py_exc_raiser)\n    bounce_call_py.bind0(bounce_call)\n"
  },
  {
    "path": "openage/cppinterface/pyobject.pyx",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\nfrom libc.stdint cimport int64_t\nfrom libcpp cimport bool as cppbool\nfrom libcpp.string cimport string\nfrom libcpp.vector cimport vector\n\nfrom cpython.ref cimport Py_XINCREF, Py_XDECREF, PyObject\n\nfrom libopenage.pyinterface.functional cimport Func1\n\nfrom libopenage.pyinterface.pyobject cimport (\n    PyObjectRef,\n\n    py_xincref,\n    py_xdecref,\n\n    py_str,\n    py_repr,\n    py_bytes,\n    py_len,\n    py_callable,\n    py_call0,\n    py_calln,\n    py_hasattr,\n    py_getattr,\n    py_setattr,\n    py_isinstance,\n    py_to_bool,\n    py_to_int,\n    py_dir,\n    py_equals,\n    py_eval,\n    py_exec,\n    py_get,\n    py_in,\n    py_type,\n    py_modulename,\n    py_classname,\n\n    py_builtin,\n    py_import,\n    py_createstr,\n    py_createbytes,\n    py_createint,\n    py_createdict,\n    py_createlist,\n\n    None as none_obj,\n    True as true_obj,\n    False as false_obj,\n)\n\n\nimport atexit\nimport builtins\nimport importlib\n\n\ncdef void xincref(PyObject *ptr) noexcept with gil:\n    Py_XINCREF(ptr)\n\n\ncdef void xdecref(PyObject *ptr) noexcept with gil:\n    Py_XDECREF(ptr)\n\n\ncdef string str_impl(PyObject *ptr) except * with gil:\n    return str(<object> ptr).encode()\n\n\ncdef string repr_impl(PyObject *ptr) except * with gil:\n    return repr(<object> ptr).encode()\n\n\ncdef string bytes_impl(PyObject *ptr) except * with gil:\n    return bytes(<object> ptr)\n\n\ncdef int len_impl(PyObject *ptr) except * with gil:\n    return len(<object> ptr)\n\n\ncdef cppbool callable_impl(PyObject *ptr) except * with gil:\n    return callable(<object> ptr)\n\n\ncdef void call0_impl(PyObjectRef *result_ref, PyObject *func) except * with gil:\n    cdef object result_obj = (<object> func)()\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void calln_impl(PyObjectRef *result_ref, PyObject *func,\n                     vector[PyObject *]& args) except * with gil:\n\n    arg_list = list()\n\n    for arg in args:\n        arg_list.append(<object> arg)\n\n    cdef object result_obj = (<object> func)(*arg_list)\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef cppbool hasattr_impl(PyObject *ptr, string name) except * with gil:\n    return hasattr(<object> ptr, name.decode())\n\n\ncdef void getattr_impl(PyObjectRef *result_ref, PyObject *ptr, string name) except * with gil:\n    cdef object result_obj = getattr(<object> ptr, name.decode())\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void setattr_impl(PyObject *ptr, string name, PyObject *attr) except * with gil:\n    setattr(\n        <object> ptr,\n        name.decode(),\n        <object> attr)\n\n\ncdef cppbool isinstance_impl(PyObject *ptr, PyObject *typeptr) except * with gil:\n    return isinstance(<object> ptr, <object> typeptr)\n\n\ncdef cppbool to_bool_impl(PyObject *ptr) except * with gil:\n    return bool(<object> ptr)\n\n\ncdef int64_t to_int_impl(PyObject *ptr) except * with gil:\n    # TODO: probably we should check for overflows here\n    #       the python int can be much larger than the int64_t\n    return int(<object> ptr)\n\n\ncdef void dir_impl(PyObject *ptr, Func1[void, string] callback) except * with gil:\n    for name in dir(<object> ptr):\n        callback.call(name.encode())\n\n\ncdef cppbool equals_impl(PyObject *ptr, PyObject *other) except * with gil:\n    return (<object> ptr) == (<object> other)\n\n\ncdef void exec_impl(PyObject *context, string statement) except * with gil:\n    exec(statement.decode(), <object> context)\n\n\ncdef void eval_impl(PyObject *context, PyObjectRef *result_ref, string expr) except * with gil:\n    cdef object result_obj = eval(expr.decode(), <object> context)\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void get_impl(PyObject *ptr, PyObjectRef *result_ref, PyObject *key) except * with gil:\n    cdef object result_obj = (<object> <PyObject *> ptr)[<object> key]\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef cppbool in_impl(PyObject *ptr, PyObject *container) except * with gil:\n    return (<object> ptr) in (<object> container)\n\n\ncdef void type_impl(PyObject *ptr, PyObjectRef *result_ref) except * with gil:\n    cdef object result_obj = type(<object> <PyObject *> ptr)\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef string modulename_impl(PyObject *ptr) except * with gil:\n    return type(<object> ptr).__module__.encode()\n\n\ncdef string classname_impl(PyObject *ptr) except * with gil:\n    return type(<object> ptr).__name__.encode()\n\n\n## convenience functions for python object creation:\n\ncdef void builtin_impl(PyObjectRef *result_ref, const string &name) except * with gil:\n    cdef object result_obj = getattr(builtins, (<string> name).decode())\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void import_impl(PyObjectRef *result_ref, const string &name) except * with gil:\n    cdef object result_obj = importlib.import_module((<string> name).decode())\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void createstr_impl(PyObjectRef *result_ref, const string &value) except * with gil:\n    cdef object result_obj = (<string> value).decode()\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void createbytes_impl(PyObjectRef *result_ref, const string &value) except * with gil:\n    cdef object result_obj = bytes(value)\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void createint_impl(PyObjectRef *result_ref, int value) except * with gil:\n    cdef object result_obj = value\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void createdict_impl(PyObjectRef *result_ref) except * with gil:\n    cdef object result_obj = dict()\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ncdef void createlist_impl(PyObjectRef *result_ref) except * with gil:\n    cdef object result_obj = list()\n    result_ref.set_ref(<PyObject *> result_obj)\n\n\ndef setup():\n    py_xincref.bind_noexcept0(xincref)\n    py_xdecref.bind_noexcept0(xdecref)\n\n    py_str.bind0(str_impl)\n    py_repr.bind0(repr_impl)\n    py_bytes.bind0(bytes_impl)\n    py_len.bind0(len_impl)\n    py_callable.bind0(callable_impl)\n    py_call0.bind0(call0_impl)\n    py_calln.bind0(calln_impl)\n    py_hasattr.bind0(hasattr_impl)\n    py_getattr.bind0(getattr_impl)\n    py_setattr.bind0(setattr_impl)\n    py_isinstance.bind0(isinstance_impl)\n    py_to_bool.bind0(to_bool_impl)\n    py_to_int.bind0(to_int_impl)\n    py_dir.bind0(dir_impl)\n    py_equals.bind0(equals_impl)\n    py_eval.bind0(eval_impl)\n    py_exec.bind0(exec_impl)\n    py_get.bind0(get_impl)\n    py_in.bind0(in_impl)\n    py_type.bind0(type_impl)\n    py_modulename.bind0(modulename_impl)\n    py_classname.bind0(classname_impl)\n\n    py_builtin.bind0(builtin_impl)\n    py_import.bind0(import_impl)\n    py_createstr.bind0(createstr_impl)\n    py_createbytes.bind0(createbytes_impl)\n    py_createint.bind0(createint_impl)\n    py_createdict.bind0(createdict_impl)\n    py_createlist.bind0(createlist_impl)\n\n    none_obj.set_ref(<PyObject *><object>None)\n    true_obj.set_ref(<PyObject *><object>True)\n    false_obj.set_ref(<PyObject *><object>False)\n\n    # we need a teardown function as the none_obj etc are global in C++\n    # and would call pyx_decref when the libopenage is unloaded.\n    # but then the python interpreter is already shut down,\n    # so we need to set the objects to nullptr when python shuts down.\n    def teardown():\n        none_obj.set_ref(<PyObject*> 0)\n        true_obj.set_ref(<PyObject*> 0)\n        false_obj.set_ref(<PyObject*> 0)\n\n    atexit.register(teardown)\n"
  },
  {
    "path": "openage/cppinterface/setup.py",
    "content": "# Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n\"\"\"\nContains the function that initializes the C++ interface.\n\"\"\"\n\nfrom ..util.decorators import run_once\n\nfrom ..log import dbg\n\nfrom ..log.log_cpp import enable_log_translation\n\n\n@run_once\ndef setup(args):\n    \"\"\"\n    After a call to setup(), the C++ interface is in a usable state.\n\n    setup() automatically checks whether there are any remaining un-initialized\n    PyFunc objects, and raises an Exception if so.\n\n    Must be invoked before any functions from libopenage are invoked.\n    Runs only once; any subsequent invocations are a no-op, so don't hesitate\n    to call this method whenever likely.\n\n    Do _not_ invoke from a .pyx extension module that itself provides\n    a setup method (circular imports)!\n    \"\"\"\n    dbg(\"initializing libopenage...\")\n\n    # this is where calls to the setup methods of all other modules belong.\n    from .exctranslate import setup as exctranslate_setup\n    exctranslate_setup(args)\n\n    from .exctranslate_tests import setup as exctranslate_tests_setup\n    exctranslate_tests_setup()\n\n    from .pyobject import setup as pyobject_setup\n    pyobject_setup()\n\n    from ..util.filelike.cpp import setup as filelike_setup\n    filelike_setup()\n\n    from ..util.fslike.cpp import setup as fslike_setup\n    fslike_setup()\n\n    from ..cvar.cvar import setup as cvar_setup\n    cvar_setup()\n\n    # verify that everything has been properly initialized.\n    from .setup_checker import check\n    check()\n\n    enable_log_translation()\n\n    dbg(\"C++ <-> Python interface has been initialized\")\n"
  },
  {
    "path": "openage/cppinterface/setup_checker.pyx",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nWraps openage::pyinterface::check, which checks whether setup has\nproperly initialized all interface components.\n\"\"\"\n\nfrom libopenage.pyinterface.setup cimport check as check_cpp\n\n\ndef check():\n    check_cpp()\n"
  },
  {
    "path": "openage/cppinterface/typedefs.pxd",
    "content": "# Copyright 2015-2017 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSome types that are not available in Cython's shipped include files.\n\"\"\"\n\n\n# in some cases, pointers can't be properly used for template arguments.\nctypedef void *voidptr\n"
  },
  {
    "path": "openage/cvar/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tconfig_file.py\n\tlocation.py\n)\n\nadd_cython_modules(\n\tcvar.pyx\n)\n"
  },
  {
    "path": "openage/cvar/__init__.py",
    "content": "# Copyright 2016-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nHandles cvar access.\n\"\"\"\n"
  },
  {
    "path": "openage/cvar/config_file.py",
    "content": "# Copyright 2016-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nLoad and save the configuration : file <-> console var system\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nfrom ..log import info, spam\n\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.path import Path\n\n\ndef load_config_file(path: Path, set_cvar_func: typing.Callable, loaded_files: set = None) -> None:\n    \"\"\"\n    Load a config file, with possible subfile, into the cvar system.\n\n    set_cvar is a function that accepts (key, value) to actually\n    add the data.\n    \"\"\"\n\n    if not loaded_files:\n        loaded_files = set()\n\n    if not path.is_file():\n        info(f\"config file {path} not found.\")\n        return\n\n    # file is already loaded?\n    # the repr(path) is pretty hacky but does its job.\n    # better solution would be to implement __hash__\n    if repr(path) in loaded_files:\n        return\n\n    info(f\"loading config file {path}...\")\n\n    loaded_files.add(repr(path))\n\n    with path.open() as config:\n        for line in config:\n            spam(f\"Reading config line: {line}\")\n            lstrip = line.lstrip()\n            if not lstrip or lstrip.startswith(\"#\"):\n                continue\n\n            strip = lstrip.rstrip()\n            split = strip.split()\n\n            if split[0] == \"set\" and len(split) >= 3:\n                set_cvar_func(split[1], \" \".join(split[2:]))\n\n            elif split[0] == \"load\" and len(split) >= 2:\n                for sub_path in split[1:]:\n                    new_path = path.parent / sub_path\n                    load_config_file(new_path, set_cvar_func, loaded_files)\n"
  },
  {
    "path": "openage/cvar/cvar.pyx",
    "content": "# Copyright 2016-2017 the openage authors. See copying.md for legal info.\n\nfrom os.path import expanduser, expandvars\nfrom pathlib import Path\n\nfrom libcpp.string cimport string\n\nfrom libopenage.cvar.cvar cimport (\n    CVarManager,\n    pyx_load_config_file\n)\nfrom libopenage.util.path cimport Path as Path\nfrom openage.util.fslike.cpp cimport cpppath_to_pypath\n\nfrom .config_file import load_config_file\nfrom .. import default_dirs\n\n\n# TODO: turn this into a config and user profile system.\n\n\ncdef void cy_load_config_file(CVarManager *manager, const Path &path) except * with gil:\n    \"\"\"\n    Relay the call to load values from a config file\n    into the configuration manager.\n\n    Effectively glues together the call from C++ to Python.\n    \"\"\"\n\n    def set_func(key, val):\n        manager.set(key.encode(), val.encode())\n\n    py_path = cpppath_to_pypath(path)\n    load_config_file(py_path, set_func)\n\n\ndef setup():\n    pyx_load_config_file.bind0(cy_load_config_file)\n"
  },
  {
    "path": "openage/cvar/location.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nDetermine the config file location and set up mounts.\n\"\"\"\n\nimport os\nimport pathlib\n\nfrom .. import config, default_dirs\nfrom ..util.fslike.directory import Directory\nfrom ..util.fslike.union import Union\nfrom ..util.fslike.wrapper import WriteBlocker\n\n\ndef get_config_path(custom_cfg_dir: str = None) -> Directory:\n    \"\"\"\n    Locates the main configuration file by name in some searchpaths.\n    Optionally, mount a custom directory with highest priority.\n    \"\"\"\n\n    # if we're in devmode, use only the build source config folder\n    if config.DEVMODE:\n        return Directory(os.path.join(config.BUILD_SRC_DIR, \"cfg\")).root\n\n    # else, mount the possible locations in an union\n    # to overlay the global dir and the user dir.\n    result = Union().root\n\n    # mount the global config dir\n    # we don't use xdg config_dirs because that would be /etc/xdg/openage\n    # and nobody would ever look in there.\n    global_configs = pathlib.Path(config.GLOBAL_CONFIG_DIR)\n    if global_configs.is_dir():\n        result.mount(WriteBlocker(Directory(global_configs).root).root)\n\n    # then the per-user config dir (probably ~/.config/openage)\n    home_cfg = default_dirs.get_dir(\"config_home\") / \"openage\"\n    result.mount(\n        Directory(\n            home_cfg,\n            create_if_missing=True\n        ).root\n    )\n\n    # the optional command line argument overrides it all\n    if custom_cfg_dir:\n        result.mount(Directory(custom_cfg_dir).root)\n\n    return result\n"
  },
  {
    "path": "openage/cython_check.pyx",
    "content": "# Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThe only purpose of this module is to be imported by __main__.py\nin order to verify if the cython modules were correctly compiled\nand called from the build directory.\n\"\"\"\n\n\ndef this_is_true():\n    return True\n"
  },
  {
    "path": "openage/default_dirs.py",
    "content": "# Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\n\nCode for locating the game assets.\n\nAll access to game assets should happen through objects obtained from get().\n\"\"\"\n\nimport os\nimport pathlib\nimport sys\n\n# TODO: use os.pathsep for multipath variables\n\n# Linux-specific dirs according to the freedesktop basedir standard:\n# https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html\n#\n# concretely:\n# $XDG_CONFIG_HOME = $HOME/.config\n# $XDG_DATA_HOME = $HOME/.local/share\n# $XDG_DATA_DIRS = /usr/local/share/:/usr/share/\n# $XDG_CONFIG_DIRS = /etc/xdg\n# $XDG_CACHE_HOME = $HOME/.cache\n# $XDG_RUNTIME_DIR = /run/user/$UID\n#\nLINUX_DIRS = {\n    \"config_home\": (\"XDG_CONFIG_HOME\", (\"{HOME}/.config\", {\"HOME\"})),\n    \"data_home\": (\"XDG_DATA_HOME\", (\"{HOME}/.local/share\", {\"HOME\"})),\n    \"data_dirs\": (\"XDG_DATA_DIRS\", (\"/usr/local/share/:/usr/share/\", {})),\n    \"config_dirs\": (\"XDG_CONFIG_DIRS\", (\"/etc/xdg\", {})),\n    \"cache_home\": (\"XDG_CACHE_HOME\", (\"{HOME}/.cache\", {\"HOME\"})),\n    \"runtime_dir\": (\"XDG_RUNTIME_DIR\", (\"/run/user/$UID\")),\n}\n\n\n# Windows-specific paths\nWINDOWS_DIRS = {\n    \"config_home\": (\"APPDATA\", (False, None)),\n    \"data_home\": (\"APPDATA\", (False, None)),\n    \"config_dirs\": (\"ALLUSERSPROFILE\", (False, None)),\n    # TODO: other windows paths\n}\n\n\ndef get_dir(which):\n    \"\"\"\n    Returns directories used for data and config storage.\n    returns pathlib.Path\n    \"\"\"\n\n    platform_table = None\n\n    if sys.platform.startswith(\"linux\"):\n        platform_table = LINUX_DIRS\n\n    elif sys.platform.startswith(\"darwin\"):\n        raise RuntimeError(\"macOS not really supported\")\n\n    elif sys.platform.startswith(\"win32\"):\n        platform_table = WINDOWS_DIRS\n\n    else:\n        raise RuntimeError(f\"unsupported platform: '{sys.platform}'\")\n\n    if which not in platform_table:\n        raise ValueError(f\"unknown directory requested: '{which}'\")\n\n    # fetch the directory template\n    env_var, (default_template, required_envs) = platform_table[which]\n\n    # then create the result from the environment\n    env_val = os.environ.get(env_var)\n\n    if env_val:\n        path = env_val\n\n    elif default_template:\n        env_vars = {var: os.environ.get(var) for var in required_envs}\n        if not all(env_vars.values()):\n            env_var_str = ', '.join([var for (var, val) in env_vars.items()\n                                     if val is None])\n            raise RuntimeError(f\"could not reconstruct {which}, \"\n                               f\"missing env variables: '{env_var_str}'\")\n\n        path = default_template.format(**env_vars)\n\n    else:\n        raise RuntimeError(f\"could not find '{which}' in environment\")\n\n    return pathlib.Path(path)\n"
  },
  {
    "path": "openage/devmode.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThe only purpose of this module is _not_ to be installed, so it's only\navailable when running openage from its development directory.\n\nconfig.py uses this fact to set its DEVMODE member variable.\n\"\"\"\n"
  },
  {
    "path": "openage/event/CMakeLists.txt",
    "content": "add_cython_modules(\n\tdemo.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n)\n"
  },
  {
    "path": "openage/event/__init__.py",
    "content": "# Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThis package contains the python parts of the event simulation submodule.\nIt is the game simulation core.\n\"\"\"\n"
  },
  {
    "path": "openage/event/demo.pyx",
    "content": "# Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\ndemo for the event system.\n\"\"\"\n\nimport argparse\nfrom libcpp cimport bool\n\nimport openage.config as config\nfrom openage.util.math import clamp\nfrom openage.log import set_loglevel\nfrom libopenage.event.demo.main cimport curvepong as curvepong_c\n\n\ndef curvepong(list argv):\n    \"\"\"\n    starts curvepong, the event prediction demonstration with pong.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... curvepong',\n        description='Curvepong demonstration for the event prediction engine')\n\n    if config.WITH_NCURSES:\n        cmd.add_argument(\"--disable-gui\", action='store_true',\n                         help=\"Don't show the ncurses GUI\")\n\n    cmd.add_argument(\"--no-human\", action='store_true',\n                     help=\"Don't allow the human to play, use 2 AIs instead\")\n    cmd.add_argument(\"-v\", \"--verbose\", action=\"count\", default=0,\n                     help=\"increase program verbosity\")\n    cmd.add_argument(\"-q\", \"--quiet\", action=\"count\", default=0,\n                     help=\"decrease program verbosity\")\n\n    args = cmd.parse_args(argv)\n\n    # default = 30 WARNING, the q and v args in/decrease it\n    set_loglevel(clamp(30 - (args.verbose - args.quiet) * 10, 0, 50))\n\n    cdef bool disable_gui = False\n    if config.WITH_NCURSES:\n        disable_gui = args.disable_gui\n\n    cdef bool no_human = args.no_human\n\n    with nogil:\n        curvepong_c(disable_gui, no_human)\n"
  },
  {
    "path": "openage/game/CMakeLists.txt",
    "content": "add_cython_modules(\n\tmain_cpp.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n\tmain.py\n)\n"
  },
  {
    "path": "openage/game/__init__.py",
    "content": "# Copyright 2013-2015 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPackage for openage's actual game code.\n\nContains the method that invokes the game.\n\"\"\"\n"
  },
  {
    "path": "openage/game/main.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-locals\n\n\"\"\"\nHolds the game entry point for openage.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom ..log import info\n\nif typing.TYPE_CHECKING:\n    from argparse import ArgumentParser\n\n\ndef init_subparser(cli: ArgumentParser) -> None:\n    \"\"\" Initializes the parser for game-specific args. \"\"\"\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\n        \"--gl-debug\", action='store_true',\n        help=\"throw exceptions directly from the OpenGL calls\")\n\n    cli.add_argument(\n        \"--headless\", action='store_true',\n        help=\"run without displaying graphics\")\n\n    cli.add_argument(\n        \"--modpacks\", nargs=\"+\", required=True, type=str,\n        help=\"list of modpacks to load\")\n\n    cli.add_argument(\n        \"--check-updates\", action='store_true',\n        help=\"Check if the assets are up to date\"\n    )\n\n    cli.add_argument(\n        \"--window-size\", nargs=2, type=int, default=[1024, 768],\n        metavar=('WIDTH', 'HEIGHT'),\n        help=\"Initial window size in pixels\")\n\n    cli.add_argument(\n        \"--vsync\", action='store_true',\n        help=\"Enable vertical synchronization\")\n\n    cli.add_argument(\n        \"--window-mode\", choices=[\"fullscreen\", \"borderless\", \"windowed\"], default=\"windowed\",\n        help=\"Set the window mode\")\n\n\ndef main(args, error):\n    \"\"\"\n    Makes sure that the assets have been converted,\n    and jumps into the C++ main method.\n    \"\"\"\n    del error  # unused\n\n    # we have to import stuff inside the function\n    # as it depends on generated/compiled code\n    from .main_cpp import run_game\n    from .. import config\n    from ..assets import get_asset_path\n    from ..convert.tool.api_export import export_api\n    from ..convert.service.init.api_export_required import api_export_required\n    from ..convert.service.init.changelog import check_updates\n    from ..convert.service.init.modpack_search import enumerate_modpacks\n    from ..cppinterface.setup import setup as cpp_interface_setup\n    from ..cvar.location import get_config_path\n    from ..util.fslike.union import Union\n\n    # initialize libopenage\n    cpp_interface_setup(args)\n\n    info(\"launching openage %s\", config.VERSION)\n    info(\"compiled by %s\", config.COMPILER)\n\n    if config.DEVMODE:\n        info(\"running in DEVMODE\")\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    asset_path = get_asset_path(args.asset_dir)\n    root[\"assets\"].mount(asset_path)\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n    args.cfg_dir = root[\"cfg\"]\n\n    # ensure that the openage API is present\n    if api_export_required(asset_path):\n        # export to assets folder\n        converted_path = asset_path / \"converted\"\n        converted_path.mkdirs()\n        export_api(converted_path)\n\n    # ensure that modpacks are available\n    modpack_dir = asset_path / \"converted\"\n    available_modpacks = enumerate_modpacks(modpack_dir, exclude={\"engine\"})\n    for modpack in args.modpacks:\n        if modpack not in available_modpacks:\n            raise FileNotFoundError(f\"Modpack '{modpack}' not found in {modpack_dir}\")\n\n    # check if the converted modpacks are up to date\n    if args.check_updates:\n        check_updates(available_modpacks, args.cfg_dir / \"converter\" / \"games\")\n\n    # encode modpacks as bytes for the C++ interface\n    args.modpacks = [modpack.encode('utf-8') for modpack in args.modpacks]\n\n    # Pass window parameters to engine\n    args.window_args = {\n        \"width\": args.window_size[0],\n        \"height\": args.window_size[1],\n        \"vsync\": args.vsync,\n        \"window_mode\": args.window_mode,\n    }\n\n    # start the game, continue in main_cpp.pyx!\n    return run_game(args, root)\n"
  },
  {
    "path": "openage/game/main_cpp.pyx",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\nfrom cpython.ref cimport PyObject\nfrom libcpp.string cimport string\nfrom libcpp.vector cimport vector\n\nfrom libopenage.main cimport main_arguments, run_game as run_game_cpp\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom libopenage.error.handlers cimport set_exit_ok\n\n\ndef run_game(args, root_path):\n    \"\"\"\n    Launches the game after arguments were translated.\n    \"\"\"\n    cdef int result\n\n    # argument translation\n    cdef main_arguments args_cpp\n\n    set_exit_ok(False)\n    try:\n        # root_path is a util.fslike.Path object from python\n        args_cpp.root_path = Path_cpp(PyObj(<PyObject*>root_path.fsobj),\n                                    root_path.parts)\n\n        # opengl debugging\n        args_cpp.gl_debug = args.gl_debug\n\n        # headless mode\n        args_cpp.headless = args.headless\n\n        # mods\n        if args.modpacks is not None:\n            args_cpp.mods = args.modpacks\n        else:\n            args_cpp.mods = vector[string]()\n\n        # window\n        args_cpp.window_args.width = args.window_args[\"width\"]\n        args_cpp.window_args.height = args.window_args[\"height\"]\n        args_cpp.window_args.vsync = args.window_args[\"vsync\"]\n        args_cpp.window_args.mode = args.window_args[\"window_mode\"].encode('utf-8')\n\n        # run the game!\n        with nogil:\n            result = run_game_cpp(args_cpp)\n\n        return result\n    finally:\n        set_exit_ok(True)\n"
  },
  {
    "path": "openage/gamestate/CMakeLists.txt",
    "content": "add_cython_modules(\n\ttests.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n)\n"
  },
  {
    "path": "openage/gamestate/__init__.py",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nopenage game simulation\n\"\"\"\n"
  },
  {
    "path": "openage/gamestate/tests.pyx",
    "content": "# Copyright 2023-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\ntests for the games simulation.\n\"\"\"\n\nimport argparse\n\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom cpython.ref cimport PyObject\nfrom libopenage.gamestate.demo.tests cimport simulation_demo as simulation_demo_c\n\n\ndef simulation_demo(list argv):\n    \"\"\"\n    invokes the available simulation demos.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... simulation_demo',\n        description='Demo of the game simulation')\n    cmd.add_argument(\"test_id\", type=int, help=\"id of the demo to run.\")\n    cmd.add_argument(\"--asset-dir\",\n                         help=\"Use this as an additional asset directory.\")\n    cmd.add_argument(\"--cfg-dir\",\n                         help=\"Use this as an additional config directory.\")\n\n    args = cmd.parse_args(argv)\n\n    from ..cvar.location import get_config_path\n    from ..assets import get_asset_path\n    from ..util.fslike.union import Union\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    root[\"assets\"].mount(get_asset_path(args.asset_dir))\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n\n    cdef int simulation_test_id = args.test_id\n\n    cdef Path_cpp root_cpp = Path_cpp(PyObj(<PyObject*>root.fsobj),\n                                  root.parts)\n\n    with nogil:\n        simulation_demo_c(simulation_test_id, root_cpp)\n"
  },
  {
    "path": "openage/log/CMakeLists.txt",
    "content": "add_cython_modules(\n\tlog_cpp.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n\ttests.py\n)\n"
  },
  {
    "path": "openage/log/__init__.py",
    "content": "# Copyright 2014-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPython logging.\n\nLog messages get redirected to the CPP logging system if the library has been\nloaded.\n\"\"\"\n\nimport logging\nfrom os import environ\n\nfrom ..util.math import clamp\n\nPYTHON_TO_CPP_LOG_LEVEL = {}\n\n\nclass CppHandler(logging.Handler):\n    \"\"\"\n    CppHandler calls into the CPP logging system if the library has been loaded.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n    def setLevel(self, level):\n        \"\"\" sets the log level \"\"\"\n        cpp_level = PYTHON_TO_CPP_LOG_LEVEL.get(level, None)\n        if cpp_level is not None:\n            # the C++ interface is initialized\n            super().setLevel(level)\n            from .log_cpp import set_level as set_level_cpp\n            set_level_cpp(cpp_level)\n\n    def emit(self, record):\n        \"\"\" logs a message \"\"\"\n        cpp_level = PYTHON_TO_CPP_LOG_LEVEL.get(record.levelno, None)\n        if cpp_level is None:\n            # the C++ interface is uninitialized\n            print(\"\\x1b[\" + level_colorcode(record.levelno) + \"m\" +\n                  record.levelname.rjust(4) + \"\\x1b[m \" +\n                  record.getMessage())\n        else:\n            from .log_cpp import log as log_cpp\n            log_cpp(cpp_level, record.getMessage(), record.filename, record.funcName, record.lineno)\n\n\nCPP_HANDLER = CppHandler()\n\n\ndef level_colorcode(lvl):\n    \"\"\" returns the same color codes as in libopenage/log/level.cpp. \"\"\"\n\n    colorcodes = {\n        logging.WARNING: \"33\",\n        logging.ERROR: \"31;1\",\n        logging.CRITICAL: \"31;1;47\",\n    }\n    return colorcodes.get(lvl, \"\")\n\n\nSPAM = 5\n\n\ndef _spam(self, msg, *args, **kwargs):\n    \"\"\" Log 'msg % args' with severity 'SPAM'. \"\"\"\n    if self.isEnabledFor(SPAM):\n        self._log(SPAM, msg, args, **kwargs)  # pylint: disable=W0212\n\n\ndef setup_logging():\n    \"\"\" setup the logging system \"\"\"\n    logging.Logger.spam = _spam\n\n    # do not overwrite any of the predefined levels\n    # https://docs.python.org/3/library/logging.html#logging-levels\n    logging.addLevelName(1, \"MIN\")\n    logging.addLevelName(SPAM, \"SPAM\")\n    logging.addLevelName(51, \"MAX\")\n\n    root = logging.getLogger()\n    root.addHandler(CPP_HANDLER)\n    logging.spam = root.spam\n\n\ndef set_loglevel(level):\n    \"\"\" sets the log level \"\"\"\n    old_level = get_loglevel()\n    logging.root.setLevel(level)\n    CPP_HANDLER.setLevel(level)\n    return old_level\n\n\ndef get_loglevel():\n    \"\"\" gets the log level \"\"\"\n    return logging.root.level\n\n\ndef spam(msg, *args, **kwargs):\n    \"\"\" spam message \"\"\"\n    logging.spam(msg, *args, **kwargs)\n\n\ndef dbg(msg, *args, **kwargs):\n    \"\"\" debug message \"\"\"\n    logging.debug(msg, *args, **kwargs)\n\n\ndef info(msg, *args, **kwargs):\n    \"\"\" info message \"\"\"\n    logging.info(msg, *args, **kwargs)\n\n\ndef warn(msg, *args, **kwargs):\n    \"\"\" warning message \"\"\"\n    logging.warning(msg, *args, **kwargs)\n\n\ndef err(msg, *args, **kwargs):\n    \"\"\" error message \"\"\"\n    logging.error(msg, *args, **kwargs)\n\n\ndef crit(msg, *args, **kwargs):\n    \"\"\" critical error message \"\"\"\n    logging.critical(msg, *args, **kwargs)\n\n\ndef verbosity_to_level(verbosity):\n    \"\"\"\n    Translates an integer verbosity to a log level.\n    \"\"\"\n    levels = [logging.getLevelName(\"MIN\"),\n              logging.getLevelName(\"SPAM\"),\n              logging.DEBUG,\n              logging.INFO,\n              logging.WARNING,\n              logging.ERROR,\n              logging.CRITICAL,\n              logging.getLevelName(\"MAX\")]\n    # return INFO when verbosity is 0\n    return levels[clamp(-verbosity + 3, 0, len(levels) - 1)]\n\n\ndef env_verbosity():\n    \"\"\"\n    Tries to retrieve verbosity from the VERBOSITY environment variable.\n    \"\"\"\n    val = environ.get('VERBOSE', '0')\n\n    if val.lower() in {'y', 'yes', 'true'}:\n        return logging.WARNING\n\n    if val.lower() in {'max'}:\n        return logging.CRITICAL\n\n    try:\n        return int(val)\n    except ValueError:\n        return logging.INFO\n\n\nENV_VERBOSITY = env_verbosity()\n"
  },
  {
    "path": "openage/log/log_cpp.pyx",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTranslates Python log messages to C++ log messages.\n\"\"\"\n\nfrom libcpp.memory cimport unique_ptr\n\nfrom libopenage.log.message cimport message\nfrom libopenage.log.level cimport (\n    level,\n\n    MIN,\n    spam,\n    dbg,\n    info,\n    warn,\n    err,\n    crit,\n    MAX\n)\n\nfrom libopenage.log.named_logsource cimport NamedLogSource\nfrom libopenage.log.log cimport set_level as cpp_set_level\n\nimport logging\nfrom inspect import getframeinfo\nfrom ..log import get_loglevel, PYTHON_TO_CPP_LOG_LEVEL\n\n\ncdef class CPPLevel:\n    \"\"\"\n    Holds a C++ 'level' object.\n    \"\"\"\n    cdef level value\n\n    cdef level get(self):\n        return self.value\n\n    @staticmethod\n    cdef wrap(level lvl):\n        cdef CPPLevel result = CPPLevel()\n        result.value = lvl\n        return result\n\ncdef unique_ptr[NamedLogSource] PY_LOGSOURCE\n\n\ndef enable_log_translation():\n    \"\"\"\n    To be called from setup.setup after the interface has been\n    initialized.\n    \"\"\"\n    PY_LOGSOURCE.reset(new NamedLogSource(b\"py\"))\n\n    PYTHON_TO_CPP_LOG_LEVEL[logging.getLevelName(\"MIN\")] = CPPLevel.wrap(MIN)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.getLevelName(\"SPAM\")] = CPPLevel.wrap(spam)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.DEBUG] = CPPLevel.wrap(dbg)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.INFO] = CPPLevel.wrap(info)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.WARNING] = CPPLevel.wrap(warn)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.ERROR] = CPPLevel.wrap(err)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.CRITICAL] = CPPLevel.wrap(crit)\n    PYTHON_TO_CPP_LOG_LEVEL[logging.getLevelName(\"MAX\")] = CPPLevel.wrap(MAX)\n\n    set_level(PYTHON_TO_CPP_LOG_LEVEL[get_loglevel()])\n\n\ndef log(CPPLevel lvl, str msg, filename, function, lineno):\n    \"\"\"\n    Forwards the message to C++.\n    \"\"\"\n    cdef message cpp_msg\n\n    cpp_msg.init_with_metadata_copy(\n        filename.encode(),\n        function.encode())\n\n    cpp_msg.lineno = lineno\n    cpp_msg.lvl = lvl.value\n\n    cpp_msg.text = msg.encode()\n\n    PY_LOGSOURCE.get().log(cpp_msg)\n\n\ndef set_level(CPPLevel lvl):\n    cpp_set_level(lvl.get())\n"
  },
  {
    "path": "openage/log/tests.py",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n\"\"\" Testing code for the openage.log package. \"\"\"\n\nimport argparse\nfrom multiprocessing.pool import ThreadPool\n\nfrom . import spam, dbg, info, warn, err, crit, \\\n    set_loglevel, ENV_VERBOSITY, verbosity_to_level\n\n\ndef demo(args):\n    \"\"\" Demonstrates the Python logging facility. \"\"\"\n\n    cli = argparse.ArgumentParser()\n    cli.add_argument(\"--verbose\", \"-v\", action='count', default=ENV_VERBOSITY)\n    cli.add_argument(\"--quiet\", \"-q\", action='count', default=0)\n    args = cli.parse_args(args)\n\n    level = verbosity_to_level(args.verbose - args.quiet)\n\n    info(\"new log level: %s\", level)\n    old_level = set_loglevel(level)\n    info(\"old level was: %s\", old_level)\n\n    info(\"printing some messages with different log levels\")\n\n    spam(\"rofl\")\n    dbg(\"wtf?\")\n    info(\"foo\")\n    warn(\"WARNING!!!!\")\n    err(\"that didn't go so well\")\n    crit(\"pretty critical, huh?\")\n\n    info(\"restoring old loglevel\")\n\n    set_loglevel(old_level)\n\n    info(\"old loglevel restored\")\n    info(\"running some threaded stuff\")\n\n    pool = ThreadPool()\n    for i in range(8):\n        pool.apply_async(info, (\"async message #%s\", i))\n    pool.close()\n    pool.join()\n"
  },
  {
    "path": "openage/main/CMakeLists.txt",
    "content": "add_cython_modules(\n\tmain_cpp.pyx\n\ttests.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n\tmain.py\n)\n"
  },
  {
    "path": "openage/main/__init__.py",
    "content": "# Copyright 2018-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\nMain engine entry point.\n\"\"\"\n"
  },
  {
    "path": "openage/main/main.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nMain engine entry point for openage.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom ..log import info\n\nif typing.TYPE_CHECKING:\n    from argparse import ArgumentParser\n\n\ndef init_subparser(cli: ArgumentParser):\n    \"\"\" Initializes the parser for game-specific args. \"\"\"\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\n        \"--gl-debug\", action='store_true',\n        help=\"throw exceptions directly from the OpenGL calls\")\n\n    cli.add_argument(\n        \"--headless\", action='store_true',\n        help=\"run without displaying graphics\")\n\n    cli.add_argument(\n        \"--modpacks\", nargs=\"+\", type=str,\n        help=\"list of modpacks to load\")\n\n    cli.add_argument(\n        \"--window-size\", nargs=2, type=int, default=[1024, 768],\n        metavar=('WIDTH', 'HEIGHT'),\n        help=\"Initial window size in pixels\")\n\n    cli.add_argument(\n        \"--vsync\", action='store_true',\n        help=\"Enable vertical synchronization\")\n\n    cli.add_argument(\n        \"--window-mode\", choices=[\"fullscreen\", \"borderless\", \"windowed\"], default=\"windowed\",\n        help=\"Set the window mode\")\n\n\ndef main(args, error):\n    \"\"\"\n    Makes sure that the assets have been converted,\n    and jumps into the C++ main method.\n    \"\"\"\n    # pylint: disable=too-many-locals\n    del error  # unused\n\n    # we have to import stuff inside the function\n    # as it depends on generated/compiled code\n    from .main_cpp import run_game\n    from .. import config\n    from ..assets import get_asset_path\n    from ..convert.main import convert_assets\n    from ..convert.service.init.api_export_required import api_export_required\n    from ..convert.service.init.changelog import check_updates\n    from ..convert.service.init.modpack_search import enumerate_modpacks, query_modpack\n    from ..convert.tool.api_export import export_api\n    from ..convert.tool.subtool.acquire_sourcedir import wanna_convert\n    from ..cppinterface.setup import setup as cpp_interface_setup\n    from ..cvar.location import get_config_path\n    from ..util.fslike.union import Union\n\n    # initialize libopenage\n    cpp_interface_setup(args)\n\n    info(\"launching openage %s\", config.VERSION)\n    info(\"compiled by %s\", config.COMPILER)\n\n    if config.DEVMODE:\n        info(\"running in DEVMODE\")\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    asset_path = get_asset_path(args.asset_dir)\n    root[\"assets\"].mount(asset_path)\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n    args.cfg_dir = root[\"cfg\"]\n\n    # ensure that the openage API is present\n    if api_export_required(asset_path):\n        # export to assets folder\n        converted_path = asset_path / \"converted\"\n        converted_path.mkdirs()\n        export_api(converted_path)\n\n    # check if modpacks need to be converted\n    if wanna_convert():\n        convert_assets(asset_path, args)\n\n    available_modpacks = enumerate_modpacks(asset_path / \"converted\", exclude={\"engine\"})\n    if len(available_modpacks) == 0:\n        info(\"No modpacks have been found\")\n        if not args.modpacks:\n            info(\"Starting bare 'engine' mode\")\n            args.modpacks = [\"engine\"]\n\n    # check if the converted modpacks are up to date\n    check_updates(available_modpacks, args.cfg_dir / \"converter\" / \"games\")\n\n    # pass modpacks to engine\n    if args.modpacks:\n        # ensure that specified modpacks are available\n        for modpack in args.modpacks:\n            if modpack not in available_modpacks:\n                raise FileNotFoundError(\n                    f\"Modpack '{modpack}' not found in {asset_path / 'converted'}\")\n\n        args.modpacks = [modpack.encode(\"utf-8\") for modpack in args.modpacks]\n\n    else:\n        args.modpacks = [query_modpack(list(available_modpacks.keys())).encode(\"utf-8\")]\n\n    # Pass window parameters to engine\n    args.window_args = {\n        \"width\": args.window_size[0],\n        \"height\": args.window_size[1],\n        \"vsync\": args.vsync,\n        \"window_mode\": args.window_mode,\n    }\n\n    # start the game, continue in main_cpp.pyx!\n    return run_game(args, root)\n"
  },
  {
    "path": "openage/main/main_cpp.pyx",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\nfrom cpython.ref cimport PyObject\nfrom libcpp.string cimport string\nfrom libcpp.vector cimport vector\n\nfrom libopenage.main cimport main_arguments, run_game as run_game_cpp\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom libopenage.error.handlers cimport set_exit_ok\n\n\ndef run_game(args, root_path):\n    \"\"\"\n    Launches the game after arguments were translated.\n    \"\"\"\n    cdef int result\n\n    # argument translation\n    cdef main_arguments args_cpp\n\n    set_exit_ok(False)\n    try:\n        # root_path is a util.fslike.Path object from python\n        args_cpp.root_path = Path_cpp(PyObj(<PyObject*>root_path.fsobj),\n                                    root_path.parts)\n\n        # opengl debugging\n        args_cpp.gl_debug = args.gl_debug\n\n        # headless mode\n        args_cpp.headless = args.headless\n\n        # mods\n        if args.modpacks is not None:\n            args_cpp.mods = args.modpacks\n        else:\n            args_cpp.mods = vector[string]()\n\n        # window\n        args_cpp.window_args.width = args.window_args[\"width\"]\n        args_cpp.window_args.height = args.window_args[\"height\"]\n        args_cpp.window_args.vsync = args.window_args[\"vsync\"]\n        args_cpp.window_args.mode = args.window_args[\"window_mode\"].encode('utf-8')\n\n        # run the game!\n        with nogil:\n            result = run_game_cpp(args_cpp)\n\n        return result\n    finally:\n        set_exit_ok(True)\n"
  },
  {
    "path": "openage/main/tests.pyx",
    "content": "# Copyright 2018-2019 the openage authors. See copying.md for legal info.\n\n\"\"\"\ntests for the engine itself.\n\"\"\"\n\nimport argparse\n\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom cpython.ref cimport PyObject\nfrom libopenage.main.tests cimport engine_demo as engine_demo_c\n\n\ndef engine_demo(list argv):\n    \"\"\"\n    Invokes the available engine demos.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... engine_demo',\n        description='Demo of the game engine features')\n    cmd.add_argument(\"test_id\", type=int, help=\"id of the demo to run.\")\n    cmd.add_argument(\"--asset-dir\",\n                     help=\"Use this as an additional asset directory.\")\n    cmd.add_argument(\"--cfg-dir\",\n                     help=\"Use this as an additional config directory.\")\n\n    args = cmd.parse_args(argv)\n\n    from ..cvar.location import get_config_path\n    from ..assets import get_asset_path\n    from ..util.fslike.union import Union\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    root[\"assets\"].mount(get_asset_path(args.asset_dir))\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n\n    cdef int engine_test_id = args.test_id\n\n    cdef Path_cpp root_cpp = Path_cpp(PyObj(<PyObject*>root.fsobj),\n                                      root.parts)\n\n    with nogil:\n        engine_demo_c(engine_test_id, root_cpp)\n"
  },
  {
    "path": "openage/nyan/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\timport_tree.py\n\tnyan_structs.py\n)\n"
  },
  {
    "path": "openage/nyan/__init__.py",
    "content": "# Copyright 2022-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nNyan-compatible object creation and export for use with\nhttps://github.com/SFTtech/nyan\n\"\"\"\n"
  },
  {
    "path": "openage/nyan/import_tree.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nTree structure for resolving imports.\n\"\"\"\nfrom __future__ import annotations\nfrom enum import Enum\nimport typing\n\nfrom openage.log import warn\n\nif typing.TYPE_CHECKING:\n    from openage.convert.entity_object.export.formats.nyan_file import NyanFile\n    from openage.nyan.nyan_structs import NyanObject\n\n\nclass NodeType(Enum):\n    \"\"\"\n    Types for nodes.\n    \"\"\"\n    ROOT      = \"r\"     # tree root\n    FILESYS   = \"f\"     # directory or file\n    OBJECT    = \"o\"     # object in file (top level)\n    NESTED    = \"no\"    # nested object\n\n\nclass Node:\n    \"\"\"\n    Node in the import tree. This can be a directory, a file\n    or an object.\n    \"\"\"\n\n    __slots__ = ('name', 'node_type', 'parent', 'depth', 'children', 'alias')\n\n    def __init__(self, name: str, node_type: NodeType, parent):\n        \"\"\"\n        Create a node for an import tree.\n\n        :param name: Name of the node.\n        :type name: str\n        :param node_type: Type of the node.\n        :type node_type: NodeType\n        :param parent: Parent node of this node.\n        :type parent: Node\n        \"\"\"\n\n        self.name = name\n        self.node_type = node_type\n        self.parent: Node = parent\n\n        if not self.parent and self.node_type is not NodeType.ROOT:\n            raise TypeError(\"Only node with type ROOT are allowed to have no parent\")\n\n        self.depth = 0\n        if self.node_type is NodeType.ROOT:\n            self.depth = 0\n\n        else:\n            self.depth = self.parent.depth + 1\n\n        self.children = {}\n\n        self.alias = \"\"\n\n    def add_child(self, child_node) -> None:\n        \"\"\"\n        Adds a child node to this node.\n        \"\"\"\n        self.children.update({child_node.name: child_node})\n\n    def has_ancestor(self, ancestor_node, max_distance: int = 128) -> bool:\n        \"\"\"\n        Checks is the node has a given node as ancestor.\n\n        :param ancestor_node: Ancestor candidate node.\n        :type ancestor_node: Node\n        \"\"\"\n        current_node = self\n        distance = 0\n        while distance < max_distance:\n            if current_node.parent is ancestor_node:\n                return True\n\n            if not current_node.parent:\n                return False\n\n            current_node = current_node.parent\n            distance += 1\n\n        return False\n\n    def has_child(self, name: str) -> bool:\n        \"\"\"\n        Checks if a child with the given name exists.\n\n        :param name: Name of the child node.\n        :type name: str\n        \"\"\"\n        return name in self.children\n\n    def get_child(self, name: str):\n        \"\"\"\n        Returns the child noe with the given name.\n\n        :param name: Name of the child node.\n        :type name: str\n        \"\"\"\n        return self.children[name]\n\n    def get_fqon(self) -> tuple[str]:\n        \"\"\"\n        Get the fqon that is associated with this node by traversing the tree upwards.\n        \"\"\"\n        current_node = self\n        fqon = []\n        while current_node.node_type is not NodeType.ROOT:\n            fqon.insert(0, current_node.name)\n            current_node = current_node.parent\n\n        return tuple(fqon)\n\n    def set_alias(self, alias: str) -> None:\n        \"\"\"\n        Give this node an alias name.\n\n        :param alias: Alias for the node.\n        :type alias: str\n        \"\"\"\n        if self.node_type is not NodeType.FILESYS:\n            raise TypeError(\"Only nodes of type FILESYS can have aliases\")\n\n        self.alias = alias\n\n\nclass ImportTree:\n    \"\"\"\n    Tree for storing nyan object references.\n    \"\"\"\n\n    __slots__ = ('root', 'alias_nodes', 'import_nodes')\n\n    def __init__(self):\n        self.root = Node(\"\", NodeType.ROOT, None)\n\n        # Saves nodes for the import dict that have an alias\n        self.alias_nodes: set[Node] = set()\n\n        # Saves nodes for the import dict that don't have an alias\n        self.import_nodes: set[Node] = set()\n\n    def add_alias(self, fqon: tuple[str], alias: str) -> None:\n        \"\"\"\n        Adds an alias to the node with the specified fqon.\n\n        :param fqon: Identifier of the node.\n        :type fqon: tuple[str]\n        :param alias: Alias for the node.\n        :type alias: str\n        \"\"\"\n        current_node = self.root\n        for node_str in fqon:\n            try:\n                current_node = current_node.get_child(node_str)\n\n            except KeyError:  # as err:\n                # TODO: Fail when the fqon is not found in the tree\n                warn(f\"fqon '{'.'.join(fqon)}' \"\n                     \"could not be found in import tree\")\n                return\n                # raise KeyError(f\"fqon '{'.'.join(fqon)}' \"\n                #               \"could not be found in import tree\") from err\n\n        current_node.set_alias(alias)\n\n    def clear_marks(self) -> None:\n        \"\"\"\n        Remove all alias marks from the tree.\n        \"\"\"\n        self.alias_nodes.clear()\n        self.import_nodes.clear()\n\n    def expand_from_file(self, nyan_file: NyanFile) -> None:\n        \"\"\"\n        Expands the tree from a nyan file.\n\n        :param nyan_file: File with nyan objects.\n        :type nyan_file: NyanFile\n        \"\"\"\n        current_node = self.root\n        fqon = nyan_file.get_fqon()\n        node_type = NodeType.FILESYS\n\n        for node_str in fqon:\n            if current_node.has_child(node_str):\n                # Choose the already created node\n                current_node = current_node.get_child(node_str)\n\n            else:\n                # Add a new node\n                new_node = Node(node_str, node_type, current_node)\n                current_node.add_child(new_node)\n                current_node = new_node\n\n        # Process fqons of the contained objects\n        for nyan_object in nyan_file.nyan_objects:\n            self.expand_from_object(nyan_object)\n\n    def expand_from_object(self, nyan_object: NyanObject) -> None:\n        \"\"\"\n        Expands the tree from a nyan object.\n\n        :param nyan_object: A nyan object.\n        :type nyan_object: NyanObject\n        \"\"\"\n        # Process the object\n        fqon = nyan_object.get_fqon()\n\n        if fqon[0] != \"engine\":\n            current_node = self.root\n            node_type = NodeType.OBJECT\n\n            for node_str in fqon:\n                if current_node.has_child(node_str):\n                    # Choose the already created node\n                    current_node = current_node.get_child(node_str)\n\n                else:\n                    # Add a new node\n                    new_node = Node(node_str, node_type, current_node)\n                    current_node.add_child(new_node)\n                    current_node = new_node\n\n        else:\n            # Workaround for API objects because they are not loaded\n            # from files currently.\n            # TODO: Remove workaround when API is loaded from files.\n            current_node = self.root\n            index = 0\n            while index < len(fqon):\n                node_str = fqon[index]\n                if current_node.has_child(node_str):\n                    # Choose the already created node\n                    current_node = current_node.get_child(node_str)\n\n                else:\n                    # Add a new node\n                    if node_str[0].islower():\n                        # By convention, directory and file names are lower case\n                        # We can check for that to determine the node type.\n                        node_type = NodeType.FILESYS\n\n                    else:\n                        node_type = NodeType.OBJECT\n\n                    new_node = Node(node_str, node_type, current_node)\n                    current_node.add_child(new_node)\n                    current_node = new_node\n\n                index += 1\n\n        self._expand_nested_objects(nyan_object)\n\n    def _expand_nested_objects(self, nyan_object: NyanObject):\n        \"\"\"\n        Recursively search the nyan objects for nested objects\n        \"\"\"\n        unsearched_objects = []\n        unsearched_objects.extend(nyan_object.get_nested_objects())\n        found_nested_objects = []\n\n        while len(unsearched_objects) > 0:\n            current_nested_object = unsearched_objects[0]\n            unsearched_objects.extend(current_nested_object.get_nested_objects())\n            found_nested_objects.append(current_nested_object)\n\n            unsearched_objects.remove(current_nested_object)\n\n        # Process fqons of the nested objects\n        for nested_object in found_nested_objects:\n            current_node = self.root\n            node_type = NodeType.NESTED\n            fqon = nested_object.get_fqon()\n\n            for node_str in fqon:\n                if current_node.has_child(node_str):\n                    # Choose the already created node\n                    current_node = current_node.get_child(node_str)\n\n                else:\n                    # Add a new node\n                    new_node = Node(node_str, node_type, current_node)\n                    current_node.add_child(new_node)\n                    current_node = new_node\n\n    def get_alias_dict(self) -> dict[str, tuple[str]]:\n        \"\"\"\n        Get the fqons of the nodes that are used for aliases, i.e. fqons of all\n        nodes in self.alias_nodes. The dict can be used for creating imports\n        of a nyan file.\n\n        Call this function after all object references in a file have been\n        searched for aliases with get_alias_fqon().\n        \"\"\"\n        aliases = {}\n        for current_node in self.alias_nodes:\n            if current_node.alias in aliases:\n                raise ValueError(f\"duplicate alias: {current_node.alias}\")\n\n            aliases.update({current_node.alias: current_node.get_fqon()})\n\n        # Sort by imported name because it looks NICE!\n        aliases = dict(sorted(aliases.items(), key=lambda item: item[1]))\n\n        return aliases\n\n    def get_import_list(self) -> list[tuple[str]]:\n        \"\"\"\n        Get the fqons of the nodes that are plain imports, i.e. fqons of all\n        nodes in self.import_nodes. The dict can be used for creating imports\n        of a nyan file.\n\n        Call this function after all object references in a file have been\n        searched for aliases with get_alias_fqon().\n        \"\"\"\n        imports = []\n        for current_node in self.import_nodes:\n            imports.append(current_node.get_fqon())\n\n        # Sort by imported name because it looks NICE!\n        imports.sort()\n\n        return imports\n\n    def get_alias_fqon(self, fqon: tuple[str], namespace: tuple[str] = None) -> tuple[str]:\n        \"\"\"\n        Find the (shortened) fqon by traversing the tree to the fqon node and\n        then going upwards until an alias is found.\n\n        :param fqon: Object reference for which an alias should be found.\n        :type fqon: tuple[str]\n        :param namespace: Identifier of a namespace. If this is a (nested) object,\n                          we check if the fqon is in the namespace before\n                          searching for an alias.\n        :type namespace: tuple[str]\n        \"\"\"\n        if namespace:\n            current_node = self.root\n\n            if len(namespace) <= len(fqon):\n                # Check if the fqon is in the namespace by comparing their identifiers\n                for index, namespace_part in enumerate(namespace):\n                    current_node = current_node.get_child(namespace_part)\n\n                    if namespace_part != fqon[index]:\n                        break\n\n                else:\n                    # Check if the namespace node is an object\n                    if current_node.node_type in (NodeType.OBJECT, NodeType.NESTED):\n                        # The object with the fqon is nested and we don't have to look\n                        # up an alias\n                        return (fqon[-1],)\n\n        # Traverse the tree downwards\n        current_node = self.root\n        for part in fqon:\n            current_node = current_node.get_child(part)\n\n        # Traverse the tree upwards\n        sfqon = []\n        file_node = None\n        while current_node.depth > 0:\n            if file_node is None and current_node.node_type == NodeType.FILESYS:\n                file_node = current_node\n\n            if current_node.alias:\n                sfqon.insert(0, current_node.alias)\n                self.alias_nodes.add(current_node)\n                break\n\n            sfqon.insert(0, current_node.name)\n\n            current_node = current_node.parent\n\n        else:\n            # There is no alias so we have to import the correct file\n            if file_node:\n                self.import_nodes.add(file_node)\n\n        return tuple(sfqon)\n"
  },
  {
    "path": "openage/nyan/nyan_structs.py",
    "content": "# Copyright 2019-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-many-lines,too-many-arguments,too-many-return-statements,too-many-locals\n\n\"\"\"\nNyan structs.\n\nSimple implementation to store nyan objects and\nmembers for usage in the converter. This is not\na real nyan^TM implementation, but rather a \"dumb\"\nstorage format.\n\nPython does not enforce static types, so be careful\n and only use the provided functions, please. :)\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\n\nfrom enum import Enum\nimport re\n\nfrom ..util.ordered_set import OrderedSet\n\nif typing.TYPE_CHECKING:\n    from openage.nyan.import_tree import ImportTree\n\n\nINDENT = \"    \"\nMAX_LINE_WIDTH = 130\n\n\nclass NyanObject:\n    \"\"\"\n    Superclass for nyan objects.\n    \"\"\"\n\n    __slots__ = ('name', '_fqon', '_parents', '_inherited_members', '_members',\n                 '_nested_objects', '_children')\n\n    def __init__(\n        self,\n        name: str,\n        parents: OrderedSet[NyanObject] = None,\n        members: OrderedSet[NyanMember] = None,\n        nested_objects: OrderedSet[NyanObject] = None\n    ):\n        \"\"\"\n        Initializes the object and does some correctness\n        checks, for your convenience.\n        \"\"\"\n        self.name = name\n\n        # unique identifier (in modpack)\n        self._fqon: tuple[str] = (self.name,)\n\n        # parent objects\n        self._parents: OrderedSet[NyanObject] = OrderedSet()\n        # members inherited from parents\n        self._inherited_members: OrderedSet[InheritedNyanMember] = OrderedSet()\n        if parents:\n            self._parents.update(parents)\n\n        # members unique to this object\n        self._members: OrderedSet[NyanMember] = OrderedSet()\n        if members:\n            self._members.update(members)\n\n        # nested objects\n        self._nested_objects: OrderedSet[NyanObject]  = OrderedSet()\n        if nested_objects:\n            self._nested_objects.update(nested_objects)\n\n            for nested_object in self._nested_objects:\n                nested_object.set_fqon(f\"{self._fqon}.{nested_object.get_name()}\")\n\n        # Set of children\n        self._children: OrderedSet[NyanObject] = OrderedSet()\n\n        self._sanity_check()\n\n        if len(self._parents) > 0:\n            self._process_inheritance()\n\n    def add_nested_object(self, new_nested_object: NyanObject) -> None:\n        \"\"\"\n        Adds a nested object to the nyan object.\n        \"\"\"\n        if not isinstance(new_nested_object, NyanObject):\n            raise TypeError(\"nested object must have <NyanObject> type\")\n\n        if new_nested_object is self:\n            raise ValueError(\n                \"nyan object must not contain itself as nested object\")\n\n        self._nested_objects.add(new_nested_object)\n\n        new_nested_object.set_fqon((*self._fqon,\n                                    new_nested_object.get_name()))\n\n    def add_member(self, new_member: NyanMember) -> None:\n        \"\"\"\n        Adds a member to the nyan object.\n        \"\"\"\n        if new_member.is_inherited():\n            raise TypeError(\"added member cannot be inherited\")\n\n        if not isinstance(new_member, NyanMember):\n            raise TypeError(\"added member must have <NyanMember> type\")\n\n        self._members.add(new_member)\n\n        # Update child objects\n        for child in self._children:\n            # Create a new member for every child with self as parent and origin\n            inherited_member = InheritedNyanMember(\n                new_member.get_name(),\n                new_member.get_member_type(),\n                self,\n                self,\n                None,\n                None,\n                0\n            )\n            child.update_inheritance(inherited_member)\n\n    def add_child(self, new_child: NyanObject) -> None:\n        \"\"\"\n        Registers another object as a child.\n        \"\"\"\n        if not isinstance(new_child, NyanObject):\n            raise TypeError(\"children must have <NyanObject> type\")\n\n        self._children.add(new_child)\n\n        # Pass members and inherited members to the child object\n        for member in self._members:\n            # Create a new member with self as parent and origin\n            inherited_member = InheritedNyanMember(\n                member.get_name(),\n                member.get_member_type(),\n                self,\n                self,\n                None,\n                None,\n                0\n            )\n            new_child.update_inheritance(inherited_member)\n\n        for inherited in self._inherited_members:\n            # Create a new member with self as parent\n            inherited_member = InheritedNyanMember(\n                inherited.get_name(),\n                inherited.get_member_type(),\n                self,\n                inherited.get_origin(),\n                None,\n                None,\n                0\n            )\n            new_child.update_inheritance(inherited_member)\n\n    def has_member(self, member_name: str, origin: NyanObject = None) -> bool:\n        \"\"\"\n        Returns True if the NyanMember with the specified name exists.\n        \"\"\"\n        if origin and origin is not self:\n            for inherited_member in self._inherited_members:\n                if origin == inherited_member.get_origin():\n                    if inherited_member.get_name() == member_name:\n                        return True\n\n        else:\n            for member in self._members:\n                if member.get_name() == member_name:\n                    return True\n\n        return False\n\n    def get_fqon(self) -> tuple[str]:\n        \"\"\"\n        Returns the fqon of the nyan object.\n        \"\"\"\n        return self._fqon\n\n    def get_members(self) -> OrderedSet[NyanMember]:\n        \"\"\"\n        Returns all NyanMembers of the object, including inherited members.\n        \"\"\"\n        return self._members.union(self._inherited_members)\n\n    def get_member_by_name(self, member_name: str, origin: NyanObject = None) -> NyanMember:\n        \"\"\"\n        Returns the NyanMember with the specified name.\n        \"\"\"\n        if origin and origin is not self:\n            # Inherited member: Search in the inheritance tree\n            for inherited_member in self._inherited_members:\n                if origin == inherited_member.get_origin():\n                    if inherited_member.get_name() == member_name:\n                        return inherited_member\n\n            raise ValueError(f\"{repr(self)} has no member '{member_name}' with origin '{origin}'\")\n\n        # Else: Member should be a direct member of this nyan object\n        for member in self._members:\n            if member.get_name() == member_name:\n                return member\n\n        raise ValueError(f\"{self} has no member '{member_name}'\")\n\n    def get_uninitialized_members(self) -> list:\n        \"\"\"\n        Returns all uninitialized NyanMembers of the object.\n        \"\"\"\n        uninit_members = []\n        for member in self.get_members():\n            if not member.is_initialized():\n                uninit_members.append(member)\n\n        return uninit_members\n\n    def get_name(self) -> str:\n        \"\"\"\n        Returns the name of the object.\n        \"\"\"\n        return self.name\n\n    def get_nested_objects(self) -> OrderedSet[NyanObject]:\n        \"\"\"\n        Returns all nested NyanObjects of this object.\n        \"\"\"\n        return self._nested_objects\n\n    def get_parents(self) -> OrderedSet[NyanObject]:\n        \"\"\"\n        Returns all nested parents of this object.\n        \"\"\"\n        return self._parents\n\n    def has_ancestor(self, nyan_object: NyanObject) -> bool:\n        \"\"\"\n        Returns True if the given nyan object is an ancestor\n        of this nyan object.\n        \"\"\"\n        for parent in self._parents:\n            if parent is nyan_object:\n                return True\n\n        for parent in self._parents:\n            if parent.has_ancestor(nyan_object):\n                return True\n\n        return False\n\n    def is_abstract(self) -> bool:\n        \"\"\"\n        Returns True if any unique or inherited members are uninitialized.\n        \"\"\"\n        return len(self.get_uninitialized_members()) > 0\n\n    @staticmethod\n    def is_patch() -> bool:\n        \"\"\"\n        Returns True if the object is a NyanPatch.\n        \"\"\"\n        return False\n\n    def set_fqon(self, new_fqon: tuple[str]):\n        \"\"\"\n        Set a new value for the fqon.\n        \"\"\"\n        if isinstance(new_fqon, str):\n            self._fqon = new_fqon.split(\".\")\n\n        elif isinstance(new_fqon, tuple):\n            self._fqon = new_fqon\n\n        else:\n            raise TypeError(f\"{self}: Fqon must be a tuple(str) not {type(new_fqon)}\")\n\n        # Recursively set fqon for nested objects\n        for nested_object in self._nested_objects:\n            nested_fqon = (*new_fqon, nested_object.get_name())\n            nested_object.set_fqon(nested_fqon)\n\n    def update_inheritance(self, new_inherited_member: InheritedNyanMember) -> None:\n        \"\"\"\n        Add an inherited member to the object. Should only be used by\n        parent objects.\n        \"\"\"\n        if not self.has_ancestor(new_inherited_member.get_origin()):\n            raise ValueError(f\"{repr(self)}: cannot add inherited member \"\n                             f\"{new_inherited_member} because \"\n                             f\"{new_inherited_member.get_origin()} is not \"\n                             f\"an ancestor of {repr(self)}\")\n\n        if not isinstance(new_inherited_member, InheritedNyanMember):\n            raise TypeError(\"added member must have <InheritedNyanMember> type\")\n\n        # Only add it, if it was not inherited before\n        if not self.has_member(new_inherited_member.get_name(),\n                               new_inherited_member.get_origin()):\n            self._inherited_members.add(new_inherited_member)\n\n        # Update child objects\n        for child in self._children:\n            # Create a new member for every child with self as parent\n            inherited_member = InheritedNyanMember(\n                new_inherited_member.get_name(),\n                new_inherited_member.get_member_type(),\n                self,\n                new_inherited_member.get_origin(),\n                None,\n                None,\n                0\n            )\n            child.update_inheritance(inherited_member)\n\n    def dump(self, indent_depth: int = 0, import_tree: ImportTree = None) -> str:\n        \"\"\"\n        Returns the string representation of the object.\n        \"\"\"\n        # Header\n        output_str = f\"{self.get_name()}\"\n\n        output_str += self._prepare_inheritance_content(import_tree=import_tree)\n\n        # Members\n        output_str += self._prepare_object_content(indent_depth, import_tree=import_tree)\n\n        return output_str\n\n    def _prepare_object_content(self, indent_depth: int, import_tree: ImportTree = None) -> None:\n        \"\"\"\n        Returns a string containing the nyan object's content\n        (members, nested objects).\n\n        Subroutine of dump().\n        \"\"\"\n        output_str = \"\"\n        empty = True\n\n        if len(self._inherited_members) > 0:\n            for inherited_member in self._inherited_members:\n                if inherited_member.has_value():\n                    empty = False\n                    member_str = inherited_member.dump(\n                        indent_depth + 1,\n                        import_tree=import_tree,\n                        namespace=self.get_fqon()\n                    )\n                    output_str += f\"{(indent_depth + 1) * INDENT}{member_str}\\n\"\n\n            if not empty:\n                output_str += \"\\n\"\n\n        if len(self._members) > 0:\n            empty = False\n            for member in self._members:\n                if self.is_patch():\n                    # Patches do not need the type definition\n                    member_str = member.dump_short(\n                        indent_depth + 1,\n                        import_tree=import_tree,\n                        namespace=self.get_fqon()\n                    )\n\n                else:\n                    member_str = member.dump(\n                        indent_depth + 1,\n                        import_tree=import_tree,\n                        namespace=self.get_fqon()\n                    )\n\n                output_str += f\"{(indent_depth + 1) * INDENT}{member_str}\\n\"\n\n            output_str += \"\\n\"\n\n        # Nested objects\n        if len(self._nested_objects) > 0:\n            empty = False\n            for nested_object in self._nested_objects:\n                nested_str = nested_object.dump(\n                    indent_depth + 1,\n                    import_tree=import_tree\n                )\n                output_str += f\"{(indent_depth + 1) * INDENT}{nested_str}\\n\"\n\n            output_str = output_str[:-1]\n\n        # Empty objects need a 'pass' line\n        if empty:\n            output_str += f\"{(indent_depth + 1) * INDENT}pass\\n\\n\"\n\n        return output_str\n\n    def _prepare_inheritance_content(self, import_tree: ImportTree = None) -> None:\n        \"\"\"\n        Returns a string containing the nyan object's inheritance set\n        in the header.\n\n        Subroutine of dump().\n        \"\"\"\n        output_str = \"(\"\n\n        if len(self._parents) > 0:\n            for parent in self._parents:\n                if import_tree:\n                    sfqon = \".\".join(import_tree.get_alias_fqon(\n                        parent.get_fqon(),\n                        namespace=self.get_fqon()\n                    ))\n\n                else:\n                    sfqon = \".\".join(parent.get_fqon())\n\n                output_str += f\"{sfqon}, \"\n\n            output_str = output_str[:-2]\n\n        output_str += \"):\\n\"\n\n        return output_str\n\n    def _process_inheritance(self) -> None:\n        \"\"\"\n        Notify parents of the object.\n        \"\"\"\n        for parent in self._parents:\n            parent.add_child(self)\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the object conforms to nyan grammar rules. Also does\n        a bunch of type checks.\n        \"\"\"\n        # self.name must be a string\n        if not isinstance(self.name, str):\n            raise TypeError(f\"{repr(self)}: 'name' must be a string\")\n\n        # self.name must conform to nyan grammar rules\n        if not re.fullmatch(r\"[a-zA-Z_][a-zA-Z0-9_]*\", self.name):\n            raise SyntaxError(f\"{repr(self)}: 'name' is not well-formed\")\n\n        # self._parents must be NyanObjects\n        for parent in self._parents:\n            if not isinstance(parent, NyanObject):\n                raise TypeError(f\"{repr(self)}: {repr(parent)} must have NyanObject type\")\n\n        # self._members must be NyanMembers\n        for member in self._members:\n            if not isinstance(member, NyanMember):\n                raise TypeError(f\"{repr(self)}: {repr(member)} must have NyanMember type\")\n\n            # a member in self._members must also not be inherited\n            if isinstance(member, InheritedNyanMember):\n                raise TypeError(f\"{repr(self)}: regular member {repr(member)} must not \"\n                                \"have InheritedNyanMember type\")\n\n        # self._nested_objects must be NyanObjects\n        for nested_object in self._nested_objects:\n            if not isinstance(nested_object, NyanObject):\n                raise TypeError(f\"{repr(self)}: {repr(nested_object)} must have NyanObject type\")\n\n            if nested_object is self:\n                raise ValueError(f\"{repr(self)}: must not contain itself as nested object\")\n\n    def __repr__(self):\n        return f\"NyanObject<{self.name}>\"\n\n\nclass NyanPatch(NyanObject):\n    \"\"\"\n    Superclass for nyan patches.\n    \"\"\"\n\n    __slots__ = ('_target', '_add_inheritance')\n\n    def __init__(\n        self,\n        name: str,\n        parents: OrderedSet[NyanObject] = None,\n        members: OrderedSet[NyanObject] = None,\n        nested_objects: OrderedSet[NyanObject] = None,\n        target: NyanObject = None,\n        add_inheritance: OrderedSet[NyanObject] = None\n    ):\n\n        self._target = target                  # patch target (can be added later)\n        self._add_inheritance = OrderedSet()   # new inheritance\n        if add_inheritance:\n            self._add_inheritance.update(add_inheritance)\n\n        super().__init__(name, parents, members, nested_objects)\n\n    def get_target(self) -> NyanObject:\n        \"\"\"\n        Returns the target of the patch.\n        \"\"\"\n        return self._target\n\n    def is_abstract(self) -> bool:\n        \"\"\"\n        Returns True if unique or inherited members were\n        not initialized or the patch target is not set.\n        \"\"\"\n        return super().is_abstract() or not self._target\n\n    @staticmethod\n    def is_patch() -> bool:\n        \"\"\"\n        Returns True if the object is a nyan patch.\n        \"\"\"\n        return True\n\n    def set_target(self, target: NyanObject) -> NyanObject:\n        \"\"\"\n        Set the target of the patch.\n        \"\"\"\n        self._target = target\n\n        if not isinstance(self._target, NyanObject):\n            raise TypeError(f\"{repr(self)}: '_target' must have NyanObject type\")\n\n    def dump(self, indent_depth: int = 0, import_tree: ImportTree = None) -> str:\n        \"\"\"\n        Returns the string representation of the object.\n        \"\"\"\n        # Header\n        output_str = f\"{self.get_name()}\"\n\n        if import_tree:\n            sfqon = \".\".join(import_tree.get_alias_fqon(self._target.get_fqon()))\n\n        else:\n            sfqon = \".\".join(self._target.get_fqon())\n\n        output_str += f\"<{sfqon}>\"\n\n        if len(self._add_inheritance) > 0:\n            output_str += \"[\"\n\n            for new_inheritance in self._add_inheritance:\n                if import_tree:\n                    sfqon = \".\".join(import_tree.get_alias_fqon(new_inheritance.get_fqon()))\n\n                else:\n                    sfqon = \".\".join(new_inheritance.get_fqon())\n\n                if new_inheritance[0] == \"FRONT\":\n                    output_str += f\"+{sfqon}, \"\n                elif new_inheritance[0] == \"BACK\":\n                    output_str += f\"{sfqon}+, \"\n\n            output_str = output_str[:-2] + \"]\"\n\n        output_str += super()._prepare_inheritance_content(import_tree = import_tree)\n\n        # Members\n        output_str += super()._prepare_object_content(indent_depth = indent_depth,\n                                                      import_tree = import_tree)\n\n        return output_str\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the object conforms to nyan grammar rules. Also does\n        a bunch of type checks.\n        \"\"\"\n        super()._sanity_check()\n\n        # Target must be a nyan object\n        if self._target:\n            if not isinstance(self._target, NyanObject):\n                raise TypeError(f\"{repr(self)}: '_target' must have NyanObject type\")\n\n        # Added inheritance must be tuples of \"FRONT\"/\"BACK\"\n        # and a nyan object\n        if len(self._add_inheritance) > 0:\n            for inherit in self._add_inheritance:\n                if not isinstance(inherit, tuple):\n                    raise TypeError(f\"{repr(self)}: '_add_inheritance' must be a tuple\")\n\n                if len(inherit) != 2:\n                    raise SyntaxError(f\"{repr(self)}: '_add_inheritance' tuples must have length 2\")\n\n                if inherit[0] not in (\"FRONT\", \"BACK\"):\n                    raise ValueError(f\"{repr(self)}: added inheritance must be FRONT or BACK mode\")\n\n                if not isinstance(inherit[1], NyanObject):\n                    raise ValueError(f\"{repr(self)}: added inheritance must contain NyanObject\")\n\n    def __repr__(self):\n        return f\"NyanPatch<{self.name}<{self._target.name}>>\"\n\n\nclass NyanMemberType:\n    \"\"\"\n    Superclass for nyan member types.\n    \"\"\"\n\n    __slots__ = ('_member_type', '_element_types')\n\n    def __init__(\n        self,\n        member_type: typing.Union[str, MemberType, NyanObject],\n        element_types: typing.Collection[NyanMemberType] = None\n    ):\n        \"\"\"\n        Initializes the member type and does some correctness\n        checks, for your convenience.\n        \"\"\"\n        if isinstance(member_type, NyanObject):\n            self._member_type = member_type\n\n        else:\n            self._member_type = MemberType(member_type)\n\n        self._element_types = None\n        if element_types:\n            self._element_types = tuple(element_types)\n\n        # check for errors in the initilization\n        self._sanity_check()\n\n    def get_type(self) -> MemberType:\n        \"\"\"\n        Returns the member type.\n        \"\"\"\n        return self._member_type\n\n    def get_real_type(self) -> MemberType:\n        \"\"\"\n        Returns the member type without wrapping modifiers.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].get_real_type()\n\n        return self._member_type\n\n    def get_element_types(self) -> tuple[NyanMemberType, ...]:\n        \"\"\"\n        Returns the element types.\n        \"\"\"\n        return self._element_types\n\n    def get_real_element_types(self) -> tuple[NyanMemberType, ...]:\n        \"\"\"\n        Returns the element types without wrapping modifiers.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].get_real_element_types()\n\n        return self._element_types\n\n    def is_primitive(self) -> bool:\n        \"\"\"\n        Returns True if the member type is a single value.\n        \"\"\"\n        return self._member_type in (MemberType.INT,\n                                     MemberType.FLOAT,\n                                     MemberType.TEXT,\n                                     MemberType.FILE,\n                                     MemberType.BOOLEAN)\n\n    def is_real_primitive(self) -> bool:\n        \"\"\"\n        Returns True if the member type is a primitive wrapped in a modifier.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].is_real_primitive()\n\n        return self.is_primitive()\n\n    def is_complex(self) -> bool:\n        \"\"\"\n        Returns True if the member type is a collection.\n        \"\"\"\n        return self._member_type in (MemberType.SET,\n                                     MemberType.ORDEREDSET,\n                                     MemberType.DICT)\n\n    def is_real_complex(self) -> bool:\n        \"\"\"\n        Returns True if the member type is a collection wrapped in a modifier.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].is_real_complex()\n\n        return self.is_complex()\n\n    def is_object(self) -> bool:\n        \"\"\"\n        Returns True if the member type is an object.\n        \"\"\"\n        return isinstance(self._member_type, NyanObject)\n\n    def is_real_object(self) -> bool:\n        \"\"\"\n        Returns True if the member type is an object wrapped in a modifier.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].is_real_object()\n\n        return self.is_object()\n\n    def is_modifier(self) -> bool:\n        \"\"\"\n        Returns True if the member type is a modifier.\n        \"\"\"\n        return self._member_type in (MemberType.ABSTRACT,\n                                     MemberType.CHILDREN,\n                                     MemberType.OPTIONAL)\n\n    def is_composite(self) -> bool:\n        \"\"\"\n        Returns True if the member is a composite type with at least one element type.\n        \"\"\"\n        return self.is_complex() or self.is_modifier()\n\n    def accepts_op(self, operator: MemberOperator) -> bool:\n        \"\"\"\n        Check if an operator is compatible with the member type.\n        \"\"\"\n        if self.is_modifier():\n            return self._element_types[0].accepts_op(operator)\n\n        if self._member_type in (MemberType.INT, MemberType.FLOAT)\\\n            and operator not in (MemberOperator.ASSIGN,\n                                 MemberOperator.ADD,\n                                 MemberOperator.SUBTRACT,\n                                 MemberOperator.MULTIPLY,\n                                 MemberOperator.DIVIDE):\n            return False\n\n        if self._member_type is MemberType.TEXT\\\n                and operator not in (MemberOperator.ASSIGN,\n                                     MemberOperator.ADD):\n            return False\n\n        if self._member_type is MemberType.FILE\\\n                and operator is not MemberOperator.ASSIGN:\n            return False\n\n        if self._member_type is MemberType.BOOLEAN\\\n                and operator not in (MemberOperator.ASSIGN,\n                                     MemberOperator.AND,\n                                     MemberOperator.OR):\n            return False\n\n        if self._member_type is MemberType.SET\\\n                and operator not in (MemberOperator.ASSIGN,\n                                     MemberOperator.ADD,\n                                     MemberOperator.SUBTRACT,\n                                     MemberOperator.AND,\n                                     MemberOperator.OR):\n            return False\n\n        if self._member_type is MemberType.ORDEREDSET\\\n                and operator not in (MemberOperator.ASSIGN,\n                                     MemberOperator.ADD,\n                                     MemberOperator.SUBTRACT,\n                                     MemberOperator.AND,\n                                     MemberOperator.OR):\n            return False\n\n        if self._member_type is MemberType.DICT\\\n                and operator not in (MemberOperator.ASSIGN,\n                                     MemberOperator.ADD,\n                                     MemberOperator.SUBTRACT,\n                                     MemberOperator.AND,\n                                     MemberOperator.OR):\n            return False\n\n        return True\n\n    def accepts_value(self, value) -> bool:\n        \"\"\"\n        Check if a value is compatible with the member type.\n        \"\"\"\n        # Member values can only be NYAN_NONE if the member is optional\n        if value is MemberSpecialValue.NYAN_NONE:\n            return self._member_type is MemberType.OPTIONAL\n\n        if self.is_modifier():\n            return self._element_types[0].accepts_value(value)\n\n        # inf is only used for ints and floats\n        if value is MemberSpecialValue.NYAN_INF and\\\n                self._member_type not in (MemberType.INT, MemberType.FLOAT):\n            return False\n\n        # Values that are nyan objects must be the member type\n        # or the children of the member type\n        if self.is_object():\n            if not (value is self._member_type or\n                    value.has_ancestor(self._member_type)):\n                return False\n\n        return True\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the member type and element types are compatiable.\n        \"\"\"\n        if self.is_composite():\n            # if the member type is a composite, then the element types need\n            # to be initialized\n            if not self._element_types:\n                raise TypeError(f\"{repr(self)}: element types are required for composite types\")\n\n            if self.is_complex():\n                # element types of complex types cannot be complex\n                for elem_type in self._element_types:\n                    if elem_type.is_real_complex():\n                        raise TypeError(\n                            f\"{repr(self)}: element types cannot be complex \"\n                            f\"but contains {elem_type}\")\n\n        else:\n            # if the member is not a composite, the element types should be None\n            if self._element_types:\n                raise TypeError(\n                    f\"{repr(self)}: member type has element types \"\n                    \"but is not a composite\")\n\n    def dump(self, import_tree: ImportTree = None, namespace: tuple[str] = None) -> str:\n        \"\"\"\n        Returns the nyan string representation of the member type.\n        \"\"\"\n        if self.is_primitive():\n            return self._member_type.value\n\n        if self.is_object():\n            if import_tree:\n                sfqon = \".\".join(import_tree.get_alias_fqon(\n                    self._member_type.get_fqon(),\n                    namespace\n                ))\n\n            else:\n                sfqon = \".\".join(self._member_type.get_fqon())\n\n            return sfqon\n\n        # Composite types\n        return (\n            f\"{self._member_type.value}(\"\n            f\"{', '.join(elem_type.dump(import_tree) for elem_type in self._element_types)})\"\n        )\n\n    def __repr__(self):\n        return f\"NyanMemberType<{self.dump()}>\"\n\n\nclass NyanMember:\n    \"\"\"\n    Superclass for all nyan members.\n    \"\"\"\n\n    __slots__ = ('name', '_member_type', 'value', '_operator', '_override_depth')\n\n    def __init__(\n        self,\n        name: str,\n        member_type: NyanMemberType,\n        value = None,\n        operator: MemberOperator = None,\n        override_depth: int = 0\n    ):\n        \"\"\"\n        Initializes the member and does some correctness\n        checks, for your convenience.\n        \"\"\"\n        self.name = name                                # identifier\n\n        if isinstance(member_type, NyanMemberType):     # type\n            self._member_type = member_type\n\n        else:\n            raise TypeError(f\"NyanMember<{self.name}>: Expected NyanMemberType for member_type \"\n                            f\"but got {type(member_type)}\")\n\n        self._override_depth = override_depth           # override depth\n\n        self._operator: MemberOperator = None\n        if operator:\n            operator = MemberOperator(operator)   # operator type\n\n        self.value = None                               # value\n        if value is not None:\n            # Needs to check for None because 0 is also False\n            self.set_value(value, operator)\n\n        # check for errors in the initilization\n        self._sanity_check()\n\n    def get_name(self) -> str:\n        \"\"\"\n        Returns the name of the member.\n        \"\"\"\n        return self.name\n\n    def get_member_type(self) -> NyanMemberType:\n        \"\"\"\n        Returns the type of the member.\n        \"\"\"\n        return self._member_type\n\n    def get_operator(self) -> MemberOperator:\n        \"\"\"\n        Returns the operator of the member.\n        \"\"\"\n        return self._operator\n\n    def get_override_depth(self) -> int:\n        \"\"\"\n        Returns the override depth of the member.\n        \"\"\"\n        return self._override_depth\n\n    def get_value(self):\n        \"\"\"\n        Returns the value of the member.\n        \"\"\"\n        return self.value\n\n    def is_primitive(self) -> bool:\n        \"\"\"\n        Returns True if the member is a single value.\n        \"\"\"\n        return self._member_type.is_real_primitive()\n\n    def is_complex(self) -> bool:\n        \"\"\"\n        Returns True if the member is a collection.\n        \"\"\"\n        return self._member_type.is_real_complex()\n\n    def is_object(self) -> bool:\n        \"\"\"\n        Returns True if the member is an object.\n        \"\"\"\n        return self._member_type.is_real_object()\n\n    def is_initialized(self) -> bool:\n        \"\"\"\n        Returns True if the member has a value.\n        \"\"\"\n        return self.value is not None\n\n    @ staticmethod\n    def is_inherited() -> bool:\n        \"\"\"\n        Returns True if the member is inherited from another object.\n        \"\"\"\n        return False\n\n    def has_value(self) -> bool:\n        \"\"\"\n        Returns True if the member has a value.\n        \"\"\"\n        return self.value is not None\n\n    def set_value(self, value, operator: MemberOperator = None) -> None:\n        \"\"\"\n        Set the value of the nyan member to the specified value and\n        optionally, the operator.\n        \"\"\"\n        if not self.value and not operator:\n            raise ValueError(f\"Setting a value for an uninitialized member {repr(self)} \"\n                             \"requires also setting the operator\")\n\n        self.value = value\n        self._operator = operator\n\n        if self.value not in (MemberSpecialValue.NYAN_INF, MemberSpecialValue.NYAN_NONE):\n            self._type_conversion()\n\n        self._sanity_check()\n\n    def dump(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of the member.\n        \"\"\"\n        output_str = f\"{self.name} : {self._member_type.dump(import_tree=import_tree)}\"\n\n        if self.is_initialized():\n            value_str = self._get_value_str(\n                indent_depth,\n                import_tree=import_tree,\n                namespace=namespace\n            )\n            output_str += (f\" {'@' * self._override_depth}{self._operator.value} {value_str}\")\n\n        return output_str\n\n    def dump_short(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of the member, but\n        without the type definition.\n        \"\"\"\n        value_str = self._get_value_str(\n            indent_depth,\n            import_tree=import_tree,\n            namespace=namespace\n        )\n        return f\"{self.get_name()} {'@' * self._override_depth}{self._operator.value} {value_str}\"\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the member conforms to nyan grammar rules. Also does\n        a bunch of type checks.\n        \"\"\"\n        # self.name must be a string\n        if not isinstance(self.name, str):\n            raise TypeError(f\"{repr(self)}: 'name' must be a string\")\n\n        # self.name must conform to nyan grammar rules\n        if not re.fullmatch(r\"[a-zA-Z_][a-zA-Z0-9_]*\", self.name[0]):\n            raise SyntaxError(f\"{repr(self)}: 'name' is not well-formed\")\n\n        if (self.is_initialized() and not self.is_inherited()) or\\\n                (self.is_inherited() and self.has_value()):\n            # override depth must be a non-negative integer\n            if not (isinstance(self._override_depth, int) and\n                    self._override_depth >= 0):\n                raise ValueError(f\"{repr(self)}: override depth must be a non-negative integer\")\n\n            # Check if operator type matches with member type\n            if not self._member_type.accepts_op(self._operator):\n                raise TypeError((\n                    f\"{repr(self)}: {self._operator} is not a valid\"\n                    f\"operator for member type {self._member_type}\"\n                ))\n\n            # Check if value is compatible with member type\n            if not self._member_type.accepts_value(self.value):\n                raise TypeError(f\"{repr(self)}: value '{self.value}' is not compatible \"\n                                f\"with type '{self._member_type}'\")\n\n    def _type_conversion(self) -> None:\n        \"\"\"\n        Explicit type conversion of the member value.\n\n        This lets us convert data fields without worrying about the\n        correct types too much, e.g. if a boolean is stored as uint8.\n        \"\"\"\n        if self._member_type.get_real_type() is MemberType.INT and\\\n                self._operator not in (MemberOperator.DIVIDE, MemberOperator.MULTIPLY):\n            self.value = int(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.FLOAT:\n            self.value = float(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.TEXT:\n            self.value = str(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.FILE:\n            self.value = str(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.BOOLEAN:\n            self.value = bool(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.SET:\n            self.value = OrderedSet(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.ORDEREDSET:\n            self.value = OrderedSet(self.value)\n\n        elif self._member_type.get_real_type() is MemberType.DICT:\n            self.value = dict(self.value)\n\n    @staticmethod\n    def _get_primitive_value_str(\n        member_type: NyanMemberType,\n        value,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of primitive values.\n\n        Subroutine of _get_value_str(..)\n        \"\"\"\n        if member_type.get_real_type() in (MemberType.TEXT, MemberType.FILE):\n            return f\"\\\"{value}\\\"\"\n\n        if member_type.is_real_object():\n            if import_tree:\n                sfqon = \".\".join(import_tree.get_alias_fqon(\n                    value.get_fqon(),\n                    namespace\n                ))\n\n            else:\n                sfqon = \".\".join(value.get_fqon())\n\n            return sfqon\n\n        return f\"{value}\"\n\n    def _get_complex_value_str(\n        self,\n        indent_depth: int,\n        member_type: NyanMemberType,\n        value,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of complex values.\n\n        Subroutine of _get_value_str()\n        \"\"\"\n        output_str = \"\"\n\n        if member_type.get_real_type() is MemberType.ORDEREDSET:\n            output_str += \"o\"\n\n        output_str += \"{\"\n\n        # Store the values for formatting\n        # TODO: Dicts\n        stored_values = []\n\n        if member_type.get_real_type() is MemberType.DICT:\n            for key, val in value.items():\n                subtype = member_type.get_real_element_types()[0]\n                key_str = self._get_primitive_value_str(\n                    subtype,\n                    key,\n                    import_tree=import_tree,\n                    namespace=namespace\n                )\n\n                subtype = member_type.get_real_element_types()[1]\n                val_str = self._get_primitive_value_str(\n                    subtype,\n                    val,\n                    import_tree=import_tree,\n                    namespace=namespace\n                )\n\n                stored_values.append(f\"{key_str}: {val_str}\")\n\n        else:\n            for val in value:\n                subtype = member_type.get_real_element_types()[0]\n                stored_values.append(self._get_primitive_value_str(\n                    subtype,\n                    val,\n                    import_tree=import_tree,\n                    namespace=namespace\n                ))\n\n        # Check if the line gets too long\n        # TODO: this does not account for a type definition\n        concat_values = \", \".join(stored_values)\n        line_length = len(indent_depth * INDENT) + len((\n            f\"{self.name} \"\n            f\"{'@' * self._override_depth}\"\n            f\"{self._operator.value} \"\n            f\"{concat_values}\"\n        ))\n\n        if line_length < MAX_LINE_WIDTH:\n            output_str += concat_values\n\n        elif stored_values:\n            output_str += \"\\n\"\n\n            # How much space is left per formatted line\n            space_left = MAX_LINE_WIDTH - len((indent_depth + 1) * INDENT)\n\n            # Find the longest value's length\n            longest_len = len(max(stored_values, key=len))\n\n            # How man values of that length fit in one line\n            values_per_line = space_left // longest_len\n            values_per_line = max(values_per_line, 1)\n\n            output_str += (indent_depth + 1) * INDENT\n\n            val_index = 0\n            end_index = len(stored_values)\n            for val in stored_values:\n                val_index += 1\n                output_str += val\n\n                if val_index % values_per_line == 0:\n                    output_str += \",\\n\"\n\n                    if val_index != end_index:\n                        output_str += ((indent_depth + 1) * INDENT)\n\n                else:\n                    output_str += \", \"\n\n            output_str = output_str[:-2] + \"\\n\"\n            output_str += (indent_depth * INDENT)\n\n        output_str = output_str + \"}\"\n\n        return output_str\n\n    def _get_value_str(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of the value.\n        \"\"\"\n        if not self.is_initialized():\n            return f\"UNINITIALIZED VALUE {repr(self)}\"\n\n        if self.value is MemberSpecialValue.NYAN_NONE:\n            return MemberSpecialValue.NYAN_NONE.value\n\n        if self.value is MemberSpecialValue.NYAN_INF:\n            return MemberSpecialValue.NYAN_INF.value\n\n        if self.is_primitive() or self.is_object():\n            return self._get_primitive_value_str(\n                self._member_type,\n                self.value,\n                import_tree=import_tree,\n                namespace=namespace\n            )\n\n        if self.is_complex():\n            return self._get_complex_value_str(\n                indent_depth,\n                self._member_type,\n                self.value,\n                import_tree=import_tree,\n                namespace=namespace\n            )\n\n        raise TypeError(f\"{repr(self)} has no valid type\")\n\n    def __str__(self):\n        return self._get_value_str(indent_depth=0)\n\n    def __repr__(self):\n        return f\"NyanMember<{self.name}: {self._member_type}>\"\n\n\nclass NyanPatchMember(NyanMember):\n    \"\"\"\n    Nyan members for patches.\n    \"\"\"\n\n    __slots__ = ('_patch_target', '_member_origin')\n\n    def __init__(\n        self,\n        name: str,\n        patch_target: NyanObject,\n        member_origin: NyanObject,\n        value,\n        operator: MemberOperator,\n        override_depth: int = 0\n    ):\n        \"\"\"\n        Initializes the member and does some correctness checks,\n        for your convenience. Other than the normal members,\n        patch members must initialize all values in the constructor\n        \"\"\"\n        # the target object of the patch\n        self._patch_target = patch_target\n\n        # the origin of the patched member from the patch target\n        self._member_origin = member_origin\n\n        target_member_type = self._get_target_member_type(name, member_origin)\n\n        super().__init__(name, target_member_type, value, operator, override_depth)\n\n    def get_name_with_origin(self) -> str:\n        \"\"\"\n        Returns the name of the member in <member_origin>.<name> form.\n        \"\"\"\n        return f\"{self._member_origin.name}.{self.name}\"\n\n    def dump(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the string representation of the member.\n        \"\"\"\n        return self.dump_short(indent_depth, import_tree=import_tree, namespace=namespace)\n\n    def dump_short(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of the member, but\n        without the type definition.\n        \"\"\"\n        value_str = self._get_value_str(\n            indent_depth,\n            import_tree=import_tree,\n            namespace=namespace\n        )\n        return (f\"{self.get_name_with_origin()} {'@' * self._override_depth}\"\n                f\"{self._operator.value} {value_str}\")\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the member conforms to nyan grammar rules. Also does\n        a bunch of type checks.\n        \"\"\"\n        super()._sanity_check()\n\n        # patch target must be a nyan object\n        if not isinstance(self._patch_target, NyanObject):\n            raise TypeError(f\"{self}: '_patch_target' must have NyanObject type\")\n\n        # member origin must be a nyan object\n        if not isinstance(self._member_origin, NyanObject):\n            raise TypeError(f\"{self}: '_member_origin' must have NyanObject type\")\n\n    def _get_target_member_type(self, name: str, origin: NyanObject):\n        \"\"\"\n        Retrieves the type of the patched member.\n        \"\"\"\n        target_member = self._member_origin.get_member_by_name(name, origin)\n\n        return target_member.get_member_type()\n\n    def __repr__(self):\n        return f\"NyanPatchMember<{self.name}: {self._member_type}>\"\n\n\nclass InheritedNyanMember(NyanMember):\n    \"\"\"\n    Nyan members inherited from other objects.\n    \"\"\"\n\n    __slots__ = ('_parent', '_origin')\n\n    def __init__(\n        self,\n        name: str,\n        member_type: NyanMemberType,\n        parent: NyanObject,\n        origin: NyanObject,\n        value=None,\n        operator: MemberOperator = None,\n        override_depth: int = 0\n    ):\n        \"\"\"\n        Initializes the member and does some correctness\n        checks, for your convenience.\n        \"\"\"\n        self._parent = parent   # the direct parent of the object which contains the member\n        self._origin = origin   # nyan object which originally defined the member\n\n        super().__init__(name, member_type, value, operator, override_depth)\n\n    def get_name_with_origin(self) -> str:\n        \"\"\"\n        Returns the name of the member in <origin>.<name> form.\n        \"\"\"\n        return f\"{self._origin.name}.{self.name}\"\n\n    def get_origin(self) -> NyanObject:\n        \"\"\"\n        Returns the origin of the member.\n        \"\"\"\n        return self._origin\n\n    def get_parent(self) -> NyanObject:\n        \"\"\"\n        Returns the direct parent of the member.\n        \"\"\"\n        return self._parent\n\n    @staticmethod\n    def is_inherited() -> bool:\n        \"\"\"\n        Returns True if the member is inherited from another object.\n        \"\"\"\n        return True\n\n    def is_initialized(self) -> bool:\n        \"\"\"\n        Returns True if self or the parent is initialized.\n        \"\"\"\n        return super().is_initialized() or\\\n            self._parent.get_member_by_name(self.name, self._origin).is_initialized()\n\n    def dump(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the string representation of the member.\n        \"\"\"\n        return self.dump_short(indent_depth, import_tree=import_tree, namespace=namespace)\n\n    def dump_short(\n        self,\n        indent_depth: int,\n        import_tree: ImportTree = None,\n        namespace: tuple[str] = None\n    ) -> str:\n        \"\"\"\n        Returns the nyan string representation of the member, but\n        without the type definition.\n        \"\"\"\n        value_str = self._get_value_str(\n            indent_depth,\n            import_tree=import_tree,\n            namespace=namespace\n        )\n        return (f\"{self.get_name_with_origin()} {'@' * self._override_depth}\"\n                f\"{self._operator.value} {value_str}\")\n\n    def _sanity_check(self) -> None:\n        \"\"\"\n        Check if the member conforms to nyan grammar rules. Also does\n        a bunch of type checks.\n        \"\"\"\n        super()._sanity_check()\n\n        # parent must be a nyan object\n        if not isinstance(self._parent, NyanObject):\n            raise TypeError(f\"{repr(self)}: '_parent' must have NyanObject type\")\n\n        # origin must be a nyan object\n        if not isinstance(self._origin, NyanObject):\n            raise TypeError(f\"{repr(self)}: '_origin' must have NyanObject type\")\n\n    def __repr__(self):\n        return f\"InheritedNyanMember<{self.name}: {self._member_type}>\"\n\n\nclass MemberType(Enum):\n    \"\"\"\n    Symbols for nyan member types.\n    \"\"\"\n\n    # Primitive types\n    INT        = \"int\"\n    FLOAT      = \"float\"\n    TEXT       = \"text\"\n    FILE       = \"file\"\n    BOOLEAN    = \"bool\"\n\n    # Complex types\n    SET        = \"set\"\n    ORDEREDSET = \"orderedset\"\n    DICT       = \"dict\"\n\n    # Modifier types\n    ABSTRACT   = \"abstract\"\n    CHILDREN   = \"children\"\n    OPTIONAL   = \"optional\"\n\n\nclass MemberSpecialValue(Enum):\n    \"\"\"\n    Symbols for special nyan values.\n    \"\"\"\n    # nyan none type\n    NYAN_NONE = \"None\"\n\n    # infinite value for float and int\n    NYAN_INF  = \"inf\"\n\n\nclass MemberOperator(Enum):\n    \"\"\"\n    Symbols for nyan member operators.\n    \"\"\"\n\n    ASSIGN    = \"=\"      # assignment\n    ADD       = \"+=\"     # addition, append, insertion, union\n    SUBTRACT  = \"-=\"     # subtraction, remove\n    MULTIPLY  = \"*=\"     # multiplication\n    DIVIDE    = \"/=\"     # division\n    AND       = \"&=\"     # logical AND, intersect\n    OR        = \"|=\"     # logical OR, union\n"
  },
  {
    "path": "openage/pathfinding/CMakeLists.txt",
    "content": "add_cython_modules(\n\ttests.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n)\n"
  },
  {
    "path": "openage/pathfinding/__init__.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nopenage pathfinding system\n\"\"\"\n"
  },
  {
    "path": "openage/pathfinding/tests.pyx",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\ntests for the pathfinding system.\n\"\"\"\n\nimport argparse\n\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom cpython.ref cimport PyObject\nfrom libopenage.pathfinding.demo.tests cimport path_demo as path_demo_c\n\ndef path_demo(list argv):\n    \"\"\"\n    invokes the available pathfinding demos.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... path_demo',\n        description='Demo of the pathfinding system')\n    cmd.add_argument(\"test_id\", type=int, help=\"id of the demo to run.\")\n    cmd.add_argument(\"--asset-dir\",\n                         help=\"Use this as an additional asset directory.\")\n    cmd.add_argument(\"--cfg-dir\",\n                         help=\"Use this as an additional config directory.\")\n\n    args = cmd.parse_args(argv)\n\n    from ..cvar.location import get_config_path\n    from ..assets import get_asset_path\n    from ..util.fslike.union import Union\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    root[\"assets\"].mount(get_asset_path(args.asset_dir))\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n\n    cdef int demo_id = args.test_id\n\n    cdef Path_cpp root_cpp = Path_cpp(PyObj(<PyObject*>root.fsobj),\n                                  root.parts)\n\n    with nogil:\n        path_demo_c(demo_id, root_cpp)\n"
  },
  {
    "path": "openage/renderer/CMakeLists.txt",
    "content": "add_cython_modules(\n\trenderer_cpp.pyx\n\ttests.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n)\n"
  },
  {
    "path": "openage/renderer/__init__.py",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\n\"\"\"\nopenage graphics renderer\n\"\"\"\n"
  },
  {
    "path": "openage/renderer/renderer_cpp.pyx",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n"
  },
  {
    "path": "openage/renderer/tests.pyx",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\ntests for the graphics renderer.\n\"\"\"\n\nimport argparse\n\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom cpython.ref cimport PyObject\nfrom libopenage.renderer.demo.tests cimport renderer_demo as renderer_demo_c\nfrom libopenage.renderer.demo.tests cimport renderer_stresstest as renderer_stresstest_c\n\ndef renderer_demo(list argv):\n    \"\"\"\n    invokes the available render demos.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... renderer_demo',\n        description='Demo of the renderer')\n    cmd.add_argument(\"test_id\", type=int, help=\"id of the demo to run.\")\n    cmd.add_argument(\"--asset-dir\",\n                         help=\"Use this as an additional asset directory.\")\n    cmd.add_argument(\"--cfg-dir\",\n                         help=\"Use this as an additional config directory.\")\n\n    args = cmd.parse_args(argv)\n\n    from ..cvar.location import get_config_path\n    from ..assets import get_asset_path\n    from ..util.fslike.union import Union\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    root[\"assets\"].mount(get_asset_path(args.asset_dir))\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n\n    cdef int renderer_test_id = args.test_id\n\n    cdef Path_cpp root_cpp = Path_cpp(PyObj(<PyObject*>root.fsobj),\n                                  root.parts)\n\n    with nogil:\n        renderer_demo_c(renderer_test_id, root_cpp)\n\n\ndef renderer_stresstest(list argv):\n    \"\"\"\n    invokes the available render demos.\n    \"\"\"\n\n    cmd = argparse.ArgumentParser(\n        prog='... renderer_stresstest',\n        description='Stresstest for the renderer')\n    cmd.add_argument(\"test_id\", type=int, help=\"id of the demo to run.\")\n    cmd.add_argument(\"--asset-dir\",\n                         help=\"Use this as an additional asset directory.\")\n    cmd.add_argument(\"--cfg-dir\",\n                         help=\"Use this as an additional config directory.\")\n\n    args = cmd.parse_args(argv)\n\n    from ..cvar.location import get_config_path\n    from ..assets import get_asset_path\n    from ..util.fslike.union import Union\n\n    # create virtual file system for data paths\n    root = Union().root\n\n    # mount the assets folder union at \"assets/\"\n    root[\"assets\"].mount(get_asset_path(args.asset_dir))\n\n    # mount the config folder at \"cfg/\"\n    root[\"cfg\"].mount(get_config_path(args.cfg_dir))\n\n    cdef int renderer_test_id = args.test_id\n\n    cdef Path_cpp root_cpp = Path_cpp(PyObj(<PyObject*>root.fsobj),\n                                  root.parts)\n\n    with nogil:\n        renderer_stresstest_c(renderer_test_id, root_cpp)\n"
  },
  {
    "path": "openage/testing/CMakeLists.txt",
    "content": "add_cython_modules(\n\tcpp_testing.pyx\n\tmisc_cpp.pyx\n)\n\nadd_py_modules(\n\t__init__.py\n\tdoctest.py\n\tlist_processor.py\n\tmain.py\n\ttesting.py\n\ttestlist.py\n\tbenchmark.py\n)\n"
  },
  {
    "path": "openage/testing/__init__.py",
    "content": "# Copyright 2013-2015 the openage authors. See copying.md for legal info.\n\n\"\"\"\nHandles all testing and demos.\n\"\"\"\n"
  },
  {
    "path": "openage/testing/benchmark.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\" Benchmarking tools for the tests. \"\"\"\n\nfrom timeit import timeit\nfrom sys import stdout\nfrom time import sleep\nfrom typing import Callable\n\n\ndef benchmark_test_function() -> None:\n    \"\"\" Simple function to call in for benchmarking. \"\"\"\n    sleep(0.1)\n\n\ndef benchmark(func: Callable) -> None:\n    \"\"\"\n    Benchmark the given function. Repeated execution helps to give a maximum to\n    the consumed time, until one iteration takes more than 5s, summed up 10s.\n    \"\"\"\n\n    result = [(0, 1)]\n    number = 1\n    total = [0, 0]\n    str_row_format = \"{:10} {:12}  {:11}\"\n    row_format1 = \"{:10} \"\n    row_format2 = \"{:11.8f}s  {:10.8f}s\"\n    row_format = row_format1 + row_format2\n\n    print(str_row_format.format(\"Iterations\", \"Total time\", \"Average time per execution\"))\n    while number < 4 or result[-1][0] < 5 and number < 65537:\n        print(row_format1.format(number), end=\"\")\n        stdout.flush()\n\n        time = timeit(stmt=func, number=number)\n        result.append((time, number))\n        print(row_format2.format(time, time / number))\n        total[0] += number\n        total[1] += time\n        stdout.flush()\n        number *= 2\n\n    del result[0]\n\n    print()\n    print(\"Benchmark Results: \")\n    print(\"------------------\")\n    print(str_row_format.format(\"Iterations\", \"Total time\", \"Average time per execution\"))\n    print(row_format.format(total[0], total[1], total[1] / total[0]))\n"
  },
  {
    "path": "openage/testing/cpp_testing.pyx",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides a wrapper for openage::testing::run_method.\n\"\"\"\n\nfrom libopenage.testing.testlist cimport run_method as run_method\n\n\ndef run_cpp_method(str methodname):\n    \"\"\" Runs the given C++ void () method, by its name. \"\"\"\n    run_method(methodname.encode())\n"
  },
  {
    "path": "openage/testing/doctest.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nPerforms doctest on all modules listed in list_doctest_modules().\n\"\"\"\n\nimport importlib\nfrom doctest import testmod\n\nfrom .testing import TestError\nfrom .testlist import doctest_modules\n\n\ndef test():\n    \"\"\"\n    Runs doctest on the modules listed in testlist.DOCTEST_MODULES.\n    \"\"\"\n    for modname in doctest_modules():\n        mod = importlib.import_module(modname)\n        if testmod(mod, report=False).failed:\n            raise TestError(\"Errors have been detected during doctest\")\n"
  },
  {
    "path": "openage/testing/list_processor.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\" Processes the raw test lists from the testlist module. \"\"\"\n\n\nfrom collections import OrderedDict\nfrom importlib import import_module\nfrom typing import Callable\n\nfrom ..util.strings import lstrip_once\n\n\ndef list_targets(test_lister: Callable, demo_lister: Callable, benchmark_lister: Callable):\n    \"\"\"\n    Yields tuples of (testname, type, description, condition_function)\n    for given test and demo listers.\n\n    A processing step between the raw lists in testlist, and get_all_tests().\n    \"\"\"\n\n    def default_cond(_):\n        \"\"\" default condition test to enable a test \"\"\"\n        return True\n\n    for test in test_lister():\n        if not isinstance(test, tuple):\n            test = (test,)\n\n        if not test:\n            raise ValueError(\"empty test definition encountered\")\n\n        if len(test) == 3:\n            condfun = test[2]\n        else:\n            condfun = default_cond\n\n        if len(test) >= 2:\n            desc = test[1]\n        else:\n            desc = \"\"\n\n        name = test[0]\n\n        yield name, 'test', desc, condfun\n\n    for demo in demo_lister():\n        name, desc = demo\n        yield name, 'demo', desc, default_cond\n\n    for benchmark in benchmark_lister():\n        name, desc = benchmark\n        yield name, 'benchmark', desc, default_cond\n\n\ndef list_targets_py():\n    \"\"\" Invokes list_targets() with the py-specific listers. \"\"\"\n    from .testlist import tests_py, demos_py, benchmark_py\n    for val in list_targets(tests_py, demos_py, benchmark_py):\n        yield val\n\n\ndef list_targets_cpp():\n    \"\"\" Invokes list_targets() with the C++-specific listers. \"\"\"\n    from .testlist import tests_cpp, demos_cpp, benchmark_cpp\n    for val in list_targets(tests_cpp, demos_cpp, benchmark_cpp):\n        yield val\n\n\ndef get_all_targets() -> OrderedDict:\n    \"\"\"\n    Reads the Python and C++ testspec.\n\n    returns an OrderedDict of\n    {(testname, type): conditionfun, lang, description, testfun}.\n\n    type is in {'demo', 'test'},\n    lang is in {'cpp', 'py'},\n    conditionfun is a callable which determines if the test is\n        to be run in the given environment\n    description is a str, and\n    testfun is callable and takes 0 args for tests / list(str) for demos.\n    \"\"\"\n    from .cpp_testing import run_cpp_method\n\n    result = OrderedDict()\n\n    for name, type_, description, conditionfun in list_targets_py():\n        modulename, objectname = name.rsplit('.', maxsplit=1)\n\n        try:\n            module = import_module(modulename)\n            func = getattr(module, objectname)\n        except Exception as exc:\n            raise ValueError(\"no such function: \" + name) from exc\n\n        try:\n            name = lstrip_once(name, 'openage.')\n        except ValueError as exc:\n            raise ValueError(\"Unexpected Python test/demo name\") from exc\n\n        result[name, type_] = conditionfun, 'py', description, func\n\n    for name, type_, description, conditionfun in list_targets_cpp():\n        if type_ == 'demo':\n            def runner(args, name=name):\n                \"\"\" runs the demo func, and ensures that args is empty. \"\"\"\n                if args:\n                    raise ValueError(\"C++ demos can't take arguments. \"\n                                     \"You should write a Python demo that \"\n                                     \"calls to C++ then, with arguments.\")\n                run_cpp_method(name)\n        elif type_ in ['test', 'benchmark']:\n            def runner(name=name):\n                \"\"\" simply runs the func. \"\"\"\n                run_cpp_method(name)\n        else:\n            raise ValueError(\"Unknown type \" + type_)\n\n        try:\n            name = lstrip_once(name, 'openage::')\n        except ValueError as exc:\n            raise ValueError(\"Unexpected C++ test/demo name\") from exc\n\n        result[name, type_] = conditionfun, 'cpp', description, runner\n\n    return result\n"
  },
  {
    "path": "openage/testing/main.py",
    "content": "# Copyright 2014-2023 the openage authors. See copying.md for legal info.\n\n\"\"\" CLI module for running all tests. \"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nimport argparse\nimport sys\n\nfrom ..util.strings import format_progress\n\nfrom .benchmark import benchmark\nfrom .testing import TestError\nfrom .list_processor import get_all_targets\n\nif typing.TYPE_CHECKING:\n    from argparse import Namespace\n\n\ndef print_test_list(test_list: typing.OrderedDict) -> None:\n    \"\"\"\n    Prints a list of all tests and demos in test_list.\n    \"\"\"\n    namelen = max(len(name) for name, _ in test_list.keys())\n\n    for current_type in ['test', 'demo', 'benchmark']:\n        for (name, type_), (_, lang, desc, _) in test_list.items():\n            if type_ == current_type:\n                print(f\"[{type_} {lang:3}] {name:{namelen}}  {desc}\")\n\n    print(\"\")\n    print(\"To see how to run them, add --help to your invocation!\")\n    print(\"\")\n    print(\"Remember: Testing is the future, and the future starts with: You.\")\n    print(\"\")\n\n\ndef init_subparser(cli):\n    \"\"\" Initializes the subparser for the testing command. \"\"\"\n    cli.set_defaults(entrypoint=main)\n\n    cli.add_argument(\"--list\", \"-l\", action='store_true',\n                     help=\"list all tests and demos\")\n    cli.add_argument(\"--run-all-tests\", \"-a\", action='store_true',\n                     help=\"run all tests\")\n    cli.add_argument(\"--have-assets\", action='store_true',\n                     help=\"additionally, run tests that need asset files\")\n\n    cli.add_argument(\"--demo\", \"-d\", nargs=argparse.REMAINDER,\n                     help=(\"run the given demo; the remaining arguments \"\n                           \"are passed to the demo.\"))\n    cli.add_argument(\"--benchmark\", \"-b\", nargs=argparse.REMAINDER,\n                     help=\"run the given benchmark\")\n    cli.add_argument(\"test\", nargs='*', help=\"run this test\")\n\n\ndef process_args(args: Namespace, error):\n    \"\"\" Processes the given args, detecting errors. \"\"\"\n    if not (args.run_all_tests or args.demo or args.test or args.benchmark):\n        args.list = True\n\n    if args.have_assets and not args.run_all_tests:\n        error(\"you have to run all tests, \"\n              \"otherwise I don't care if you have assets\")\n\n    if args.run_all_tests and (args.test or args.demo or args.benchmark):\n        error(\"can't run individual test or demo or benchmark when running \"\n              \"all tests\")\n\n    if bool(args.test) + bool(args.demo) + bool(args.benchmark) > 1:\n        error(\"can only run one of demo, benchmarks or tests\")\n\n    # link python and c++ so it hopefully works when testing\n    from openage.cppinterface.setup import setup\n    setup(args)\n\n    test_list = get_all_targets()\n\n    # the current test environment can have influence on the tests itself.\n    test_environment = {\n        \"has_assets\": args.have_assets,\n    }\n\n    # if we wanna run all the tests, only run the ones that\n    # are happy with the environment\n    if args.run_all_tests:\n        tests = [name for (name, test_type), (test_condition, _, _, _)\n                 in test_list.items()\n                 if test_type == \"test\" and test_condition(test_environment)]\n        args.test.extend(tests)\n\n    # double-check for unknown tests and demos, maybe we can match them\n    for test in args.test:\n        matched = False\n        if (test, 'test') not in test_list:\n            # If the test was not found explicit in the testlist, try to find\n            # all prefixed tests and run them instead.\n            matched = [elem[0] for elem in test_list\n                       if elem[0].startswith(test) and elem[1] == \"test\"]\n\n            if matched:\n                args.test.extend(matched)\n            else:\n                error(\"no such test: \" + test)\n            args.test.remove(test)\n\n    if args.demo and (args.demo[0], 'demo') not in test_list:\n        error(\"no such demo: \" + args.demo[0])\n\n    if args.benchmark and (args.benchmark[0], 'benchmark') not in test_list:\n        error(\"no such benchmark: \" + args.benchmark[0])\n\n    return test_list\n\n\ndef main(args, error):\n    \"\"\" CLI main method. \"\"\"\n    test_list = process_args(args, error)\n\n    if args.list:\n        print_test_list(test_list)\n\n    if args.test:\n        failed = 0\n\n        for idx, name in enumerate(args.test):\n            _, lang, _, testfun = test_list[name, 'test']\n\n            print(f\"\\x1b[32m[{format_progress(idx, len(args.test))}]\\x1b[m {lang:3} {name}\")\n\n            try:\n                testfun()\n            except BaseException as exc:\n                print(\"\\x1b[31;1mTest failed\\x1b[m\")\n                if not isinstance(exc, TestError):\n                    print(\"\\x1b[31;1mUnexpected exception\\x1b[m\")\n                failed += 1\n                import traceback\n                traceback.print_exc()\n\n        if failed:\n            print(f\"\\x1b[31;1m{failed:d} out of {len(args.test):d} tests have failed\\x1b[m\")\n\n            sys.exit(1)\n\n    if args.demo:\n        _, _, _, demofun = test_list[args.demo[0], 'demo']\n        sys.exit(demofun(args.demo[1:]))\n\n    if args.benchmark:\n        _, _, _, benchmarktest = test_list[args.benchmark[0], 'benchmark']\n        benchmark(benchmarktest)\n"
  },
  {
    "path": "openage/testing/misc_cpp.pyx",
    "content": "# Copyright 2015-2018 the openage authors. See copying.md for legal info.\n\nfrom cython.operator cimport dereference as deref\n\nfrom libcpp.string cimport string\n\nfrom libopenage.util.enum_test cimport (\n    testenum,\n\n    foo,\n    bar,\n)\n\n\nfrom .testing import TestError, assert_value\n\n\ndef enum():\n    cdef testenum test = foo\n\n    if test != foo:\n        raise TestError()\n\n    if test == bar:\n        raise TestError()\n\n    if foo == bar:\n        raise TestError()\n\n    assert_value(test.get().name, b\"foo\")\n    assert_value(test.get().numeric, 1)\n    assert_value(test.get().stuff, b\"foooooooooooooooooo\")\n\n    assert_value(foo > bar, False)\n    assert_value(foo < bar, True)\n    assert_value(foo >= bar, False)\n    assert_value(foo <= bar, True)\n"
  },
  {
    "path": "openage/testing/testing.py",
    "content": "# Copyright 2014-2022 the openage authors. See copying.md for legal info.\n\n\"\"\" Testing utilities, such as TestError, assert_value, assert_raises. \"\"\"\n\nfrom contextlib import contextmanager\nfrom typing import NoReturn\n\n\nclass TestError(Exception):\n    \"\"\"\n    Raised by assert_value and assert_raises, but may be manually raised\n    to indicate a test error.\n    \"\"\"\n\n\ndef assert_value(value, expected=None, validator=None):\n    \"\"\"\n    Checks 'value' for equality with 'expected', or, if validator is given,\n    checks bool(validator(value)) == True. Raises TestError on failure.\n\n    Example usage:\n\n    assert_value(fibonacci(0), 1)\n    \"\"\"\n    if expected is not None and validator is not None:\n        raise ValueError(\"can't have both 'expected' and 'validator'\")\n\n    if validator is None:\n        success = value == expected\n    else:\n        success = validator(value)\n\n    if success:\n        return\n\n    raise TestError(\"unexpected result: \" + repr(value))\n\n\ndef result(value) -> NoReturn:\n    \"\"\"\n    Shall be called when a result is unexpectedly returned in an assert_raises\n    block.\n    \"\"\"\n    raise TestError(\"expected exception, but got result: \" + repr(value))\n\n\n@contextmanager\ndef assert_raises(expectedexception):\n    \"\"\"\n    Context guard that asserts that a certain exception is raised inside.\n\n    On successful execution (if the error failed to show up),\n    result() shall be called.\n\n    Example usage:\n\n    # we expect fibonacci to raise ValueError for negative values.\n    with assert_raises(ValueError):\n        result(fibonacci(-3))\n    \"\"\"\n    # pylint: disable=try-except-raise\n    try:\n        yield\n    except expectedexception:\n        return\n    except TestError:\n        raise\n    except BaseException as exc:\n        raise TestError(\"unexpected exception\") from exc\n    else:\n        raise TestError(\"got neither an exception, nor a result\")\n"
  },
  {
    "path": "openage/testing/testlist.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\" Lists of all possible tests; enter your tests here. \"\"\"\n\n\ndef doctest_modules():\n    \"\"\"\n    Yields the names of all Python modules that shall be tested during doctest.\n    \"\"\"\n\n    yield \"openage.util.math\"\n    yield \"openage.util.strings\"\n    yield \"openage.util.system\"\n\n\ndef tests_py():\n    \"\"\"\n    Yields tuples of (name, description, condition_function)\n    for all Python test methods.\n\n    If no description is required, just the name may be yielded.\n    \"\"\"\n\n    yield (\"openage.testing.doctest.test\",\n           \"doctest on all modules from DOCTEST_MODULES\")\n    yield \"openage.assets.test\"\n    yield (\"openage.cabextract.test.test\", \"test CAB archive extraction\",\n           lambda env: env[\"has_assets\"])\n    yield \"openage.cppinterface.exctranslate_tests.cpp_to_py\"\n    yield (\"openage.cppinterface.exctranslate_tests.cpp_to_py_bounce\",\n           \"translates the exception back and forth a few times\")\n    yield (\"openage.testing.misc_cpp.enum\",\n           \"tests the interface for C++'s util::Enum class\")\n    yield (\"openage.util.fslike.test.test\",\n           \"test the filesystem abstraction subsystem\")\n    yield \"openage.util.threading.test_concurrent_chain\"\n\n\ndef demos_py():\n    \"\"\"\n    Yields tuples of (name, description) for all Python demo methods.\n    \"\"\"\n\n    yield (\"openage.cppinterface.exctranslate_tests.cpp_to_py_demo\",\n           \"translates a C++ exception and its causes to python\")\n    yield (\"openage.log.tests.demo\",\n           \"demonstrates the translation of Python log messages\")\n    yield (\"openage.convert.service.export.opus.demo.convert\",\n           \"encodes an opus file from a wave file\")\n    yield (\"openage.event.demo.curvepong\",\n           \"play pong on steroids through future prediction\")\n    yield (\"openage.gamestate.tests.simulation_demo\",\n           \"showcases the game simulation\")\n    yield (\"openage.pathfinding.tests.path_demo\",\n           \"showcases the pathfinding system\")\n    yield (\"openage.renderer.tests.renderer_demo\",\n           \"showcases the renderer\")\n    yield (\"openage.renderer.tests.renderer_stresstest\",\n           \"stresstests for the renderer\")\n    yield (\"openage.main.tests.engine_demo\",\n           \"showcases the engine features\")\n\n\ndef benchmark_py():\n    \"\"\"\n    Yields tuples of (name, description) for python benchmark\n    methods.\n    \"\"\"\n\n    # TODO Add a real benchmark here, and remove this one\n    yield (\"openage.testing.benchmark.benchmark_test_function\",\n           \"Benchmark yourself\")\n\n\ndef tests_cpp():\n    \"\"\"\n    Yields tuples of (name, description, condition_function)\n    for all C++ test methods.\n\n    If no description is required, just the name may be yielded.\n    \"\"\"\n\n    yield \"openage::coord::tests::coord\"\n    yield \"openage::datastructure::tests::concurrent_queue\"\n    yield \"openage::datastructure::tests::constexpr_map\"\n    yield \"openage::datastructure::tests::pairing_heap\"\n    yield \"openage::job::tests::test_job_manager\"\n    yield \"openage::path::tests::path_node\", \"pathfinding\"\n    yield \"openage::path::tests::flow_field\", \"pathfinding\"\n    yield \"openage::pyinterface::tests::pyobject\"\n    yield \"openage::pyinterface::tests::err_py_to_cpp\"\n    yield \"openage::renderer::tests::font\"\n    yield \"openage::renderer::tests::font_manager\"\n    yield \"openage::rng::tests::run\"\n    yield \"openage::util::tests::constinit_vector\"\n    yield \"openage::util::tests::enum_\"\n    yield \"openage::util::tests::fixed_point\"\n    yield \"openage::util::tests::init\"\n    yield \"openage::util::tests::matrix\"\n    yield \"openage::util::tests::quaternion\"\n    yield \"openage::util::tests::vector\"\n    yield \"openage::util::tests::siphash\"\n    yield \"openage::util::tests::array_conversion\"\n    yield \"openage::curve::tests::container\"\n    yield \"openage::curve::tests::curve_types\"\n    yield \"openage::event::tests::eventtrigger\"\n\n\ndef demos_cpp():\n    \"\"\"\n    Yields tuples of (name, description) for all C++ demo methods.\n    \"\"\"\n\n    yield (\"openage::console::tests::render\",\n           \"prints a few test lines to a buffer, and renders it to stdout\")\n    yield (\"openage::console::tests::interactive\",\n           \"showcases console as an interactive terminal on your current tty\")\n    yield (\"openage::error::demo\",\n           \"showcases the openage exceptions, including backtraces\")\n    yield (\"openage::gamestate::tests::activity_demo\",\n           \"showcases the activity system in the gamestate\")\n    yield (\"openage::input::tests::action_demo\",\n           \"showcases the low-level input system\")\n    yield (\"openage::log::tests::demo\",\n           \"showcases the logging system\")\n    yield (\"openage::pyinterface::tests::err_py_to_cpp_demo\",\n           \"translates a Python exception to C++\")\n    yield (\"openage::pyinterface::tests::pyobject_demo\",\n           \"a tiny interactive interpreter using PyObjectRef\")\n\n\ndef benchmark_cpp():\n    \"\"\"\n    Yields tuples of (name, description) for C++ benchmark\n    methods.\n    \"\"\"\n\n    # TODO Add a real benchmark here!\n    yield (\"openage::test::benchmark\", \"Test the benchmark\")\n"
  },
  {
    "path": "openage/util/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tbytequeue.py\n\tcontext.py\n\tdecorators.py\n\tdll.py\n\tfiles.py\n\tfsprinting.py\n\thash.py\n\titerators.py\n\tmath.py\n\tobserver.py\n\tordered_set.py\n\tprofiler.py\n\tstrings.py\n\tstruct.py\n\tsystem.py\n\tthreading.py\n\tversion.py\n)\n\nadd_subdirectory(filelike)\nadd_subdirectory(fslike)\n"
  },
  {
    "path": "openage/util/__init__.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nVarious utility classes and functions, for use by all the python code of\nopenage.\n\"\"\"\n"
  },
  {
    "path": "openage/util/bytequeue.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides ByteQueue, a high-performance queue for bytes objects.\n\"\"\"\n\nfrom collections import deque\nfrom bisect import bisect\nfrom typing import Generator\n\n\nclass ByteQueue:\n    \"\"\"\n    Queue for bytes\n    Can append bytes objects at the right,\n    and pop arbitrary-size bytes objects at the left.\n\n    The naive implementation would look like this:\n\n    append(self, data):\n        self.buf += data\n\n    popleft(self, size):\n        result, self.buf = self.buf[:size], self.buf[size:]\n        return result\n\n    However, due to python's nature, that would be extremely slow.\n\n    Thus, the bytes objects are actually stored - unmodified -\n    in an internal queue, and only concatenated during popleft().\n    \"\"\"\n\n    def __init__(self):\n        self.bufs = deque()\n\n        # stores the amount of data that's currently available.\n        self.size = 0\n\n    def __len__(self):\n        \"\"\"\n        Size of all currently-stored data.\n        \"\"\"\n        return self.size\n\n    def append(self, data: bytes) -> None:\n        \"\"\"\n        Adds bytes to the buffer.\n        \"\"\"\n        if not isinstance(data, bytes):\n            raise TypeError(\"expected a bytes object, but got \" + repr(data))\n\n        self.bufs.append(data)\n        self.size += len(data)\n\n    def popleft(self, size: int) -> bytes:\n        \"\"\"\n        Returns the requested amount of bytes from the buffer.\n        \"\"\"\n        if size > self.size:\n            raise ValueError(\"ByteQueue does not contain enough bytes\")\n\n        self.size -= size\n\n        resultbufs = []\n        required = size\n\n        while required > 0:\n            buf = self.bufs.popleft()\n            resultbufs.append(buf)\n            required -= len(buf)\n\n        if required < 0:\n            # we requested too much; split the last buffer\n            buf = resultbufs.pop()\n\n            popped = buf[:required]\n            kept = buf[required:]\n\n            resultbufs.append(popped)\n            self.bufs.appendleft(kept)\n\n        return b\"\".join(resultbufs)\n\n\nclass ByteBuffer:\n    \"\"\"\n    Similar to ByteQueue, but instead of popleft, allows reading random slices,\n    and trimleft, which discards data from the left.\n    \"\"\"\n\n    def __init__(self):\n        # holds all appended bytes objects\n        # discarded bytes objects are replaced by None.\n        self.bufs = [None]\n\n        # holds the absolute position of the end of each of the byte objects.\n        self.index = [0]\n\n        # holds the number of discarded buffers at the left of self.bufs\n        # (i.e. the index of the first non-discarded buffer)\n        self.discardedbufs = 1\n\n        self.discardedbytes = 0\n\n    def __len__(self):\n        return self.index[-1]\n\n    def append(self, data: bytes) -> None:\n        \"\"\"\n        appends new data to the right of the buffer\n        \"\"\"\n        if not isinstance(data, bytes):\n            raise TypeError(\"expected bytes, but got \" + repr(data))\n\n        self.bufs.append(data)\n        self.index.append(len(self) + len(data))\n\n    def discardleft(self, keep: int) -> None:\n        \"\"\"\n        discards data at the beginning of the buffer.\n        keeps at least the 'keep' most recent bytes.\n        \"\"\"\n        discardamount = len(self) - keep\n        if discardamount <= self.discardedbytes:\n            return\n\n        # the index of the first block that is not to be discarded\n        discardto = bisect(self.index, discardamount)\n\n        for idx in range(self.discardedbufs, discardto):\n            self.bufs[idx] = None\n\n        self.discardedbufs = discardto\n        self.discardedbytes = discardamount\n\n    def hasbeendiscarded(self, position: int) -> bool:\n        \"\"\"\n        returns True if the given position has already been discarded\n        and is no longer valid.\n        \"\"\"\n        return position < self.discardedbytes\n\n    def __getitem__(self, pos):\n        \"\"\"\n        a slice with default stepping is required.\n\n        when attempting to access already-discarded data, a ValueError\n        is raised.\n        \"\"\"\n\n        if not isinstance(pos, slice):\n            raise TypeError(\"expected slice\")\n\n        if pos.step is not None:\n            raise TypeError(\"slicing with steps is not supported\")\n\n        start, end = pos.start, pos.stop\n\n        # emulate proper slicing behavior\n        if start is None:\n            start = 0\n\n        if end is None:\n            end = len(self)\n\n        if start < 0:\n            start = len(self) + start\n\n        if end < 0:\n            end = len(self) + end\n\n        return b\"\".join(self.get_buffers(start, end))\n\n    class DiscardedError(Exception):\n        \"\"\"\n        raised by get_buffers and the indexing operator if the requested\n        data has already been discarded.\n        \"\"\"\n\n    def get_buffers(self, start: int, end: int) -> Generator[bytes, None, None]:\n        \"\"\"\n        yields any amount of bytes objects that constitute the data\n        between start and end.\n\n        used internally by __getitem__, but may be useful externally\n        as well.\n\n        performs bounds checking, but end must be greater than start.\n        \"\"\"\n        # sanitize the start and end values.\n        start = max(start, 0)\n        end = min(end, len(self))\n\n        if end <= start:\n            yield b\"\"\n            return\n\n        # check for discard\n        if self.hasbeendiscarded(start):\n            raise self.DiscardedError(start, end)\n\n        idx = bisect(self.index, start)\n        buf = self.bufs[idx]\n\n        # cut off superfluous parts at the left of the first buffer.\n        # the negative index is intentional.\n        # pylint: disable=unsubscriptable-object\n        buf = buf[start - self.index[idx]:]\n\n        remaining = end - start\n        while remaining > len(buf):\n            remaining -= len(buf)\n            yield buf\n\n            # get next buffer\n            idx += 1\n            buf = self.bufs[idx]\n\n        # cut of superfluous parts at the right of the last buffer.\n        buf = buf[:remaining]\n        yield buf\n"
  },
  {
    "path": "openage/util/context.py",
    "content": "# Copyright 2015-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides some utility context guards.\n\"\"\"\n\n\nclass DummyGuard:\n    \"\"\" Context guard that does nothing. \"\"\"\n    # pylint: disable=too-few-public-methods\n\n    @staticmethod\n    def __enter__():\n        pass\n\n    @staticmethod\n    def __exit__(exc_type, exc_value, traceback):\n        pass\n"
  },
  {
    "path": "openage/util/decorators.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nSome utility function decorators\n\"\"\"\n\n\nfrom typing import Callable\n\n\ndef run_once(func: Callable) -> Callable:\n    \"\"\"\n    Decorator to run func only at its first invocation.\n\n    Set func.has_run to False to manually re-run.\n    \"\"\"\n\n    def wrapper(*args, **kwargs):\n        \"\"\" Returned function wrapper. \"\"\"\n        if wrapper.has_run:\n            return None\n\n        wrapper.has_run = True\n        return func(*args, **kwargs)\n\n    wrapper.has_run = False\n    return wrapper\n"
  },
  {
    "path": "openage/util/dll.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nWindows-specific loading of compiled Python modules and DLLs.\n\"\"\"\n\nimport inspect\nimport os\nimport sys\n\n# python.dll location\nDEFAULT_PYTHON_DLL_DIR = os.path.dirname(sys.executable)\n\n# openage.dll locations (relative to this file)\nDEFAULT_OPENAGE_DLL_DIRs = [\n    \"../../libopenage/Debug\",\n    \"../../libopenage/Release\",\n    \"../../libopenage/RelWithDebInfo\",\n    \"../../libopenage/MinSizeRel\",\n]\n\n# nyan.dll locations (relative to this file)\nDEFAULT_NYAN_DLL_DIRS = [\n    \"../../../../nyan/build/nyan/Debug\",\n    \"../../../../nyan/build/nyan/Release\",\n    \"../../../../nyan/build/nyan/RelWithDebInfo\",\n    \"../../../../nyan/build/nyan/MinSizeRel\",\n    \"../../nyan-external/bin/nyan/Debug\",\n    \"../../nyan-external/bin/nyan/Release\",\n    \"../../nyan-external/bin/nyan/RelWithDebInfo\",\n    \"../../nyan-external/bin/nyan/MinSizeRel\",\n]\n\n\nclass DllDirectoryManager:\n    \"\"\"\n    Manages directories that should be added to/removed from Python's DLL search path.\n\n    All dependent DLLs or compiled cython modules that are not in Python's default search path\n    mst be added manually at runtime. Basically, this applies to all openage-specific libraries.\n    \"\"\"\n\n    def __init__(self, directory_paths: list[str]):\n        \"\"\"\n        Create a new DLL directory manager.\n\n        :param directory_paths: Absolute paths to the directories that are added.\n        \"\"\"\n        # Directory paths\n        self.directories = directory_paths\n\n        # Store handles for added directories\n        self.handles = []\n\n    def add_directories(self):\n        \"\"\"\n        Add the manager's directories to Python's DLL search path.\n        \"\"\"\n        for directory in self.directories:\n            handle = os.add_dll_directory(directory)\n            self.handles.append(handle)\n\n    def remove_directories(self):\n        \"\"\"\n        Remove the manager's directories from Python's DLL search path.\n        \"\"\"\n        for handle in self.handles:\n            handle.close()\n\n        self.handles = []\n\n    def __del__(self):\n        \"\"\"\n        Ensure that DLL paths are removed when the object is deleted.\n        \"\"\"\n        self.remove_directories()\n\n    def __enter__(self):\n        \"\"\"\n        Enter a context guard.\n        \"\"\"\n        self.add_directories()\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        \"\"\"\n        Exit a context guard.\n        \"\"\"\n        self.remove_directories()\n\n    def __getstate__(self):\n        \"\"\"\n        Change pickling behavior so that directory handles are not serialized.\n        \"\"\"\n        content = self.__dict__\n        content[\"handles\"] = []\n        return content\n\n\ndef default_paths() -> list[str]:\n    \"\"\"\n    Create a list of default paths.\n    \"\"\"\n    directory_paths = []\n\n    # Add Python DLL search path\n    directory_paths.append(DEFAULT_PYTHON_DLL_DIR)\n\n    file_dir = os.path.dirname(os.path.abspath(inspect.getsourcefile(lambda: 0)))\n\n    # Add openage DLL search paths\n    for candidate in DEFAULT_OPENAGE_DLL_DIRs:\n        path = os.path.join(file_dir, candidate)\n        if os.path.exists(path):\n            directory_paths.append(path)\n\n    # Add nyan DLL search paths\n    for candidate in DEFAULT_NYAN_DLL_DIRS:\n        path = os.path.join(file_dir, candidate)\n        if os.path.exists(path):\n            directory_paths.append(path)\n\n    return directory_paths\n"
  },
  {
    "path": "openage/util/filelike/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tabstract.py\n\tfifo.py\n\treadonly.py\n\tstream.py\n)\n\nadd_cython_modules(\n\tcpp.pyx\n)\n"
  },
  {
    "path": "openage/util/filelike/__init__.py",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFile-like object utility package.\n\nTypes that implement analogous objects for the built-in open() result.\n\nAlso used from C++ for more complicated file access.\n\"\"\"\n"
  },
  {
    "path": "openage/util/filelike/abstract.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides the FileLikeObject abstract base class, which specifies a file-like\ninterface, and various classes that implement the interface.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom io import UnsupportedOperation\nimport os\n\n\nclass FileLikeObject(ABC):\n    \"\"\"\n    Abstract base class for file-like objects.\n\n    Note that checking isinstance(obj, FileLikeObject) is a bad idea, because\n    that would exclude actual files, and Python's built-in file-like objects.\n\n    Does not implement/force implementation of line-reading functionality.\n    \"\"\"\n\n    def __init__(self):\n        self.closed = False\n\n    @abstractmethod\n    def read(self, size: int = -1) -> bytes:\n        \"\"\"\n        Read at most size bytes (less if EOF has been reached).\n\n        Shall raise UnsupportedOperation for write-only objects.\n        \"\"\"\n\n    @abstractmethod\n    def readable(self) -> bool:\n        \"\"\"\n        Returns True if read() is allowed.\n        \"\"\"\n\n    @abstractmethod\n    def write(self, data) -> None:\n        \"\"\"\n        Writes all of data to the file.\n\n        Shall raise UnsupportedOperation for read-only object.\n\n        There is no return value.\n        \"\"\"\n\n    @abstractmethod\n    def writable(self) -> bool:\n        \"\"\"\n        Returns True if write() is allowed.\n        \"\"\"\n\n    @abstractmethod\n    def seek(self, offset: int, whence=os.SEEK_SET) -> None:\n        \"\"\"\n        Seeks to a given position.\n\n        May raise UnsupportedOperation for any or all arguments, in case of\n        unseekable streams.\n\n        For testing seek capabilities, it's recommended to call seek(0)\n        immediately after object creation.\n\n        There is no return value.\n        \"\"\"\n\n    @abstractmethod\n    def seekable(self) -> bool:\n        \"\"\"\n        Returns True if seek() is allowed.\n        \"\"\"\n\n    @abstractmethod\n    def tell(self):\n        \"\"\"\n        Returns the current position in the file.\n\n        Must work properly for all file-like objects.\n        \"\"\"\n\n    @abstractmethod\n    def close(self):\n        \"\"\"\n        Frees internal resources, making the object unusable.\n        May be a no-op.\n        \"\"\"\n\n    @abstractmethod\n    def flush(self):\n        \"\"\"\n        Syncs data with the disk, or something\n        May be a no-op.\n        \"\"\"\n\n    @abstractmethod\n    def get_size(self) -> int:\n        \"\"\"\n        Returns the size of the object, if known.\n        Returns -1 otherwise.\n\n        Note: Actual file objects don't have this method;\n              it exists mostly for internal usage.\n        \"\"\"\n\n    # allow usage with 'with'\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        del exc_type, exc_val, exc_tb  # unused\n\n        self.close()\n\n    def seek_helper(self, offset: int, whence) -> int:\n        \"\"\"\n        Helper function for use by implementations of seek().\n\n        Calculates the new cursor position relative to file start\n        from offset, whence and self.tell().\n\n        If size is given, it works for whence=os.SEEK_END;\n        otherwise, UnsupportedOperation is raised.\n        \"\"\"\n\n        if whence == os.SEEK_SET:\n            target = offset\n        elif whence == os.SEEK_CUR:\n            target = offset + self.tell()\n        elif whence == os.SEEK_END:\n            size = self.get_size()\n            if size < 0:\n                raise UnsupportedOperation(\n                    \"can only seek relative to file start or cursor\")\n            target = offset + size\n        else:\n            raise UnsupportedOperation(\"unsupported seek mode\")\n\n        if target < 0:\n            raise ValueError(\"can not seek to a negative file position\")\n\n        return target\n"
  },
  {
    "path": "openage/util/filelike/cpp.pyx",
    "content": "# Copyright 2017-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFunctions called from C++ to perform method calls on\nfilelike python objects.\n\"\"\"\n\nimport os\n\nfrom cpython.ref cimport PyObject\nfrom libc.string cimport memcpy\nfrom libcpp cimport bool\nfrom libcpp.memory cimport shared_ptr\nfrom libcpp.string cimport string\n\nfrom libopenage.util.file cimport File as File_cpp\nfrom libopenage.util.filelike.filelike cimport (\n    FileLike,\n    seek_t,\n    seek_t_SET,\n    seek_t_CUR,\n    seek_t_END\n)\nfrom libopenage.util.filelike.python cimport (\n    Python as FileLikePython,\n    pyx_file_read,\n    pyx_file_read_to,\n    pyx_file_readable,\n    pyx_file_write,\n    pyx_file_writable,\n    pyx_file_seek,\n    pyx_file_seekable,\n    pyx_file_tell,\n    pyx_file_close,\n    pyx_file_flush,\n    pyx_file_size,\n)\nfrom .abstract import FileLikeObject\n\n\ncdef class FileLikeCPPWrapper:\n    \"\"\"\n    Wraps a C++ filelike object so it can be used\n    from python.\n    \"\"\"\n\n    # pointer to the cpp filelike object\n    cdef shared_ptr[FileLike] filelike\n\n    @staticmethod\n    cdef wrap(shared_ptr[FileLike] c_fileobj):\n        wrp = FileLikeCPPWrapper()\n        wrp.filelike = c_fileobj\n        return wrp\n\n    def read(self, size=-1):\n        return self.filelike.get().read(size)\n\n    def readable(self):\n        return self.filelike.get().readable()\n\n    def write(self, data):\n        self.filelike.get().write(data)\n\n    def writable(self):\n        return self.filelike.get().readable()\n\n    def seek(self, offset, whence=os.SEEK_SET):\n        cdef seek_t whence_c\n\n        if whence == os.SEEK_SET:\n            whence_c = seek_t_SET\n        elif whence == os.SEEK_CUR:\n            whence_c = seek_t_CUR\n        elif whence == os.SEEK_END:\n            whence_c = seek_t_END\n        else:\n            raise ValueError(f\"invalid whence: {whence}\")\n\n        self.filelike.get().seek(offset, whence_c)\n\n    def seekable(self):\n        return self.filelike.get().seekable()\n\n    def tell(self):\n        return self.filelike.get().tell()\n\n    def close(self):\n        self.filelike.get().close()\n\n    def flush(self):\n        self.filelike.get().flush()\n\n    def get_size(self):\n        return self.filelike.get().get_size()\n\n\nclass FileLikeCPP(FileLikeObject):\n    \"\"\"\n    Wraps the C++ wrapper again so it can inherit from\n    FileLikeObject.\n    Relays all calls to the C++ wrapper.\n    \"\"\"\n\n    def __init__(self, cpp_wrapper):\n        self.fileobj = cpp_wrapper\n\n    def read(self, size=-1):\n        return self.fileobj.read(size)\n\n    def readable(self):\n        return self.fileobj.readable()\n\n    def write(self, data):\n        self.fileobj.write(data)\n\n    def writable(self):\n        return self.fileobj.readable()\n\n    def seek(self, offset, whence=os.SEEK_SET):\n        self.fileobj.seek(offset, whence)\n\n    def seekable(self):\n        return self.fileobj.seekable()\n\n    def tell(self):\n        return self.fileobj.tell()\n\n    def close(self):\n        self.fileobj.close()\n\n    def flush(self):\n        self.fileobj.flush()\n\n    def get_size(self):\n        return self.fileobj.get_size()\n\n\ncdef cppfile_to_pyfile(const File_cpp &file):\n    cdef shared_ptr[FileLike] fileobj = file.get_fileobj();\n    cdef FileLikePython *py_filelike\n\n    # we could also use typeid() here\n\n    # if the fileobject is from python, we can use it without wrapping\n    if fileobj.get().is_python_native():\n        # extract the python filelike object and use it directly\n        py_filelike = <FileLikePython *> fileobj.get()\n        return (<object> py_filelike.get_py_fileobj().get_ref())\n\n    else:\n        # wrap cpp filelike object for python (twice..):\n        # first wrap contains the c++ pointer, second allows\n        # python inheritance.\n        return FileLikeCPP(FileLikeCPPWrapper.wrap(fileobj))\n\n\ncdef string file_read(PyObject *filelike, ssize_t max) except * with gil:\n    return (<object> filelike).read(max)\n\n\ncdef size_t file_read_to(PyObject *filelike, void *buf, ssize_t max) except * with gil:\n    cdef bytes data = (<object> filelike).read(max)\n    cdef size_t count = len(data)\n    memcpy(buf, <const char *>data, count)\n    return count\n\n\ncdef bool file_readable(PyObject *filelike) except * with gil:\n    return (<object> filelike).readable()\n\n\ncdef void file_write(PyObject *filelike, const string &data) except * with gil:\n    (<object> filelike).write(data)\n\n\ncdef bool file_writable(PyObject *filelike) except * with gil:\n    return (<object> filelike).writable()\n\n\ncdef void file_seek(PyObject *filelike, ssize_t offset, int how) except * with gil:\n    # how is SEEK_POS=0, SEEK_CUR=1, SEEK_END=2\n    (<object> filelike).seek(offset, how)\n\n\ncdef bool file_seekable(PyObject *filelike) except * with gil:\n    return (<object> filelike).seekable()\n\n\ncdef size_t file_tell(PyObject *filelike) except * with gil:\n    return (<object> filelike).tell()\n\n\ncdef void file_close(PyObject *filelike) except * with gil:\n    (<object> filelike).close()\n\n\ncdef void file_flush(PyObject *filelike) except * with gil:\n    (<object> filelike).flush()\n\n\ncdef ssize_t file_size(PyObject *filelike) except * with gil:\n    return (<object> filelike).size()\n\n\ndef setup():\n    pyx_file_read.bind0(file_read)\n    pyx_file_read_to.bind0(file_read_to)\n    pyx_file_readable.bind0(file_readable)\n    pyx_file_write.bind0(file_write)\n    pyx_file_writable.bind0(file_writable)\n    pyx_file_seek.bind0(file_seek)\n    pyx_file_seekable.bind0(file_seekable)\n    pyx_file_tell.bind0(file_tell)\n    pyx_file_close.bind0(file_close)\n    pyx_file_flush.bind0(file_flush)\n    pyx_file_size.bind0(file_size)\n"
  },
  {
    "path": "openage/util/filelike/fifo.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides a FileLikeObject that acts like a FIFO.\n\"\"\"\n\nimport os\nfrom io import UnsupportedOperation\nfrom typing import NoReturn\n\nfrom .abstract import FileLikeObject\nfrom ..bytequeue import ByteQueue\n\n\nclass FIFO(FileLikeObject):\n    \"\"\"\n    File-like wrapper around ByteQueue.\n\n    Data written via write() can later be retrieved via read().\n\n    EOF handling is a bit tricky:\n\n    EOF in the data source (the writer) can not be auto-detected, and must\n    be manually indicated by calling seteof().\n\n    Only then will read() show the desired behavior for EOF.\n\n    If EOF has not yet been set, read() will raise ValueError if more data\n    than currently available has been requested.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.eof = False\n        self.queue = ByteQueue()\n        self.pos = 0\n\n    def tell(self) -> int:\n        \"\"\"\n        Warning: Returns the position for reading.\n\n        Due to the FIFO nature, the position for writing is further-advanced.\n        \"\"\"\n        return self.pos\n\n    def tellw(self) -> int:\n        \"\"\"\n        Returns the position for writing.\n        \"\"\"\n        return self.pos + len(self.queue)\n\n    def seek(self, offset, whence=os.SEEK_SET) -> NoReturn:\n        \"\"\"\n        Unsupported because this is a FIFO.\n        \"\"\"\n        del offset, whence  # unused\n\n        raise UnsupportedOperation(\"unseekable stream\")\n\n    def seekable(self) -> bool:\n        return False\n\n    def __len__(self):\n        \"\"\"\n        Returns the amount of currently-enqueued data.\n        \"\"\"\n        return len(self.queue)\n\n    def seteof(self) -> None:\n        \"\"\"\n        Declares that no more data will be added using write().\n\n        Note that this does _not_ mean that no more data is available\n        through write; the queue may still hold some data.\n        \"\"\"\n        self.eof = True\n\n    def write(self, data: bytes) -> None:\n        \"\"\"\n        Works until seteof() has been called; accepts bytes objects.\n        \"\"\"\n        if self.eof:\n            raise ValueError(\"EOF has been set; can't write more data\")\n\n        self.queue.append(data)\n\n    def writable(self) -> bool:\n        return True\n\n    def read(self, size: int = -1) -> bytes:\n        \"\"\"\n        If seteof() has not been called yet, requesting more data than\n        len(self) raises a ValueError.\n\n        When called without arguments, all currently-enqueued data is\n        returned; if seteof() has not been set yet, this doesn't\n        indicate EOF, though.\n        \"\"\"\n        if size < 0:\n            size = len(self.queue)\n        elif self.eof and size > len(self.queue):\n            size = len(self.queue)\n\n        self.pos += size\n\n        return self.queue.popleft(size)\n\n    def readable(self) -> bool:\n        return True\n\n    def get_size(self) -> int:\n        return len(self.queue)\n\n    def flush(self) -> None:\n        # no-op\n        pass\n\n    def close(self) -> None:\n        self.closed = True\n        self.queue = None\n"
  },
  {
    "path": "openage/util/filelike/readonly.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides an abstract read-only FileLikeObject.\n\"\"\"\n\nimport os\nfrom io import UnsupportedOperation\nfrom typing import NoReturn\n\nfrom .abstract import FileLikeObject\n\n\nclass ReadOnlyFileLikeObject(FileLikeObject):\n    \"\"\"\n    Most FileLikeObjects are read-only, and don't need to implement flush\n    or write.\n\n    This abstract class avoids code duplication.\n    \"\"\"\n\n    # pylint doesn't understand that this class is supposed to be abstract.\n    # pylint: disable=abstract-method\n\n    def flush(self) -> None:\n        # no flushing is needed for read-only objects.\n        pass\n\n    def readable(self) -> bool:\n        return True\n\n    def write(self, data) -> NoReturn:\n        del data  # unused\n        raise UnsupportedOperation(\"read-only file\")\n\n    def writable(self) -> bool:\n        return False\n\n\nclass PosSavingReadOnlyFileLikeObject(ReadOnlyFileLikeObject):\n    \"\"\"\n    Stores the current seek position in self.pos.\n\n    Avoids code duplication.\n    \"\"\"\n\n    # pylint doesn't understand that this file is supposed to be abstract.\n    # pylint: disable=abstract-method\n\n    def __init__(self):\n        super().__init__()\n        self.pos = 0\n\n    def seek(self, offset: int, whence=os.SEEK_SET) -> None:\n        self.pos = self.seek_helper(offset, whence)\n\n    def seekable(self) -> bool:\n        return True\n\n    def tell(self) -> int:\n        return self.pos\n"
  },
  {
    "path": "openage/util/filelike/stream.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides FileLikeObject for binary stream interaction.\n\"\"\"\n\nfrom ..math import INF, clamp\n\nfrom .readonly import PosSavingReadOnlyFileLikeObject\nfrom ..bytequeue import ByteBuffer\n\n\nclass StreamSeekBuffer(PosSavingReadOnlyFileLikeObject):\n    \"\"\"\n    Wrapper file-like object that adds seek functionality to a read-only,\n    unseekable stream.\n\n    For this purpose, all data read from that stream is cached.\n\n    Constructor arguments:\n\n    @param wrappee:\n        The non-seekable, read-only stream that is to be wrapped.\n\n    @param keepbuffered:\n        If given, only this amount of bytes is guaranteed to stay in the\n        seekback buffer.\n        If too large of a seekback is requested, an attept at wrappee.reset()\n        is made; if that doesn't work, tough luck.\n\n    @param minread:\n        If given, read calls to wrappee will request at least this amount\n        of bytes (performance optimization).\n        By default, entire megabytes are read at once.\n    \"\"\"\n\n    def __init__(self, wrappee, keepbuffered: int = INF, minread: int = 1048576):\n        super().__init__()\n\n        self.wrapped = wrappee\n        self.keepbuffered = keepbuffered\n        self.minread = minread\n\n        # invariant: len(self.buf) == self.wrapped.tell()\n        self.buf = ByteBuffer()\n\n    def resetwrappeed(self) -> None:\n        \"\"\"\n        resets the wrappeed object, and clears self.buf.\n        \"\"\"\n        self.wrapped.reset()\n        self.buf = ByteBuffer()\n\n    def read(self, size: int = -1) -> bytes:\n        if size < 0:\n            size = INF\n\n        # see if we have already discarded the requested data\n        if self.buf.hasbeendiscarded(self.pos):\n            # shit.\n            # try to reset the stream and start over, hoping that the stream\n            # actually implements .reset().\n            self.wrapped.reset()\n            self.buf = ByteBuffer()\n\n        # see if we need to request some more data from the input stream.\n        needed = self.pos + size - len(self.buf)\n\n        while needed > 0:\n            # read at most 64 MiB at once\n            # read at least self.minread\n            amount = clamp(needed, self.minread, 67108864)\n\n            data = self.wrapped.read(amount)\n            self.buf.append(data)\n\n            needed -= len(data)\n\n            self.buf.discardleft(max(self.keepbuffered, size - needed))\n\n            if len(data) < amount:\n                # wrapped stream has EOFed.\n                break\n\n        data = self.buf[self.pos: self.pos + size]\n        self.pos += len(data)\n        return data\n\n    def get_size(self):\n        return self.wrapped.get_size()\n\n    def close(self):\n        self.closed = True\n        del self.buf\n        del self.wrapped\n\n\nclass StreamFragment(PosSavingReadOnlyFileLikeObject):\n    \"\"\"\n    Represents a definite part of an other file-like, read-only seekable\n    stream.\n\n    Constructor arguments:\n\n    @param stream\n        The stream; must implement read(), and seek() with whence=os.SEEK_SET.\n\n        The stream's cursor is explicitly positioned before each call to\n        read(); this allows multiple PartialStream objects to use the stream\n        in parallel.\n\n    @param start\n        The first position of the stream that is used in this object.\n\n    @param size\n        The size of the stream fragment (in bytes).\n    \"\"\"\n\n    def __init__(self, stream, start, size):\n        super().__init__()\n\n        self.stream = stream\n        self.start = start\n        self.size = size\n\n        if size < 0:\n            raise ValueError(\"size must be positive\")\n\n    def read(self, size: int = -1) -> None:\n        if size < 0:\n            size = INF\n\n        size = clamp(size, 0, self.size - self.pos)\n\n        if not size:\n            return b\"\"\n\n        self.stream.seek(self.start + self.pos)\n        data = self.stream.read(size)\n\n        if len(data) != size:\n            raise EOFError(\"unexpected EOF in stream when attempting to read \"\n                           \"stream fragment\")\n\n        self.pos += len(data)\n        return data\n\n    def get_size(self) -> int:\n        return self.size\n\n    def close(self) -> None:\n        self.closed = True\n        del self.stream\n"
  },
  {
    "path": "openage/util/files.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\"\"\"\nSome file handling utilities\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\nimport os\nfrom typing import Union\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.abstract import FSLikeObject\n\n\ndef read_guaranteed(fileobj: FSLikeObject, size: int) -> bytes:\n    \"\"\"\n    As regular fileobj.read(size), but raises EOFError if fewer bytes\n    than requested are returned.\n    \"\"\"\n    remaining = size\n    result = []\n\n    while remaining:\n        data = fileobj.read(remaining)\n        if not data:\n            raise EOFError()\n\n        remaining -= len(data)\n        result.append(data)\n\n    return b\"\".join(result)\n\n\ndef read_nullterminated_string(fileobj: FSLikeObject, maxlen: int = 255) -> bytes:\n    \"\"\"\n    Reads bytes until a null terminator is reached.\n    \"\"\"\n    result = bytearray()\n\n    while True:\n        char = ord(read_guaranteed(fileobj, 1))\n\n        if char == 0:\n            break\n\n        result.append(char)\n\n        if len(result) > maxlen:\n            raise SyntaxError(\"Null-terminated string too long.\")\n\n    return bytes(result)\n\n\ndef which(filename: str) -> Union[str, None]:\n    \"\"\"\n    Like the which (1) tool to get the full path of a command\n    by looking at the PATH environment variable.\n    \"\"\"\n\n    def is_executable(fpath: str) -> bool:\n        \"\"\"\n        Test if the given file exists and has an executable bit.\n        \"\"\"\n        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)\n\n    fpath = os.path.split(filename)[0]\n    if fpath:\n        if is_executable(filename):\n            return filename\n    else:\n        for path in os.environ[\"PATH\"].split(os.pathsep):\n            path = path.strip('\"')\n            exe_file = os.path.join(path, filename)\n            if is_executable(exe_file):\n                return exe_file\n\n    return None\n"
  },
  {
    "path": "openage/util/fslike/CMakeLists.txt",
    "content": "add_py_modules(\n\t__init__.py\n\tabstract.py\n\tdirectory.py\n\tfilecollection.py\n\tpath.py\n\ttest.py\n\tunion.py\n\twrapper.py\n)\n\nadd_pxds(\n\tcpp.pxd\n)\n\nadd_cython_modules(\n\tcpp.pyx\n)\n"
  },
  {
    "path": "openage/util/fslike/__init__.py",
    "content": "# Copyright 2015-2016 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFilesystem-like object utility package.\n\nTypes that implement FSLikeObject provide pathlib.Path-analogous VirtualPath\nobjects, which in turn provide file-like objects.\n\nIn addition, a C++ interface for VirtualPath objects is provided.\n\"\"\"\n"
  },
  {
    "path": "openage/util/fslike/abstract.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides filesystem-like interfaces:\n\n - FileSystemLikeObject (abstract class)\n    an abstract class for objects that represent file systems.\n\n - ReadOnlyFileSystemLikeObject (abstract class)\n    implements all write access functions to raise UnsupportedOperation\n\nFor interface implementations, see the fslike module.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom abc import ABC, abstractmethod\nfrom io import UnsupportedOperation\n\nfrom .path import Path\n\nif typing.TYPE_CHECKING:\n    from io import BufferedReader\n\n\nclass FSLikeObject(ABC):\n    \"\"\"\n    To be implemented by any filesystem-like objects that wish to provide\n    their contents via Path-like and file-like objects.\n\n    Normally, you use the Path provided by self.root for example!\n\n    The abstract member methods take a list or tuple of path component\n    bytes objects, e.g.: [b'etc', b'passwd'].\n\n    If a request can not be fulfilled for whatever reason (file doesn't exist,\n    the object is read-only, the given method is not implemented, ...),\n    they may and shall raise an appropriate instance of IOError.\n    \"\"\"\n\n    # sorry pylint, we need those methods.\n    # pylint: disable=too-many-public-methods\n\n    @property\n    def root(self):\n        \"\"\"\n        Returns a path-like object for the root of this file system.\n\n        This is the main interface that is used normally.\n        \"\"\"\n        return Path(self, [])\n\n    def pretty(self, parts) -> str:\n        \"\"\"\n        pretty-format a path in this filesystem like object.\n        \"\"\"\n        return f\"[{str(self)}]:{b'/'.join(parts).decode(errors='replace')}\"\n\n    @abstractmethod\n    def open_r(self, parts) -> BufferedReader:\n        \"\"\" Shall return a BufferedReader for the given file (\"mode 'rb'\"). \"\"\"\n\n    @abstractmethod\n    def open_w(self, parts) -> BufferedReader:\n        \"\"\" Shall return a BufferedWriter for the given file (\"mode 'wb'\"). \"\"\"\n\n    def open_rw(self, parts) -> BufferedReader:\n        \"\"\" Shall return a BufferedWriter for the given file (\"mode 'r+'\"). \"\"\"\n\n    def open_a(self, parts) -> BufferedReader:\n        \"\"\" Shall return a BufferedWriter for the given file (\"mode 'a'\"). \"\"\"\n\n    def open_ar(self, parts) -> BufferedReader:\n        \"\"\" Shall return a BufferedWriter for the given file (\"mode 'a+'\"). \"\"\"\n\n    def exists(self, parts):\n        \"\"\" Test if the parts are a file or a directory \"\"\"\n        return self.is_file(parts) or self.is_dir(parts)\n\n    def resolve_r(self, parts) -> typing.Union[Path, None]:\n        \"\"\"\n        Returns a new, flattened, Path if the target exists.\n        The fslike parts in between may be skipped,\n        so that just the resulting path is returned.\n\n        Returns None if the path does not exist.\n        \"\"\"\n        return Path(self, parts) if self.exists(parts) else None\n\n    def resolve_w(self, parts) -> typing.Union[Path, None]:\n        \"\"\"\n        Returns a new flattened path. This skips funny mounts in between.\n\n        Returns None if the path does not exist or is not writable.\n        \"\"\"\n        return Path(self, parts) if self.writable(parts) else None\n\n    def get_native_path(self, parts) -> typing.ByteString:  # pylint: disable=no-self-use,unused-argument,useless-return\n        \"\"\"\n        Return the path bytestring that represents a location usable\n        by your kernel.\n        If the path can't be represented natively, return None.\n        \"\"\"\n        # By default, return None. It's overridden by subclasses.\n        return None\n\n    @abstractmethod\n    def list(self, parts) -> typing.Generator[str | bytes, None, None]:\n        \"\"\" Shall yield the entry names of the given directory. \"\"\"\n\n    @abstractmethod\n    def filesize(self, parts) -> int:\n        \"\"\"\n        Shall determine the file size (bytes),\n        and return None if unknown.\n        \"\"\"\n\n    @abstractmethod\n    def mtime(self, parts) -> typing.Union[float, None]:\n        \"\"\"\n        Shall determine the last modification time (UNIX timestamp),\n        and return None if unknown.\n        \"\"\"\n\n    @abstractmethod\n    def mkdirs(self, parts) -> None:\n        \"\"\" Shall ensure that the directory exists. \"\"\"\n\n    @abstractmethod\n    def rmdir(self, parts) -> None:\n        \"\"\" Shall remove an empty directory. \"\"\"\n\n    @abstractmethod\n    def unlink(self, parts) -> None:\n        \"\"\" Shall remove a single file. \"\"\"\n\n    @abstractmethod\n    def touch(self, parts) -> None:\n        \"\"\" Shall create the file or update its timestamp. \"\"\"\n\n    @abstractmethod\n    def rename(self, srcparts, tgtparts) -> None:\n        \"\"\" Shall rename a file or directory to the target name. \"\"\"\n\n    @abstractmethod\n    def is_file(self, parts) -> bool:\n        \"\"\"\n        Shall return true if the path is a file (or symlink to one).\n        Shall not raise.\n        \"\"\"\n\n    @abstractmethod\n    def is_dir(self, parts) -> bool:\n        \"\"\"\n        Shall return true if the path is a directory (or symlink to one).\n        Shall not raise.\n        \"\"\"\n\n    @abstractmethod\n    def writable(self, parts) -> bool:\n        \"\"\"\n        Shall return an educated guess whether the path can be written to.\n        Shall not raise.\n        \"\"\"\n\n    @abstractmethod\n    def watch(self, parts, callback) -> bool:\n        \"\"\"\n        Shall install callback as a watcher for the given path, if supported.\n        Shall return True if a watcher was installed, False if no operation was\n        performed.\n        Shall not raise.\n        \"\"\"\n\n    @abstractmethod\n    def poll_watches(self):\n        \"\"\"\n        Shall poll all file watches and invoke the associated callbacks if\n        any of the files have changed. Shall have a low performance impact.\n        Shall not raise.\n        \"\"\"\n\n\nclass ReadOnlyFSLikeObject(FSLikeObject):\n    \"\"\"\n    Specialization of FSLikeObject where all writing methods are implemented to\n    raise IOError.\n    \"\"\"\n    # pylint doesn't understand that this class is supposed to be abstract.\n    # pylint: disable=abstract-method\n\n    def read_only_error(self, parts) -> typing.NoReturn:\n        \"\"\" Helper method to be called from all other methods. \"\"\"\n        del parts  # unused\n        raise UnsupportedOperation(\"read-only: \" + str(self))\n\n    def open_w(self, parts) -> typing.NoReturn:\n        self.read_only_error(parts)\n\n    def mkdirs(self, parts) -> typing.NoReturn:\n        self.read_only_error(parts)\n\n    def rmdir(self, parts) -> typing.NoReturn:\n        self.read_only_error(parts)\n\n    def unlink(self, parts) -> typing.NoReturn:\n        self.read_only_error(parts)\n\n    def touch(self, parts) -> typing.NoReturn:\n        self.read_only_error(parts)\n\n    def rename(self, srcparts, tgtparts) -> typing.NoReturn:\n        del tgtparts  # unused\n        self.read_only_error(srcparts)\n\n    def writable(self, parts) -> bool:\n        del parts  # unused\n        return False\n"
  },
  {
    "path": "openage/util/fslike/cpp.pxd",
    "content": "# Copyright 2017-2017 the openage authors. See copying.md for legal info.\n\nfrom libopenage.util.path cimport Path\n\n\ncdef cpppath_to_pypath(const Path &path)\n"
  },
  {
    "path": "openage/util/fslike/cpp.pyx",
    "content": "# Copyright 2017-2021 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFunctions called from C++ to perform method calls on\nfilelike python objects.\n\"\"\"\n\nimport os\n\nfrom cpython.ref cimport PyObject\nfrom libc.stdint cimport uint64_t\nfrom libcpp cimport bool\nfrom libcpp.cast cimport static_cast\nfrom libcpp.memory cimport shared_ptr\nfrom libcpp.string cimport string\nfrom libcpp.utility cimport pair\nfrom libcpp.vector cimport vector\n\nfrom libopenage.util.file cimport File as File_cpp\nfrom libopenage.util.fslike.fslike cimport FSLike\nfrom libopenage.util.fslike.python cimport (\n    Python as FSLikePython,\n    pyx_fs_is_file,\n    pyx_fs_is_dir,\n    pyx_fs_writable,\n    pyx_fs_list,\n    pyx_fs_mkdirs,\n    pyx_fs_open_r,\n    pyx_fs_open_w,\n    pyx_fs_open_rw,\n    pyx_fs_open_a,\n    pyx_fs_open_ar,\n    pyx_fs_resolve_r,\n    pyx_fs_resolve_w,\n    pyx_fs_get_native_path,\n    pyx_fs_rename,\n    pyx_fs_rmdir,\n    pyx_fs_touch,\n    pyx_fs_unlink,\n    pyx_fs_get_mtime,\n    pyx_fs_get_filesize,\n    pyx_fs_is_fslike_directory,\n)\nfrom libopenage.util.path cimport Path as Path_cpp\nfrom libopenage.pyinterface.pyobject cimport PyObj\nfrom .directory import Directory\nfrom .abstract import FSLikeObject\nfrom ..fslike.path import Path as Path_py\n\n\ncdef class FSLikeCPPWrapper:\n    \"\"\"\n    Wraps a c++ fslike object in python.\n    This relays the call to the c++ object.\n\n    This fslike object is wrapped again by a pure python class,\n    which can then inherit from the FSLikeObject.\n    \"\"\"\n\n    # pointer to the cpp fslike object\n    cdef shared_ptr[FSLike] fsobj\n\n    @staticmethod\n    cdef wrap(shared_ptr[FSLike] c_fsobj):\n        wrp = FSLikeCPPWrapper()\n        wrp.fsobj = c_fsobj\n        return wrp\n\n    def open_r(self, parts):\n        cdef File_cpp file = self.fsobj.get().open_r(parts)\n        return None\n\n    def open_w(self, parts):\n        cdef File_cpp file = self.fsobj.get().open_w(parts)\n        return None\n\n    def resolve_r(self, parts):\n        cdef pair[bool, Path] result = self.fsobj.get().resolve_r(parts)\n\n        if not result.first:\n            return None\n        else:\n            return cpppath_to_pypath(result.second)\n\n    def resolve_w(self, parts):\n        cdef pair[bool, Path] result = self.fsobj.get().resolve_w(parts)\n\n        if not result.first:\n            return None\n        else:\n            return cpppath_to_pypath(result.second)\n\n    def get_native_path(self, parts):\n        cdef string native_path = self.fsobj.get().get_native_path(parts)\n        txt = bytes(native_path)\n\n        if txt:\n            return txt\n        else:\n            return None\n\n    def list(self, parts):\n        cdef Path.parts_t result = self.fsobj.get().list(parts)\n\n        for entry in result:\n            yield from str(entry)\n\n    def filesize(self, parts):\n        return self.fsobj.get().get_filesize(parts)\n\n    def mtime(self, parts):\n        return self.fsobj.get().get_mtime(parts)\n\n    def mkdirs(self, parts):\n        return self.fsobj.get().mkdirs(parts)\n\n    def rmdir(self, parts):\n        return self.fsobj.get().rmdir(parts)\n\n    def unlink(self, parts):\n        return self.fsobj.get().unlink(parts)\n\n    def touch(self, parts):\n        self.fsobj.get().touch(parts)\n\n    def rename(self, srcparts, tgtparts):\n        self.fsobj.get().rename(srcparts, tgtparts)\n\n    def is_file(self, parts):\n        return self.fsobj.get().is_file(parts)\n\n    def is_dir(self, parts):\n        return self.fsobj.get().is_dir(parts)\n\n    def writable(self, parts):\n        return self.fsobj.get().writable(parts)\n\n\nclass FSLikeCPP(FSLikeObject):\n    \"\"\"\n    Pure python cpp fslike wrapper.\n    Wrapps the above translation class again so\n    we can inherit from FSLikeObject.\n    \"\"\"\n\n    def __init__(self, cpp_wrapper):\n        self.fsobj = cpp_wrapper\n\n    def open_r(self, parts):\n        return self.fsobj.open_r(parts)\n\n    def open_w(self, parts):\n        return self.fsobj.open_w(parts)\n\n    def resolve_r(self, parts):\n        return self.fsobj.resolve_r(parts)\n\n    def resolve_w(self, parts):\n        return self.fsobj.resolve_w(parts)\n\n    def get_native_path(self, parts):\n        return self.fsobj.get_native_path()\n\n    def list(self, parts):\n        yield from self.fsobj.list(parts)\n\n    def filesize(self, parts):\n        return self.fsobj.get_filesize(parts)\n\n    def mtime(self, parts):\n        return self.fsobj.get_mtime(parts)\n\n    def mkdirs(self, parts):\n        return self.fsobj.mkdirs(parts)\n\n    def rmdir(self, parts):\n        return self.fsobj.rmdir(parts)\n\n    def unlink(self, parts):\n        return self.fsobj.unlink(parts)\n\n    def touch(self, parts):\n        self.fsobj.touch(parts)\n\n    def rename(self, srcparts, tgtparts):\n        self.fsobj.rename(srcparts, tgtparts)\n\n    def is_file(self, parts):\n        return self.fsobj.is_file(parts)\n\n    def is_dir(self, parts):\n        return self.fsobj.is_dir(parts)\n\n    def writable(self, parts):\n        return self.fsobj.is_writable(parts)\n\n\ncdef cpppath_to_pypath(const Path_cpp &path):\n    cdef FSLike *fsobj = path.get_fsobj();\n    cdef FSLikePython *py_fslike\n\n    # we could also use typeid() here, but dat mangling..\n\n    # if the fslike is from python anyway, we can bypass the\n    # language barrier (hue hue hue)\n    if fsobj.is_python_native():\n        # extract the python fslike object and transfer it\n        # to the python path\n        py_fslike = <FSLikePython *> fsobj\n        return Path_py(<object>py_fslike.get_py_fsobj().get_ref(),\n                       path.get_parts())\n\n    else:\n        # wrap cpp fslike to relay calls\n        # then pack it into the python path\n        return Path_py(\n            FSLikeCPP(\n                FSLikeCPPWrapper.wrap(fsobj.shared_from_this()),\n                path.get_parts()\n            )\n        )\n\n\ncdef bool fs_is_file(PyObject *fslike,\n                     const vector[string]& parts) except * with gil:\n    return (<object> fslike).is_file(parts)\n\n\ncdef bool fs_is_dir(PyObject *fslike,\n                    const vector[string]& parts) except * with gil:\n    return (<object> fslike).is_dir(parts)\n\n\ncdef bool fs_writable(PyObject *fslike,\n                      const vector[string]& parts) except * with gil:\n    return (<object> fslike).writable(parts)\n\n\ncdef vector[string] fs_list(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).list(parts)\n\n\ncdef bool fs_mkdirs(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).mkdirs(parts)\n\n\ncdef File_cpp fs_open(object path, int mode) except *:\n    if path is None:\n        raise Exception(\"fs_open can't open a path that is None\")\n\n    cdef PyObj ref\n\n    native_path = path._get_native_path()\n    if native_path is not None:\n        # open it in c++, 0=read, 1=write\n        return File_cpp(native_path, mode)\n\n    else:\n        # sync with filelike/filelike.h enum class mode_t\n        # (and the calls to fs_open_* below)\n        if mode == 0:\n            access_mode = 'rb'\n        elif mode == 1:\n            access_mode = 'wb'\n        elif mode == 2:\n            access_mode = 'r+b'\n        elif mode == 3:\n            access_mode = 'ab'\n        elif mode == 4:\n            access_mode = 'a+b'\n        else:\n            raise ValueError(f\"unknown file open mode id: {mode}\")\n\n        # open it the python-way and wrap it\n        filelike = path.open(access_mode)\n        ref = PyObj(<PyObject*> filelike)\n        return File_cpp(ref)\n\n\ncdef check_file_exists(object path, object fslike, const vector[string]& parts):\n    if path is None:\n        raise FileNotFoundError(\"file could not be found in filesystem %s \"\n                                \"for path '%s'\" % (\n                                    fslike,\n                                    b\"/\".join(parts).decode(errors='ignore')\n                                ))\n\n\ncdef File_cpp fs_open_r(PyObject *fslike, const vector[string]& parts) except * with gil:\n    open_path = (<object> fslike).resolve_r(parts)\n    check_file_exists(open_path, <object> fslike, parts)\n    return fs_open(open_path, 0)\n\n\ncdef File_cpp fs_open_w(PyObject *fslike, const vector[string]& parts) except * with gil:\n    open_path = (<object> fslike).resolve_w(parts)\n    check_file_exists(open_path, <object> fslike, parts)\n    return fs_open(open_path, 1)\n\n\ncdef File_cpp fs_open_rw(PyObject *fslike, const vector[string]& parts) except * with gil:\n    open_path = (<object> fslike).resolve_w(parts)\n    check_file_exists(open_path, <object> fslike, parts)\n    return fs_open(open_path, 2)\n\n\ncdef File_cpp fs_open_a(PyObject *fslike, const vector[string]& parts) except * with gil:\n    open_path = (<object> fslike).resolve_w(parts)\n    check_file_exists(open_path, <object> fslike, parts)\n    return fs_open(open_path, 3)\n\n\ncdef File_cpp fs_open_ar(PyObject *fslike, const vector[string]& parts) except * with gil:\n    open_path = (<object> fslike).resolve_w(parts)\n    check_file_exists(open_path, <object> fslike, parts)\n    return fs_open(open_path, 4)\n\n\ncdef Path_cpp fs_resolve_r(PyObject *fslike, const vector[string]& parts) except * with gil:\n    path = (<object> fslike).resolve_r(parts)\n    if path is not None:\n        return Path_cpp(PyObj(<PyObject*>path.fsobj), path.parts)\n    else:\n        return Path_cpp()\n\n\ncdef Path_cpp fs_resolve_w(PyObject *fslike, const vector[string]& parts) except * with gil:\n    path = (<object> fslike).resolve_w(parts)\n    if path is not None:\n        return Path_cpp(PyObj(<PyObject*>path.fsobj), path.parts)\n    else:\n        return Path_cpp()\n\n\ncdef PyObj fs_get_native_path(PyObject *fslike,\n                              const vector[string]& parts) except * with gil:\n\n    path = (<object> fslike).get_native_path(parts)\n    return PyObj(<PyObject*>path)\n\n\ncdef bool fs_rename(PyObject *fslike,\n                    const vector[string]& parts,\n                    const vector[string]& target_parts) except * with gil:\n\n    return (<object> fslike).rename(parts, target_parts)\n\n\ncdef bool fs_rmdir(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).rmdir(parts)\n\n\ncdef bool fs_touch(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).touch(parts)\n\n\ncdef bool fs_unlink(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).unlink(parts)\n\n\ncdef int fs_get_mtime(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).mtime(parts)\n\n\ncdef uint64_t fs_get_filesize(PyObject *fslike, const vector[string]& parts) except * with gil:\n    return (<object> fslike).filesize(parts)\n\n\ncdef bool fs_is_fslike_directory(PyObject *fslike) except * with gil:\n    return isinstance(<object> fslike, Directory)\n\n\ndef setup():\n    pyx_fs_is_file.bind0(fs_is_file)\n    pyx_fs_is_dir.bind0(fs_is_dir)\n    pyx_fs_writable.bind0(fs_writable)\n    pyx_fs_list.bind0(fs_list)\n    pyx_fs_mkdirs.bind0(fs_mkdirs)\n    pyx_fs_open_r.bind0(fs_open_r)\n    pyx_fs_open_w.bind0(fs_open_w)\n    pyx_fs_open_rw.bind0(fs_open_rw)\n    pyx_fs_open_a.bind0(fs_open_a)\n    pyx_fs_open_ar.bind0(fs_open_ar)\n    pyx_fs_resolve_r.bind0(fs_resolve_r)\n    pyx_fs_resolve_w.bind0(fs_resolve_w)\n    pyx_fs_get_native_path.bind0(fs_get_native_path)\n    pyx_fs_rename.bind0(fs_rename)\n    pyx_fs_rmdir.bind0(fs_rmdir)\n    pyx_fs_touch.bind0(fs_touch)\n    pyx_fs_unlink.bind0(fs_unlink)\n    pyx_fs_get_mtime.bind0(fs_get_mtime)\n    pyx_fs_get_filesize.bind0(fs_get_filesize)\n    pyx_fs_is_fslike_directory.bind0(fs_is_fslike_directory)\n"
  },
  {
    "path": "openage/util/fslike/directory.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFSLikeObjects that represent actual file system paths:\n\n - Directory: enforces case\n - CaseIgnoringReadOnlyDirectory\n\"\"\"\n\nfrom __future__ import annotations\nimport typing\n\nimport os\nimport pathlib\n\nfrom typing import Union\n\nfrom .abstract import FSLikeObject\n\nif typing.TYPE_CHECKING:\n    from io import BufferedReader\n\n\nclass Directory(FSLikeObject):\n    \"\"\"\n    Provides an actual file system directory's contents as-they-are.\n\n    Initialized from some real path that is mounted already by your system.\n    \"\"\"\n\n    def __init__(self, path_, create_if_missing=False):\n        if isinstance(path_, pathlib.Path):\n            path = bytes(path_)\n        elif isinstance(path_, str):\n            path = path_.encode()\n        elif isinstance(path_, bytes):\n            path = path_\n        else:\n            raise TypeError(f\"incompatible type for path: {type(path_)}\")\n\n        if not os.path.isdir(path):\n            if create_if_missing:\n                os.makedirs(path)\n            else:\n                raise FileNotFoundError(path)\n\n        self.path = path\n\n    def __repr__(self):\n        return f\"Directory({self.path.decode(errors='replace')})\"\n\n    def resolve(self, parts) -> Union[str, bytes]:\n        \"\"\" resolves parts to an actual path name. \"\"\"\n        return os.path.join(self.path, *parts)\n\n    def open_r(self, parts) -> BufferedReader:\n        return open(self.resolve(parts), 'rb')\n\n    def open_w(self, parts) -> BufferedReader:\n        return open(self.resolve(parts), 'wb')\n\n    def open_rw(self, parts) -> BufferedReader:\n        return open(self.resolve(parts), 'r+b')\n\n    def open_a(self, parts) -> BufferedReader:\n        return open(self.resolve(parts), 'ab')\n\n    def open_ar(self, parts) -> BufferedReader:\n        return open(self.resolve(parts), 'a+b')\n\n    def get_native_path(self, parts) -> Union[str, bytes]:\n        return self.resolve(parts)\n\n    def list(self, parts) -> typing.Generator[str | bytes, None, None]:\n        # TODO migrate to scandir, once we're on py 3.5.\n        yield from os.listdir(self.resolve(parts))\n\n    def filesize(self, parts) -> int:\n        return os.path.getsize(self.resolve(parts))\n\n    def mtime(self, parts) -> float:\n        return os.path.getmtime(self.resolve(parts))\n\n    def mkdirs(self, parts) -> None:\n        return os.makedirs(self.resolve(parts), exist_ok=True)\n\n    def rmdir(self, parts) -> None:\n        return os.rmdir(self.resolve(parts))\n\n    def unlink(self, parts) -> None:\n        return os.unlink(self.resolve(parts))\n\n    def touch(self, parts) -> None:\n        try:\n            os.utime(self.resolve(parts))\n        except FileNotFoundError:\n            with open(self.resolve(parts), 'ab') as directory:\n                directory.close()\n\n    def rename(self, srcparts, tgtparts) -> None:\n        return os.rename(self.resolve(srcparts), self.resolve(tgtparts))\n\n    def is_file(self, parts) -> bool:\n        return os.path.isfile(self.resolve(parts))\n\n    def is_dir(self, parts) -> bool:\n        return os.path.isdir(self.resolve(parts))\n\n    def writable(self, parts) -> bool:\n        parts = list(parts)\n        path = self.resolve(parts)\n\n        while not os.path.exists(path):\n            if not parts:\n                raise FileNotFoundError(self.path)\n\n            parts.pop()\n            path = self.resolve(parts)\n\n        return os.access(path, os.W_OK)\n\n    def watch(self, parts, callback) -> None:\n        # TODO\n        pass\n\n    def poll_watches(self) -> None:\n        # TODO\n        pass\n\n\nclass CaseIgnoringDirectory(Directory):\n    \"\"\"\n    Like directory, but all given paths must be lower-case,\n    and will be resolved to the actual correct case.\n\n    The one exception is the constructor argument:\n    It _must_ be in the correct case.\n    \"\"\"\n\n    def __init__(self, path, create_if_missing=False):\n        super().__init__(path, create_if_missing)\n        self.cache = {(): ()}\n        self.listings = {}\n\n    def __repr__(self):\n        return f\"Directory({self.path.decode(errors='replace')})\"\n\n    def actual_name(self, stem: list, name: str) -> str:\n        \"\"\"\n        If the (lower-case) path that's given in stem exists,\n        fetches the actual name for the given lower-case name.\n        \"\"\"\n        try:\n            listing = self.listings[tuple(stem)]\n        except KeyError:\n            # the directory has not been listed yet.\n            try:\n                filelist = os.listdir(os.path.join(self.path, *stem))\n            except FileNotFoundError:\n                filelist = []\n\n            listing = {}\n            for filename in filelist:\n                if filename.lower() != filename:\n                    listing[filename.lower()] = filename\n            self.listings[tuple(stem)] = listing\n\n        try:\n            return listing[name]\n        except KeyError:\n            return name\n\n    def resolve(self, parts) -> Union[str, bytes]:\n        parts = [part.lower() for part in parts]\n\n        i = 0\n        for i in range(len(parts), -1, -1):\n            try:\n                result = list(self.cache[tuple(parts[:i])])\n                break\n            except KeyError:\n                pass\n        else:\n            raise RuntimeError(\"code flow error\")\n\n        # result now contains the case-corrected path for parts[:i].\n        # we need to append the path for parts[i:].\n        for part in parts[i:]:\n            result.append(self.actual_name(result, part))\n            self.cache[tuple(parts[:len(result)])] = tuple(result)\n\n        return os.path.join(self.path, *result)\n\n    def list(self, parts) -> typing.Generator[str | bytes, None, None]:\n        for name in super().list(parts):\n            yield name.lower()\n\n\n# TODO add CaseEnforcingDirectory, with resolve() similar to that of\n#      CaseIgnoringDirectory.\n#      CaseEnforcingDirectory would prevent modders from getting sloppy with\n#      file name case, which would lead to mods stopping to work on Linux.\n"
  },
  {
    "path": "openage/util/fslike/filecollection.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides Filecollection, a utility class for combining multiple file-like\nobjects to a FSLikeObject.\n\"\"\"\nfrom __future__ import annotations\nimport typing\n\nfrom collections import OrderedDict\nfrom io import UnsupportedOperation\nfrom typing import NoReturn\n\nfrom .abstract import FSLikeObject\nfrom .path import Path\n\nif typing.TYPE_CHECKING:\n    from openage.util.filelike.stream import StreamFragment\n\n\nclass FileCollection(FSLikeObject):\n    \"\"\"\n    FSLikeObject that holds several individual files.\n\n    Uses lambdas to access files somewhere else on the fly.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n        # stores lambdas to access the files\n        # {name: open_r, open_w, size, mtime}, {name: subdir}\n        self.rootentries = OrderedDict(), OrderedDict()\n\n    @property\n    def root(self):\n        return FileCollectionPath(self, [])\n\n    def get_direntries(self, parts=None, create: bool = False) -> tuple[OrderedDict, OrderedDict]:\n        \"\"\"\n        Fetches the fileentries, subdirentries tuple for the given dir.\n\n        If create == False, raises FileNotFoundError if the directory doesn't\n        exist.\n\n        Helper method for internal use.\n        \"\"\"\n        if parts is None:\n            parts = []\n\n        entries = self.rootentries\n        for idx, subdir in enumerate(parts):\n            if subdir not in entries[1]:\n                if create:\n                    if subdir in entries[0]:\n                        raise FileExistsError(b\"/\".join(parts[:idx + 1]))\n                    entries[1][subdir] = OrderedDict(), OrderedDict()\n                else:\n                    raise FileNotFoundError(\n                        \"No such directory: \" +\n                        b\"/\".join(parts[:idx + 1]).decode(errors='replace'))\n\n            entries = entries[1][subdir]\n\n        return entries\n\n    def add_fileentry(self, parts, fileentry: FileEntry):\n        \"\"\"\n        Adds a file entry (and parent directory entries, if needed).\n\n        This method should not be called directly; instead, use the\n        add_file method of Path objects that were obtained from this.\n        \"\"\"\n        if not parts:\n            raise IsADirectoryError(\"FileCollection.root is a directory\")\n\n        entries = self.get_direntries(parts[:-1], create=True)\n\n        name = parts[-1]\n        if name in entries[1]:\n            raise IsADirectoryError(b\"/\".join(parts))\n\n        entries[0][name] = fileentry\n\n    def get_fileentry(self, parts) -> FileEntry:\n        \"\"\"\n        Gets a file entry. Helper method for internal use.\n        \"\"\"\n        if not parts:\n            raise IsADirectoryError(\n                \"FileCollection.root is a directory\")\n\n        entries = self.get_direntries(parts[:-1])\n\n        name = parts[-1]\n\n        if name in entries[1]:\n            raise IsADirectoryError(b\"/\".join(parts))\n\n        if name not in entries[0]:\n            raise FileNotFoundError(b\"/\".join(parts))\n\n        return entries[0][name]\n\n    def open_r(self, parts: list[bytes]) -> StreamFragment:\n        entry = self.get_fileentry(parts)\n\n        open_r = entry.open_r()\n\n        if open_r is None:\n            raise UnsupportedOperation(\n                \"not readable: \" +\n                b\"/\".join(parts).decode(errors='replace'))\n\n        return open_r\n\n    def open_w(self, parts: list[bytes]):\n        entry = self.get_fileentry(parts)\n\n        open_w = entry.open_w()\n\n        if open_w is None:\n            raise UnsupportedOperation(\n                \"not writable: \" +\n                b\"/\".join(parts).decode(errors='replace'))\n\n        return open_w\n\n    def list(self, parts):\n        fileentries, subdirs = self.get_direntries(parts)\n\n        yield from subdirs\n        yield from fileentries\n\n    def filesize(self, parts) -> int:\n        entry = self.get_fileentry(parts)\n\n        return entry.size()\n\n    def mtime(self, parts) -> float:\n        entry = self.get_fileentry(parts)\n\n        return entry.mtime()\n\n    def mkdirs(self, parts) -> None:\n        self.get_direntries(parts, create=True)\n\n    def rmdir(self, parts) -> None:\n        if not parts:\n            raise UnsupportedOperation(\"can't rmdir FileCollection.root\")\n\n        parent_files, parent_dirs = self.get_direntries(parts[:-1])\n        name = parts[-1]\n\n        if name in parent_files:\n            raise NotADirectoryError(b'/'.join(parts))\n\n        try:\n            files, subdirs = parent_dirs[name]\n        except KeyError:\n            raise FileNotFoundError(b'/'.join(parts)) from None\n\n        if files or subdirs:\n            raise IOError(\"Directory not empty: \" +\n                          b'/'.join(parts).decode(errors='replace'))\n\n        del parent_dirs[name]\n\n    def unlink(self, parts) -> None:\n        if not parts:\n            raise IsADirectoryError(\"FileCollection.root\")\n\n        parent_files, parent_dirs = self.get_direntries(parts[:-1])\n        name = parts[-1]\n\n        if name in parent_dirs:\n            raise IsADirectoryError(b'/'.join(parts))\n\n        try:\n            del parent_files[name]\n        except KeyError:\n            raise FileNotFoundError(b'/'.join(parts)) from None\n\n    def touch(self, parts) -> NoReturn:\n        raise UnsupportedOperation(\"FileCollection.touch\")\n\n    def rename(self, srcparts, tgtparts) -> NoReturn:\n        raise UnsupportedOperation(\"FileCollection.rename\")\n\n    def is_file(self, parts) -> bool:\n        try:\n            self.get_fileentry(parts)\n            return True\n        except IOError:\n            return False\n\n    def is_dir(self, parts) -> bool:\n        try:\n            self.get_direntries(parts)\n            return True\n        except IOError:\n            return False\n\n    def writable(self, parts) -> bool:\n        try:\n            _, open_w, _, _ = self.get_fileentry(parts)\n            return open_w is not None\n        except IOError:\n            # generally, directories are not writable,\n            # though some of the existing files inside might be.\n            return False\n\n    def watch(self, parts, callback) -> bool:\n        del self, parts, callback  # unused\n        return False\n\n    def poll_watches(self) -> None:\n        pass\n\n\nclass FileCollectionPath(Path):\n    \"\"\"\n    Provides an additional method for adding a file at this path.\n    \"\"\"\n\n    def add_file(\n        self,\n        open_r=None,\n        open_w=None,\n        filesize: int = None,\n        mtime: float = None\n    ) -> bool:\n        \"\"\"\n        All parent directories are 'created', if needed.\n\n        Any arguments may be None, and shall be callable otherwise.\n        If open_r/open_w are None, the file will write-/read-only.\n        \"\"\"\n        return self.fsobj.add_fileentry(\n            self.parts, (open_r, open_w, filesize, mtime))\n\n    def add_file_from_path(self, path: Path) -> None:\n        \"\"\"\n        Like add_file, but uses a Path object instead of callables.\n        \"\"\"\n        if path.writable():\n            open_w = path.open_w\n        else:\n            open_w = None\n\n        self.add_file(path.open_r, open_w, path.filesize, path.mtime)\n\n\nclass FileEntry:\n    \"\"\"\n    Entry in a file collection archive.\n    \"\"\"\n    # pylint: disable=no-self-use\n\n    def open_r(self) -> StreamFragment:\n        \"\"\"\n        Returns a file-like object for reading.\n        \"\"\"\n        raise UnsupportedOperation(\"FileEntry.open_r\")\n\n    def open_w(self):\n        \"\"\"\n        Returns a file-like object for writing.\n        \"\"\"\n        raise UnsupportedOperation(\"FileEntry.open_w\")\n\n    def size(self) -> int:\n        \"\"\"\n        Returns the size of the entr<.\n        \"\"\"\n        raise UnsupportedOperation(\"FileEntry.size\")\n\n    def mtime(self) -> float:\n        \"\"\"\n        Returns the modification time of the entry.\n        \"\"\"\n        raise UnsupportedOperation(\"FileEntry.mtime\")\n"
  },
  {
    "path": "openage/util/fslike/path.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides Path, which is analogous to pathlib.Path,\nand the type of FSLikeObject.root.\n\"\"\"\nfrom typing import NoReturn, Union\n\nfrom io import UnsupportedOperation, TextIOWrapper\nimport os\nimport pathlib\nimport tempfile\n\n\nclass Path:\n    \"\"\"\n    Implements an interface somewhat similar to that of pathlib.Path,\n    but some methods are missing or have different usage, and some new have\n    been added.\n\n    Represents a specific path in a given FS-Like object; mostly, that\n    object's member methods are simply wrapped.\n\n    fsobj: fs-like object that is e.g. a cab-archive, a real\n           Directory(\"/lol\"), or anything that is like some filesystem.\n\n    parts: starting path in the above fsobj,\n           e.g. [\"folder\", \"file\"],\n           or \"folder/file\"\n           or b\"folder/file\".\n    \"\"\"\n\n    # We're re-implementing pathlib.Path's interface, we have no choice about\n    # this. Also, some of these are properties, so the effective count is\n    # lower.\n    # pylint: disable=too-many-public-methods\n\n    def __init__(self, fsobj, parts: Union[str, bytes, bytearray, list, tuple] = None):\n        if isinstance(parts, str):\n            parts = parts.encode()\n\n        if isinstance(parts, (bytes, bytearray)):\n            parts = parts.split(b'/')\n\n        if parts is None:\n            parts = []\n\n        if not isinstance(parts, (list, tuple)):\n            raise ValueError(\"path parts must be str, bytes, list or tuple, \"\n                             f\"but not: {type(parts)}\")\n\n        result = []\n        for part in parts:\n            if isinstance(part, str):\n                part = part.encode()\n\n            if part in (b'.', b''):\n                pass\n            elif part == b'..':\n                try:\n                    result.pop()\n                except IndexError:\n                    pass\n            else:\n                result.append(part)\n\n        self.fsobj = fsobj\n\n        # Set to True by create_temp_file or create_temp_dir\n        self.is_temp: bool = False\n\n        # use tuple instead of list to prevent accidential modification\n        self.parts = tuple(result)\n\n    def __str__(self):\n        return self.fsobj.pretty(self.parts)\n\n    def __repr__(self):\n        if not self.parts:\n            return repr(self.fsobj) + \".root\"\n\n        return f\"Path({repr(self.fsobj)}, {repr(self.parts)})\"\n\n    def exists(self) -> bool:\n        \"\"\" True if path exists \"\"\"\n        return self.fsobj.exists(self.parts)\n\n    def is_dir(self) -> bool:\n        \"\"\" True if path points to dir (or symlink to one) \"\"\"\n        return self.fsobj.is_dir(self.parts)\n\n    def is_file(self) -> bool:\n        \"\"\" True if path points to file (or symlink to one) \"\"\"\n        return self.fsobj.is_file(self.parts)\n\n    def writable(self) -> bool:\n        \"\"\" True if path is probably writable \"\"\"\n        return self.fsobj.writable(self.parts)\n\n    def list(self):\n        \"\"\" Yields path names for all members of this dir \"\"\"\n        yield from self.fsobj.list(self.parts)\n\n    def iterdir(self):\n        \"\"\" Yields path objects for all members of this dir \"\"\"\n        for name in self.fsobj.list(self.parts):\n            yield type(self)(self.fsobj, self.parts + (name,))\n\n    def mkdirs(self) -> None:\n        \"\"\" Creates this path (including parents). No-op if path exists. \"\"\"\n        return self.fsobj.mkdirs(self.parts)\n\n    def open(self, mode=\"r\"):\n        \"\"\" Opens the file at this path; returns a file-like object. \"\"\"\n\n        dmode = mode.replace(\"b\", \"\")\n\n        if dmode == \"r\":\n            handle = self.fsobj.open_r(self.parts)\n\n        elif dmode == \"w\":\n            handle = self.fsobj.open_w(self.parts)\n\n        elif dmode in (\"r+\", \"rw\"):\n            handle = self.fsobj.open_rw(self.parts)\n\n        elif dmode == \"a\":\n            handle = self.fsobj.open_a(self.parts)\n\n        elif dmode in (\"a+\", \"ar\"):\n            handle = self.fsobj.open_ar(self.parts)\n\n        else:\n            raise UnsupportedOperation(\"unsupported open mode: \" + mode)\n\n        if handle is None:\n            raise IOError(f\"failed to acquire valid file handle for {self} in mode {mode}\")\n\n        if \"b\" in mode:\n            return handle\n\n        return TextIOWrapper(handle)\n\n    def open_r(self):\n        \"\"\" open with mode='rb' \"\"\"\n        return self.fsobj.open_r(self.parts)\n\n    def open_w(self):\n        \"\"\" open with mode='wb' \"\"\"\n        return self.fsobj.open_w(self.parts)\n\n    def open_a(self):\n        \"\"\" open with mode='ab' \"\"\"\n        return self.fsobj.open_a(self.parts)\n\n    def _get_native_path(self):\n        \"\"\"\n        return the native path (usable by your kernel) of this path,\n        or None if the path is not natively usable.\n\n        Don't use this method directly, use the resolve methods below.\n        \"\"\"\n        return self.fsobj.get_native_path(self.parts)\n\n    def _resolve_r(self):\n        \"\"\"\n        Flatten the path recursively for read access.\n        Used to cancel out some wrappers in between.\n        \"\"\"\n        return self.fsobj.resolve_r(self.parts)\n\n    def _resolve_w(self):\n        \"\"\"\n        Flatten the path recursively for write access.\n        Used to cancel out some wrappers in between.\n        \"\"\"\n        return self.fsobj.resolve_w(self.parts)\n\n    def resolve_native_path(self, mode=\"r\"):\n        \"\"\"\n        Minimize the path and possibly return a native one.\n        Returns None if there was no native path.\n        \"\"\"\n        if mode == \"r\":\n            return self.resolve_native_path_r()\n\n        if mode == \"w\":\n            return self.resolve_native_path_w()\n\n        raise UnsupportedOperation(\"unsupported resolve mode: \" + mode)\n\n    def resolve_native_path_r(self):\n        \"\"\"\n        Resolve the path for read access and possibly return\n        a native equivalent.\n        If no native path was found, return None.\n        \"\"\"\n        resolved_path = self._resolve_r()\n        if resolved_path:\n            # pylint: disable=protected-access\n            return resolved_path._get_native_path()\n        return None\n\n    def resolve_native_path_w(self):\n        \"\"\"\n        Resolve the path for write access and try to return\n        a native equivalent.\n        If no native path could be determined, return None.\n        \"\"\"\n        resolved_path = self._resolve_w()\n        if resolved_path:\n            # pylint: disable=protected-access\n            return resolved_path._get_native_path()\n        return None\n\n    def rename(self, targetpath):\n        \"\"\" renames to targetpath \"\"\"\n        if self.fsobj != targetpath.fsobj:\n            raise UnsupportedOperation(\"can't rename across two FSLikeObjects\")\n        return self.fsobj.rename(self.parts, targetpath.parts)\n\n    def rmdir(self):\n        \"\"\" Removes the empty directory at this path. \"\"\"\n        return self.fsobj.rmdir(self.parts)\n\n    def touch(self):\n        \"\"\" Creates the file at this path, or updates the timestamp. \"\"\"\n        return self.fsobj.touch(self.parts)\n\n    def unlink(self):\n        \"\"\" Removes the file at this path. \"\"\"\n        return self.fsobj.unlink(self.parts)\n\n    def removerecursive(self):\n        \"\"\" Recursively deletes this file or directory. \"\"\"\n        if self.is_dir():\n            for path in self.iterdir():\n                path.removerecursive()\n            self.rmdir()\n        else:\n            self.unlink()\n\n    @ property\n    def mtime(self):\n        \"\"\" Returns the time of last modification of the file or directory. \"\"\"\n        return self.fsobj.mtime(self.parts)\n\n    @ property\n    def filesize(self):\n        \"\"\" Returns the file size. \"\"\"\n        return self.fsobj.filesize(self.parts)\n\n    def watch(self, callback):\n        \"\"\"\n        Installs 'callback' as callback that gets invoked whenever the file at\n        this path changes.\n\n        Returns True if the callback was installed, and false if not\n        (e.g. because the some OS limit was reached, or the underlying\n         FSLikeObject doesn't support watches).\n        \"\"\"\n        return self.fsobj.watch(self.parts, callback)\n\n    def poll_fs_watches(self):\n        \"\"\" Polls the installed watches for the entire file-system. \"\"\"\n        self.fsobj.poll_watches()\n\n    @ property\n    def parent(self):\n        \"\"\" Parent path object. The parent of root is root. \"\"\"\n        return type(self)(self.fsobj, self.parts[:-1])\n\n    @ property\n    def name(self):\n        \"\"\" The name of the topmost component (str). \"\"\"\n        return self.parts[-1].decode()\n\n    @ property\n    def suffix(self):\n        \"\"\" The last suffix of the name of the topmost component (str). \"\"\"\n        name = self.name\n        pos = name.rfind('.')\n        if pos <= 0:\n            return \"\"\n        return name[pos:]\n\n    @ property\n    def suffixes(self):\n        \"\"\" The suffixes of the name of the topmost component (str list). \"\"\"\n        name = self.name\n        if name.startswith('.'):\n            name = name[1:]\n        return ['.' + suffix for suffix in name.split('.')[1:]]\n\n    @ property\n    def stem(self):\n        \"\"\" Name without suffix (such that stem + suffix == name). \"\"\"\n        name = self.name\n        pos = name.rfind('.')\n        if pos <= 0:\n            return name\n\n        return name[:pos]\n\n    def joinpath(self, subpath):\n        \"\"\" Returns path for the given subpath. \"\"\"\n        if isinstance(subpath, str):\n            subpath = subpath.encode()\n\n        if isinstance(subpath, bytes):\n            subpath = subpath.split(b'/')\n\n        return type(self)(self.fsobj, self.parts + tuple(subpath))\n\n    def __getitem__(self, subpath):\n        \"\"\" Like joinpath. \"\"\"\n        return self.joinpath(subpath)\n\n    def __truediv__(self, subpath):\n        \"\"\" Like joinpath. \"\"\"\n        return self.joinpath(subpath)\n\n    def __eq__(self, other):\n        \"\"\" comparison by fslike and parts \"\"\"\n        return (self.fsobj == other.fsobj) and (self.parts == other.parts)\n\n    def with_name(self, name):\n        \"\"\" Returns path for differing name (same parent). \"\"\"\n        return self.parent.joinpath(name)\n\n    def with_suffix(self, suffix):\n        \"\"\" Returns path for different suffix (same parent and stem). \"\"\"\n        if isinstance(suffix, bytes):\n            suffix = suffix.decode()\n\n        return self.parent.joinpath(self.stem + suffix)\n\n    def mount(self, pathobj, priority=0) -> NoReturn:\n        \"\"\"This is only valid for UnionPath, don't call here\"\"\"\n        # pylint: disable=no-self-use,unused-argument\n        # TODO: https://github.com/PyCQA/pylint/issues/2329\n        raise PermissionError(\"Do not call mount on Path instances!\")\n\n    @staticmethod\n    def get_temp_file():\n        \"\"\"\n        Creates a temporary file.\n        \"\"\"\n        temp_fd, temp_file = tempfile.mkstemp()\n\n        # Close the file descriptor to release resources\n        os.close(temp_fd)\n\n        # Wrap the temporary file path in a Path object and return it\n        path = Path(pathlib.Path(temp_file))\n        path.is_temp = True\n\n        return path\n\n    @staticmethod\n    def get_temp_dir():\n        \"\"\"\n        Creates a temporary directory.\n        \"\"\"\n        # Create a temporary directory using tempfile.mkdtemp\n        temp_dir = tempfile.mkdtemp()\n\n        # Wrap the temporary directory path in a Path object and return it\n        path = Path(pathlib.Path(temp_dir))\n        path.is_temp = True\n\n        return path\n"
  },
  {
    "path": "openage/util/fslike/test.py",
    "content": "# Copyright 2017-2023 the openage authors. See copying.md for legal info.\n\"\"\"\nTests for the filesystem-like abstraction.\n\"\"\"\n\nimport os\n\nfrom io import UnsupportedOperation\nfrom tempfile import gettempdir, NamedTemporaryFile\n\nfrom openage.testing.testing import assert_value, assert_raises, result\n\nfrom .directory import Directory, CaseIgnoringDirectory\nfrom .union import Union\nfrom .wrapper import WriteBlocker, DirectoryCreator\n\n\ndef test_path(root_path, root_dir):\n    \"\"\"\n    Test basic functionality of fslike.Path\n    \"\"\"\n\n    # multi dir creation\n    deeper = root_path / \"let's go\" / \"deeper\"\n    assert_value(deeper.parent, root_path[\"let's go\"])\n    deeper.mkdirs()\n    assert_value(deeper.is_dir(), True)\n    assert_value(deeper.resolve_native_path().decode(),\n                 os.path.join(root_dir, \"let's go\", \"deeper\"))\n\n    insert = deeper[\"insertion.stuff.test\"]\n    insert.touch()\n    assert_value(insert.filesize, 0)\n    assert_value(insert.suffix, \".test\")\n    assert_value(insert.suffixes, [\".stuff\", \".test\"])\n    assert_value(insert.stem, \"insertion.stuff\")\n    assert_value(insert.with_name(\"insertion.stuff.test\").exists(), True)\n    assert_value(insert.with_suffix(\".test\").exists(), True)\n\n    root_path[\"let's go\"].removerecursive()\n\n\ndef test_union(root_path, root_dir):\n    \"\"\"\n    Union functionality testing.\n\n    Procedure:\n    create and write a file in r\n    create union with w and r mount. r is readonly.\n    read file, should be from r.\n    write file, whould go to w.\n    read file, should be from w.\n    unmount w, file content should be from r again.\n    unmount r, union should be empty now.\n    \"\"\"\n\n    test_dir_w = os.path.join(root_dir, \"w\")\n    test_dir_r = os.path.join(root_dir, \"r\")\n\n    # automated directory creation:\n    path_w = DirectoryCreator(\n        Directory(test_dir_w, create_if_missing=True).root\n    ).root\n    path_r = Directory(test_dir_r, create_if_missing=True).root\n\n    assert_value(path_r[\"some_file\"].is_file(), False)\n\n    with path_r[\"some_file\"].open(\"wb\") as fil:\n        fil.write(b\"some data\")\n\n    with path_r[\"some_file\"].open(\"rb\") as fil:\n        assert_value(b\"some data\", fil.read())\n\n    assert_value(path_r.exists(), True)\n    assert_value(path_r.is_dir(), True)\n    assert_value(path_r.is_file(), False)\n    assert_value(path_r[\"some_file\"].is_file(), True)\n    assert_value(path_r.writable(), True)\n\n    # protect the r-path\n    path_protected = WriteBlocker(path_r).root\n    assert_value(path_protected.writable(), False)\n\n    with assert_raises(UnsupportedOperation):\n        result(path_protected.open('wb'))\n\n    # mount the above into one virtual file system\n    target = Union().root\n\n    # first, mount the read-directory read-only\n    target.mount(path_protected)\n\n    # then, mount the writable folder\n    target.mount(path_w)\n\n    # read the data\n    with target[\"some_file\"].open(\"rb\") as fil:\n        test_data = fil.read()\n\n    # overwrite the data:\n    with target[\"some_file\"].open(\"wb\") as fil:\n        fil.write(b\"we changed it\")\n\n    # get back changed data\n    with target[\"some_file\"].open(\"rb\") as fil:\n        changed_test_data = fil.read()\n\n    assert_value(test_data != changed_test_data, True)\n    assert_value(changed_test_data, b\"we changed it\")\n\n    # ther should be nothing else here.\n    assert_value(set(root_path.list()), {b\"r\", b\"w\"})\n\n    # unmount the change-overlay\n    target.unmount(path_w)\n\n    with (target / \"some_file\").open(\"rb\") as fil:\n        unchanged_test_data = fil.read()\n\n    assert_value(test_data, unchanged_test_data)\n\n    # unmount the source-overlay:\n    target.unmount()\n\n    # now the target mount should be completely empty.\n    assert_value(target[\"some_file\"].exists(), False)\n\n    assert_value(list(target.list()), [])\n    assert_value(len(list(target.iterdir())), 0)\n\n\ndef is_filesystem_case_sensitive():\n    \"\"\"\n    Utility function to verify if filesystem is case-sensitive.\n    \"\"\"\n\n    with NamedTemporaryFile() as tmpf:\n        # we now have a file with a \"tmp\" prefixed name\n        # if it exists in upper case also, filesystem is not case-sensitive\n        return not os.path.exists(tmpf.name.upper())\n\n\ndef test_case_ignoring(root_path, root_dir):\n    \"\"\"\n    Test the case ignoring directory,\n    which mimics the windows filename selection behavior.\n    \"\"\"\n\n    # create a file with known name\n    with root_path[\"lemme_in\"].open(\"wb\") as fil:\n        fil.write(b\"pwnt\")\n\n    ignorecase_dir = CaseIgnoringDirectory(root_dir).root\n\n    # open it with wrong-case name\n    with ignorecase_dir[\"LeMmE_In\"].open(\"rb\") as fil:\n        assert_value(fil.readable(), True)\n        assert_value(fil.writable(), False)\n        assert_value(fil.read(), b\"pwnt\")\n\n    # then write it with wrong-case name\n    with ignorecase_dir[\"LeMmE_In\"].open(\"wb\") as fil:\n        assert_value(fil.readable(), False)\n        assert_value(fil.writable(), True)\n        fil.write(b\"yay\")\n\n    # check if changes went to known-name file\n    with root_path[\"lemme_in\"].open(\"rb\") as fil:\n        assert_value(fil.read(), b\"yay\")\n\n    # create new file with CamelCase name\n    ignorecase_dir[\"WeirdCase\"].touch()\n\n    # check if the CamelCase file was actually created as `camelcase`\n    assert_value(root_path[\"weirdcase\"].is_file(), True)\n\n    # touching the same file\n    root_path[\"a\"].touch()\n    ignorecase_dir[\"A\"].touch()\n\n    if is_filesystem_case_sensitive():\n        # 'A' should not exist, 'a' should have been touched.\n        assert_value(root_path[\"A\"].is_file(), False)\n    else:\n        # The underlying fs should treat A as a.\n        assert_value(root_path[\"A\"].is_file(), True)\n\n\ndef test_append(root_path):\n    \"\"\"\n    Test the content append modes.\n    \"\"\"\n\n    # create initial content\n    with root_path[\"appendfile\"].open(\"wb\") as fil:\n        fil.write(b\"just\")\n\n    # append some data\n    with root_path[\"appendfile\"].open(\"ab\") as fil:\n        assert_value(fil.readable(), False)\n        assert_value(fil.writable(), True)\n        fil.write(b\" some\")\n\n    # append some more data and then read it\n    with root_path[\"appendfile\"].open(\"arb\") as fil:\n        assert_value(fil.readable(), True)\n        assert_value(fil.writable(), True)\n        fil.write(b\" test\")\n\n    # use the read-write mode to first read, then write, then read.\n    with root_path[\"appendfile\"].open(\"rwb\") as fil:\n        assert_value(fil.readable(), True)\n        assert_value(fil.writable(), True)\n        assert_value(fil.read(), b\"just some test\")\n        fil.seek(0)\n        fil.write(b\"overwritten\")\n\n        fil.seek(0)\n        assert_value(fil.read(), b\"overwrittenest\")\n\n\ndef test():\n    \"\"\"\n    Perform functionality tests for the filesystem abstraction interface.\n    \"\"\"\n\n    # create a clean test folder in /tmp\n    root_dir = os.path.join(gettempdir(), \"openage_fslike_test\")\n    root_path = Directory(root_dir, create_if_missing=True).root\n    root_path.removerecursive()\n\n    # test basic path functions\n    test_path(root_path, root_dir)\n\n    # test the union\n    test_union(root_path, root_dir)\n\n    # test the case ignoring dir\n    test_case_ignoring(root_path, root_dir)\n\n    # test appending content\n    test_append(root_path)\n\n    # and remove all the things we just created\n    assert_value(root_path.is_dir(), True)\n    root_path.removerecursive()\n    assert_value(root_path.is_dir(), False)\n"
  },
  {
    "path": "openage/util/fslike/union.py",
    "content": "# Copyright 2015-2023 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides Union, a utility class for combining multiple FSLikeObjects to a\nsingle one.\n\"\"\"\n\nfrom io import UnsupportedOperation\n\nfrom .abstract import FSLikeObject\nfrom .path import Path\n\n\nclass Union(FSLikeObject):\n    \"\"\"\n    FSLikeObject that provides a structure for mounting several path objects.\n\n    Unlike in POSIX, mounts may overlap.\n    If multiple mounts match for a directory, those that have a higher\n    priority are preferred.\n    In case of equal priorities, later mounts are preferred.\n    \"\"\"\n\n    # we can hardly reduce the method amount...\n    # pylint: disable=too-many-public-methods\n\n    def __init__(self):\n        super().__init__()\n\n        # (mountpoint, pathobj, priority), sorted by priority.\n        self.mounts = []\n\n        # mountpoints and their parent directories, {name: {...}}.\n        # these are the virtual empty folders where mounts can be done\n        self.dirstructure = {}\n\n    def __str__(self):\n        content = \", \".join([f\"{repr(pnt[1])} @ {repr(pnt[0])}\"\n                             for pnt in self.mounts])\n        return f\"Union({content})\"\n\n    @ property\n    def root(self):\n        return UnionPath(self, [])\n\n    def add_mount(self, pathobj: Path, mountpoint, priority: int) -> None:\n        \"\"\"\n        This method should not be called directly; instead, use the mount\n        method of Path objects that were obtained from this.\n\n        Mounts pathobj at mountpoint, with the given priority.\n        \"\"\"\n\n        if not isinstance(pathobj, Path):\n            raise PermissionError(f\"only a fslike.Path can be mounted, not {type(pathobj)}\")\n\n        # search for the right place to insert the mount.\n        idx = len(self.mounts) - 1\n        while idx >= 0 and priority >= self.mounts[idx][2]:\n            idx -= 1\n\n        self.mounts.insert(idx + 1, (tuple(mountpoint), pathobj, priority))\n\n        # 'create' parent directories as needed.\n        dirstructure = self.dirstructure\n        for subdir in mountpoint:\n            dirstructure = dirstructure.setdefault(subdir, {})\n\n    def remove_mount(self, search_mountpoint, source_pathobj: Path = None) -> None:\n        \"\"\"\n        Remove a mount from the union by searching for the source\n        that provides the given mountpoint.\n        Additionally, can check if the source equals the given pathobj.\n        \"\"\"\n\n        unmount = []\n\n        for idx, (mountpoint, pathobj, _) in enumerate(self.mounts):\n            # cut the search so prefixes can be matched.\n            if mountpoint == tuple(search_mountpoint[:len(mountpoint)]):\n                if not source_pathobj or source_pathobj == pathobj:\n                    unmount.append(idx)\n\n        if unmount:\n            # reverse the order so that the indices never shift.\n            for idx in reversed(sorted(unmount)):\n                del self.mounts[idx]\n\n        else:\n            raise ValueError(\"could not find mounted source\")\n\n    def candidate_paths(self, parts):\n        \"\"\"\n        Helper method.\n\n        Yields path objects from all mounts that match parts, in the order of\n        their priorities.\n        \"\"\"\n\n        for mountpoint, pathobj, _ in self.mounts:\n            cut_parts = tuple(parts[:len(mountpoint)])\n            if mountpoint == cut_parts:\n                yield pathobj.joinpath(parts[len(mountpoint):])\n\n    def open_r(self, parts):\n        for path in self.candidate_paths(parts):\n            if path.is_file():\n                return path.open_r()\n        raise FileNotFoundError(b'/'.join(parts))\n\n    def open_w(self, parts):\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                return path.open_w()\n\n        raise UnsupportedOperation(\n            \"not writable: \" + b'/'.join(parts).decode(errors='replace'))\n\n    def open_a(self, parts):\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                return path.open_a()\n\n        raise UnsupportedOperation(\n            \"not appendable: \" + b'/'.join(parts).decode(errors='replace'))\n\n    def resolve_r(self, parts):\n        for path in self.candidate_paths(parts):\n            if path.is_file() or path.is_dir():\n                # pylint: disable=protected-access\n                return path._resolve_r()\n        return None\n\n    def resolve_w(self, parts):\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                # pylint: disable=protected-access\n                return path._resolve_w()\n        return None\n\n    def list(self, parts):\n        duplicates = set()\n\n        dir_exists = False\n\n        dirstructure = self.dirstructure\n        try:\n            # \"cd\" into the virtual dirstructure\n            for subdir in parts:\n                dirstructure = dirstructure[subdir]\n\n            dir_exists = True\n\n            # yield the virtual folders in this folder\n            yield from dirstructure\n            duplicates.update(dirstructure)\n\n        except KeyError:\n            dir_exists = False\n\n        for path in self.candidate_paths(parts):\n            if path.is_file():\n                raise NotADirectoryError(repr(path))\n            if not path.is_dir():\n                continue\n\n            dir_exists = True\n\n            for name in path.list():\n                if name not in duplicates:\n                    yield name\n                    duplicates.add(name)\n\n        if not dir_exists:\n            raise FileNotFoundError(b'/'.join(parts))\n\n    def filesize(self, parts) -> int:\n        for path in self.candidate_paths(parts):\n            if path.is_file():\n                return path.filesize\n\n        raise FileNotFoundError(b'/'.join(parts))\n\n    def mtime(self, parts) -> float:\n        for path in self.candidate_paths(parts):\n            if path.exists():\n                return path.mtime\n\n        raise FileNotFoundError(b'/'.join(parts))\n\n    def mkdirs(self, parts) -> None:\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                return path.mkdirs()\n        return None\n\n    def rmdir(self, parts) -> None:\n        found = False\n\n        # remove the directory in all mounts where it exists\n        for path in self.candidate_paths(parts):\n            if path.is_dir():\n                path.rmdir()\n                found = True\n\n        if not found:\n            raise FileNotFoundError(b'/'.join(parts))\n\n    def unlink(self, parts) -> None:\n        found = False\n\n        # remove the file in all mounts where it exists\n        for path in self.candidate_paths(parts):\n            if path.is_file():\n                path.unlink()\n                found = True\n\n        if not found:\n            raise FileNotFoundError(b'/'.join(parts))\n\n    def touch(self, parts) -> None:\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                return path.touch()\n\n        raise FileNotFoundError(b'/'.join(parts))\n\n    def rename(self, srcparts, tgtparts) -> None:\n        found = False\n\n        for srcpath in self.candidate_paths(srcparts):\n            if srcpath.exists():\n                found = True\n                if srcpath.writable():\n                    for tgtpath in self.candidate_paths(tgtparts):\n                        if tgtpath.writable():\n                            return srcpath.rename(tgtpath)\n\n        if found:\n            raise UnsupportedOperation(\n                \"read-only rename: \" +\n                b'/'.join(srcparts).decode(errors='replace') + ' to ' +\n                b'/'.join(tgtparts).decode(errors='replace'))\n        raise FileNotFoundError(b'/'.join(srcparts))\n\n    def is_file(self, parts) -> bool:\n        for path in self.candidate_paths(parts):\n            if path.is_file():\n                return True\n\n        return False\n\n    def is_dir(self, parts) -> bool:\n        try:\n            dirstructure = self.dirstructure\n            for part in parts:\n                dirstructure = dirstructure[part]\n            return True\n        except KeyError:\n            pass\n\n        for path in self.candidate_paths(parts):\n            if path.is_dir():\n                return True\n\n        return False\n\n    def writable(self, parts) -> bool:\n        for path in self.candidate_paths(parts):\n            if path.writable():\n                return True\n\n        return False\n\n    def watch(self, parts, callback) -> bool:\n        watching = False\n        for path in self.candidate_paths(parts):\n            if path.exists():\n                watching = watching or path.watch(callback)\n\n        return watching\n\n    def poll_watches(self):\n        for _, pathobj, _ in self.mounts:\n            pathobj.poll_fs_watches()\n\n\nclass UnionPath(Path):\n    \"\"\"\n    Provides an additional method for mounting an other path at this path.\n    \"\"\"\n\n    def mount(self, pathobj: Path, priority: int = 0) -> None:\n        \"\"\"\n        Mounts pathobj here. All parent directories are 'created', if needed.\n        \"\"\"\n        return self.fsobj.add_mount(pathobj, self.parts, priority)\n\n    def unmount(self, pathobj: Path = None) -> None:\n        \"\"\"\n        Unmount a path from the union described by this path.\n        This is like \"unmounting /home\", no matter what the source was.\n        If you provide `pathobj`, that source is checked, additionally.\n\n        It will error if that path was not mounted.\n        \"\"\"\n        self.fsobj.remove_mount(self.parts, pathobj)\n"
  },
  {
    "path": "openage/util/fslike/wrapper.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides\n\n - Wrapper, a utility class for implementing wrappers around FSLikeObject.\n - WriteBlocker, a wrapper that blocks all writing.\n - Synchronizer, which adds thread-safety to a FSLikeObject\n                 by wrapping a threading.Lock.\n - DirectoryCreator, a wrapper that transparently creates nonexisting\n                     directories.\n\"\"\"\n\nimport os\nfrom threading import Lock\n\nfrom ..context import DummyGuard\nfrom ..filelike.abstract import FileLikeObject\n\nfrom .abstract import FSLikeObject, ReadOnlyFSLikeObject\nfrom .path import Path\n\n\nclass Wrapper(FSLikeObject):\n    \"\"\"\n    Wraps a Path, implementing all methods as pass-through.\n\n    Inherit to override individual methods.\n    Pass a context guard to protect calls.\n    \"\"\"\n\n    def __init__(self, obj: Path, contextguard = None):\n        if not isinstance(obj, Path):\n            raise TypeError(f\"Path expected as obj, got '{type(obj)}'\")\n\n        self.obj = obj\n        if contextguard is None:\n            self.contextguard = DummyGuard()\n        else:\n            self.contextguard = contextguard\n\n    def __repr__(self):\n        if isinstance(self.contextguard, DummyGuard):\n            return f\"{type(self).__name__}({repr(self.obj)})\"\n\n        return f\"{type(self).__name__}({repr(self.obj)}, {repr(self.contextguard)})\"\n\n    def open_r(self, parts):\n        with self.contextguard:\n            fileobj = self.obj.joinpath(parts).open_r()\n\n        if isinstance(self.contextguard, DummyGuard):\n            return fileobj\n\n        return GuardedFile(fileobj, self.contextguard)\n\n    def open_w(self, parts):\n        with self.contextguard:\n            fileobj = self.obj.joinpath(parts).open_w()\n\n        if isinstance(self.contextguard, DummyGuard):\n            return fileobj\n\n        return GuardedFile(fileobj, self.contextguard)\n\n    def resolve_r(self, parts):\n        return self.obj.joinpath(parts) if self.exists(parts) else None\n\n    def resolve_w(self, parts):\n        return self.obj.joinpath(parts) if self.writable(parts) else None\n\n    def get_native_path(self, parts):\n        return self.obj.joinpath(parts).resolve_native_path() if self.exists(parts) else None\n\n    def list(self, parts):\n        with self.contextguard:\n            return list(self.obj.joinpath(parts).list())\n\n    def filesize(self, parts) -> int:\n        with self.contextguard:\n            return self.obj.joinpath(parts).filesize\n\n    def mtime(self, parts) -> float:\n        with self.contextguard:\n            return self.obj.joinpath(parts).mtime\n\n    def mkdirs(self, parts) -> None:\n        with self.contextguard:\n            return self.obj.joinpath(parts).mkdirs()\n\n    def rmdir(self, parts) -> None:\n        with self.contextguard:\n            return self.obj.joinpath(parts).rmdir()\n\n    def unlink(self, parts) -> None:\n        with self.contextguard:\n            return self.obj.joinpath(parts).unlink()\n\n    def touch(self, parts) -> None:\n        with self.contextguard:\n            return self.obj.joinpath(parts).touch()\n\n    def rename(self, srcparts, tgtparts) -> None:\n        with self.contextguard:\n            return self.obj.joinpath(srcparts).rename(\n                self.obj.joinpath(tgtparts))\n\n    def is_file(self, parts) -> bool:\n        with self.contextguard:\n            return self.obj.joinpath(parts).is_file()\n\n    def is_dir(self, parts) -> bool:\n        with self.contextguard:\n            return self.obj.joinpath(parts).is_dir()\n\n    def writable(self, parts) -> bool:\n        with self.contextguard:\n            return self.obj.joinpath(parts).writable()\n\n    def watch(self, parts, callback) -> bool:\n        with self.contextguard:\n            return self.obj.joinpath(parts).watch(callback)\n\n    def poll_watches(self):\n        with self.contextguard:\n            return self.obj.poll_watches()\n\n\nclass WriteBlocker(ReadOnlyFSLikeObject, Wrapper):\n    \"\"\"\n    Wraps a FSLikeObject, transparently passing through all read-only calls.\n\n    All writing calls raise IOError, and writable returns False.\n    \"\"\"\n\n    def __repr__(self):\n        return f\"WriteBlocker({repr(self.obj)})\"\n\n\nclass Synchronizer(Wrapper):\n    \"\"\"\n    Wraps a FSLikeObject, securing all wrapped calls with a mutex.\n    \"\"\"\n\n    def __init__(self, obj):\n        self.lock = Lock()\n        super().__init__(obj, self.lock)\n\n    def __repr__(self):\n        # TODO: remove override once pylint is fixed.\n        with self.lock:  # pylint: disable=not-context-manager\n            return f\"Synchronizer({repr(self.obj)})\"\n\n\nclass GuardedFile(FileLikeObject):\n    \"\"\"\n    Wraps file-like objects, protecting calls to their members with the given\n    context guard.\n    \"\"\"\n\n    def __init__(self, obj: FileLikeObject, guard):\n        super().__init__()\n        self.obj = obj\n        self.guard = guard\n\n    def read(self, size: int = -1):\n        with self.guard:\n            return self.obj.read(size)\n\n    def readable(self) -> bool:\n        with self.guard:\n            return self.obj.readable()\n\n    def write(self, data) -> None:\n        with self.guard:\n            return self.obj.write(data)\n\n    def writable(self) -> bool:\n        with self.guard:\n            return self.obj.writable()\n\n    def seek(self, offset: int, whence=os.SEEK_SET) -> None:\n        with self.guard:\n            return self.obj.seek(offset, whence)\n\n    def seekable(self) -> bool:\n        with self.guard:\n            return self.obj.seekable()\n\n    def tell(self):\n        with self.guard:\n            return self.obj.tell()\n\n    def close(self):\n        with self.guard:\n            return self.obj.close()\n\n    def flush(self):\n        with self.guard:\n            return self.obj.flush()\n\n    def get_size(self) -> int:\n        with self.guard:\n            return self.obj.get_size()\n\n    def __repr__(self):\n        with self.guard:\n            return f\"GuardedFile({repr(self.obj)}, {repr(self.guard)})\"\n\n\nclass DirectoryCreator(Wrapper):\n    \"\"\"\n    Wrapper around a filesystem-like object that automatically creates\n    directories when attempting to create a file.\n    \"\"\"\n\n    def open_w(self, parts):\n        self.mkdirs(parts[:-1])\n        return super().open_w(parts)\n\n    def __repr__(self):\n        return f\"DirectoryCreator({self.obj})\"\n"
  },
  {
    "path": "openage/util/fsprinting.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nMethods for printing paths and other file system-related info.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport typing\nfrom collections import OrderedDict\n\nfrom .strings import colorize\nfrom .math import INF\n\nRULE_CACHE = OrderedDict()\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.abstract import FSLikeObject\n\n\ndef get_color_rules() -> OrderedDict[str, str]:\n    \"\"\"\n    Returns a dict of pattern : colorcode, retrieved from LS_COLORS.\n    \"\"\"\n    if RULE_CACHE:\n        return RULE_CACHE\n\n    from os import environ\n\n    try:\n        rules = environ['LS_COLORS']\n    except KeyError:\n        return {}\n\n    for rule in rules.split(':'):\n        rule = rule.strip()\n        if not rule:\n            continue\n\n        try:\n            pattern, colorcode = rule.split('=', maxsplit=1)\n        except ValueError:\n            # Your LS_COLORS are broken. Go fix them.\n            # I shouldn't even be catching the error for you.\n            continue\n\n        RULE_CACHE[pattern] = colorcode\n\n    return RULE_CACHE\n\n\ndef colorize_filename(filename: str) -> str:\n    \"\"\"\n    Colorizes the filename, using the globbing rules from LS_COLORS.\n    \"\"\"\n    from fnmatch import fnmatch\n\n    rules = get_color_rules()\n\n    for pattern, colorcode in rules.items():\n        if fnmatch(filename, pattern):\n            return colorize(filename, colorcode)\n\n    return colorize(filename, rules.get('fi'))\n\n\ndef colorize_dirname(dirname: str) -> str:\n    \"\"\"\n    Colorizes the dirname, using the 'di' rule from LS_COLORS.\n    \"\"\"\n    return colorize(dirname, get_color_rules().get('di'))\n\n\ndef print_tree(\n    obj: FSLikeObject,\n    path: str = \"\",\n    prefix: str = \"\",\n    max_entries: str = INF\n) -> None:\n    \"\"\"\n    Obj is a filesystem-like object; path must be a string.\n\n    Recursively descends into subdirectories using prefix.\n\n    If max_entries is given, only that number of entries per directory\n    is printed.\n    \"\"\"\n    # entries is a list of tuples of entryname, isdir, ismeta\n    entries = []\n    for entry in obj.listdirs(path):\n        entries.append((entry, True, False))\n    for entry in obj.listfiles(path):\n        entries.append((entry, False, False))\n\n    if not entries:\n        entries.append((\"[empty]\", False, True))\n\n    if len(entries) > max_entries:\n        omit = len(entries) - max_entries + 1\n        entries = entries[:-omit] + [(f\"[{omit} omitted]\", False, True)]\n\n    from .iterators import denote_last\n    for (name, isdir, is_meta), is_last in denote_last(entries):\n        if is_last:\n            treesymbol, nextindent = '\\u2514', '   '\n        else:\n            treesymbol, nextindent = '\\u251c', '\\u2502  '\n\n        if is_meta:\n            entryindent = '\\u257c '\n        else:\n            entryindent = '\\u2500 '\n\n        if isdir:\n            print(prefix + treesymbol + entryindent + colorize_dirname(name))\n            obj.print_tree(path + '/' + name, prefix + nextindent, max_entries)\n            print(prefix + nextindent)\n        else:\n            print(prefix + treesymbol + entryindent + colorize_filename(name))\n"
  },
  {
    "path": "openage/util/hash.py",
    "content": "# Copyright 2021-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nFunctions for hashing files.\n\"\"\"\nfrom __future__ import annotations\n\nimport typing\nimport hashlib\n\nif typing.TYPE_CHECKING:\n    from openage.util.fslike.path import Path\n\n\ndef hash_file(\n    path: Path,\n    hash_algo: str = \"sha3_256\",\n    bufsize: int = 32768\n) -> str:\n    \"\"\"\n    Get the hash value of a given file.\n\n    :param path: Path of the file.\n    :type path: .fslike.path.Path\n    :param hash_algo: Hashing algorithm identifier.\n    :type hash_algo: str\n    :param bufsize: Buffer size for reading files.\n    :type bufsize: int\n    \"\"\"\n    hashfunc = hashlib.new(hash_algo)\n\n    with path.open_r() as f_in:\n        while True:\n            data = f_in.read(bufsize)\n            if not data:\n                break\n            hashfunc.update(data)\n\n    return hashfunc.hexdigest()\n"
  },
  {
    "path": "openage/util/iterators.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides all sorts of iterator-related stuff.\n\"\"\"\n\n\nfrom typing import Iterable\n\n\ndef denote_last(iterable: Iterable):\n    \"\"\"\n    Similar to enumerate, this iterates over an iterable, and yields\n    tuples of item, is_last.\n    \"\"\"\n    # pylint: disable=stop-iteration-return\n    iterator = iter(iterable)\n    current = next(iterator)\n\n    for future in iterator:\n        yield current, False\n        current = future\n\n    yield current, True\n"
  },
  {
    "path": "openage/util/math.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nHolds some math constants and helpers\n\"\"\"\n\nimport math\n\n# I tend to use this all the time. I don't get why it's not in math.\nINF = float(\"+inf\")\n\n# TAU for life!\nTAU = 2 * math.pi\nDEGSPERRAD = TAU / 360\n\n\ndef clamp(val: int, minval: int, maxval: int) -> int:\n    \"\"\"\n    clamps val to be at least minval, and at most maxval.\n\n    >>> clamp(9, 3, 7)\n    7\n    >>> clamp(1, 3, 7)\n    3\n    >>> clamp(5, 3, 7)\n    5\n    \"\"\"\n    return min(maxval, max(minval, val))\n"
  },
  {
    "path": "openage/util/observer.py",
    "content": "# Copyright 2020-2023 the openage authors. See copying.md for legal info.\n#\n# pylint: disable=too-few-public-methods\n\n\"\"\"\nImplements the Observer design pattern. Observers can be\nnotified when an object they observe (so-called Observable)\nchanges.\n\nThe implementation is modelled after the Java 8 specification\nof Observable and Observer.\n\nObserver references are weakrefs to prevent objects from being\nignored the garbage collection. Weakrefs with dead references\nare removed during notification of the observers.\n\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Any, Optional\nimport weakref\n\n\nclass Observer:\n    \"\"\"\n    Implements a Java 8-like Observer interface.\n    \"\"\"\n\n    def update(self, observable: Observable, message: Optional[Any] = None):\n        \"\"\"\n        Called by an Observable object that has registered this observer\n        whenever it changes.\n\n        :param observable: The obvervable object which was updated.\n        :type observable: Observable\n        :param message: An optional message of any type.\n        \"\"\"\n        raise NotImplementedError(f\"{self} has not implemented update()\")\n\n\nclass Observable:\n    \"\"\"\n    Implements a Java 8-like Observable object.\n    \"\"\"\n\n    def __init__(self):\n\n        self.observers: weakref.WeakSet[Observer] = weakref.WeakSet()\n        self.changed = False\n\n    def add_observer(self, observer: Observer) -> None:\n        \"\"\"\n        Adds an observer to this object's set of observers.\n\n        :param observer: An observer observing this object.\n        :type observer: Observer\n        \"\"\"\n        self.observers.add(observer)\n\n    def clear_changed(self) -> None:\n        \"\"\"\n        Indicate that this object has no longer changed.\n        \"\"\"\n        self.changed = False\n\n    def delete_observer(self, observer: Observer) -> None:\n        \"\"\"\n        Remove an observer from the set.\n\n        :param observer: An observer observing this object.\n        :type observer: Observer\n        \"\"\"\n        self.observers.remove(observer)\n\n    def delete_observers(self) -> None:\n        \"\"\"\n        Remove all currently registered observers.\n        \"\"\"\n        self.observers.clear()\n\n    def get_observer_count(self) -> int:\n        \"\"\"\n        Return the number of registered observers.\n        \"\"\"\n        return len(self.observers)\n\n    def has_changed(self) -> bool:\n        \"\"\"\n        Return whether the object has changed.\n        \"\"\"\n        return self.changed\n\n    def notify_observers(self, message: Optional[Any] = None) -> None:\n        \"\"\"\n        Notify the observers if the object has changed. Include\n        an optional message.\n\n        :param message: An optional message of any type.\n        \"\"\"\n        if self.changed:\n            for observer in self.observers:\n                observer.update(self, message=message)\n\n    def set_changed(self) -> None:\n        \"\"\"\n        Indicate that the object has changed.\n        \"\"\"\n        self.changed = True\n"
  },
  {
    "path": "openage/util/ordered_set.py",
    "content": "# Copyright 2019-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides a very simple implementation of an ordered set. We use the\nPython dictionaries as a basis because they are guaranteed to\nbe ordered since Python 3.6.\n\"\"\"\n\n\nfrom typing import Generic, Hashable, TypeVar\n\nOrderedSetItem = TypeVar(\"OrderedSetItem\")\n\n\nclass OrderedSet(Generic[OrderedSetItem]):\n    \"\"\"\n    Set that saves the input order of elements.\n    \"\"\"\n\n    __slots__ = ('ordered_set',)\n\n    def __init__(self, elements: Hashable = None):\n        self.ordered_set = {}\n\n        if elements:\n            self.update(elements)\n\n    def add(self, elem: Hashable) -> None:\n        \"\"\"\n        Set-like add that calls append_right().\n        \"\"\"\n        self.append_right(elem)\n\n    def append_left(self, elem: Hashable) -> None:\n        \"\"\"\n        Add an element to the front of the set.\n        \"\"\"\n        if elem not in self.ordered_set:\n            temp_set = {elem: 0}\n\n            # Update indices\n            for key in self.ordered_set:\n                self.ordered_set[key] += 1\n\n            temp_set.update(self.ordered_set)\n            self.ordered_set = temp_set\n\n    def append_right(self, elem: Hashable) -> None:\n        \"\"\"\n        Add an element to the back of the set.\n        \"\"\"\n        if elem not in self.ordered_set:\n            self.ordered_set[elem] = len(self)\n\n    def discard(self, elem: Hashable) -> None:\n        \"\"\"\n        Remove an element from the set.\n        \"\"\"\n        index = self.ordered_set.pop(elem, -1)\n\n        if index > -1:\n            # Update indices\n            for key, value in self.ordered_set.items():\n                if value > index:\n                    self.ordered_set[key] -= 1\n\n    def get_list(self) -> list:\n        \"\"\"\n        Returns a normal list containing the values from the ordered set.\n        \"\"\"\n        return list(self.ordered_set.keys())\n\n    def index(self, elem: Hashable) -> int:\n        \"\"\"\n        Returns the index of the element in the set or\n        -1 if it is not in the set.\n        \"\"\"\n        if elem in self.ordered_set:\n            return self.ordered_set[elem]\n\n        return -1\n\n    def intersection_update(self, other):\n        \"\"\"\n        Only keep elements that are both in self and other.\n        \"\"\"\n        keys_self = set(self.ordered_set.keys())\n        keys_other = set(other.keys())\n        intersection = keys_self & keys_other\n\n        for elem in self:\n            if elem not in intersection:\n                self.discard(elem)\n\n    def union(self, other):\n        \"\"\"\n        Returns a new ordered set with the elements from self and other.\n        \"\"\"\n        element_list = self.get_list() + other.get_list()\n        return OrderedSet(element_list)\n\n    def update(self, other) -> None:\n        \"\"\"\n        Append the elements of another iterable to the right of the\n        ordered set.\n        \"\"\"\n        for elem in other:\n            self.append_right(elem)\n\n    def __contains__(self, elem):\n        return elem in self.ordered_set\n\n    def __iter__(self):\n        return iter(self.ordered_set.keys())\n\n    def __len__(self):\n        return len(self.ordered_set)\n\n    def __reversed__(self):\n        return reversed(self.ordered_set.keys())\n\n    def __str__(self):\n        return f'OrderedSet({list(self.ordered_set.keys())})'\n\n    def __repr__(self):\n        return str(self)\n"
  },
  {
    "path": "openage/util/profiler.py",
    "content": "# Copyright 2017-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProfiling utilities\n\"\"\"\n\nimport cProfile\nimport io\nimport pstats\nimport tracemalloc\n\n\nclass Profiler:\n    \"\"\"\n    A class for quick and easy profiling.\n    Usage:\n        p = Profiler()\n        with p:\n            # call methods that need to be profiled here\n        print(p.report())\n\n    The 'with' statement can be replaced with calls to\n    p.enable() and p.disable().\n    \"\"\"\n\n    profile: cProfile.Profile = None\n    profile_stats: pstats.Stats = None\n    profile_stream = None\n\n    def __init__(self, o_stream=None):\n        # o_stream can be a file if the profile results want to be saved.\n        self.profile = cProfile.Profile()\n        self.profile_stream = o_stream\n\n    def __enter__(self):\n        \"\"\"\n        Activate data collection.\n        \"\"\"\n        self.enable()\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        \"\"\"\n        Stop profiling.\n        \"\"\"\n        self.disable()\n\n    def write_report(self, sortby: str = 'calls') -> None:\n        \"\"\"\n        Write the profile stats to profile_stream's file.\n        \"\"\"\n        self.profile_stats = pstats.Stats(self.profile, stream=self.profile_stream)\n        self.profile_stats.sort_stats(sortby)\n        self.profile_stats.print_stats()\n\n    def report(self, sortby: str = 'calls'):\n        \"\"\"\n        Return the profile_stats to the console.\n        \"\"\"\n        self.profile_stats = pstats.Stats(self.profile, stream=io.StringIO())\n        self.profile_stats.sort_stats(sortby)\n        self.profile_stats.print_stats()\n        return self.profile_stats.stream.getvalue()\n\n    def enable(self):\n        \"\"\"\n        Begins profiling calls.\n        \"\"\"\n        self.profile.enable()\n\n    def disable(self):\n        \"\"\"\n        Stop profiling calls.\n        \"\"\"\n        self.profile.disable()\n\n\nclass Tracemalloc:\n    \"\"\"\n    A class for memory profiling.\n    Usage:\n        p = Tracemalloc()\n        with p:\n            # call methods that need to be profiled here\n        print(p.report())\n\n    The 'with' statement can be replaced with calls to\n    p.enable() and p.disable().\n    \"\"\"\n\n    snapshot0 = None\n    snapshot1 = None\n    peak = None\n\n    def __init__(self, o_stream=None):\n        # o_stream can be a file if the profile results want to be saved.\n        self.tracemalloc_stream = o_stream\n\n    def __enter__(self):\n        \"\"\"\n        Activate data collection.\n        \"\"\"\n        self.enable()\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        \"\"\"\n        Stop profiling.\n        \"\"\"\n        self.disable()\n\n    def snapshot(self):\n        \"\"\"\n        Take a manual snapshot. Up to two snapshots can be saved.\n        report() compares the last two snapshots.\n        \"\"\"\n        if self.snapshot0 is None:\n            self.snapshot0 = tracemalloc.take_snapshot()\n\n        elif self.snapshot1 is None:\n            self.snapshot1 = tracemalloc.take_snapshot()\n\n        else:\n            # Push back\n            self.snapshot0 = self.snapshot1\n            self.snapshot1 = tracemalloc.take_snapshot()\n\n    def report(\n        self,\n        sortby: str = 'lineno',\n        cumulative: bool = False,\n        limit: int = 100\n    ) -> None:\n        \"\"\"\n        Return the snapshot statistics to the console.\n        \"\"\"\n        if self.snapshot1:\n            stats = self.snapshot1.compare_to(self.snapshot0, sortby, cumulative)[:limit]\n\n        else:\n            stats = self.snapshot0.statistics(sortby, cumulative)[:limit]\n\n        for stat in stats:\n            print(stat)\n\n    def get_peak(self) -> int:\n        \"\"\"\n        Return the peak memory consumption.\n        \"\"\"\n        if not self.peak:\n            return tracemalloc.get_traced_memory()[1]\n\n        return self.peak\n\n    @staticmethod\n    def enable() -> None:\n        \"\"\"\n        Begins profiling calls.\n        \"\"\"\n        tracemalloc.start()\n\n    def disable(self) -> None:\n        \"\"\"\n        Stop profiling calls.\n        \"\"\"\n        if self.snapshot0 is None:\n            self.snapshot()\n\n        self.peak = tracemalloc.get_traced_memory()[1]\n\n        tracemalloc.stop()\n"
  },
  {
    "path": "openage/util/strings.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\"\"\"\nMisc string helper functions; this includes encoding, decoding,\nmanipulation, ...\n\"\"\"\n\nfrom sys import stdout\n\n\ndef decode_until_null(data: bytes, encoding: str = 'utf-8') -> str:\n    \"\"\"\n    decodes a bytes object, aborting at the first \\\\0 character.\n\n    >>> decode_until_null(b\"foo\\\\0bar\")\n    'foo'\n    \"\"\"\n    end = data.find(0)\n    if end != -1:\n        data = data[:end]\n\n    return data.decode(encoding)\n\n\ndef try_decode(data: bytes) -> str:\n    \"\"\"\n    does its best to attempt decoding the given string of unknown encoding.\n    \"\"\"\n    try:\n        return data.decode('utf-8')\n    except UnicodeDecodeError:\n        pass\n\n    return data.decode('iso-8859-1')\n\n\ndef binstr(num: int, bits: int = None, group: int = 8) -> str:\n    \"\"\"\n    Similar to the built-in bin(), but optionally takes\n    the number of bits as an argument, and prints underscores instead of\n    zeroes.\n\n    >>> binstr(1337, 16)\n    '_____1_1 __111__1'\n    \"\"\"\n    result = bin(num)[2:]\n\n    if bits is not None:\n        result = result.rjust(bits, '0')\n\n    result = result.replace('0', '_')\n\n    if group is not None:\n        grouped = [result[i:i + group] for i in range(0, len(result), group)]\n        result = ' '.join(grouped)\n\n    return result\n\n\ndef colorize(string: str, colorcode: str) -> str:\n    \"\"\"\n    Colorizes string with the given EMCA-48 SGR code.\n\n    >>> colorize('foo', '31;1')\n    '\\\\x1b[31;1mfoo\\\\x1b[m'\n    \"\"\"\n    if colorcode:\n        colorized = f'\\x1b[{colorcode}m{string}\\x1b[m'\n    else:\n        colorized = string\n\n    return colorized\n\n\ndef lstrip_once(string: str, substr: str) -> str:\n    \"\"\"\n    Removes substr at the start of string, and raises ValueError on failure.\n\n    >>> lstrip_once(\"openage.test\", \"openage.\")\n    'test'\n    >>> lstrip_once(\"libopenage.test\", \"openage.\")\n    Traceback (most recent call last):\n    ValueError: 'libopenage.test' doesn't start with 'openage.'\n    \"\"\"\n    if not string.startswith(substr):\n        raise ValueError(f\"{repr(string)} doesn't start with {repr(substr)}\")\n\n    return string[len(substr):]\n\n\ndef rstrip_once(string: str, substr: str) -> str:\n    \"\"\"\n    Removes substr at the end of string, and raises ValueError on failure.\n\n    >>> rstrip_once(\"test.cpp\", \".cpp\")\n    'test'\n    \"\"\"\n    if not string.endswith(substr):\n        raise ValueError(\n            f\"{repr(string)} doesn't end with {repr(substr)}\")\n\n    return string[:-len(substr)]\n\n\ndef format_progress(progress: int, total: int) -> str:\n    \"\"\"\n    Formats an \"x out of y\" string with fixed width.\n\n    >>> format_progress(5, 20)\n    ' 5/20'\n    \"\"\"\n    return f\"{progress:>{len(str(total))}}/{total}\"\n\n\ndef print_progress(progress: int, total: int) -> str:\n    \"\"\"\n    Print an \"x out of y\" string with fixed width to stdout.\n    The output overwrites itself.\n    \"\"\"\n    stdout.write(format_progress(progress, total) + \"\\r\")\n"
  },
  {
    "path": "openage/util/struct.py",
    "content": "# Copyright 2015-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nProvides some classes designed to expand the functionality of struct.struct\n\"\"\"\n\n\nfrom collections import OrderedDict\nfrom struct import Struct\n\nfrom ..util.files import read_guaranteed\n\n\nclass NamedStructMeta(type):\n    \"\"\"\n    Metaclass for NamedStruct.\n\n    Not unlike the meta-class for Enum, processes all the member attributes\n    at class-creation time.\n    \"\"\"\n    @classmethod\n    def __prepare__(mcs, name, bases, **kwds):\n        del mcs, name, bases, kwds  # unused variables\n\n        return OrderedDict()\n\n    def __new__(mcs, name, bases, classdict, **kwds):\n        del kwds  # unused variable\n\n        specstr = None\n        attributes = []\n        postprocessors = {}\n\n        for membername, value in classdict.items():\n            # ignore hidden and None members\n            if membername.startswith('_') or value is None:\n                continue\n\n            valuehasspecstr = hasattr(value, \"specstr\")\n\n            # ignore member methods\n            if not valuehasspecstr:\n                if callable(value) or isinstance(value, classmethod):\n                    continue\n\n            if membername == 'endianness':\n                if specstr is not None:\n                    raise SyntaxError(\"endianness has been given multiple times\")\n\n                if value not in \"@=<>!\":\n                    raise SyntaxError(\"endianness: expected one of @=<>!\")\n\n                specstr = value\n                continue\n\n            if specstr is None:\n                raise SyntaxError(\"NamedStruct: endianness expected before \"\n                                  \"attribute \" + membername)\n\n            if valuehasspecstr:\n                postprocessors[membername], value = value, value.specstr\n            elif isinstance(value, str):\n                pass\n            else:\n                raise TypeError(\n                    f\"NamedStruct member {membername}: expected str, but got {repr(value)}\"\n                )\n\n            specstr += value\n\n            attributes.append(membername)\n\n        classdict[\"_attributes\"] = attributes\n        classdict[\"_postprocessors\"] = postprocessors\n        if specstr:\n            classdict[\"_struct\"] = Struct(specstr)\n\n        return type.__new__(mcs, name, bases, dict(classdict))\n\n\nclass NamedStruct(metaclass=NamedStructMeta):\n    \"\"\"\n    Designed to be inherited from, similar to Enum.\n\n    Specify all fields of the struct, as 'membername = specstr',\n    where specstr is a string describing the field, as in struct.Struct.\n    NamedStructMeta translates those individual specstr fragments to\n    a complete specstr.\n\n    Alternatively to a specstr, a callable object with a specstr member\n    may be passed; the specstr is used as usual, but afterwards the\n    callable is invoked to post-process the extracted data.\n    One example for such a callable is the Flags class.\n\n    Alternatively, attributes may be set to None; those are ignored,\n    and may be set manually at some later point.\n\n    The first member must be 'endianness'.\n\n    Example:\n\n    class MyStruct(NamedStruct):\n        endianness = \"<\"\n\n        mgck = \"4s\"\n        test = \"I\"\n        rofl = \"H\"\n        flag = MyFlagType\n\n    The constructor takes a bytes object of the appropriate length, and fills\n    in all the members with the struct's actual values.\n    \"\"\"\n\n    # those values are set by the metaclass.\n    _postprocessors = None\n    _struct = None\n    _attributes = None\n\n    def __init__(self, data):\n        if not self._struct:\n            raise NotImplementedError(\n                \"Abstract NamedStruct can not be instantiated\")\n\n        values = self._struct.unpack(data)\n\n        if len(self._attributes) != len(values):\n            raise SyntaxError(\"number of attributes differs from number of \"\n                              \"struct fields\")\n\n        for name, value in zip(self._attributes, values):\n            # pylint: disable=unsupported-membership-test\n            if name in self._postprocessors:\n                # pylint: disable=unsubscriptable-object\n                value = self._postprocessors[name](value)\n\n            setattr(self, name, value)\n\n    @classmethod\n    def unpack(cls, data):\n        \"\"\"\n        Unpacks data and returns a NamedStruct object that holds the fields.\n        \"\"\"\n        return cls(data)\n\n    @classmethod\n    def size(cls):\n        \"\"\"\n        Returns the size of the struct, in bytes.\n        \"\"\"\n        return cls._struct.size\n\n    @classmethod\n    def read(cls, fileobj):\n        \"\"\"\n        Reads the appropriate amount of data from fileobj, and unpacks it.\n        \"\"\"\n        data = read_guaranteed(fileobj, cls._struct.size)\n        return cls.unpack(data)\n\n    @classmethod\n    def from_nullbytes(cls):\n        \"\"\"\n        Decodes nullbytes (sort of a 'default' value).\n        \"\"\"\n        data = b\"\\x00\" * cls._struct.size\n        return cls.unpack(data)\n\n    # nobody has needed .pack() and .write() functions this far; implement\n    # them if you need them.\n\n    def __len__(self):\n        \"\"\"\n        Returns the number of fields.\n        \"\"\"\n        return len(self._attributes)\n\n    def __getitem__(self, index):\n        \"\"\"\n        Returns the n-th field, or raises IndexError.\n        \"\"\"\n        # pylint: disable=unsubscriptable-object\n        return getattr(self, self._attributes[index])\n\n    def as_dict(self):\n        \"\"\"\n        Returns a key-value dict for all attributes.\n        \"\"\"\n        # pylint: disable=not-an-iterable\n        return {attr: getattr(self, attr) for attr in self._attributes}\n\n    def __iter__(self):\n        return iter(self)\n\n    def __repr__(self):\n        return str(type(self)) + \": \" + repr(self.as_dict())\n\n    def __str__(self):\n        return type(self).__name__ + \":\\n\\t\" + \"\\n\\t\".join(\n            str(key).ljust(20) + \" = \" + str(value)\n            for key, value in sorted(self.as_dict().items())\n        )\n\n\nclass FlagsMeta(type):\n    \"\"\"\n    Metaclass for Flags. Compare to NamedStructMeta.\n    \"\"\"\n    def __new__(mcs, name, bases, classdict, **kwds):\n        del kwds  # unused variable\n\n        # we don't need to know the order of the flags, so we don't need\n        # to do the whole 'OrderedDict' dance.\n\n        # stores a mapping of flag value <-> flag name\n        flags = {}\n        specstr_found = False\n\n        for membername, value in classdict.items():\n            if membername.startswith('_'):\n                continue\n\n            if membername == \"specstr\":\n                specstr_found = True\n\n                if not isinstance(value, str):\n                    raise TypeError(\n                        \"expected str as value for specstr, \"\n                        \"but got \" + repr(value))\n\n                continue\n\n            if callable(value) or isinstance(value, classmethod):\n                continue\n\n            if not isinstance(value, int):\n                raise TypeError(\n                    \"expected int as value for flag \" + membername + \", \"\n                    \"but got \" + repr(value))\n\n            flagvalue = 1 << value\n            flags[flagvalue] = membername\n\n        if flags and not specstr_found:\n            raise SyntaxError(\"expected a 'specstr' attribute\")\n\n        classdict[\"_flags\"] = flags\n\n        return type.__new__(mcs, name, bases, classdict)\n\n\nclass Flags(metaclass=FlagsMeta):\n    \"\"\"\n    Designed to be inherited from, similar to Enum.\n\n    Used to generate flag parsers (for boolean flags that\n    are stored in an integer value).\n\n    Specify the bit numbers of all possible flags as attributes,\n    e.g.:\n\n    class MyFlags(Flags):\n        thisflag = 0\n        thatflag = 1\n\n    The constructor of the class takes an integer argument,\n    which is parsed; all the boolean values are stored in the\n    attributes.\n    If any unknown bits are set, self.unknown() is called.\n    \"\"\"\n\n    # set by the metaclass\n    _flags = None\n\n    def __init__(self, val):\n        for flagvalue, flagname in self._flags.items():\n            if val & flagvalue:\n                setattr(self, flagname, True)\n                val &= ~flagvalue\n            else:\n                setattr(self, flagname, False)\n\n        if val:\n            self.unknown(val)\n\n    def unknown(self, unknownflags):\n        \"\"\"\n        Default handler for any unknown bits. Overload if needed.\n        \"\"\"\n        raise ValueError(\n            \"unknown flag values: \" + bin(unknownflags) + \" \"\n            \"in addition to existing flags: \" + str(self.as_dict()))\n\n    def as_dict(self):\n        \"\"\"\n        Returns a key-value dict for all flags.\n        \"\"\"\n        return {flagname: getattr(self, flagname)\n                for flagname in self._flags.values()}\n\n    def __repr__(self):\n        return repr(type(self)) + \": \" + repr(self.as_dict())\n"
  },
  {
    "path": "openage/util/system.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nVarious OS utilities\n\"\"\"\n\nimport re\nfrom sys import platform\n\nfrom .math import INF\n\n\ndef free_memory() -> int:\n    \"\"\"\n    Returns the amount of free bytes of memory.\n    On failure, returns +inf.\n\n    >>> free_memory() > 0\n    True\n    \"\"\"\n    memory = INF\n\n    if platform.startswith('linux'):\n        pattern = re.compile('^MemAvailable: +([0-9]+) kB\\n$')\n        with open('/proc/meminfo', encoding='utf8') as meminfo:\n            for line in meminfo:\n                match = pattern.match(line)\n                if match:\n                    memory = 1024 * int(match.group(1))\n                    break\n    elif platform == \"darwin\":\n        # TODO\n        pass\n    elif platform == \"win32\":\n        # TODO\n        pass\n\n    return memory\n"
  },
  {
    "path": "openage/util/threading.py",
    "content": "# Copyright 2015-2022 the openage authors. See copying.md for legal info.\n\n\"\"\"\nThreading utilities.\n\"\"\"\n\nfrom concurrent.futures import ThreadPoolExecutor\nfrom enum import Enum\nimport itertools\nimport os\nfrom queue import Queue\n\n\ndef concurrent_chain(generators, jobs=None):\n    \"\"\"\n    Similar to itertools.chain(), but runs the individual generators in a\n    thread pool. The resulting items may be out of order accordingly.\n\n    When one generator raises an exception, all other currently-running\n    generators are stopped (they may run until their next 'yield' statement).\n    The exception is then raised.\n    \"\"\"\n    if jobs is None:\n        jobs = os.cpu_count()\n\n    if jobs == 1:\n        # we don't need to do all that threading stuff;\n        # let's just behave _precisely_ like itertools.chain.\n        for generator in generators:\n            yield from generator\n        return\n\n    queue = ClosableQueue()\n    running_generator_count = 0\n\n    with ThreadPoolExecutor(jobs) as pool:\n        for generator in generators:\n            pool.submit(generator_to_queue, generator, queue)\n            running_generator_count += 1\n\n        while running_generator_count > 0:\n            event_type, value = queue.get()\n\n            if event_type == GeneratorEvent.VALUE:\n                yield value\n            elif event_type == GeneratorEvent.EXCEPTION:\n                queue.close(\"Exception in different generator\")\n                raise value\n            elif event_type == GeneratorEvent.STOP_ITERATION:\n                running_generator_count -= 1\n\n\nclass ClosableQueue(Queue):\n    \"\"\"\n    For use in concurrent_chain.\n\n    Behaves like Queue until close() has been called.\n    After that, any call to put() raises RuntimeError.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.closed = False\n        self.close_reason = None\n\n    def put(self, item, block=True, timeout=None):\n        self.raise_if_closed()\n        super().put(item, block, timeout)\n\n    def close(self, reason=None):\n        \"\"\"\n        Any subsequent calls to put() or raise_if_closed()\n        will raise RuntimeError(reason).\n        \"\"\"\n        with self.mutex:  # pylint: disable=not-context-manager\n            self.closed = True\n            self.close_reason = reason\n\n    def raise_if_closed(self):\n        \"\"\"\n        Raises RuntimeError(reason) if the queue has been closed.\n        Returns None elsewise.\n        \"\"\"\n        with self.mutex:  # pylint: disable=not-context-manager\n            if self.closed:\n                raise RuntimeError(self.close_reason)\n\n\nclass GeneratorEvent(Enum):\n    \"\"\"\n    For use by concurrent_chain.\n    Represents any event that a generator may cause.\n    \"\"\"\n    VALUE = 0\n    EXCEPTION = 1\n    STOP_ITERATION = 2\n\n\ndef generator_to_queue(generator, queue: ClosableQueue) -> None:\n    \"\"\"\n    For use by concurrent_chain.\n    Appends all of the generator's events to the queue,\n    as tuples of (event type, value).\n    \"\"\"\n    try:\n        queue.raise_if_closed()\n        for item in generator:\n            queue.put((GeneratorEvent.VALUE, item))\n\n        queue.put((GeneratorEvent.STOP_ITERATION, None))\n    except BaseException as exc:\n        queue.put((GeneratorEvent.EXCEPTION, exc))\n\n\ndef test_concurrent_chain() -> None:\n    \"\"\" Tests concurrent_chain \"\"\"\n    from ..testing.testing import assert_value, assert_raises, result\n\n    def errorgen():\n        \"\"\" Test generator that raises an exception \"\"\"\n        yield \"errorgen\"\n        raise ValueError()\n\n    assert_value(list(concurrent_chain([], 2)), [])\n\n    assert_value(list(concurrent_chain([range(10)], 2)), list(range(10)))\n\n    assert_value(\n        sorted(list(concurrent_chain([range(10), range(20)], 2))),\n        sorted(list(itertools.chain(range(10), range(20))))\n    )\n\n    chain = concurrent_chain([range(10), range(20), errorgen(), range(30)], 2)\n    with assert_raises(ValueError):\n        result(list(chain))\n"
  },
  {
    "path": "openage/util/version.py",
    "content": "# Copyright 2024-2024 the openage authors. See copying.md for legal info.\n\n\"\"\"\nHandling of version information for openage.\n\"\"\"\nfrom __future__ import annotations\n\nimport re\n\nSEMVER_REGEX = re.compile(\n    (r\"^(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)\"\n     r\"(?:-(?P<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)\"\n     r\"(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?\"\n     r\"(?:\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"))\n\n\nclass SemanticVersion:\n    \"\"\"\n    Semantic versioning information.\n    \"\"\"\n\n    def __init__(self, version: str) -> None:\n        \"\"\"\n        Create a new semantic version object from a version string.\n\n        :param version: The version string to parse.\n        \"\"\"\n        match = SEMVER_REGEX.match(version)\n        if not match:\n            raise ValueError(f\"Invalid semantic version: {version}\")\n\n        self.major = int(match.group(\"major\"))\n        self.minor = int(match.group(\"minor\"))\n        self.patch = int(match.group(\"patch\"))\n        self.prerelease = match.group(\"prerelease\")\n        self.buildmetadata = match.group(\"buildmetadata\")\n\n    def __lt__(self, other: SemanticVersion) -> bool:\n        if self.major < other.major:\n            return True\n        if self.minor < other.minor:\n            return True\n        if self.patch < other.patch:\n            return True\n\n        return False\n\n    def __le__(self, other: SemanticVersion) -> bool:\n        if self.major <= other.major:\n            return True\n\n        if self.minor <= other.minor:\n            return True\n\n        if self.patch <= other.patch:\n            return True\n\n        return False\n\n    def __eq__(self, other: SemanticVersion) -> bool:\n        return (self.major == other.major and\n                self.minor == other.minor and\n                self.patch == other.patch)\n\n    def __ne__(self, other: SemanticVersion) -> bool:\n        return not self.__eq__(other)\n\n    def __gt__(self, other: SemanticVersion) -> bool:\n        return not self.__le__(other)\n\n    def __ge__(self, other: SemanticVersion) -> bool:\n        return not self.__lt__(other)\n\n    def __str__(self) -> str:\n        version = f\"{self.major}.{self.minor}.{self.patch}\"\n        if self.prerelease:\n            version += f\"-{self.prerelease}\"\n        if self.buildmetadata:\n            version += f\"+{self.buildmetadata}\"\n\n        return version\n\n    def __repr__(self) -> str:\n        return f\"SemanticVersion('{str(self)}')\"\n"
  },
  {
    "path": "openage/versions/CMakeLists.txt",
    "content": "add_cython_modules(\n\tversions.pyx\n)\n\nadd_py_modules(\n    __init__.py\n)\n"
  },
  {
    "path": "openage/versions/__init__.py",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\n\"\"\"\nHandles version numbers.\n\"\"\"\n"
  },
  {
    "path": "openage/versions/versions.pyx",
    "content": "# Copyright 2020-2020 the openage authors. See copying.md for legal info.\n\nfrom libcpp.map cimport map as cpp_map\nfrom cython.operator import dereference, postincrement\nfrom libcpp.string cimport string\n\nfrom libopenage.versions.versions cimport get_version_numbers as c_get_version_numbers\n\ndef get_version_numbers():\n\n    vn = c_get_version_numbers()\n    toreturn = {}\n\n    cdef cpp_map[string,string].iterator it = vn.begin()\n\n    while(it != vn.end()):\n        toreturn[dereference(it).first] = dereference(it).second\n        postincrement(it) # Increment the iterator to the net element\n    return toreturn\n"
  },
  {
    "path": "openage_version",
    "content": "0.6.0\n"
  },
  {
    "path": "packaging/CMakeLists.txt",
    "content": "# Copyright 2017-2019 the openage authors. See copying.md for legal info.\n\n# Keeps all required CPack configuration in one place and include(CPack)\n\n\n########################################################################\n# Basic information\n\nset(CPACK_PACKAGE_VENDOR \"The openage authors\")\nset(CPACK_PACKAGE_CONTACT \"openage-Maintainers https://github.com/SFTtech/openage#contact\")\nset(CPACK_PACKAGE_DESCRIPTION \"Free (as in freedom) open source clone of the Age of Empires II engine 🚀\")\nset(CPACK_PACKAGE_DESCRIPTION_SUMMARY \"A FLOSS RTS-engine in tribute to Genie 🚀\")\nset(CPACK_PACKAGE_HOMEPAGE_URL \"https://openage.dev/\")\n\n\n########################################################################\n# CPackOptions for Generators + Version details\nset(CPACK_VERSION_FULL_STRING \"${VERSION_FULL_STRING}\")\nset(CPACK_PROJECT_CONFIG_FILE \"${CMAKE_SOURCE_DIR}/packaging/CPackOptions.cmake\")\n\n\n########################################################################\n# Environment\n\nif(MSVC)\n\t# HACKHACK: Update the following if vcpkg breaks compatibility for\n\t# internal directories, scripts, or variables.\n\tset(vcpkg_dir \"${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}\")\n\tfind_program(windeployqt windeployqt.exe)\n\n\tconfigure_file(\n\t\t\"${BUILDSYSTEM_DIR}/templates/ForwardVariables.cmake.in\"\n\t\t\"${CMAKE_CURRENT_BINARY_DIR}/ForwardVariables.cmake\"\n\t)\n\tinstall(SCRIPT \"${CMAKE_CURRENT_BINARY_DIR}/ForwardVariables.cmake\")\n\tinstall(SCRIPT \"${BUILDSYSTEM_DIR}/scripts/EmbedWinDependencies.cmake\")\n\tforeach(_UTILITY ${REQUIRED_UTILITIES})\n\t\tinstall(FILES \"${${_UTILITY}_EXECUTABLE}\" DESTINATION \"${CMAKE_INSTALL_BINDIR}\")\n\tendforeach()\n\tconfigure_file(\n\t\t\"${BUILDSYSTEM_DIR}/templates/openage.bat.in\"\n\t\t\"${CMAKE_CURRENT_BINARY_DIR}/openage.bat\"\n\t)\n\tinstall(FILES \"${CMAKE_CURRENT_BINARY_DIR}/openage.bat\" DESTINATION \".\")\n\n\tinclude(InstallRequiredSystemLibraries)\nendif()\n\n\n########################################################################\n# Resource files\n\nset(CPACK_RESOURCE_FILE_LICENSE \"${CMAKE_SOURCE_DIR}/copying.md\")\nset(CPACK_RESOURCE_FILE_README \"${CMAKE_SOURCE_DIR}/README.md\")\nset(CPACK_PACKAGE_DESCRIPTION_FILE \"${CMAKE_SOURCE_DIR}/README.md\")\n\n# TODO: Still really blurry, another possibility to get a higher res pic in there?\n#set(CPACK_PACKAGE_ICON \"${CMAKE_SOURCE_DIR}/assets/logo\\\\\\\\cpack.bmp\")\n\n########################################################################\n# File output\nset(CPACK_PACKAGE_INSTALL_DIRECTORY \"${PROJECT_NAME}\")\nset(CPACK_PACKAGE_CHECKSUM \"SHA256\")\n\n########################################################################\n# General package config\n\nset(CPACK_GENERATOR \"NSIS\" \"7Z\")\n\n\ninclude(CPack)\n"
  },
  {
    "path": "packaging/CPackOptions.cmake",
    "content": "# Copyright 2019-2019 the openage authors. See copying.md for legal info.\n\n# Keeps all required CPack configuration for the Generators in one place and\n# CPack calls this file for every generator, keep this in mind, if you use\n# variables or functions outside of the conditional branch\n\n\n########################################################################\n# Continuous Integration\n\n# Filename addition for architecture, set from environment variables\nset(CPACK_PACKAGE_ARCHITECTURE \"$ENV{TARGET_PLATFORM}\")\n\n# Filename addition for nightly-builds, set from environment variables\nif(DEFINED ENV{IS_NIGHTLY})\n    set(PACKAGE_VERSION_STRING \"${CPACK_VERSION_FULL_STRING}\")\n    set(PACKAGE_NIGHTLY \"_NIGHTLY\")\nelse()\n    set(PACKAGE_VERSION_STRING \"v${PROJECT_VERSION}\")\n    message(DEBUG \"${PACKAGE_VERSION_STRING}\")\n    set(PACKAGE_NIGHTLY \"\")\nendif()\n\n\nif(CPACK_GENERATOR MATCHES \"NSIS\")\n\n    # Explanations to the following commands you can find here:\n    # https://cmake.org/cmake/help/latest/cpack_gen/nsis.html#cpack_gen:CPack%20NSIS%20Generator\n\n    ########################################################################\n    # Filename\n    message(DEBUG \"${PROJECT_VERSION}\")\n    set(CPACK_PACKAGE_FILE_NAME \"${CPACK_PACKAGE_NAME}-${PACKAGE_VERSION_STRING}-${CPACK_PACKAGE_ARCHITECTURE}-installer${PACKAGE_NIGHTLY}\")\n\n\n    ########################################################################\n    # Installer\n\n    set(CPACK_NSIS_MODIFY_PATH \"OFF\")\n    set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL \"ON\")\n    set(CPACK_NSIS_PACKAGE_NAME \"${CPACK_PACKAGE_NAME} v${CPACK_PACKAGE_VERSION}\")\n    set(CPACK_NSIS_DISPLAY_NAME \"${CPACK_PACKAGE_NAME} v${CPACK_PACKAGE_VERSION}\")\n    set(CPACK_NSIS_URL_INFO_ABOUT \"https://github.com/SFTtech/openage\")\n    set(CPACK_NSIS_HELP_LINK \"https://github.com/SFTtech/openage/blob/master/doc/troubleshooting.md\")\n\n    # TODO: create welcome page\n    #set(CPACK_RESOURCE_FILE_WELCOME \"\")\n\n\n    ########################################################################\n    # Shortcuts\n\n    # TODO: create startmenu shortcut\n    #set(CPACK_PACKAGE_EXECUTABLES \"\")\n    # TODO: create desktop shortcut\n    #set(CPACK_CREATE_DESKTOP_LINKS \"\")\n\n    # Link to launcher (exe) can go here, *.bat is not working\n    #set(CPACK_NSIS_INSTALLED_ICON_NAME \"${CMAKE_CURRENT_BINARY_DIR}\\\\\\\\openage.bat\")\n    #SET(CPACK_PACKAGE_EXECUTABLES \"openage.bat;openage\")\n\nendif()\n\n\nif(CPACK_GENERATOR MATCHES \"7Z\")\n    message(DEBUG \"${PROJECT_VERSION}\")\n    set(CPACK_PACKAGE_FILE_NAME \"${CPACK_PACKAGE_NAME}-${PACKAGE_VERSION_STRING}-${CPACK_PACKAGE_ARCHITECTURE}-portable${PACKAGE_NIGHTLY}\")\nendif()\n"
  },
  {
    "path": "packaging/docker/devenv/Dockerfile.ubuntu.2404",
    "content": "FROM ubuntu:24.04\n\nRUN apt-get update && DEBIAN_FRONTEND=\"noninteractive\" apt-get install -y sudo \\\n    && sudo apt-get update \\\n    && sudo DEBIAN_FRONTEND=\"noninteractive\" apt-get install -y \\\n    apt-utils \\\n    build-essential \\\n    cmake \\\n    cython3 \\\n    flex \\\n    gcc \\\n    g++ \\\n    git \\\n    libeigen3-dev \\\n    libepoxy-dev \\\n    libfontconfig1-dev \\\n    libfreetype-dev \\\n    libharfbuzz-dev \\\n    libogg-dev \\\n    libopus-dev \\\n    libopusfile-dev \\\n    libpng-dev \\\n    libtoml11-dev \\\n    make \\\n    ninja-build \\\n    python3-dev \\\n    python3-mako \\\n    python3-numpy \\\n    python3-lz4 \\\n    python3-pil \\\n    python3-pip \\\n    python3-pygments \\\n    python3-toml \\\n    qml6-module-qtquick-controls \\\n    qt6-declarative-dev \\\n    qt6-multimedia-dev \\\n    qml6-module-qtquick3d-spatialaudio \\\n    && sudo apt-get clean \\\n    && truncate -s 0 ~/.bash_history\n\n# At least cython >= 3.0.10 < 4.0.0 is required to avoid runtime errors\n# TODO: Remove this line once cython is upgraded in Ubuntu 24.04.3 (expected around August 2025)\nRUN pip install \"cython>=3.0.10,<4.0.0\" --break-system-packages"
  },
  {
    "path": "run.py.in",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\nOpenage can, and should, be launched via python3 -m openage.\n\nHowever, for dynamic analysis using LLVM's sanitizer, a self-compiled\nexecutable is needed.\n\nThis file is Cythonized with an embedded interpreter, producing ./run,\nwhich satisifies that requirement.\n\"\"\"\n\nif __name__ == '__main__@SOME_UNDEFINED_VARIABLE_CMAKE_WILL_REMOVE@':\n    # This is stupid but without it, Cython/Python cannot find the openage module.\n    import os\n    import sys\n    sys.path.append(os.getcwd())\n\n    from openage.__main__ import main\n    main()\nelse:\n    print(\"Running this in the source directory is not supported.\",\n          \"Please use `make run` or `bin/run.py` to start instead.\")\n"
  },
  {
    "path": "shell.nix",
    "content": "{ pkgs? import <nixpkgs> {} }:\npkgs.mkShell {\n  nativeBuildInputs = [\n    pkgs.gcc\n    pkgs.clang\n    #pkgs.glibc\n    #pkgs.libcxx\n    #pkgs.lld\n    #pkgs.gdb\n    pkgs.cmake\n    pkgs.gnumake\n    pkgs.qt6.full\n    #pkgs.qtcreator\n\n    pkgs.eigen\n    pkgs.python39\n    pkgs.python39Packages.mako\n    pkgs.python39Packages.pillow\n    pkgs.python39Packages.numpy\n    pkgs.python39Packages.lz4\n    pkgs.python39Packages.pygments\n    pkgs.python39Packages.cython\n    pkgs.libepoxy\n    pkgs.libogg\n    pkgs.libpng\n    pkgs.dejavu_fonts\n    pkgs.ftgl\n    pkgs.fontconfig\n    pkgs.harfbuzz\n    pkgs.opusfile\n    pkgs.libopus\n    pkgs.python39Packages.pylint\n    pkgs.python39Packages.toml\n    pkgs.libsForQt6.qt6.qtdeclarative\n    pkgs.libsForQt6.qt6.qtquickcontrols\n    pkgs.libsForQt6.qt6.qtmultimedia\n  ];\n}\n"
  }
]